@lumaui/angular 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1350 +0,0 @@
1
- import { Component, DebugElement } from '@angular/core';
2
- import { ComponentFixture, TestBed } from '@angular/core/testing';
3
-
4
- import { ButtonDirective } from './button.directive';
5
- import { By } from '@angular/platform-browser';
6
-
7
- // ============================================================
8
- // TEST HOST COMPONENTS
9
- // ============================================================
10
-
11
- @Component({
12
- template: `
13
- <button
14
- lumaButton
15
- [lmVariant]="lmVariant"
16
- [lmSize]="lmSize"
17
- [lmDisabled]="lmDisabled"
18
- [lmType]="lmType"
19
- >
20
- Test Button
21
- </button>
22
- `,
23
- imports: [ButtonDirective],
24
- })
25
- class ButtonTestHostComponent {
26
- lmVariant: 'primary' | 'outline' | 'ghost' | 'danger' = 'primary';
27
- lmSize: 'sm' | 'md' | 'lg' | 'full' = 'md';
28
- lmDisabled = false;
29
- lmType: 'button' | 'submit' | 'reset' = 'button';
30
- }
31
-
32
- @Component({
33
- template: `<a lumaButton [lmVariant]="lmVariant" href="/test"
34
- >Link Button</a
35
- >`,
36
- imports: [ButtonDirective],
37
- })
38
- class AnchorButtonTestHostComponent {
39
- lmVariant: 'primary' | 'outline' | 'ghost' | 'danger' = 'primary';
40
- }
41
-
42
- @Component({
43
- template: `<button lumaButton lmType="submit">Submit Button</button>`,
44
- imports: [ButtonDirective],
45
- })
46
- class SubmitButtonTestHostComponent {}
47
-
48
- @Component({
49
- template: `<button lumaButton lmType="reset">Reset Button</button>`,
50
- imports: [ButtonDirective],
51
- })
52
- class ResetButtonTestHostComponent {}
53
-
54
- // ============================================================
55
- // TOKEN DEFINITIONS
56
- // ============================================================
57
-
58
- const BUTTON_TOKENS = {
59
- primary: {
60
- bg: 'oklch(0.54 0.1 230)',
61
- bgHover: 'oklch(0.49 0.09 230)',
62
- bgActive: 'oklch(0.44 0.08 230)',
63
- text: 'oklch(1 0 0)',
64
- },
65
- outline: {
66
- border: 'oklch(0.5 0.01 0)',
67
- borderHover: 'oklch(0.2 0.005 0)',
68
- bgHover: 'oklch(0.96 0.01 230)',
69
- text: 'oklch(0.2 0.005 0)',
70
- },
71
- ghost: {
72
- bg: 'rgba(0, 0, 0, 0)',
73
- bgHover: 'oklch(0.96 0.01 230)',
74
- text: 'oklch(0.2 0.005 0)',
75
- },
76
- danger: {
77
- bg: 'oklch(0.55 0.22 25)',
78
- bgHover: 'oklch(0.50 0.20 25)',
79
- bgActive: 'oklch(0.45 0.18 25)',
80
- text: 'oklch(1 0 0)',
81
- },
82
- padding: {
83
- xSm: '1rem',
84
- xMd: '1.5rem',
85
- xLg: '2rem',
86
- ySm: '0.5rem',
87
- yMd: '0.75rem',
88
- yLg: '1rem',
89
- },
90
- radius: '10px',
91
- focus: {
92
- ringWidth: '2px',
93
- ringColor: 'oklch(0.54 0.1 230 / 0.25)',
94
- },
95
- transition: {
96
- duration: '200ms',
97
- timing: 'ease-out',
98
- },
99
- } as const;
100
-
101
- const DARK_TOKENS = {
102
- primary: {
103
- bg: 'oklch(0.64 0.12 230)',
104
- bgHover: 'oklch(0.69 0.12 230)',
105
- bgActive: 'oklch(0.74 0.13 230)',
106
- text: 'oklch(0.1 0 0)',
107
- },
108
- outline: {
109
- border: 'oklch(0.65 0.01 0)',
110
- borderHover: 'oklch(0.98 0.002 0)',
111
- bgHover: 'oklch(0.20 0.01 230)',
112
- text: 'oklch(0.98 0.002 0)',
113
- },
114
- ghost: {
115
- bgHover: 'oklch(0.20 0.01 230)',
116
- text: 'oklch(0.98 0.002 0)',
117
- },
118
- danger: {
119
- bg: 'oklch(0.60 0.24 25)',
120
- bgHover: 'oklch(0.65 0.25 25)',
121
- bgActive: 'oklch(0.70 0.26 25)',
122
- text: 'oklch(0.1 0 0)',
123
- },
124
- focus: {
125
- ringColor: 'oklch(0.64 0.12 230 / 0.25)',
126
- },
127
- } as const;
128
-
129
- // ============================================================
130
- // TOKEN SETUP/CLEANUP UTILITIES
131
- // ============================================================
132
-
133
- function setupButtonTokens(): void {
134
- const root = document.documentElement;
135
-
136
- // Primary variant
137
- root.style.setProperty('--luma-button-primary-bg', BUTTON_TOKENS.primary.bg);
138
- root.style.setProperty(
139
- '--luma-button-primary-bg-hover',
140
- BUTTON_TOKENS.primary.bgHover,
141
- );
142
- root.style.setProperty(
143
- '--luma-button-primary-bg-active',
144
- BUTTON_TOKENS.primary.bgActive,
145
- );
146
- root.style.setProperty(
147
- '--luma-button-primary-text',
148
- BUTTON_TOKENS.primary.text,
149
- );
150
-
151
- // Outline variant
152
- root.style.setProperty(
153
- '--luma-button-outline-border',
154
- BUTTON_TOKENS.outline.border,
155
- );
156
- root.style.setProperty(
157
- '--luma-button-outline-border-hover',
158
- BUTTON_TOKENS.outline.borderHover,
159
- );
160
- root.style.setProperty(
161
- '--luma-button-outline-bg-hover',
162
- BUTTON_TOKENS.outline.bgHover,
163
- );
164
- root.style.setProperty(
165
- '--luma-button-outline-text',
166
- BUTTON_TOKENS.outline.text,
167
- );
168
-
169
- // Ghost variant
170
- root.style.setProperty('--luma-button-ghost-bg', BUTTON_TOKENS.ghost.bg);
171
- root.style.setProperty(
172
- '--luma-button-ghost-bg-hover',
173
- BUTTON_TOKENS.ghost.bgHover,
174
- );
175
- root.style.setProperty('--luma-button-ghost-text', BUTTON_TOKENS.ghost.text);
176
-
177
- // Danger variant
178
- root.style.setProperty('--luma-button-danger-bg', BUTTON_TOKENS.danger.bg);
179
- root.style.setProperty(
180
- '--luma-button-danger-bg-hover',
181
- BUTTON_TOKENS.danger.bgHover,
182
- );
183
- root.style.setProperty(
184
- '--luma-button-danger-bg-active',
185
- BUTTON_TOKENS.danger.bgActive,
186
- );
187
- root.style.setProperty(
188
- '--luma-button-danger-text',
189
- BUTTON_TOKENS.danger.text,
190
- );
191
-
192
- // Padding
193
- root.style.setProperty(
194
- '--luma-button-padding-x-sm',
195
- BUTTON_TOKENS.padding.xSm,
196
- );
197
- root.style.setProperty(
198
- '--luma-button-padding-x-md',
199
- BUTTON_TOKENS.padding.xMd,
200
- );
201
- root.style.setProperty(
202
- '--luma-button-padding-x-lg',
203
- BUTTON_TOKENS.padding.xLg,
204
- );
205
- root.style.setProperty(
206
- '--luma-button-padding-y-sm',
207
- BUTTON_TOKENS.padding.ySm,
208
- );
209
- root.style.setProperty(
210
- '--luma-button-padding-y-md',
211
- BUTTON_TOKENS.padding.yMd,
212
- );
213
- root.style.setProperty(
214
- '--luma-button-padding-y-lg',
215
- BUTTON_TOKENS.padding.yLg,
216
- );
217
-
218
- // Radius
219
- root.style.setProperty('--luma-button-radius', BUTTON_TOKENS.radius);
220
-
221
- // Focus
222
- root.style.setProperty(
223
- '--luma-button-focus-ring-width',
224
- BUTTON_TOKENS.focus.ringWidth,
225
- );
226
- root.style.setProperty(
227
- '--luma-button-focus-ring-color',
228
- BUTTON_TOKENS.focus.ringColor,
229
- );
230
-
231
- // Transition
232
- root.style.setProperty(
233
- '--luma-button-transition-duration',
234
- BUTTON_TOKENS.transition.duration,
235
- );
236
- root.style.setProperty(
237
- '--luma-button-transition-timing',
238
- BUTTON_TOKENS.transition.timing,
239
- );
240
- }
241
-
242
- function cleanupButtonTokens(): void {
243
- const root = document.documentElement;
244
- const tokenNames = [
245
- '--luma-button-primary-bg',
246
- '--luma-button-primary-bg-hover',
247
- '--luma-button-primary-bg-active',
248
- '--luma-button-primary-text',
249
- '--luma-button-outline-border',
250
- '--luma-button-outline-border-hover',
251
- '--luma-button-outline-bg-hover',
252
- '--luma-button-outline-text',
253
- '--luma-button-ghost-bg',
254
- '--luma-button-ghost-bg-hover',
255
- '--luma-button-ghost-text',
256
- '--luma-button-danger-bg',
257
- '--luma-button-danger-bg-hover',
258
- '--luma-button-danger-bg-active',
259
- '--luma-button-danger-text',
260
- '--luma-button-padding-x-sm',
261
- '--luma-button-padding-x-md',
262
- '--luma-button-padding-x-lg',
263
- '--luma-button-padding-y-sm',
264
- '--luma-button-padding-y-md',
265
- '--luma-button-padding-y-lg',
266
- '--luma-button-radius',
267
- '--luma-button-focus-ring-width',
268
- '--luma-button-focus-ring-color',
269
- '--luma-button-transition-duration',
270
- '--luma-button-transition-timing',
271
- ];
272
-
273
- tokenNames.forEach((name) => root.style.removeProperty(name));
274
- root.classList.remove('dark');
275
- }
276
-
277
- function applyDarkTheme(): void {
278
- const root = document.documentElement;
279
- root.classList.add('dark');
280
-
281
- // Primary
282
- root.style.setProperty('--luma-button-primary-bg', DARK_TOKENS.primary.bg);
283
- root.style.setProperty(
284
- '--luma-button-primary-bg-hover',
285
- DARK_TOKENS.primary.bgHover,
286
- );
287
- root.style.setProperty(
288
- '--luma-button-primary-bg-active',
289
- DARK_TOKENS.primary.bgActive,
290
- );
291
- root.style.setProperty(
292
- '--luma-button-primary-text',
293
- DARK_TOKENS.primary.text,
294
- );
295
-
296
- // Outline
297
- root.style.setProperty(
298
- '--luma-button-outline-border',
299
- DARK_TOKENS.outline.border,
300
- );
301
- root.style.setProperty(
302
- '--luma-button-outline-border-hover',
303
- DARK_TOKENS.outline.borderHover,
304
- );
305
- root.style.setProperty(
306
- '--luma-button-outline-bg-hover',
307
- DARK_TOKENS.outline.bgHover,
308
- );
309
- root.style.setProperty(
310
- '--luma-button-outline-text',
311
- DARK_TOKENS.outline.text,
312
- );
313
-
314
- // Ghost
315
- root.style.setProperty(
316
- '--luma-button-ghost-bg-hover',
317
- DARK_TOKENS.ghost.bgHover,
318
- );
319
- root.style.setProperty('--luma-button-ghost-text', DARK_TOKENS.ghost.text);
320
-
321
- // Danger
322
- root.style.setProperty('--luma-button-danger-bg', DARK_TOKENS.danger.bg);
323
- root.style.setProperty(
324
- '--luma-button-danger-bg-hover',
325
- DARK_TOKENS.danger.bgHover,
326
- );
327
- root.style.setProperty(
328
- '--luma-button-danger-bg-active',
329
- DARK_TOKENS.danger.bgActive,
330
- );
331
- root.style.setProperty('--luma-button-danger-text', DARK_TOKENS.danger.text);
332
-
333
- // Focus
334
- root.style.setProperty(
335
- '--luma-button-focus-ring-color',
336
- DARK_TOKENS.focus.ringColor,
337
- );
338
- }
339
-
340
- // ============================================================
341
- // TEST SUITES
342
- // ============================================================
343
-
344
- describe('ButtonDirective', () => {
345
- let fixture: ComponentFixture<ButtonTestHostComponent>;
346
- let hostComponent: ButtonTestHostComponent;
347
- let buttonElement: DebugElement;
348
- let directive: ButtonDirective;
349
-
350
- beforeEach(async () => {
351
- await TestBed.configureTestingModule({
352
- imports: [
353
- ButtonDirective,
354
- ButtonTestHostComponent,
355
- SubmitButtonTestHostComponent,
356
- ResetButtonTestHostComponent,
357
- ],
358
- }).compileComponents();
359
-
360
- fixture = TestBed.createComponent(ButtonTestHostComponent);
361
- hostComponent = fixture.componentInstance;
362
- buttonElement = fixture.debugElement.query(By.directive(ButtonDirective));
363
- directive = buttonElement.injector.get(ButtonDirective);
364
-
365
- setupButtonTokens();
366
- // Note: Don't call detectChanges() here - let each test/describe handle it
367
- // to avoid ExpressionChangedAfterItHasBeenCheckedError
368
- });
369
-
370
- afterEach(() => {
371
- cleanupButtonTokens();
372
- });
373
-
374
- // ============================================================
375
- // BASIC DIRECTIVE TESTS
376
- // ============================================================
377
-
378
- describe('Basic Directive Creation', () => {
379
- it('should create the directive', () => {
380
- expect(directive).toBeTruthy();
381
- });
382
-
383
- it('should apply as directive on button element', () => {
384
- fixture.detectChanges();
385
- expect(buttonElement.nativeElement.tagName).toBe('BUTTON');
386
- });
387
-
388
- it('should have signal-based inputs', () => {
389
- expect(typeof directive.lmVariant).toBe('function');
390
- expect(typeof directive.lmSize).toBe('function');
391
- expect(typeof directive.lmDisabled).toBe('function');
392
- expect(typeof directive.lmType).toBe('function');
393
- });
394
-
395
- it('should have computed classes signal', () => {
396
- expect(typeof directive.classes).toBe('function');
397
- expect(typeof directive.classes()).toBe('string');
398
- });
399
-
400
- it('should apply default variant (primary) when not specified', () => {
401
- fixture.detectChanges();
402
- expect(directive.lmVariant()).toBe('primary');
403
- });
404
-
405
- it('should apply default size (md) when not specified', () => {
406
- fixture.detectChanges();
407
- expect(directive.lmSize()).toBe('md');
408
- });
409
-
410
- it('should set type attribute to button by default', () => {
411
- fixture.detectChanges();
412
- expect(buttonElement.nativeElement.getAttribute('type')).toBe('button');
413
- });
414
-
415
- it('should maintain consistent class generation across multiple calls', () => {
416
- fixture.detectChanges();
417
- const classes1 = directive.classes();
418
- const classes2 = directive.classes();
419
- expect(classes1).toBe(classes2);
420
- });
421
- });
422
-
423
- // ============================================================
424
- // DESIGN TOKEN DEFINITION TESTS
425
- // ============================================================
426
-
427
- describe('Design Token Definition', () => {
428
- describe('Primary Variant Tokens', () => {
429
- it('should define --luma-button-primary-bg css variable', () => {
430
- const value = getComputedStyle(document.documentElement)
431
- .getPropertyValue('--luma-button-primary-bg')
432
- .trim();
433
- expect(value).toBe(BUTTON_TOKENS.primary.bg);
434
- });
435
-
436
- it('should define --luma-button-primary-bg-hover css variable', () => {
437
- const value = getComputedStyle(document.documentElement)
438
- .getPropertyValue('--luma-button-primary-bg-hover')
439
- .trim();
440
- expect(value).toBe(BUTTON_TOKENS.primary.bgHover);
441
- });
442
-
443
- it('should define --luma-button-primary-bg-active css variable', () => {
444
- const value = getComputedStyle(document.documentElement)
445
- .getPropertyValue('--luma-button-primary-bg-active')
446
- .trim();
447
- expect(value).toBe(BUTTON_TOKENS.primary.bgActive);
448
- });
449
-
450
- it('should define --luma-button-primary-text css variable', () => {
451
- const value = getComputedStyle(document.documentElement)
452
- .getPropertyValue('--luma-button-primary-text')
453
- .trim();
454
- expect(value).toBe(BUTTON_TOKENS.primary.text);
455
- });
456
- });
457
-
458
- describe('Outline Variant Tokens', () => {
459
- it('should define --luma-button-outline-border css variable', () => {
460
- const value = getComputedStyle(document.documentElement)
461
- .getPropertyValue('--luma-button-outline-border')
462
- .trim();
463
- expect(value).toBe(BUTTON_TOKENS.outline.border);
464
- });
465
-
466
- it('should define --luma-button-outline-border-hover css variable', () => {
467
- const value = getComputedStyle(document.documentElement)
468
- .getPropertyValue('--luma-button-outline-border-hover')
469
- .trim();
470
- expect(value).toBe(BUTTON_TOKENS.outline.borderHover);
471
- });
472
-
473
- it('should define --luma-button-outline-bg-hover css variable', () => {
474
- const value = getComputedStyle(document.documentElement)
475
- .getPropertyValue('--luma-button-outline-bg-hover')
476
- .trim();
477
- expect(value).toBe(BUTTON_TOKENS.outline.bgHover);
478
- });
479
-
480
- it('should define --luma-button-outline-text css variable', () => {
481
- const value = getComputedStyle(document.documentElement)
482
- .getPropertyValue('--luma-button-outline-text')
483
- .trim();
484
- expect(value).toBe(BUTTON_TOKENS.outline.text);
485
- });
486
- });
487
-
488
- describe('Ghost Variant Tokens', () => {
489
- it('should define --luma-button-ghost-bg css variable', () => {
490
- const value = getComputedStyle(document.documentElement)
491
- .getPropertyValue('--luma-button-ghost-bg')
492
- .trim();
493
- expect(value).toBe(BUTTON_TOKENS.ghost.bg);
494
- });
495
-
496
- it('should define --luma-button-ghost-bg-hover css variable', () => {
497
- const value = getComputedStyle(document.documentElement)
498
- .getPropertyValue('--luma-button-ghost-bg-hover')
499
- .trim();
500
- expect(value).toBe(BUTTON_TOKENS.ghost.bgHover);
501
- });
502
-
503
- it('should define --luma-button-ghost-text css variable', () => {
504
- const value = getComputedStyle(document.documentElement)
505
- .getPropertyValue('--luma-button-ghost-text')
506
- .trim();
507
- expect(value).toBe(BUTTON_TOKENS.ghost.text);
508
- });
509
- });
510
-
511
- describe('Danger Variant Tokens', () => {
512
- it('should define --luma-button-danger-bg css variable', () => {
513
- const value = getComputedStyle(document.documentElement)
514
- .getPropertyValue('--luma-button-danger-bg')
515
- .trim();
516
- expect(value).toBe(BUTTON_TOKENS.danger.bg);
517
- });
518
-
519
- it('should define --luma-button-danger-bg-hover css variable', () => {
520
- const value = getComputedStyle(document.documentElement)
521
- .getPropertyValue('--luma-button-danger-bg-hover')
522
- .trim();
523
- expect(value).toBe(BUTTON_TOKENS.danger.bgHover);
524
- });
525
-
526
- it('should define --luma-button-danger-bg-active css variable', () => {
527
- const value = getComputedStyle(document.documentElement)
528
- .getPropertyValue('--luma-button-danger-bg-active')
529
- .trim();
530
- expect(value).toBe(BUTTON_TOKENS.danger.bgActive);
531
- });
532
-
533
- it('should define --luma-button-danger-text css variable', () => {
534
- const value = getComputedStyle(document.documentElement)
535
- .getPropertyValue('--luma-button-danger-text')
536
- .trim();
537
- expect(value).toBe(BUTTON_TOKENS.danger.text);
538
- });
539
- });
540
-
541
- describe('Dimension Tokens', () => {
542
- it('should define --luma-button-padding-x-sm css variable', () => {
543
- const value = getComputedStyle(document.documentElement)
544
- .getPropertyValue('--luma-button-padding-x-sm')
545
- .trim();
546
- expect(value).toBe(BUTTON_TOKENS.padding.xSm);
547
- });
548
-
549
- it('should define --luma-button-padding-x-md css variable', () => {
550
- const value = getComputedStyle(document.documentElement)
551
- .getPropertyValue('--luma-button-padding-x-md')
552
- .trim();
553
- expect(value).toBe(BUTTON_TOKENS.padding.xMd);
554
- });
555
-
556
- it('should define --luma-button-padding-x-lg css variable', () => {
557
- const value = getComputedStyle(document.documentElement)
558
- .getPropertyValue('--luma-button-padding-x-lg')
559
- .trim();
560
- expect(value).toBe(BUTTON_TOKENS.padding.xLg);
561
- });
562
-
563
- it('should define --luma-button-padding-y-sm css variable', () => {
564
- const value = getComputedStyle(document.documentElement)
565
- .getPropertyValue('--luma-button-padding-y-sm')
566
- .trim();
567
- expect(value).toBe(BUTTON_TOKENS.padding.ySm);
568
- });
569
-
570
- it('should define --luma-button-padding-y-md css variable', () => {
571
- const value = getComputedStyle(document.documentElement)
572
- .getPropertyValue('--luma-button-padding-y-md')
573
- .trim();
574
- expect(value).toBe(BUTTON_TOKENS.padding.yMd);
575
- });
576
-
577
- it('should define --luma-button-padding-y-lg css variable', () => {
578
- const value = getComputedStyle(document.documentElement)
579
- .getPropertyValue('--luma-button-padding-y-lg')
580
- .trim();
581
- expect(value).toBe(BUTTON_TOKENS.padding.yLg);
582
- });
583
-
584
- it('should define --luma-button-radius css variable', () => {
585
- const value = getComputedStyle(document.documentElement)
586
- .getPropertyValue('--luma-button-radius')
587
- .trim();
588
- expect(value).toBe(BUTTON_TOKENS.radius);
589
- });
590
- });
591
-
592
- describe('Focus Tokens', () => {
593
- it('should define --luma-button-focus-ring-width css variable', () => {
594
- const value = getComputedStyle(document.documentElement)
595
- .getPropertyValue('--luma-button-focus-ring-width')
596
- .trim();
597
- expect(value).toBe(BUTTON_TOKENS.focus.ringWidth);
598
- });
599
-
600
- it('should define --luma-button-focus-ring-color css variable', () => {
601
- const value = getComputedStyle(document.documentElement)
602
- .getPropertyValue('--luma-button-focus-ring-color')
603
- .trim();
604
- expect(value).toBe(BUTTON_TOKENS.focus.ringColor);
605
- });
606
- });
607
-
608
- describe('Transition Tokens', () => {
609
- it('should define --luma-button-transition-duration css variable', () => {
610
- const value = getComputedStyle(document.documentElement)
611
- .getPropertyValue('--luma-button-transition-duration')
612
- .trim();
613
- expect(value).toBe(BUTTON_TOKENS.transition.duration);
614
- });
615
-
616
- it('should define --luma-button-transition-timing css variable', () => {
617
- const value = getComputedStyle(document.documentElement)
618
- .getPropertyValue('--luma-button-transition-timing')
619
- .trim();
620
- expect(value).toBe(BUTTON_TOKENS.transition.timing);
621
- });
622
- });
623
- });
624
-
625
- // ============================================================
626
- // TOKEN CONSUMPTION TESTS
627
- // ============================================================
628
- // These tests verify that when a variant is selected, the corresponding
629
- // tokens are accessible and the correct classes are applied.
630
- // CSS variables are inherited from document.documentElement.
631
-
632
- describe('Token Consumption', () => {
633
- describe('Primary Variant', () => {
634
- beforeEach(() => {
635
- hostComponent.lmVariant = 'primary';
636
- fixture.detectChanges();
637
- });
638
-
639
- it('should have access to --luma-button-primary-bg token', () => {
640
- // Token is set on document root and inherited by all elements
641
- const value = getComputedStyle(document.documentElement)
642
- .getPropertyValue('--luma-button-primary-bg')
643
- .trim();
644
- expect(value).toBe(BUTTON_TOKENS.primary.bg);
645
- });
646
-
647
- it('should have access to --luma-button-primary-text token', () => {
648
- const value = getComputedStyle(document.documentElement)
649
- .getPropertyValue('--luma-button-primary-text')
650
- .trim();
651
- expect(value).toBe(BUTTON_TOKENS.primary.text);
652
- });
653
-
654
- it('should have access to --luma-button-primary-bg-hover token', () => {
655
- const value = getComputedStyle(document.documentElement)
656
- .getPropertyValue('--luma-button-primary-bg-hover')
657
- .trim();
658
- expect(value).toBe(BUTTON_TOKENS.primary.bgHover);
659
- });
660
- });
661
-
662
- describe('Outline Variant', () => {
663
- beforeEach(() => {
664
- hostComponent.lmVariant = 'outline';
665
- fixture.detectChanges();
666
- });
667
-
668
- it('should have access to --luma-button-outline-border token', () => {
669
- const value = getComputedStyle(document.documentElement)
670
- .getPropertyValue('--luma-button-outline-border')
671
- .trim();
672
- expect(value).toBe(BUTTON_TOKENS.outline.border);
673
- });
674
-
675
- it('should have access to --luma-button-outline-text token', () => {
676
- const value = getComputedStyle(document.documentElement)
677
- .getPropertyValue('--luma-button-outline-text')
678
- .trim();
679
- expect(value).toBe(BUTTON_TOKENS.outline.text);
680
- });
681
-
682
- it('should have access to --luma-button-outline-bg-hover token', () => {
683
- const value = getComputedStyle(document.documentElement)
684
- .getPropertyValue('--luma-button-outline-bg-hover')
685
- .trim();
686
- expect(value).toBe(BUTTON_TOKENS.outline.bgHover);
687
- });
688
- });
689
-
690
- describe('Ghost Variant', () => {
691
- beforeEach(() => {
692
- hostComponent.lmVariant = 'ghost';
693
- fixture.detectChanges();
694
- });
695
-
696
- it('should have access to --luma-button-ghost-bg token', () => {
697
- const value = getComputedStyle(document.documentElement)
698
- .getPropertyValue('--luma-button-ghost-bg')
699
- .trim();
700
- expect(value).toBe(BUTTON_TOKENS.ghost.bg);
701
- });
702
-
703
- it('should have access to --luma-button-ghost-text token', () => {
704
- const value = getComputedStyle(document.documentElement)
705
- .getPropertyValue('--luma-button-ghost-text')
706
- .trim();
707
- expect(value).toBe(BUTTON_TOKENS.ghost.text);
708
- });
709
-
710
- it('should have access to --luma-button-ghost-bg-hover token', () => {
711
- const value = getComputedStyle(document.documentElement)
712
- .getPropertyValue('--luma-button-ghost-bg-hover')
713
- .trim();
714
- expect(value).toBe(BUTTON_TOKENS.ghost.bgHover);
715
- });
716
- });
717
-
718
- describe('Danger Variant', () => {
719
- beforeEach(() => {
720
- hostComponent.lmVariant = 'danger';
721
- fixture.detectChanges();
722
- });
723
-
724
- it('should have access to --luma-button-danger-bg token', () => {
725
- const value = getComputedStyle(document.documentElement)
726
- .getPropertyValue('--luma-button-danger-bg')
727
- .trim();
728
- expect(value).toBe(BUTTON_TOKENS.danger.bg);
729
- });
730
-
731
- it('should have access to --luma-button-danger-text token', () => {
732
- const value = getComputedStyle(document.documentElement)
733
- .getPropertyValue('--luma-button-danger-text')
734
- .trim();
735
- expect(value).toBe(BUTTON_TOKENS.danger.text);
736
- });
737
-
738
- it('should have access to --luma-button-danger-bg-hover token', () => {
739
- const value = getComputedStyle(document.documentElement)
740
- .getPropertyValue('--luma-button-danger-bg-hover')
741
- .trim();
742
- expect(value).toBe(BUTTON_TOKENS.danger.bgHover);
743
- });
744
- });
745
-
746
- describe('Dimension Tokens', () => {
747
- it('should have access to --luma-button-padding-x-md token', () => {
748
- hostComponent.lmSize = 'md';
749
- fixture.detectChanges();
750
-
751
- const value = getComputedStyle(document.documentElement)
752
- .getPropertyValue('--luma-button-padding-x-md')
753
- .trim();
754
- expect(value).toBe(BUTTON_TOKENS.padding.xMd);
755
- });
756
-
757
- it('should have access to --luma-button-padding-y-md token', () => {
758
- hostComponent.lmSize = 'md';
759
- fixture.detectChanges();
760
-
761
- const value = getComputedStyle(document.documentElement)
762
- .getPropertyValue('--luma-button-padding-y-md')
763
- .trim();
764
- expect(value).toBe(BUTTON_TOKENS.padding.yMd);
765
- });
766
-
767
- it('should have access to --luma-button-radius token', () => {
768
- fixture.detectChanges();
769
-
770
- const value = getComputedStyle(document.documentElement)
771
- .getPropertyValue('--luma-button-radius')
772
- .trim();
773
- expect(value).toBe(BUTTON_TOKENS.radius);
774
- });
775
- });
776
- });
777
-
778
- // ============================================================
779
- // TOKEN OVERRIDE TESTS
780
- // ============================================================
781
-
782
- describe('Token Override', () => {
783
- it('should respect custom radius token override', () => {
784
- const customRadius = '20px';
785
- document.documentElement.style.setProperty(
786
- '--luma-button-radius',
787
- customRadius,
788
- );
789
- fixture.detectChanges();
790
-
791
- const value = getComputedStyle(document.documentElement)
792
- .getPropertyValue('--luma-button-radius')
793
- .trim();
794
- expect(value).toBe(customRadius);
795
- });
796
-
797
- it('should respect custom padding-x override', () => {
798
- const customPadding = '3rem';
799
- document.documentElement.style.setProperty(
800
- '--luma-button-padding-x-md',
801
- customPadding,
802
- );
803
- fixture.detectChanges();
804
-
805
- const value = getComputedStyle(document.documentElement)
806
- .getPropertyValue('--luma-button-padding-x-md')
807
- .trim();
808
- expect(value).toBe(customPadding);
809
- });
810
-
811
- it('should respect custom padding-y override', () => {
812
- const customPadding = '1.5rem';
813
- document.documentElement.style.setProperty(
814
- '--luma-button-padding-y-md',
815
- customPadding,
816
- );
817
- fixture.detectChanges();
818
-
819
- const value = getComputedStyle(document.documentElement)
820
- .getPropertyValue('--luma-button-padding-y-md')
821
- .trim();
822
- expect(value).toBe(customPadding);
823
- });
824
-
825
- it('should respect custom primary background override', () => {
826
- const customColor = 'oklch(0.6 0.15 250)';
827
- document.documentElement.style.setProperty(
828
- '--luma-button-primary-bg',
829
- customColor,
830
- );
831
- hostComponent.lmVariant = 'primary';
832
- fixture.detectChanges();
833
-
834
- const value = getComputedStyle(document.documentElement)
835
- .getPropertyValue('--luma-button-primary-bg')
836
- .trim();
837
- expect(value).toBe(customColor);
838
- });
839
-
840
- it('should respect custom focus ring width override', () => {
841
- const customWidth = '4px';
842
- document.documentElement.style.setProperty(
843
- '--luma-button-focus-ring-width',
844
- customWidth,
845
- );
846
- fixture.detectChanges();
847
-
848
- const value = getComputedStyle(document.documentElement)
849
- .getPropertyValue('--luma-button-focus-ring-width')
850
- .trim();
851
- expect(value).toBe(customWidth);
852
- });
853
-
854
- it('should respect custom transition duration override', () => {
855
- const customDuration = '300ms';
856
- document.documentElement.style.setProperty(
857
- '--luma-button-transition-duration',
858
- customDuration,
859
- );
860
- fixture.detectChanges();
861
-
862
- const value = getComputedStyle(document.documentElement)
863
- .getPropertyValue('--luma-button-transition-duration')
864
- .trim();
865
- expect(value).toBe(customDuration);
866
- });
867
- });
868
-
869
- // ============================================================
870
- // CLASS APPLICATION TESTS
871
- // ============================================================
872
-
873
- describe('Class Application', () => {
874
- describe('Base Classes', () => {
875
- beforeEach(() => {
876
- fixture.detectChanges();
877
- });
878
-
879
- it('should apply inline-flex for layout', () => {
880
- expect(directive.classes()).toContain('inline-flex');
881
- });
882
-
883
- it('should apply items-center for vertical alignment', () => {
884
- expect(directive.classes()).toContain('items-center');
885
- });
886
-
887
- it('should apply justify-center for horizontal alignment', () => {
888
- expect(directive.classes()).toContain('justify-center');
889
- });
890
-
891
- it('should apply font-medium for typography', () => {
892
- expect(directive.classes()).toContain('font-medium');
893
- });
894
-
895
- it('should apply leading-snug for line height', () => {
896
- expect(directive.classes()).toContain('leading-snug');
897
- });
898
-
899
- it('should remove default focus outline', () => {
900
- expect(directive.classes()).toContain('focus:outline-none');
901
- });
902
-
903
- it('should apply focus-visible ring class', () => {
904
- expect(directive.classes()).toContain(
905
- 'focus-visible:lm-ring-button-focus',
906
- );
907
- });
908
-
909
- it('should apply transition class with CSS variables', () => {
910
- expect(directive.classes()).toContain(
911
- 'transition-[color_var(--luma-button-transition-duration)_var(--luma-button-transition-timing)]',
912
- );
913
- });
914
- });
915
-
916
- describe('Primary Variant Classes', () => {
917
- beforeEach(() => {
918
- hostComponent.lmVariant = 'primary';
919
- fixture.detectChanges();
920
- });
921
-
922
- it('should apply primary background class', () => {
923
- expect(directive.classes()).toContain('lm-bg-button-primary-bg');
924
- });
925
-
926
- it('should apply primary text class', () => {
927
- expect(directive.classes()).toContain('lm-text-button-primary-text');
928
- });
929
-
930
- it('should apply primary hover background class', () => {
931
- expect(directive.classes()).toContain(
932
- 'hover:lm-bg-button-primary-bg-hover',
933
- );
934
- });
935
-
936
- it('should apply primary active background class', () => {
937
- expect(directive.classes()).toContain(
938
- 'active:lm-bg-button-primary-bg-active',
939
- );
940
- });
941
- });
942
-
943
- describe('Outline Variant Classes', () => {
944
- beforeEach(() => {
945
- hostComponent.lmVariant = 'outline';
946
- fixture.detectChanges();
947
- });
948
-
949
- it('should apply transparent background', () => {
950
- expect(directive.classes()).toContain('bg-transparent');
951
- });
952
-
953
- it('should apply outline text class', () => {
954
- expect(directive.classes()).toContain('lm-text-button-outline-text');
955
- });
956
-
957
- it('should apply border class', () => {
958
- expect(directive.classes()).toContain('border');
959
- });
960
-
961
- it('should apply outline border class', () => {
962
- expect(directive.classes()).toContain(
963
- 'lm-border-button-outline-border',
964
- );
965
- });
966
-
967
- it('should apply border hover class', () => {
968
- expect(directive.classes()).toContain(
969
- 'hover:lm-border-button-outline-border-hover',
970
- );
971
- });
972
-
973
- it('should apply background hover class', () => {
974
- expect(directive.classes()).toContain(
975
- 'hover:lm-bg-button-outline-bg-hover',
976
- );
977
- });
978
- });
979
-
980
- describe('Ghost Variant Classes', () => {
981
- beforeEach(() => {
982
- hostComponent.lmVariant = 'ghost';
983
- fixture.detectChanges();
984
- });
985
-
986
- it('should apply ghost background class', () => {
987
- expect(directive.classes()).toContain('lm-bg-button-ghost-bg');
988
- });
989
-
990
- it('should apply ghost text class', () => {
991
- expect(directive.classes()).toContain('lm-text-button-ghost-text');
992
- });
993
-
994
- it('should apply ghost hover background class', () => {
995
- expect(directive.classes()).toContain(
996
- 'hover:lm-bg-button-ghost-bg-hover',
997
- );
998
- });
999
- });
1000
-
1001
- describe('Danger Variant Classes', () => {
1002
- beforeEach(() => {
1003
- hostComponent.lmVariant = 'danger';
1004
- fixture.detectChanges();
1005
- });
1006
-
1007
- it('should apply danger background class', () => {
1008
- expect(directive.classes()).toContain('lm-bg-button-danger-bg');
1009
- });
1010
-
1011
- it('should apply danger text class', () => {
1012
- expect(directive.classes()).toContain('lm-text-button-danger-text');
1013
- });
1014
-
1015
- it('should apply danger hover background class', () => {
1016
- expect(directive.classes()).toContain(
1017
- 'hover:lm-bg-button-danger-bg-hover',
1018
- );
1019
- });
1020
-
1021
- it('should apply danger active background class', () => {
1022
- expect(directive.classes()).toContain(
1023
- 'active:lm-bg-button-danger-bg-active',
1024
- );
1025
- });
1026
- });
1027
-
1028
- describe('Size Classes', () => {
1029
- it('should apply small size classes', () => {
1030
- hostComponent.lmSize = 'sm';
1031
- fixture.detectChanges();
1032
-
1033
- expect(directive.classes()).toContain(
1034
- 'px-[var(--luma-button-padding-x-sm)]',
1035
- );
1036
- expect(directive.classes()).toContain(
1037
- 'py-[var(--luma-button-padding-y-sm)]',
1038
- );
1039
- expect(directive.classes()).toContain('text-sm');
1040
- expect(directive.classes()).toContain('lm-rounded-button');
1041
- });
1042
-
1043
- it('should apply medium size classes', () => {
1044
- hostComponent.lmSize = 'md';
1045
- fixture.detectChanges();
1046
-
1047
- expect(directive.classes()).toContain(
1048
- 'px-[var(--luma-button-padding-x-md)]',
1049
- );
1050
- expect(directive.classes()).toContain(
1051
- 'py-[var(--luma-button-padding-y-md)]',
1052
- );
1053
- expect(directive.classes()).toContain('lm-text-size-base');
1054
- expect(directive.classes()).toContain('lm-rounded-button');
1055
- });
1056
-
1057
- it('should apply large size classes', () => {
1058
- hostComponent.lmSize = 'lg';
1059
- fixture.detectChanges();
1060
-
1061
- expect(directive.classes()).toContain(
1062
- 'px-[var(--luma-button-padding-x-lg)]',
1063
- );
1064
- expect(directive.classes()).toContain(
1065
- 'py-[var(--luma-button-padding-y-lg)]',
1066
- );
1067
- expect(directive.classes()).toContain('text-lg');
1068
- expect(directive.classes()).toContain('lm-rounded-button');
1069
- });
1070
-
1071
- it('should apply full size classes', () => {
1072
- hostComponent.lmSize = 'full';
1073
- fixture.detectChanges();
1074
-
1075
- expect(directive.classes()).toContain('w-full');
1076
- });
1077
-
1078
- it('should apply compound variant for full size with primary', () => {
1079
- hostComponent.lmSize = 'full';
1080
- hostComponent.lmVariant = 'primary';
1081
- fixture.detectChanges();
1082
-
1083
- expect(directive.classes()).toContain(
1084
- 'px-[var(--luma-button-padding-x-md)]',
1085
- );
1086
- expect(directive.classes()).toContain(
1087
- 'py-[var(--luma-button-padding-y-md)]',
1088
- );
1089
- });
1090
- });
1091
- });
1092
-
1093
- // ============================================================
1094
- // DISABLED STATE TESTS
1095
- // ============================================================
1096
-
1097
- describe('Disabled State', () => {
1098
- describe('when disabled', () => {
1099
- beforeEach(() => {
1100
- hostComponent.lmDisabled = true;
1101
- fixture.detectChanges();
1102
- });
1103
-
1104
- it('should apply disabled opacity class', () => {
1105
- expect(directive.classes()).toContain('disabled:opacity-50');
1106
- });
1107
-
1108
- it('should apply disabled cursor class', () => {
1109
- expect(directive.classes()).toContain('disabled:cursor-not-allowed');
1110
- });
1111
-
1112
- it('should set disabled attribute on element', () => {
1113
- expect(buttonElement.nativeElement.hasAttribute('disabled')).toBe(true);
1114
- });
1115
-
1116
- it('should reflect disabled input signal', () => {
1117
- expect(directive.lmDisabled()).toBe(true);
1118
- });
1119
- });
1120
-
1121
- describe('when enabled', () => {
1122
- beforeEach(() => {
1123
- hostComponent.lmDisabled = false;
1124
- fixture.detectChanges();
1125
- });
1126
-
1127
- it('should not have disabled attribute', () => {
1128
- expect(buttonElement.nativeElement.hasAttribute('disabled')).toBe(
1129
- false,
1130
- );
1131
- });
1132
-
1133
- it('should reflect enabled state in signal', () => {
1134
- expect(directive.lmDisabled()).toBe(false);
1135
- });
1136
- });
1137
- });
1138
-
1139
- // ============================================================
1140
- // ACCESSIBILITY TESTS
1141
- // ============================================================
1142
-
1143
- describe('Accessibility', () => {
1144
- it('should have button element by default', () => {
1145
- fixture.detectChanges();
1146
- expect(buttonElement.nativeElement.tagName).toBe('BUTTON');
1147
- });
1148
-
1149
- it('should apply focus-visible ring for keyboard navigation', () => {
1150
- const classes = directive.classes();
1151
- expect(classes).toContain('focus-visible:lm-ring-button-focus');
1152
- });
1153
-
1154
- it('should set type attribute correctly', () => {
1155
- fixture.detectChanges();
1156
- expect(buttonElement.nativeElement.getAttribute('type')).toBe('button');
1157
- });
1158
-
1159
- it('should allow submit type', () => {
1160
- // Use dedicated test host to avoid ExpressionChangedAfterItHasBeenCheckedError
1161
- const submitFixture = TestBed.createComponent(
1162
- SubmitButtonTestHostComponent,
1163
- );
1164
- submitFixture.detectChanges();
1165
- const submitButton = submitFixture.debugElement.query(
1166
- By.directive(ButtonDirective),
1167
- );
1168
- expect(submitButton.nativeElement.getAttribute('type')).toBe('submit');
1169
- });
1170
-
1171
- it('should allow reset type', () => {
1172
- // Use dedicated test host to avoid ExpressionChangedAfterItHasBeenCheckedError
1173
- const resetFixture = TestBed.createComponent(
1174
- ResetButtonTestHostComponent,
1175
- );
1176
- resetFixture.detectChanges();
1177
- const resetButton = resetFixture.debugElement.query(
1178
- By.directive(ButtonDirective),
1179
- );
1180
- expect(resetButton.nativeElement.getAttribute('type')).toBe('reset');
1181
- });
1182
-
1183
- it('should propagate disabled state to DOM', () => {
1184
- hostComponent.lmDisabled = true;
1185
- fixture.detectChanges();
1186
- expect(buttonElement.nativeElement.hasAttribute('disabled')).toBe(true);
1187
- });
1188
- });
1189
-
1190
- // ============================================================
1191
- // INPUT REACTIVITY TESTS
1192
- // ============================================================
1193
-
1194
- describe('Input Reactivity', () => {
1195
- it('should apply primary variant classes', () => {
1196
- hostComponent.lmVariant = 'primary';
1197
- fixture.detectChanges();
1198
- expect(directive.classes()).toContain('lm-bg-button-primary');
1199
- });
1200
-
1201
- it('should apply outline variant classes', () => {
1202
- hostComponent.lmVariant = 'outline';
1203
- fixture.detectChanges();
1204
- expect(directive.classes()).toContain('bg-transparent');
1205
- expect(directive.classes()).toContain('lm-border-button-outline-border');
1206
- });
1207
-
1208
- it('should apply sm size classes', () => {
1209
- hostComponent.lmSize = 'sm';
1210
- fixture.detectChanges();
1211
- expect(directive.classes()).toContain(
1212
- 'px-[var(--luma-button-padding-x-sm)]',
1213
- );
1214
- });
1215
-
1216
- it('should apply lg size classes', () => {
1217
- hostComponent.lmSize = 'lg';
1218
- fixture.detectChanges();
1219
- expect(directive.classes()).toContain(
1220
- 'px-[var(--luma-button-padding-x-lg)]',
1221
- );
1222
- });
1223
- });
1224
-
1225
- // ============================================================
1226
- // DARK THEME TESTS
1227
- // ============================================================
1228
-
1229
- describe('Dark Theme', () => {
1230
- beforeEach(() => {
1231
- applyDarkTheme();
1232
- });
1233
-
1234
- it('should have access to dark theme primary background', () => {
1235
- hostComponent.lmVariant = 'primary';
1236
- fixture.detectChanges();
1237
-
1238
- const value = getComputedStyle(document.documentElement)
1239
- .getPropertyValue('--luma-button-primary-bg')
1240
- .trim();
1241
- expect(value).toBe(DARK_TOKENS.primary.bg);
1242
- });
1243
-
1244
- it('should have access to dark theme primary text', () => {
1245
- hostComponent.lmVariant = 'primary';
1246
- fixture.detectChanges();
1247
-
1248
- const value = getComputedStyle(document.documentElement)
1249
- .getPropertyValue('--luma-button-primary-text')
1250
- .trim();
1251
- expect(value).toBe(DARK_TOKENS.primary.text);
1252
- });
1253
-
1254
- it('should have access to dark theme outline border', () => {
1255
- hostComponent.lmVariant = 'outline';
1256
- fixture.detectChanges();
1257
-
1258
- const value = getComputedStyle(document.documentElement)
1259
- .getPropertyValue('--luma-button-outline-border')
1260
- .trim();
1261
- expect(value).toBe(DARK_TOKENS.outline.border);
1262
- });
1263
-
1264
- it('should have access to dark theme danger background', () => {
1265
- hostComponent.lmVariant = 'danger';
1266
- fixture.detectChanges();
1267
-
1268
- const value = getComputedStyle(document.documentElement)
1269
- .getPropertyValue('--luma-button-danger-bg')
1270
- .trim();
1271
- expect(value).toBe(DARK_TOKENS.danger.bg);
1272
- });
1273
-
1274
- it('should have access to dark theme focus ring color', () => {
1275
- fixture.detectChanges();
1276
-
1277
- const value = getComputedStyle(document.documentElement)
1278
- .getPropertyValue('--luma-button-focus-ring-color')
1279
- .trim();
1280
- expect(value).toBe(DARK_TOKENS.focus.ringColor);
1281
- });
1282
-
1283
- it('should have dark class on document element', () => {
1284
- expect(document.documentElement.classList.contains('dark')).toBe(true);
1285
- });
1286
- });
1287
- });
1288
-
1289
- // ============================================================
1290
- // ANCHOR ELEMENT TESTS
1291
- // ============================================================
1292
-
1293
- describe('ButtonDirective on Anchor Element', () => {
1294
- let fixture: ComponentFixture<AnchorButtonTestHostComponent>;
1295
- let anchorElement: DebugElement;
1296
- let directive: ButtonDirective;
1297
-
1298
- beforeEach(async () => {
1299
- await TestBed.configureTestingModule({
1300
- imports: [ButtonDirective, AnchorButtonTestHostComponent],
1301
- }).compileComponents();
1302
-
1303
- fixture = TestBed.createComponent(AnchorButtonTestHostComponent);
1304
- anchorElement = fixture.debugElement.query(By.directive(ButtonDirective));
1305
- directive = anchorElement.injector.get(ButtonDirective);
1306
-
1307
- setupButtonTokens();
1308
- await fixture.whenStable();
1309
- });
1310
-
1311
- afterEach(() => {
1312
- cleanupButtonTokens();
1313
- });
1314
-
1315
- it('should create directive on anchor element', () => {
1316
- expect(directive).toBeTruthy();
1317
- expect(anchorElement.nativeElement.tagName).toBe('A');
1318
- });
1319
-
1320
- it('should apply same base classes as button element', () => {
1321
- fixture.detectChanges();
1322
-
1323
- const classes = directive.classes();
1324
- expect(classes).toContain('inline-flex');
1325
- expect(classes).toContain('items-center');
1326
- expect(classes).toContain('justify-center');
1327
- });
1328
-
1329
- it('should apply variant classes on anchor element', () => {
1330
- fixture.detectChanges();
1331
-
1332
- const classes = directive.classes();
1333
- expect(classes).toContain('lm-bg-button-primary');
1334
- expect(classes).toContain('lm-text-button-primary-text');
1335
- });
1336
-
1337
- it('should preserve href attribute', () => {
1338
- fixture.detectChanges();
1339
- expect(anchorElement.nativeElement.getAttribute('href')).toBe('/test');
1340
- });
1341
-
1342
- it('should have access to tokens on anchor element', () => {
1343
- fixture.detectChanges();
1344
-
1345
- const value = getComputedStyle(document.documentElement)
1346
- .getPropertyValue('--luma-button-primary-bg')
1347
- .trim();
1348
- expect(value).toBe(BUTTON_TOKENS.primary.bg);
1349
- });
1350
- });