@sonny-ui/core 0.1.0-alpha.15 → 0.1.0-alpha.17

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.
Files changed (34) hide show
  1. package/README.md +109 -55
  2. package/fesm2022/sonny-ui-core.mjs +1987 -4
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +1 -1
  5. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  6. package/src/lib/avatar-group/avatar-group.component.ts +89 -0
  7. package/src/lib/avatar-group/index.ts +1 -0
  8. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  9. package/src/lib/color-picker/color-picker.component.ts +537 -0
  10. package/src/lib/color-picker/color-picker.types.ts +24 -0
  11. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  12. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  13. package/src/lib/color-picker/index.ts +20 -0
  14. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  15. package/src/lib/command-palette/command-palette.component.ts +195 -0
  16. package/src/lib/command-palette/command-palette.service.ts +36 -0
  17. package/src/lib/command-palette/command-palette.types.ts +23 -0
  18. package/src/lib/command-palette/index.ts +7 -0
  19. package/src/lib/number-input/index.ts +2 -0
  20. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  21. package/src/lib/number-input/number-input.component.ts +153 -0
  22. package/src/lib/number-input/number-input.variants.ts +17 -0
  23. package/src/lib/otp-input/index.ts +2 -0
  24. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  25. package/src/lib/otp-input/otp-input.component.ts +275 -0
  26. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  27. package/src/lib/popover/index.ts +6 -0
  28. package/src/lib/popover/popover.directives.spec.ts +147 -0
  29. package/src/lib/popover/popover.directives.ts +155 -0
  30. package/src/lib/tag-input/index.ts +2 -0
  31. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  32. package/src/lib/tag-input/tag-input.component.ts +173 -0
  33. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  34. package/types/sonny-ui-core.d.ts +351 -3
