@blotch/dolan 22.2.3 → 22.2.4
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.
- package/dist/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +36 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +187 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +280 -7
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +107 -1
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.js +126 -45
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +314 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +48 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +45 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +272 -187
- package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +206 -139
- package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +431 -62
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +8 -1
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +56 -43
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +29 -23
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +36 -0
- package/dist/ui/components/icon/icon-theme.ts +36 -13
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +14 -0
- package/dist/utils/analyze.js +15 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +12 -0
- package/dist/utils/installed.js +16 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +17 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -1,111 +1,203 @@
|
|
|
1
|
-
import { signal } from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
7
|
+
import {
|
|
8
|
+
type MockInstance,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
it,
|
|
14
|
+
vi,
|
|
15
|
+
} from 'vitest';
|
|
16
|
+
import {
|
|
17
|
+
type LynxMainThread,
|
|
18
|
+
lynxMainThread,
|
|
19
|
+
} from '../../../test-utils/lynx-main-thread';
|
|
5
20
|
import { UiInput } from './input';
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
@Component({
|
|
23
|
+
imports: [UiInput],
|
|
24
|
+
template: `
|
|
25
|
+
<ui-input
|
|
26
|
+
[(value)]="email"
|
|
27
|
+
[label]="label()"
|
|
28
|
+
placeholder="you@example.com"
|
|
29
|
+
type="email"
|
|
30
|
+
[helperText]="helperText()"
|
|
31
|
+
[error]="error()"
|
|
32
|
+
[disabled]="disabled()"
|
|
33
|
+
class="w-full"
|
|
34
|
+
(changed)="changes.push($event)"
|
|
35
|
+
(focused)="events.push('focused')"
|
|
36
|
+
(blurred)="events.push('blurred')"
|
|
37
|
+
/>
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
class InputHost {
|
|
41
|
+
readonly email = signal('');
|
|
42
|
+
readonly label = signal('');
|
|
43
|
+
readonly helperText = signal('');
|
|
44
|
+
readonly error = signal('');
|
|
45
|
+
readonly disabled = signal(false);
|
|
46
|
+
readonly changes: string[] = [];
|
|
47
|
+
readonly events: string[] = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const renderInput = async () => {
|
|
51
|
+
const result = await render(InputHost);
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
host: result.componentRef.instance as InputHost,
|
|
55
|
+
field: () => result.container.querySelector('input')!,
|
|
56
|
+
// The field's own column; the host element renders as a view too.
|
|
57
|
+
column: () => result.container.querySelector('.flex-col')!,
|
|
58
|
+
wrapper: () => result.container.querySelector('.bg-muted')!,
|
|
59
|
+
// The layer that draws the focus ring around the field.
|
|
60
|
+
ring: () => result.container.querySelector('.absolute') as HTMLElement,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** `__ElementAnimate`'s first argument for starting an animation. */
|
|
65
|
+
const START = 0;
|
|
66
|
+
/** `__ElementAnimate`'s first argument for canceling an animation. */
|
|
67
|
+
const CANCEL = 3;
|
|
12
68
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const emitted: string[] = [];
|
|
19
|
-
inst.changed.subscribe((v) => emitted.push(v));
|
|
20
|
-
inst.onInput({ detail: { value: 'hello' } });
|
|
21
|
-
expect(inst.value()).toBe('hello');
|
|
22
|
-
expect(emitted).toEqual(['hello']);
|
|
69
|
+
describe('UiInput', () => {
|
|
70
|
+
// Watched per test: the testing library resets the main thread after each.
|
|
71
|
+
let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
|
|
23
74
|
});
|
|
75
|
+
afterEach(() => animate.mockRestore());
|
|
24
76
|
|
|
25
|
-
it('
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const emitted = vi.fn();
|
|
32
|
-
inst.changed.subscribe(emitted);
|
|
33
|
-
inst.onInput({ detail: { value: 'blocked' } });
|
|
34
|
-
expect(inst.value()).toBe('');
|
|
35
|
-
expect(emitted).not.toHaveBeenCalled();
|
|
77
|
+
it('renders a native input with its placeholder and type, and no label', async () => {
|
|
78
|
+
const { field, column, queryByText } = await renderInput();
|
|
79
|
+
expect(field().getAttribute('placeholder')).toBe('you@example.com');
|
|
80
|
+
expect(field().getAttribute('type')).toBe('email');
|
|
81
|
+
expect(column().classList).toContain('w-full');
|
|
82
|
+
expect(queryByText('you@example.com')).toBeNull();
|
|
36
83
|
});
|
|
37
84
|
|
|
38
|
-
it('
|
|
39
|
-
const {
|
|
40
|
-
|
|
41
|
-
onFocus: () => void;
|
|
42
|
-
};
|
|
43
|
-
const emitted = vi.fn();
|
|
44
|
-
inst.focused.subscribe(emitted);
|
|
45
|
-
inst.onFocus();
|
|
85
|
+
it('shows its label', async () => {
|
|
86
|
+
const { host, getByText } = await renderInput();
|
|
87
|
+
host.label.set('Email');
|
|
46
88
|
await waitForUpdate();
|
|
47
|
-
expect(
|
|
48
|
-
// Focus turns the overlay opacity to 1 via ringVisible().
|
|
49
|
-
const overlay = container.querySelector('[style*="box-shadow"]');
|
|
50
|
-
expect(overlay?.getAttribute('style')).toContain('opacity');
|
|
89
|
+
expect(getByText('Email').classList).toContain('font-medium');
|
|
51
90
|
});
|
|
52
91
|
|
|
53
|
-
it('
|
|
54
|
-
const {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
inst.blurred.subscribe(emitted);
|
|
61
|
-
inst.onFocus();
|
|
62
|
-
inst.onBlur();
|
|
63
|
-
expect(emitted).toHaveBeenCalledTimes(1);
|
|
92
|
+
it('updates `value` and emits `changed` as the user types', async () => {
|
|
93
|
+
const { host, field } = await renderInput();
|
|
94
|
+
// Lynx puts the typed text in the event's detail, not on its target.
|
|
95
|
+
fireEvent.input(field(), { detail: { value: 'jane@' } });
|
|
96
|
+
fireEvent.input(field(), { detail: { value: 'jane@example.com' } });
|
|
97
|
+
expect(host.email()).toBe('jane@example.com');
|
|
98
|
+
expect(host.changes).toEqual(['jane@', 'jane@example.com']);
|
|
64
99
|
});
|
|
65
100
|
|
|
66
|
-
it(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// JIT + jsdom doesn't populate template-ref viewChild queries, so we
|
|
70
|
-
// monkey-patch containerRef with a signal returning a fake ElementRef so the
|
|
71
|
-
// effect's `el` check passes and the shake path runs.
|
|
72
|
-
const { componentRef, container } = await render(UiInput);
|
|
73
|
-
const inst = componentRef.instance as UiInput;
|
|
74
|
-
const view = container.querySelector('view') as Element;
|
|
75
|
-
(inst as unknown as { containerRef: unknown }).containerRef = signal({
|
|
76
|
-
nativeElement: view,
|
|
77
|
-
});
|
|
78
|
-
setInputSignal(inst.error, 'Required');
|
|
79
|
-
await waitForUpdate();
|
|
101
|
+
it("dims and ignores typing while it's disabled", async () => {
|
|
102
|
+
const { host, field, wrapper } = await renderInput();
|
|
103
|
+
host.disabled.set(true);
|
|
80
104
|
await waitForUpdate();
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
expect(wrapper().classList).toContain('opacity-50');
|
|
106
|
+
|
|
107
|
+
fireEvent.input(field(), { detail: { value: 'blocked' } });
|
|
108
|
+
expect(host.email()).toBe('');
|
|
109
|
+
expect(host.changes).toEqual([]);
|
|
110
|
+
});
|
|
83
111
|
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
112
|
+
it('disables the native input only while disabled', async () => {
|
|
113
|
+
// Lynx treats any `disabled` value, even false, as disabled, so the
|
|
114
|
+
// attribute has to go rather than be set to false.
|
|
115
|
+
const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
|
|
116
|
+
const { host, field } = await renderInput();
|
|
117
|
+
expect(setAttribute).not.toHaveBeenCalledWith(
|
|
118
|
+
field(),
|
|
119
|
+
'disabled',
|
|
120
|
+
expect.anything(),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
host.disabled.set(true);
|
|
87
124
|
await waitForUpdate();
|
|
125
|
+
expect(setAttribute).toHaveBeenLastCalledWith(field(), 'disabled', 'true');
|
|
126
|
+
setAttribute.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('shows a focus ring while focused, and emits `focused` and `blurred`', async () => {
|
|
130
|
+
const { host, field, ring } = await renderInput();
|
|
131
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
132
|
+
|
|
133
|
+
fireEvent.focus(field());
|
|
88
134
|
await waitForUpdate();
|
|
89
|
-
|
|
135
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
136
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
137
|
+
'var(--ring)',
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
fireEvent.blur(field());
|
|
90
141
|
await waitForUpdate();
|
|
142
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('0');
|
|
143
|
+
expect(host.events).toEqual(['focused', 'blurred']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('shows its helper text', async () => {
|
|
147
|
+
const { host, getByText } = await renderInput();
|
|
148
|
+
host.helperText.set("We'll never share it.");
|
|
91
149
|
await waitForUpdate();
|
|
92
|
-
expect(
|
|
150
|
+
expect(getByText("We'll never share it.").classList).toContain(
|
|
151
|
+
'text-muted-foreground',
|
|
152
|
+
);
|
|
93
153
|
});
|
|
94
154
|
|
|
95
|
-
it('
|
|
96
|
-
const {
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
it('shows an error in place of the helper text, with a destructive ring', async () => {
|
|
156
|
+
const { host, ring, getByText, queryByText } = await renderInput();
|
|
157
|
+
host.helperText.set("We'll never share it.");
|
|
158
|
+
host.error.set('Enter a valid email');
|
|
99
159
|
await waitForUpdate();
|
|
100
|
-
expect(
|
|
160
|
+
expect(getByText('Enter a valid email').classList).toContain(
|
|
161
|
+
'text-destructive',
|
|
162
|
+
);
|
|
163
|
+
expect(queryByText("We'll never share it.")).toBeNull();
|
|
164
|
+
// An invalid field keeps its ring even when it isn't focused.
|
|
165
|
+
expect(ring().style.getPropertyValue('opacity')).toBe('1');
|
|
166
|
+
expect(ring().style.getPropertyValue('box-shadow')).toContain(
|
|
167
|
+
'var(--destructive)',
|
|
168
|
+
);
|
|
101
169
|
});
|
|
102
170
|
|
|
103
|
-
it('
|
|
104
|
-
const {
|
|
105
|
-
const
|
|
106
|
-
|
|
171
|
+
it('shakes when an error appears, but not when it only changes', async () => {
|
|
172
|
+
const { host, column } = await renderInput();
|
|
173
|
+
const shakes = () =>
|
|
174
|
+
animate.mock.calls.filter(([, [action]]) => action === START);
|
|
175
|
+
expect(shakes()).toEqual([]);
|
|
176
|
+
|
|
177
|
+
host.error.set('Required');
|
|
178
|
+
await waitForUpdate();
|
|
179
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
180
|
+
START,
|
|
181
|
+
expect.any(String),
|
|
182
|
+
expect.arrayContaining([{ transform: 'translateX(-4px)' }]),
|
|
183
|
+
expect.anything(),
|
|
184
|
+
]);
|
|
185
|
+
expect(shakes()).toHaveLength(1);
|
|
186
|
+
|
|
187
|
+
host.error.set('Enter a valid email');
|
|
107
188
|
await waitForUpdate();
|
|
108
|
-
expect(
|
|
189
|
+
expect(shakes()).toHaveLength(1);
|
|
190
|
+
|
|
191
|
+
// Once fixed, the next error shakes again, replacing the last shake.
|
|
192
|
+
host.error.set('');
|
|
193
|
+
await waitForUpdate();
|
|
194
|
+
host.error.set('Required');
|
|
195
|
+
await waitForUpdate();
|
|
196
|
+
expect(shakes()).toHaveLength(2);
|
|
197
|
+
expect(animate).toHaveBeenCalledWith(column(), [
|
|
198
|
+
CANCEL,
|
|
199
|
+
expect.any(String),
|
|
200
|
+
]);
|
|
109
201
|
});
|
|
110
202
|
|
|
111
203
|
it('makes the wrapper a stacking context so the field scrolls with it', async () => {
|
|
@@ -113,22 +205,7 @@ describe('UiInput', () => {
|
|
|
113
205
|
// when an ancestor inside the scroll content is a stacking context. Without
|
|
114
206
|
// z-0 on the wrapper, the native field (and its placeholder) stays fixed on
|
|
115
207
|
// screen while the page scrolls.
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
119
|
-
);
|
|
120
|
-
expect(wrapper?.getAttribute('class')?.split(/\s+/)).toContain('z-0');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('applies the disabled dim class when disabled', async () => {
|
|
124
|
-
// Covers the true branch of `disabled() && 'opacity-50'` in inputWrapperClass.
|
|
125
|
-
const { componentRef, container } = await render(UiInput);
|
|
126
|
-
const inst = componentRef.instance as UiInput;
|
|
127
|
-
setInputSignal(inst.disabled, true);
|
|
128
|
-
await waitForUpdate();
|
|
129
|
-
const wrapper = container.querySelector(
|
|
130
|
-
'[class*="rounded-xl"][class*="bg-muted"]',
|
|
131
|
-
);
|
|
132
|
-
expect(wrapper?.getAttribute('class')).toContain('opacity-50');
|
|
208
|
+
const { wrapper } = await renderInput();
|
|
209
|
+
expect(wrapper().classList).toContain('z-0');
|
|
133
210
|
});
|
|
134
211
|
});
|
|
@@ -102,18 +102,21 @@ export class UiInput implements FormValueControl<string> {
|
|
|
102
102
|
#shakeAnim?: AnimationHandle;
|
|
103
103
|
#previousError = '';
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Shakes the field when an error appears (a change from no error to one). A
|
|
107
|
+
* field rather than a constructor: Angular's test build keeps a function for
|
|
108
|
+
* a constructor that nothing calls, which an app's coverage report would show
|
|
109
|
+
* as uncovered.
|
|
110
|
+
*/
|
|
111
|
+
protected readonly shakeOnError = effect(() => {
|
|
112
|
+
const err = this.error();
|
|
113
|
+
const el = this.containerRef()?.nativeElement;
|
|
114
|
+
if (err && !this.#previousError && el) {
|
|
115
|
+
this.#shakeAnim?.cancel();
|
|
116
|
+
this.#shakeAnim = shake(el);
|
|
117
|
+
}
|
|
118
|
+
this.#previousError = err;
|
|
119
|
+
});
|
|
117
120
|
|
|
118
121
|
protected readonly containerClass = computed(() =>
|
|
119
122
|
cn('flex-col gap-1.5 flex', this.userClass()),
|
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
2
|
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
|
|
3
|
-
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
4
3
|
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { setInputSignal } from '../../../test-utils/set-input-signal';
|
|
6
4
|
import { UiLabel } from './label';
|
|
7
5
|
|
|
8
6
|
@Component({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
template: `<ui-label>{{ label }}</ui-label>`,
|
|
7
|
+
imports: [UiLabel],
|
|
8
|
+
template: `<ui-label [disabled]="disabled()">Name</ui-label>`,
|
|
12
9
|
})
|
|
13
10
|
class LabelHost {
|
|
14
|
-
|
|
11
|
+
readonly disabled = signal(false);
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
describe('UiLabel', () => {
|
|
18
|
-
it('renders
|
|
19
|
-
const { getByText } = await render(LabelHost);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
it('renders standalone with no host wrapper (default disabled=false)', async () => {
|
|
24
|
-
const { container } = await render(UiLabel);
|
|
25
|
-
const textEl = container.querySelector('text');
|
|
26
|
-
expect(textEl?.getAttribute('class')).toContain('font-medium');
|
|
27
|
-
});
|
|
15
|
+
it('renders its text, dimmed while disabled', async () => {
|
|
16
|
+
const { componentRef, getByText } = await render(LabelHost);
|
|
17
|
+
const label = getByText('Name');
|
|
18
|
+
expect(label.classList).toContain('font-medium');
|
|
19
|
+
expect(label.classList).not.toContain('opacity-50');
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
// JIT template bindings don't wire signal inputs — drive the input
|
|
31
|
-
// directly to exercise the `disabled() && 'opacity-50'` truthy branch.
|
|
32
|
-
const { componentRef, container } = await render(UiLabel);
|
|
33
|
-
setInputSignal((componentRef.instance as UiLabel).disabled, true);
|
|
21
|
+
(componentRef.instance as LabelHost).disabled.set(true);
|
|
34
22
|
await waitForUpdate();
|
|
35
|
-
|
|
36
|
-
expect(textEl?.getAttribute('class')).toContain('opacity-50');
|
|
23
|
+
expect(label.classList).toContain('opacity-50');
|
|
37
24
|
});
|
|
38
25
|
});
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
1
7
|
import { createChartRuntime } from '@tanstack/charts/runtime';
|
|
2
8
|
import type { ChartDefinition, SceneNode } from '@tanstack/charts/types';
|
|
3
9
|
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import {
|
|
10
|
+
import type { XYDatum } from '../chart/chart-presets';
|
|
11
|
+
import { UiLineChart, lineChartDefinition } from './line-chart';
|
|
5
12
|
|
|
6
13
|
const data = [
|
|
7
14
|
{ x: 0, y: 10 },
|
|
@@ -75,3 +82,71 @@ describe('lineChartDefinition', () => {
|
|
|
75
82
|
expect(lineChartDefinition({ data, tooltip: false }).tooltip).toBe(false);
|
|
76
83
|
});
|
|
77
84
|
});
|
|
85
|
+
|
|
86
|
+
@Component({
|
|
87
|
+
imports: [UiLineChart],
|
|
88
|
+
template: `
|
|
89
|
+
<ui-line-chart
|
|
90
|
+
[data]="data"
|
|
91
|
+
[width]="300"
|
|
92
|
+
[showDots]="showDots()"
|
|
93
|
+
color="red"
|
|
94
|
+
yAxisLabel="Revenue"
|
|
95
|
+
(pointTap)="tapped.push($event)"
|
|
96
|
+
/>
|
|
97
|
+
`,
|
|
98
|
+
})
|
|
99
|
+
class LineChartHost {
|
|
100
|
+
readonly data = data;
|
|
101
|
+
readonly showDots = signal(true);
|
|
102
|
+
/** Every `pointTap`, in order. */
|
|
103
|
+
readonly tapped: XYDatum[] = [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The SVG the chart's marks layer draws.
|
|
108
|
+
*/
|
|
109
|
+
const marksSvg = (container: Element) =>
|
|
110
|
+
container.querySelectorAll('svg')[1]!.getAttribute('content')!;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Where the chart drew each dot.
|
|
114
|
+
*/
|
|
115
|
+
const dotsDrawn = (container: Element) =>
|
|
116
|
+
[
|
|
117
|
+
...marksSvg(container).matchAll(/<circle cx="([\d.-]+)" cy="([\d.-]+)"/g),
|
|
118
|
+
].map(([, x, y]) => ({ x: Number(x), y: Number(y) }));
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Touches a spot of the chart and lifts the finger right there: a tap.
|
|
122
|
+
*/
|
|
123
|
+
const tap = async (container: Element, at: { x: number; y: number }) => {
|
|
124
|
+
const root = container.querySelector('[accessibility-label]')!;
|
|
125
|
+
fireEvent.touchstart(root, { touches: [at] });
|
|
126
|
+
fireEvent.touchend(root, { touches: [at] });
|
|
127
|
+
await waitForUpdate();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
describe('UiLineChart', () => {
|
|
131
|
+
it('draws its data with its inputs', async () => {
|
|
132
|
+
const { componentRef, container, getByText } = await render(LineChartHost);
|
|
133
|
+
expect(marksSvg(container)).toContain('stroke="red"');
|
|
134
|
+
expect(dotsDrawn(container)).toHaveLength(3);
|
|
135
|
+
expect(getByText('Revenue')).toBeTruthy();
|
|
136
|
+
|
|
137
|
+
(componentRef.instance as LineChartHost).showDots.set(false);
|
|
138
|
+
await waitForUpdate();
|
|
139
|
+
expect(dotsDrawn(container)).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("emits a tapped point's datum, and nothing for a tap off the points", async () => {
|
|
143
|
+
const { componentRef, container } = await render(LineChartHost);
|
|
144
|
+
const host = componentRef.instance as LineChartHost;
|
|
145
|
+
const middle = dotsDrawn(container)[1]!;
|
|
146
|
+
await tap(container, { x: middle.x + 1, y: middle.y + 1 });
|
|
147
|
+
expect(host.tapped).toEqual([data[1]]);
|
|
148
|
+
|
|
149
|
+
await tap(container, { x: -100, y: -100 });
|
|
150
|
+
expect(host.tapped).toEqual([data[1]]);
|
|
151
|
+
});
|
|
152
|
+
});
|