@@ -0,0 +1,151 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
4
+ import { SnyNumberInputComponent } from './number-input.component';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyNumberInputComponent],
9
+ template: `
10
+ <sny-number-input
11
+ [(value)]="num"
12
+ [min]="min()"
13
+ [max]="max()"
14
+ [step]="step()"
15
+ [disabled]="disabled()"
16
+ />
17
+ `,
18
+ })
19
+ class TestHost {
20
+ num = signal(5);
21
+ min = signal<number | null>(null);
22
+ max = signal<number | null>(null);
23
+ step = signal(1);
24
+ disabled = signal(false);
25
+ }
26
+
27
+ describe('SnyNumberInputComponent', () => {
28
+ let fixture: ComponentFixture<TestHost>;
29
+ let el: HTMLElement;
30
+
31
+ beforeEach(async () => {
32
+ await TestBed.configureTestingModule({ imports: [TestHost] }).compileComponents();
33
+ fixture = TestBed.createComponent(TestHost);
34
+ fixture.detectChanges();
35
+ el = fixture.nativeElement;
36
+ });
37
+
38
+ function getInput(): HTMLInputElement {
39
+ return el.querySelector('input') as HTMLInputElement;
40
+ }
41
+ function getButtons(): HTMLButtonElement[] {
42
+ return Array.from(el.querySelectorAll('button'));
43
+ }
44
+
45
+ it('should render with initial value', () => {
46
+ expect(getInput().value).toBe('5');
47
+ });
48
+
49
+ it('should increment on + click', () => {
50
+ getButtons()[1].click();
51
+ fixture.detectChanges();
52
+ expect(fixture.componentInstance.num()).toBe(6);
53
+ });
54
+
55
+ it('should decrement on - click', () => {
56
+ getButtons()[0].click();
57
+ fixture.detectChanges();
58
+ expect(fixture.componentInstance.num()).toBe(4);
59
+ });
60
+
61
+ it('should respect min', () => {
62
+ fixture.componentInstance.min.set(5);
63
+ fixture.detectChanges();
64
+ getButtons()[0].click();
65
+ fixture.detectChanges();
66
+ expect(fixture.componentInstance.num()).toBe(5);
67
+ });
68
+
69
+ it('should respect max', () => {
70
+ fixture.componentInstance.max.set(5);
71
+ fixture.detectChanges();
72
+ getButtons()[1].click();
73
+ fixture.detectChanges();
74
+ expect(fixture.componentInstance.num()).toBe(5);
75
+ });
76
+
77
+ it('should use step', () => {
78
+ fixture.componentInstance.step.set(10);
79
+ fixture.detectChanges();
80
+ getButtons()[1].click();
81
+ fixture.detectChanges();
82
+ expect(fixture.componentInstance.num()).toBe(15); // 5 + 10
83
+ });
84
+
85
+ it('should handle manual input on blur', () => {
86
+ const input = getInput();
87
+ input.value = '42';
88
+ input.dispatchEvent(new Event('input'));
89
+ input.dispatchEvent(new Event('blur'));
90
+ fixture.detectChanges();
91
+ expect(fixture.componentInstance.num()).toBe(42);
92
+ });
93
+
94
+ it('should revert invalid input on blur', () => {
95
+ const input = getInput();
96
+ input.value = 'abc';
97
+ input.dispatchEvent(new Event('input', { bubbles: true }));
98
+ input.dispatchEvent(new Event('blur', { bubbles: true }));
99
+ fixture.detectChanges();
100
+ // Value should remain unchanged at 5
101
+ expect(fixture.componentInstance.num()).toBe(5);
102
+ });
103
+
104
+ it('should handle ArrowUp/Down', () => {
105
+ const input = getInput();
106
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
107
+ fixture.detectChanges();
108
+ expect(fixture.componentInstance.num()).toBe(6);
109
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
110
+ fixture.detectChanges();
111
+ expect(fixture.componentInstance.num()).toBe(5);
112
+ });
113
+
114
+ it('should disable when disabled', () => {
115
+ fixture.componentInstance.disabled.set(true);
116
+ fixture.detectChanges();
117
+ expect(getInput().disabled).toBe(true);
118
+ expect(getButtons().every(b => b.disabled)).toBe(true);
119
+ });
120
+ });
121
+
122
+ @Component({
123
+ standalone: true,
124
+ imports: [ReactiveFormsModule, SnyNumberInputComponent],
125
+ template: `<sny-number-input [formControl]="ctrl" />`,
126
+ })
127
+ class ReactiveHost {
128
+ ctrl = new FormControl(10);
129
+ }
130
+
131
+ describe('SnyNumberInputComponent — Reactive Forms', () => {
132
+ let fixture: ComponentFixture<ReactiveHost>;
133
+
134
+ beforeEach(async () => {
135
+ await TestBed.configureTestingModule({ imports: [ReactiveHost] }).compileComponents();
136
+ fixture = TestBed.createComponent(ReactiveHost);
137
+ fixture.detectChanges();
138
+ });
139
+
140
+ it('should display FormControl value', () => {
141
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
142
+ expect(input.value).toBe('10');
143
+ });
144
+
145
+ it('should update FormControl on increment', () => {
146
+ const buttons = fixture.nativeElement.querySelectorAll('button');
147
+ buttons[1].click();
148
+ fixture.detectChanges();
149
+ expect(fixture.componentInstance.ctrl.value).toBe(11);
150
+ });
151
+ });
@@ -0,0 +1,153 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ forwardRef,
7
+ input,
8
+ model,
9
+ signal,
10
+ untracked,
11
+ } from '@angular/core';
12
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
13
+ import { cn } from '../core/utils/cn';
14
+ import { numberInputVariants, type NumberInputSize } from './number-input.variants';
15
+
16
+ @Component({
17
+ selector: 'sny-number-input',
18
+ standalone: true,
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ providers: [
21
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyNumberInputComponent), multi: true },
22
+ ],
23
+ template: `
24
+ <div [class]="containerClass()">
25
+ <button
26
+ type="button"
27
+ class="px-2.5 hover:bg-accent transition-colors border-r border-border disabled:opacity-40 disabled:cursor-not-allowed"
28
+ [disabled]="isDisabled() || atMin()"
29
+ (click)="decrement()"
30
+ aria-label="Decrease"
31
+ >
32
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>
33
+ </button>
34
+ <input
35
+ #inputEl
36
+ type="text"
37
+ inputmode="decimal"
38
+ class="flex-1 w-14 text-center outline-none bg-transparent font-medium"
39
+ [value]="inputValue()"
40
+ [disabled]="isDisabled()"
41
+ [placeholder]="placeholder()"
42
+ [attr.aria-label]="'Number input'"
43
+ [attr.aria-valuemin]="min() ?? null"
44
+ [attr.aria-valuemax]="max() ?? null"
45
+ [attr.aria-valuenow]="value()"
46
+ role="spinbutton"
47
+ (input)="onInput($event)"
48
+ (blur)="commitValue()"
49
+ (keydown)="onKeydown($event)"
50
+ />
51
+ <button
52
+ type="button"
53
+ class="px-2.5 hover:bg-accent transition-colors border-l border-border disabled:opacity-40 disabled:cursor-not-allowed"
54
+ [disabled]="isDisabled() || atMax()"
55
+ (click)="increment()"
56
+ aria-label="Increase"
57
+ >
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
59
+ </button>
60
+ </div>
61
+ `,
62
+ })
63
+ export class SnyNumberInputComponent implements ControlValueAccessor {
64
+ readonly value = model(0);
65
+ readonly min = input<number | null>(null);
66
+ readonly max = input<number | null>(null);
67
+ readonly step = input(1);
68
+ readonly disabled = input(false);
69
+ readonly size = input<NumberInputSize>('md');
70
+ readonly placeholder = input('');
71
+ readonly class = input<string>('');
72
+
73
+ readonly inputValue = signal('0');
74
+ private readonly _disabledByCva = signal(false);
75
+ readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
76
+
77
+ readonly atMin = computed(() => this.min() !== null && this.value() <= this.min()!);
78
+ readonly atMax = computed(() => this.max() !== null && this.value() >= this.max()!);
79
+
80
+ readonly containerClass = computed(() =>
81
+ cn(numberInputVariants({ size: this.size() }), this.isDisabled() && 'opacity-50', this.class())
82
+ );
83
+
84
+ private _onChange: (value: number) => void = () => {};
85
+ private _onTouched: () => void = () => {};
86
+
87
+ constructor() {
88
+ effect(() => {
89
+ const val = this.value();
90
+ untracked(() => this.inputValue.set(String(val)));
91
+ });
92
+ }
93
+
94
+ writeValue(val: number): void {
95
+ this.value.set(val ?? 0);
96
+ }
97
+
98
+ registerOnChange(fn: (value: number) => void): void {
99
+ this._onChange = fn;
100
+ }
101
+
102
+ registerOnTouched(fn: () => void): void {
103
+ this._onTouched = fn;
104
+ }
105
+
106
+ setDisabledState(isDisabled: boolean): void {
107
+ this._disabledByCva.set(isDisabled);
108
+ }
109
+
110
+ increment(): void {
111
+ this.setValue(this.value() + this.step());
112
+ }
113
+
114
+ decrement(): void {
115
+ this.setValue(this.value() - this.step());
116
+ }
117
+
118
+ onInput(event: Event): void {
119
+ this.inputValue.set((event.target as HTMLInputElement).value);
120
+ }
121
+
122
+ commitValue(): void {
123
+ const parsed = parseFloat(this.inputValue());
124
+ if (isNaN(parsed)) {
125
+ this.inputValue.set(String(this.value()));
126
+ } else {
127
+ this.setValue(parsed);
128
+ }
129
+ this._onTouched();
130
+ }
131
+
132
+ onKeydown(event: KeyboardEvent): void {
133
+ switch (event.key) {
134
+ case 'ArrowUp':
135
+ event.preventDefault();
136
+ this.increment();
137
+ break;
138
+ case 'ArrowDown':
139
+ event.preventDefault();
140
+ this.decrement();
141
+ break;
142
+ }
143
+ }
144
+
145
+ private setValue(raw: number): void {
146
+ let clamped = raw;
147
+ if (this.min() !== null) clamped = Math.max(this.min()!, clamped);
148
+ if (this.max() !== null) clamped = Math.min(this.max()!, clamped);
149
+ const rounded = parseFloat(clamped.toFixed(10));
150
+ this.value.set(rounded);
151
+ this._onChange(rounded);
152
+ }
153
+ }
@@ -0,0 +1,17 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const numberInputVariants = cva(
4
+ 'inline-flex items-center border border-border rounded-md bg-background transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-9 text-xs',
9
+ md: 'h-10 text-sm',
10
+ lg: 'h-11 text-base',
11
+ },
12
+ },
13
+ defaultVariants: { size: 'md' },
14
+ }
15
+ );
16
+
17
+ export type NumberInputSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,2 @@
1
+ export { SnyOtpInputComponent } from './otp-input.component';
2
+ export { otpCellVariants, type OtpInputSize, type OtpInputType } from './otp-input.variants';
@@ -0,0 +1,252 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
4
+ import { SnyOtpInputComponent } from './otp-input.component';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [SnyOtpInputComponent],
9
+ template: `
10
+ <sny-otp-input
11
+ [(value)]="otp"
12
+ [length]="length()"
13
+ [type]="type()"
14
+ [disabled]="disabled()"
15
+ [mask]="mask()"
16
+ [separator]="separator()"
17
+ [status]="status()"
18
+ [autoFocus]="false"
19
+ (completed)="lastCompleted = $event"
20
+ />
21
+ `,
22
+ })
23
+ class TestHostComponent {
24
+ otp = signal('');
25
+ length = signal(6);
26
+ type = signal<'number' | 'alphanumeric'>('number');
27
+ disabled = signal(false);
28
+ mask = signal(false);
29
+ separator = signal<number | null>(null);
30
+ status = signal<'idle' | 'loading' | 'success' | 'error'>('idle');
31
+ lastCompleted: string | null = null;
32
+ }
33
+
34
+ describe('SnyOtpInputComponent', () => {
35
+ let fixture: ComponentFixture<TestHostComponent>;
36
+ let el: HTMLElement;
37
+
38
+ beforeEach(async () => {
39
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
40
+ fixture = TestBed.createComponent(TestHostComponent);
41
+ fixture.detectChanges();
42
+ el = fixture.nativeElement;
43
+ });
44
+
45
+ function getInputs(): HTMLInputElement[] {
46
+ return Array.from(el.querySelectorAll('input'));
47
+ }
48
+
49
+ function typeChar(input: HTMLInputElement, char: string): void {
50
+ input.value = char;
51
+ input.dispatchEvent(new Event('input', { bubbles: true }));
52
+ fixture.detectChanges();
53
+ }
54
+
55
+ it('should render N inputs based on length', () => {
56
+ expect(getInputs().length).toBe(6);
57
+ });
58
+
59
+ it('should render 4 inputs when length is 4', () => {
60
+ fixture.componentInstance.length.set(4);
61
+ fixture.detectChanges();
62
+ expect(getInputs().length).toBe(4);
63
+ });
64
+
65
+ it('should accept numbers when type is number', () => {
66
+ const inputs = getInputs();
67
+ typeChar(inputs[0], '5');
68
+ expect(fixture.componentInstance.otp()).toContain('5');
69
+ });
70
+
71
+ it('should reject letters when type is number', () => {
72
+ const inputs = getInputs();
73
+ typeChar(inputs[0], 'a');
74
+ expect(inputs[0].value).toBe('');
75
+ });
76
+
77
+ it('should accept letters when type is alphanumeric', () => {
78
+ fixture.componentInstance.type.set('alphanumeric');
79
+ fixture.detectChanges();
80
+ const inputs = getInputs();
81
+ typeChar(inputs[0], 'A');
82
+ expect(fixture.componentInstance.otp()).toContain('A');
83
+ });
84
+
85
+ it('should auto-focus next input after typing', () => {
86
+ const inputs = getInputs();
87
+ inputs[0].focus();
88
+ typeChar(inputs[0], '1');
89
+ expect(document.activeElement).toBe(inputs[1]);
90
+ });
91
+
92
+ it('should handle backspace - clear current and move back', () => {
93
+ const inputs = getInputs();
94
+ typeChar(inputs[0], '1');
95
+ typeChar(inputs[1], '2');
96
+
97
+ // Backspace on empty input[2] should move to input[1]
98
+ inputs[2].focus();
99
+ inputs[2].dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true }));
100
+ fixture.detectChanges();
101
+
102
+ // Should clear input[1] and focus it
103
+ expect(document.activeElement).toBe(inputs[1]);
104
+ });
105
+
106
+ it('should handle paste', () => {
107
+ const inputs = getInputs();
108
+ inputs[0].focus();
109
+
110
+ // Create a paste event compatible with test environments
111
+ const pasteEvent = new Event('paste', { bubbles: true }) as any;
112
+ pasteEvent.clipboardData = { getData: () => '123456' };
113
+ inputs[0].dispatchEvent(pasteEvent);
114
+ fixture.detectChanges();
115
+
116
+ expect(fixture.componentInstance.otp()).toBe('123456');
117
+ });
118
+
119
+ it('should emit completed when all digits filled', () => {
120
+ const inputs = getInputs();
121
+ for (let i = 0; i < 6; i++) {
122
+ typeChar(inputs[i], String(i + 1));
123
+ }
124
+ expect(fixture.componentInstance.lastCompleted).toBe('123456');
125
+ });
126
+
127
+ it('should navigate with arrow keys', () => {
128
+ const inputs = getInputs();
129
+ inputs[2].focus();
130
+
131
+ inputs[2].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
132
+ fixture.detectChanges();
133
+ expect(document.activeElement).toBe(inputs[1]);
134
+
135
+ inputs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
136
+ fixture.detectChanges();
137
+ expect(document.activeElement).toBe(inputs[2]);
138
+ });
139
+
140
+ it('should navigate with Home/End', () => {
141
+ const inputs = getInputs();
142
+ inputs[3].focus();
143
+
144
+ inputs[3].dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
145
+ fixture.detectChanges();
146
+ expect(document.activeElement).toBe(inputs[0]);
147
+
148
+ inputs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
149
+ fixture.detectChanges();
150
+ expect(document.activeElement).toBe(inputs[5]);
151
+ });
152
+
153
+ it('should render password inputs when mask is true', () => {
154
+ fixture.componentInstance.mask.set(true);
155
+ fixture.detectChanges();
156
+ const inputs = getInputs();
157
+ expect(inputs[0].type).toBe('password');
158
+ });
159
+
160
+ it('should render separator', () => {
161
+ fixture.componentInstance.separator.set(3);
162
+ fixture.detectChanges();
163
+ const separators = el.querySelectorAll('[aria-hidden="true"]');
164
+ expect(separators.length).toBe(1);
165
+ expect(separators[0].textContent).toContain('—');
166
+ });
167
+
168
+ it('should disable all inputs when disabled', () => {
169
+ fixture.componentInstance.disabled.set(true);
170
+ fixture.detectChanges();
171
+ const inputs = getInputs();
172
+ expect(inputs.every((i) => i.disabled)).toBe(true);
173
+ });
174
+
175
+ it('should have aria-label on each input', () => {
176
+ const inputs = getInputs();
177
+ expect(inputs[0].getAttribute('aria-label')).toBe('Digit 1 of 6');
178
+ expect(inputs[5].getAttribute('aria-label')).toBe('Digit 6 of 6');
179
+ });
180
+
181
+ it('should have autocomplete one-time-code', () => {
182
+ const inputs = getInputs();
183
+ expect(inputs[0].getAttribute('autocomplete')).toBe('one-time-code');
184
+ });
185
+
186
+ it('should disable inputs when status is loading', () => {
187
+ fixture.componentInstance.status.set('loading');
188
+ fixture.detectChanges();
189
+ const inputs = getInputs();
190
+ expect(inputs.every((i) => i.disabled)).toBe(true);
191
+ });
192
+
193
+ it('should apply success styles when status is success', () => {
194
+ fixture.componentInstance.status.set('success');
195
+ fixture.detectChanges();
196
+ const inputs = getInputs();
197
+ expect(inputs[0].className).toContain('border-green-500');
198
+ });
199
+
200
+ it('should apply error styles when status is error', () => {
201
+ fixture.componentInstance.status.set('error');
202
+ fixture.detectChanges();
203
+ const inputs = getInputs();
204
+ expect(inputs[0].className).toContain('border-destructive');
205
+ });
206
+ });
207
+
208
+ // --- Reactive Forms ---
209
+ @Component({
210
+ standalone: true,
211
+ imports: [ReactiveFormsModule, SnyOtpInputComponent],
212
+ template: `<sny-otp-input [formControl]="ctrl" [autoFocus]="false" />`,
213
+ })
214
+ class ReactiveFormHost {
215
+ ctrl = new FormControl('');
216
+ }
217
+
218
+ describe('SnyOtpInputComponent — Reactive Forms', () => {
219
+ let fixture: ComponentFixture<ReactiveFormHost>;
220
+ let el: HTMLElement;
221
+
222
+ beforeEach(async () => {
223
+ await TestBed.configureTestingModule({ imports: [ReactiveFormHost] }).compileComponents();
224
+ fixture = TestBed.createComponent(ReactiveFormHost);
225
+ fixture.detectChanges();
226
+ el = fixture.nativeElement;
227
+ });
228
+
229
+ it('should populate inputs when FormControl value is set', () => {
230
+ fixture.componentInstance.ctrl.setValue('123456');
231
+ fixture.detectChanges();
232
+ const inputs = Array.from(el.querySelectorAll('input')) as HTMLInputElement[];
233
+ expect(inputs[0].value).toBe('1');
234
+ expect(inputs[5].value).toBe('6');
235
+ });
236
+
237
+ it('should update FormControl when user types', () => {
238
+ const inputs = Array.from(el.querySelectorAll('input')) as HTMLInputElement[];
239
+ inputs[0].value = '9';
240
+ inputs[0].dispatchEvent(new Event('input', { bubbles: true }));
241
+ fixture.detectChanges();
242
+
243
+ expect(fixture.componentInstance.ctrl.value).toContain('9');
244
+ });
245
+
246
+ it('should disable via FormControl.disable()', () => {
247
+ fixture.componentInstance.ctrl.disable();
248
+ fixture.detectChanges();
249
+ const inputs = Array.from(el.querySelectorAll('input')) as HTMLInputElement[];
250
+ expect(inputs.every((i) => i.disabled)).toBe(true);
251
+ });
252
+ });