@ethlete/components 1.0.0-next.74 → 1.0.0-next.76

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,526 +1,435 @@
1
- import { ApplicationRef, signal } from '@angular/core';
2
- import { TestBed } from '@angular/core/testing';
3
- import { ReplaySubject, map } from 'rxjs';
4
- import { OVERLAY_REF, SelectDirective, TabBarDirective } from '@ethlete/components';
5
-
1
+ import { ApplicationRef, signal } from "@angular/core";
2
+ import { TestBed } from "@angular/core/testing";
3
+ import { ReplaySubject, map } from "rxjs";
4
+ import { OVERLAY_REF, SelectDirective, TabBarDirective } from "@ethlete/components";
6
5
  const tick = () => TestBed.inject(ApplicationRef).tick();
7
6
  const flushFrames = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
8
- const resetOverlays = () => document.querySelectorAll('.et-overlay-runtime-entry').forEach((entry) => entry.remove());
9
- /**
10
- * The newest overlay pane in the document.
11
- *
12
- * Overlays render into the document, not into the fixture, and jsdom fires no transition events -
13
- * so a pane stuck in its leave transition stays reachable. Scope every pane query through here, or
14
- * a stale pane answers it.
15
- */
16
- const latestPane = () => Array.from(document.querySelectorAll('.et-overlay-runtime-pane')).at(-1) ?? null;
7
+ const resetOverlays = () => document.querySelectorAll(".et-overlay-runtime-entry").forEach((entry) => entry.remove());
8
+ const latestPane = () => Array.from(document.querySelectorAll(".et-overlay-runtime-pane")).at(-1) ?? null;
17
9
  const hostDirective = (fixture, type) => fixture.debugElement.children[0].injector.get(type);
18
10
  const directiveAt = (fixture, type, selector) => {
19
- const node = fixture.debugElement.query((candidate) => candidate.nativeElement?.matches?.(selector) ?? false);
20
- if (!node)
21
- throw new Error(`No element in the fixture matches "${selector}"`);
22
- return node.injector.get(type);
11
+ const node = fixture.debugElement.query((candidate) => candidate.nativeElement?.matches?.(selector) ?? false);
12
+ if (!node) throw new Error(`No element in the fixture matches "${selector}"`);
13
+ return node.injector.get(type);
23
14
  };
24
15
  const hostElement = (fixture) => fixture.debugElement.children[0].nativeElement;
25
- /**
26
- * Queries the fixture's root element, narrowing `nativeElement` (typed `any`) to `HTMLElement`
27
- * once - a generic call directly through `fixture.nativeElement.querySelectorAll<E>(...)` fails
28
- * with TS2347 and collapses every downstream member access to `{}`.
29
- */
30
16
  const queryAll = (fixture, selector) => Array.from(fixture.nativeElement.querySelectorAll(selector));
31
- /** Single-result counterpart of {@link queryAll}. */
32
17
  const query = (fixture, selector) => fixture.nativeElement.querySelector(selector);
33
18
  const pressKey = (target, key, init = {}) => {
34
- const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init });
35
- target.dispatchEvent(event);
36
- tick();
37
- return event;
19
+ const event = new KeyboardEvent("keydown", {
20
+ key,
21
+ bubbles: true,
22
+ cancelable: true,
23
+ ...init
24
+ });
25
+ target.dispatchEvent(event);
26
+ tick();
27
+ return event;
38
28
  };
39
29
  const focusEvent = (target, type) => {
40
- target.dispatchEvent(new FocusEvent(type));
41
- tick();
30
+ target.dispatchEvent(new FocusEvent(type));
31
+ tick();
42
32
  };
43
33
  const pointerEnter = (target) => {
44
- target.dispatchEvent(new PointerEvent('pointerenter', { bubbles: false }));
45
- tick();
34
+ target.dispatchEvent(new PointerEvent("pointerenter", { bubbles: false }));
35
+ tick();
46
36
  };
47
37
  const pointerEvent = (target, type, init = {}) => {
48
- target.dispatchEvent(new PointerEvent(type, { bubbles: true, ...init }));
49
- tick();
38
+ target.dispatchEvent(new PointerEvent(type, {
39
+ bubbles: true,
40
+ ...init
41
+ }));
42
+ tick();
50
43
  };
51
44
  const setInputValue = (input, value) => {
52
- input.value = value;
53
- input.dispatchEvent(new Event('input', { bubbles: true }));
54
- tick();
45
+ input.value = value;
46
+ input.dispatchEvent(new Event("input", { bubbles: true }));
47
+ tick();
55
48
  };
56
49
  const focusField = (field) => {
57
- field.focus();
58
- field.dispatchEvent(new FocusEvent('focus'));
59
- tick();
50
+ field.focus();
51
+ field.dispatchEvent(new FocusEvent("focus"));
52
+ tick();
60
53
  };
61
54
  const typeInField = (field, text) => {
62
- field.focus();
63
- setInputValue(field, text);
55
+ field.focus();
56
+ setInputValue(field, text);
64
57
  };
65
- /**
66
- * Types `text` one character at a time, each with its own `input` event and the caret left after
67
- * the inserted character - what a real keyboard produces. Use it instead of {@link typeInField}
68
- * whenever the control reads or rewrites the element between keystrokes; a whole-string
69
- * {@link setInputValue} hides every defect that only appears mid-entry.
70
- */
71
58
  const typeChars = (field, text) => {
72
- field.focus();
73
- for (const char of text) {
74
- const caret = field.selectionStart ?? field.value.length;
75
- field.value = field.value.slice(0, caret) + char + field.value.slice(caret);
76
- field.setSelectionRange(caret + 1, caret + 1);
77
- field.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }));
78
- tick();
79
- }
59
+ field.focus();
60
+ for (const char of text) {
61
+ const caret = field.selectionStart ?? field.value.length;
62
+ field.value = field.value.slice(0, caret) + char + field.value.slice(caret);
63
+ field.setSelectionRange(caret + 1, caret + 1);
64
+ field.dispatchEvent(new InputEvent("input", {
65
+ bubbles: true,
66
+ inputType: "insertText"
67
+ }));
68
+ tick();
69
+ }
80
70
  };
81
71
  const blurField = (field) => {
82
- field.blur();
83
- field.dispatchEvent(new Event('blur'));
84
- tick();
72
+ field.blur();
73
+ field.dispatchEvent(new Event("blur"));
74
+ tick();
85
75
  };
86
76
  const pointerDownOutside = () => {
87
- document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }));
88
- tick();
77
+ document.body.dispatchEvent(new Event("pointerdown", { bubbles: true }));
78
+ tick();
89
79
  };
90
- /** jsdom has no `DataTransfer`, so the clipboard payload is faked onto the event. */
91
80
  const pasteInto = (target, payload) => {
92
- const event = new Event('paste', { bubbles: true, cancelable: true });
93
- const data = typeof payload === 'string' ? { 'text/plain': payload } : payload;
94
- Object.defineProperty(event, 'clipboardData', { value: { getData: (type) => data[type] ?? '' } });
95
- target.dispatchEvent(event);
96
- tick();
81
+ const event = new Event("paste", {
82
+ bubbles: true,
83
+ cancelable: true
84
+ });
85
+ const data = typeof payload === "string" ? { "text/plain": payload } : payload;
86
+ Object.defineProperty(event, "clipboardData", { value: { getData: (type) => data[type] ?? "" } });
87
+ target.dispatchEvent(event);
88
+ tick();
97
89
  };
98
90
  const textOf = (element) => element?.textContent?.trim() ?? null;
99
-
100
- /** The host, the directive under test, and queries scoped to the fixture - what every driver needs. */
101
91
  const createControlDriver = (fixture, directiveType, { directiveSelector } = {}) => {
102
- const control = directiveSelector
103
- ? directiveAt(fixture, directiveType, directiveSelector)
104
- : hostDirective(fixture, directiveType);
105
- const root = fixture.nativeElement;
106
- const query = (selector) => root.querySelector(selector);
107
- const queryAll = (selector) => Array.from(root.querySelectorAll(selector));
108
- return {
109
- fixture,
110
- host: fixture.componentInstance,
111
- control,
112
- detectChanges: () => fixture.detectChanges(),
113
- tick,
114
- element: () => hostElement(fixture),
115
- directive: (type, selector) => selector ? directiveAt(fixture, type, selector) : hostDirective(fixture, type),
116
- query,
117
- queryAll,
118
- text: (selector) => query(selector)?.textContent?.trim() ?? null,
119
- click: (element) => {
120
- element.click();
121
- tick();
122
- },
123
- };
92
+ const control = directiveSelector ? directiveAt(fixture, directiveType, directiveSelector) : hostDirective(fixture, directiveType);
93
+ const root = fixture.nativeElement;
94
+ const query = (selector) => root.querySelector(selector);
95
+ const queryAll = (selector) => Array.from(root.querySelectorAll(selector));
96
+ return {
97
+ fixture,
98
+ host: fixture.componentInstance,
99
+ control,
100
+ detectChanges: () => fixture.detectChanges(),
101
+ tick,
102
+ element: () => hostElement(fixture),
103
+ directive: (type, selector) => selector ? directiveAt(fixture, type, selector) : hostDirective(fixture, type),
104
+ query,
105
+ queryAll,
106
+ text: (selector) => query(selector)?.textContent?.trim() ?? null,
107
+ click: (element) => {
108
+ element.click();
109
+ tick();
110
+ }
111
+ };
124
112
  };
125
-
126
- /**
127
- * The plumbing every overlay-backed form control driver needs: the host component, the directive,
128
- * the trigger, the overlay pane, and the open/close dance jsdom needs two frames for.
129
- */
130
- const createOverlayControlDriver = (fixture, directiveType, { triggerSelector = '[role="combobox"]', hide, ...controlOptions }) => {
131
- const base = createControlDriver(fixture, directiveType, controlOptions);
132
- const pane = () => latestPane();
133
- const paneEl = (selector) => pane()?.querySelector(selector) ?? null;
134
- const paneEls = (selector) => Array.from(pane()?.querySelectorAll(selector) ?? []);
135
- const trigger = () => base.query(triggerSelector);
136
- const settle = async () => {
137
- await flushFrames();
138
- tick();
139
- };
140
- const settleOpen = async () => {
141
- await settle();
142
- await settle();
143
- };
144
- return {
145
- ...base,
146
- settle,
147
- trigger,
148
- pane,
149
- paneEl,
150
- paneEls,
151
- open: async () => {
152
- trigger().click();
153
- tick();
154
- await settleOpen();
155
- },
156
- close: async () => {
157
- hide(base.control);
158
- tick();
159
- await settle();
160
- },
161
- // jsdom fires no transition events, so a leaving pane would linger and shadow the next open
162
- closeAndRemovePanes: () => {
163
- hide(base.control);
164
- tick();
165
- resetOverlays();
166
- tick();
167
- },
168
- press: (key) => pressKey(trigger(), key),
169
- escape: () => pressKey(document, 'Escape'),
170
- pointerDownOutside,
171
- clickInPane: (selector) => {
172
- paneEl(selector).click();
173
- tick();
174
- },
175
- };
113
+ const createOverlayControlDriver = (fixture, directiveType, { triggerSelector = "[role=\"combobox\"]", hide, ...controlOptions }) => {
114
+ const base = createControlDriver(fixture, directiveType, controlOptions);
115
+ const pane = () => latestPane();
116
+ const paneEl = (selector) => pane()?.querySelector(selector) ?? null;
117
+ const paneEls = (selector) => Array.from(pane()?.querySelectorAll(selector) ?? []);
118
+ const trigger = () => base.query(triggerSelector);
119
+ const settle = async () => {
120
+ await flushFrames();
121
+ tick();
122
+ };
123
+ const settleOpen = async () => {
124
+ await settle();
125
+ await settle();
126
+ };
127
+ return {
128
+ ...base,
129
+ settle,
130
+ trigger,
131
+ pane,
132
+ paneEl,
133
+ paneEls,
134
+ open: async () => {
135
+ trigger().click();
136
+ tick();
137
+ await settleOpen();
138
+ },
139
+ close: async () => {
140
+ hide(base.control);
141
+ tick();
142
+ await settle();
143
+ },
144
+ closeAndRemovePanes: () => {
145
+ hide(base.control);
146
+ tick();
147
+ resetOverlays();
148
+ tick();
149
+ },
150
+ press: (key) => pressKey(trigger(), key),
151
+ escape: () => pressKey(document, "Escape"),
152
+ pointerDownOutside,
153
+ clickInPane: (selector) => {
154
+ paneEl(selector).click();
155
+ tick();
156
+ }
157
+ };
176
158
  };
177
-
178
- /**
179
- * Types masked date/time text one character at a time, with the caret tracking the insertion -
180
- * what `InputMaskDirective`'s live reformatting needs, unlike a whole-string {@link typeInField}.
181
- */
182
159
  const typeMasked = (field, text) => typeChars(field, text);
183
- /**
184
- * Driver for every date-time control that pairs text fields with a picker overlay: date, date
185
- * range, date-time, time and time range. The picker trigger is matched by its directive, so a test
186
- * host can put it on any element; the fields are matched by CSS, defaulting to the only `input`.
187
- */
188
160
  const createDatePickerDriver = (fixture, directiveType, options = {}) => {
189
- const base = createOverlayControlDriver(fixture, directiveType, {
190
- ...options,
191
- triggerSelector: '[etdatepickertrigger]',
192
- hide: (control) => control.closePicker(),
193
- });
194
- const dayCell = (label) => base
195
- .paneEls('.et-calendar-cell')
196
- .find((cell) => textOf(cell) === label && !cell.hasAttribute('data-outside-month')) ?? null;
197
- const field = (selector = 'input') => base.query(selector);
198
- return {
199
- ...base,
200
- dayCell,
201
- clickDayCell: (label) => {
202
- dayCell(label).click();
203
- tick();
204
- },
205
- field,
206
- fields: () => base.queryAll('input'),
207
- type: (text, selector) => typeInField(field(selector), text),
208
- typeMasked: (text, selector) => typeMasked(field(selector), text),
209
- typeAndBlur: (text, selector) => {
210
- typeInField(field(selector), text);
211
- blurField(field(selector));
212
- },
213
- focusField: (selector) => focusField(field(selector)),
214
- blurField: (selector) => blurField(field(selector)),
215
- pressInField: (key, selector) => pressKey(field(selector), key),
216
- };
161
+ const base = createOverlayControlDriver(fixture, directiveType, {
162
+ ...options,
163
+ triggerSelector: "[etdatepickertrigger]",
164
+ hide: (control) => control.closePicker()
165
+ });
166
+ const dayCell = (label) => base.paneEls(".et-calendar-cell").find((cell) => textOf(cell) === label && !cell.hasAttribute("data-outside-month")) ?? null;
167
+ const field = (selector = "input") => base.query(selector);
168
+ return {
169
+ ...base,
170
+ dayCell,
171
+ clickDayCell: (label) => {
172
+ dayCell(label).click();
173
+ tick();
174
+ },
175
+ field,
176
+ fields: () => base.queryAll("input"),
177
+ type: (text, selector) => typeInField(field(selector), text),
178
+ typeMasked: (text, selector) => typeMasked(field(selector), text),
179
+ typeAndBlur: (text, selector) => {
180
+ typeInField(field(selector), text);
181
+ blurField(field(selector));
182
+ },
183
+ focusField: (selector) => focusField(field(selector)),
184
+ blurField: (selector) => blurField(field(selector)),
185
+ pressInField: (key, selector) => pressKey(field(selector), key)
186
+ };
217
187
  };
218
-
219
- /**
220
- * A standalone `OverlayRef` for unit-testing an overlay component without opening it: `close(result)`
221
- * runs the registered close guards, records the call and emits `beforeClosed` / `afterClosed`
222
- * (replayed, so a late subscriber still sees the result). Pair it with {@link provideTestOverlayRef}.
223
- *
224
- * @example
225
- * const ref = createTestOverlayRef<ProductResult>();
226
- * TestBed.configureTestingModule({ providers: [provideTestOverlayRef(ref)] });
227
- * // … press the component's save button …
228
- * expect(ref.closeCalls.at(-1)?.result).toEqual({ saved: true });
229
- */
230
188
  const createTestOverlayRef = (config = {}) => {
231
- const closeCalls = [];
232
- const closeGuards = new Set();
233
- const closed = signal(false, /* @ts-ignore */
234
- ...(ngDevMode ? [{ debugName: "closed" }] : /* istanbul ignore next */ []));
235
- const busy = signal(false, /* @ts-ignore */
236
- ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
237
- const afterOpened$ = new ReplaySubject(1);
238
- const beforeClosed$ = new ReplaySubject(1);
239
- const afterClosed$ = new ReplaySubject(1);
240
- afterOpened$.next();
241
- afterOpened$.complete();
242
- const requestClose = (result, source, forced) => {
243
- closeCalls.push({ result, source, forced });
244
- if (closed())
245
- return;
246
- const event = { result, source };
247
- if (!forced && [...closeGuards].some((guard) => !guard(event)))
248
- return;
249
- closed.set(true);
250
- beforeClosed$.next(event);
251
- beforeClosed$.complete();
252
- afterClosed$.next(event);
253
- afterClosed$.complete();
254
- };
255
- const mapResult = (subject) => subject.pipe(map((event) => event.result));
256
- return {
257
- id: config.id ?? 'et-test-overlay',
258
- elements: null,
259
- config,
260
- headerTemplate: signal(null).asReadonly(),
261
- layout: signal(null).asReadonly(),
262
- componentInstance: () => null,
263
- close: (result) => requestClose(result, 'api', false),
264
- busy,
265
- forceClose: (result, source = 'api') => requestClose(result, source, true),
266
- registerCloseGuard: (guard) => {
267
- closeGuards.add(guard);
268
- return () => closeGuards.delete(guard);
269
- },
270
- updatePositionStrategy: () => undefined,
271
- afterOpened: () => afterOpened$.asObservable(),
272
- beforeClosed: () => mapResult(beforeClosed$),
273
- beforeClosedEvent: () => beforeClosed$.asObservable(),
274
- afterClosed: () => mapResult(afterClosed$),
275
- afterClosedEvent: () => afterClosed$.asObservable(),
276
- closeCalls,
277
- isClosed: closed.asReadonly(),
278
- };
189
+ const closeCalls = [];
190
+ const closeGuards = /* @__PURE__ */ new Set();
191
+ const closed = signal(false, ...ngDevMode ? [{ debugName: "closed" }] : /* istanbul ignore next */ []);
192
+ const busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
193
+ const afterOpened$ = new ReplaySubject(1);
194
+ const beforeClosed$ = new ReplaySubject(1);
195
+ const afterClosed$ = new ReplaySubject(1);
196
+ afterOpened$.next();
197
+ afterOpened$.complete();
198
+ const requestClose = (result, source, forced) => {
199
+ closeCalls.push({
200
+ result,
201
+ source,
202
+ forced
203
+ });
204
+ if (closed()) return;
205
+ const event = {
206
+ result,
207
+ source
208
+ };
209
+ if (!forced && [...closeGuards].some((guard) => !guard(event))) return;
210
+ closed.set(true);
211
+ beforeClosed$.next(event);
212
+ beforeClosed$.complete();
213
+ afterClosed$.next(event);
214
+ afterClosed$.complete();
215
+ };
216
+ const mapResult = (subject) => subject.pipe(map((event) => event.result));
217
+ return {
218
+ id: config.id ?? "et-test-overlay",
219
+ elements: null,
220
+ config,
221
+ headerTemplate: signal(null).asReadonly(),
222
+ layout: signal(null).asReadonly(),
223
+ componentInstance: () => null,
224
+ close: (result) => requestClose(result, "api", false),
225
+ busy,
226
+ forceClose: (result, source = "api") => requestClose(result, source, true),
227
+ registerCloseGuard: (guard) => {
228
+ closeGuards.add(guard);
229
+ return () => closeGuards.delete(guard);
230
+ },
231
+ updatePositionStrategy: () => void 0,
232
+ afterOpened: () => afterOpened$.asObservable(),
233
+ beforeClosed: () => mapResult(beforeClosed$),
234
+ beforeClosedEvent: () => beforeClosed$.asObservable(),
235
+ afterClosed: () => mapResult(afterClosed$),
236
+ afterClosedEvent: () => afterClosed$.asObservable(),
237
+ closeCalls,
238
+ isClosed: closed.asReadonly()
239
+ };
279
240
  };
280
- /** Provides `ref` (a fresh {@link createTestOverlayRef} by default) as `OVERLAY_REF`, so `definition.injectRef()` resolves it. */
281
- const provideTestOverlayRef = (ref = createTestOverlayRef()) => [{ provide: OVERLAY_REF, useValue: ref }];
282
-
283
- /**
284
- * The plumbing every text-field form control driver needs: the host component, the directive, the
285
- * native field, and the value / focus / key events a user produces on it.
286
- */
287
- const createFieldControlDriver = (fixture, directiveType, { fieldSelector = 'input', ...controlOptions } = {}) => {
288
- const base = createControlDriver(fixture, directiveType, controlOptions);
289
- const field = () => base.query(fieldSelector);
290
- return {
291
- ...base,
292
- field,
293
- fieldValue: () => field().value,
294
- placeholder: () => field().placeholder,
295
- type: (text) => setInputValue(field(), text),
296
- /** Types one character at a time - what a keyboard produces, unlike `type`'s single event. */
297
- typeChars: (text) => typeChars(field(), text),
298
- typeAndBlur: (text) => {
299
- setInputValue(field(), text);
300
- blurField(field());
301
- },
302
- focus: () => focusField(field()),
303
- blur: () => blurField(field()),
304
- press: (key, init = {}) => pressKey(field(), key, init),
305
- pointer: pointerEvent,
306
- };
241
+ const provideTestOverlayRef = (ref = createTestOverlayRef()) => [{
242
+ provide: OVERLAY_REF,
243
+ useValue: ref
244
+ }];
245
+ const createFieldControlDriver = (fixture, directiveType, { fieldSelector = "input", ...controlOptions } = {}) => {
246
+ const base = createControlDriver(fixture, directiveType, controlOptions);
247
+ const field = () => base.query(fieldSelector);
248
+ return {
249
+ ...base,
250
+ field,
251
+ fieldValue: () => field().value,
252
+ placeholder: () => field().placeholder,
253
+ type: (text) => setInputValue(field(), text),
254
+ typeChars: (text) => typeChars(field(), text),
255
+ typeAndBlur: (text) => {
256
+ setInputValue(field(), text);
257
+ blurField(field());
258
+ },
259
+ focus: () => focusField(field()),
260
+ blur: () => blurField(field()),
261
+ press: (key, init = {}) => pressKey(field(), key, init),
262
+ pointer: pointerEvent
263
+ };
307
264
  };
308
-
309
265
  const createSelectDriver = (fixture, controlOptions = {}) => {
310
- const base = createOverlayControlDriver(fixture, SelectDirective, {
311
- triggerSelector: '[etselecttrigger], [role="combobox"]',
312
- hide: (select) => select.hide(),
313
- ...controlOptions,
314
- });
315
- // a trigger-inline search renders in the fixture, a panel-hosted one in the pane
316
- const searchInput = () => (base.query('input[etselectsearch]') ?? base.paneEl('input[etselectsearch]'));
317
- const options = () => base.paneEls('[role="option"]');
318
- const visibleOptions = () => base.paneEls('[role="option"]:not([data-filtered])');
319
- const activeOption = () => base.paneEl('[role="option"][data-active]');
320
- const optionByLabel = (label) => visibleOptions().find((option) => textOf(option) === label) ?? null;
321
- const chips = () => base.queryAll('et-chip');
322
- const chipRemoveButton = (index) => chips()[index].querySelector('.et-chip-remove-button');
323
- const optionGroups = () => base.paneEls('[role="group"]');
324
- const virtualBody = () => base.paneEl('.et-select-virtual-options');
325
- return {
326
- ...base,
327
- select: base.control,
328
- searchInput,
329
- valueEl: () => base.query('.et-select-value'),
330
- valueText: () => textOf(base.query('.et-select-value')),
331
- chips,
332
- chipLabels: () => chips().map(textOf),
333
- chipRemoveButton,
334
- listbox: () => base.paneEl('[role="listbox"]'),
335
- paneText: () => textOf(base.pane()),
336
- options,
337
- visibleOptions,
338
- activeOption,
339
- activeLabel: () => textOf(activeOption()),
340
- optionLabels: () => options().map(textOf),
341
- visibleLabels: () => visibleOptions().map(textOf),
342
- optionByLabel,
343
- optionGroups,
344
- groupsHidden: () => optionGroups().map((group) => group.hasAttribute('hidden')),
345
- groupOptionCounts: () => optionGroups().map((group) => group.querySelectorAll('[role="option"]').length),
346
- groupLabel: (index) => {
347
- const labelledBy = optionGroups()[index]?.getAttribute('aria-labelledby');
348
- return labelledBy ? textOf(base.paneEl(`#${labelledBy}`)) : null;
349
- },
350
- virtualPadding: () => ({
351
- start: parseFloat(virtualBody().style.paddingBlockStart),
352
- end: parseFloat(virtualBody().style.paddingBlockEnd),
353
- }),
354
- clickOption: (index) => {
355
- options()[index].click();
356
- tick();
357
- },
358
- clickOptionByLabel: (label) => {
359
- optionByLabel(label).click();
360
- tick();
361
- },
362
- removeChip: (index) => {
363
- chipRemoveButton(index).click();
364
- tick();
365
- },
366
- hover: (element) => pointerEnter(element),
367
- type: (text) => setInputValue(searchInput(), text),
368
- pressInSearch: (key) => pressKey(searchInput(), key),
369
- paste: (text) => pasteInto(searchInput(), text),
370
- };
266
+ const base = createOverlayControlDriver(fixture, SelectDirective, {
267
+ triggerSelector: "[etselecttrigger], [role=\"combobox\"]",
268
+ hide: (select) => select.hide(),
269
+ ...controlOptions
270
+ });
271
+ const searchInput = () => base.query("input[etselectsearch]") ?? base.paneEl("input[etselectsearch]");
272
+ const options = () => base.paneEls("[role=\"option\"]");
273
+ const visibleOptions = () => base.paneEls("[role=\"option\"]:not([data-filtered])");
274
+ const activeOption = () => base.paneEl("[role=\"option\"][data-active]");
275
+ const optionByLabel = (label) => visibleOptions().find((option) => textOf(option) === label) ?? null;
276
+ const chips = () => base.queryAll("et-chip");
277
+ const chipRemoveButton = (index) => chips()[index].querySelector(".et-chip-remove-button");
278
+ const optionGroups = () => base.paneEls("[role=\"group\"]");
279
+ const virtualBody = () => base.paneEl(".et-select-virtual-options");
280
+ return {
281
+ ...base,
282
+ select: base.control,
283
+ searchInput,
284
+ valueEl: () => base.query(".et-select-value"),
285
+ valueText: () => textOf(base.query(".et-select-value")),
286
+ chips,
287
+ chipLabels: () => chips().map(textOf),
288
+ chipRemoveButton,
289
+ listbox: () => base.paneEl("[role=\"listbox\"]"),
290
+ paneText: () => textOf(base.pane()),
291
+ options,
292
+ visibleOptions,
293
+ activeOption,
294
+ activeLabel: () => textOf(activeOption()),
295
+ optionLabels: () => options().map(textOf),
296
+ visibleLabels: () => visibleOptions().map(textOf),
297
+ optionByLabel,
298
+ optionGroups,
299
+ groupsHidden: () => optionGroups().map((group) => group.hasAttribute("hidden")),
300
+ groupOptionCounts: () => optionGroups().map((group) => group.querySelectorAll("[role=\"option\"]").length),
301
+ groupLabel: (index) => {
302
+ const labelledBy = optionGroups()[index]?.getAttribute("aria-labelledby");
303
+ return labelledBy ? textOf(base.paneEl(`#${labelledBy}`)) : null;
304
+ },
305
+ virtualPadding: () => ({
306
+ start: parseFloat(virtualBody().style.paddingBlockStart),
307
+ end: parseFloat(virtualBody().style.paddingBlockEnd)
308
+ }),
309
+ clickOption: (index) => {
310
+ options()[index].click();
311
+ tick();
312
+ },
313
+ clickOptionByLabel: (label) => {
314
+ optionByLabel(label).click();
315
+ tick();
316
+ },
317
+ removeChip: (index) => {
318
+ chipRemoveButton(index).click();
319
+ tick();
320
+ },
321
+ hover: (element) => pointerEnter(element),
322
+ type: (text) => setInputValue(searchInput(), text),
323
+ pressInSearch: (key) => pressKey(searchInput(), key),
324
+ paste: (text) => pasteInto(searchInput(), text)
325
+ };
371
326
  };
372
-
373
- /**
374
- * Wraps a mounted `[etTabBar]` host with the roving-tabindex vocabulary its keyboard model needs:
375
- * which trigger is currently the tab stop, and dispatching a key from wherever focus actually is.
376
- */
377
327
  const createTabBarDriver = (fixture, options = {}) => {
378
- const { triggerSelector = '[role="tab"]', ...controlOptions } = options;
379
- const base = createControlDriver(fixture, TabBarDirective, controlOptions);
380
- const triggers = () => base.queryAll(triggerSelector);
381
- const trigger = (index) => {
382
- const el = triggers()[index];
383
- if (!el)
384
- throw new Error(`No tab trigger at index ${index}`);
385
- return el;
386
- };
387
- /** The trigger the roving-tabindex model made the tab stop - `tabindex="0"` among the bar's triggers. */
388
- const tabbableTrigger = () => triggers().find((el) => el.getAttribute('tabindex') === '0') ?? null;
389
- const focusTabbable = () => {
390
- const el = tabbableTrigger();
391
- if (!el)
392
- throw new Error('No tabbable trigger - the roving-tabindex lookup found nothing');
393
- el.focus();
394
- base.tick();
395
- };
396
- /** Dispatches `key` from wherever focus currently is, falling back to the roving tab stop. */
397
- const press = (key, init) => {
398
- const target = document.activeElement ?? tabbableTrigger();
399
- if (!target)
400
- throw new Error('No focused trigger to dispatch the keydown on');
401
- return pressKey(target, key, init);
402
- };
403
- return {
404
- ...base,
405
- triggers,
406
- trigger,
407
- tabbableTrigger,
408
- focusTabbable,
409
- press,
410
- focusedIndex: () => base.control.focusedIndex(),
411
- selectedIndex: () => base.control.selectedIndex(),
412
- };
328
+ const { triggerSelector = "[role=\"tab\"]", ...controlOptions } = options;
329
+ const base = createControlDriver(fixture, TabBarDirective, controlOptions);
330
+ const triggers = () => base.queryAll(triggerSelector);
331
+ const trigger = (index) => {
332
+ const el = triggers()[index];
333
+ if (!el) throw new Error(`No tab trigger at index ${index}`);
334
+ return el;
335
+ };
336
+ const tabbableTrigger = () => triggers().find((el) => el.getAttribute("tabindex") === "0") ?? null;
337
+ const focusTabbable = () => {
338
+ const el = tabbableTrigger();
339
+ if (!el) throw new Error("No tabbable trigger - the roving-tabindex lookup found nothing");
340
+ el.focus();
341
+ base.tick();
342
+ };
343
+ const press = (key, init) => {
344
+ const target = document.activeElement ?? tabbableTrigger();
345
+ if (!target) throw new Error("No focused trigger to dispatch the keydown on");
346
+ return pressKey(target, key, init);
347
+ };
348
+ return {
349
+ ...base,
350
+ triggers,
351
+ trigger,
352
+ tabbableTrigger,
353
+ focusTabbable,
354
+ press,
355
+ focusedIndex: () => base.control.focusedIndex(),
356
+ selectedIndex: () => base.control.selectedIndex()
357
+ };
358
+ };
359
+ var ResizeObserverFake = class {
360
+ constructor(callback) {}
361
+ observe() {}
362
+ unobserve() {}
363
+ disconnect() {}
364
+ };
365
+ var IntersectionObserverFake = class {
366
+ constructor(callback) {
367
+ this.root = null;
368
+ this.rootMargin = "";
369
+ this.thresholds = [];
370
+ }
371
+ observe() {}
372
+ unobserve() {}
373
+ disconnect() {}
374
+ takeRecords() {
375
+ return [];
376
+ }
377
+ };
378
+ var AnimationFake = class extends EventTarget {
379
+ constructor() {
380
+ super();
381
+ this.currentTime = 0;
382
+ this.playState = "finished";
383
+ this.onfinish = null;
384
+ this.oncancel = null;
385
+ this.settled = false;
386
+ this.finished = Promise.resolve(this);
387
+ queueMicrotask(() => this.finish());
388
+ }
389
+ cancel() {
390
+ if (this.settled) return;
391
+ this.settled = true;
392
+ this.playState = "idle";
393
+ this.dispatchEvent(new Event("cancel"));
394
+ this.oncancel?.();
395
+ }
396
+ finish() {
397
+ if (this.settled) return;
398
+ this.settled = true;
399
+ this.playState = "finished";
400
+ this.dispatchEvent(new Event("finish"));
401
+ this.onfinish?.();
402
+ }
403
+ play() {}
404
+ pause() {}
405
+ reverse() {}
406
+ commitStyles() {}
413
407
  };
414
-
415
- class ResizeObserverFake {
416
- constructor(callback) {
417
- void callback;
418
- }
419
- observe() {
420
- return undefined;
421
- }
422
- unobserve() {
423
- return undefined;
424
- }
425
- disconnect() {
426
- return undefined;
427
- }
428
- }
429
- class IntersectionObserverFake {
430
- constructor(callback) {
431
- this.root = null;
432
- this.rootMargin = '';
433
- this.thresholds = [];
434
- void callback;
435
- }
436
- observe() {
437
- return undefined;
438
- }
439
- unobserve() {
440
- return undefined;
441
- }
442
- disconnect() {
443
- return undefined;
444
- }
445
- takeRecords() {
446
- return [];
447
- }
448
- }
449
- class AnimationFake extends EventTarget {
450
- constructor() {
451
- super();
452
- this.currentTime = 0;
453
- this.playState = 'finished';
454
- this.onfinish = null;
455
- this.oncancel = null;
456
- // An animation settles once: without the latch a `cancel()` before the queued auto-finish would fire
457
- // `oncancel` and then `onfinish`. It is set before notifying, so a handler that re-enters
458
- // `cancel()`/`finish()` is a no-op.
459
- this.settled = false;
460
- this.finished = Promise.resolve(this);
461
- queueMicrotask(() => this.finish());
462
- }
463
- cancel() {
464
- if (this.settled)
465
- return;
466
- this.settled = true;
467
- this.playState = 'idle';
468
- this.dispatchEvent(new Event('cancel'));
469
- this.oncancel?.();
470
- }
471
- finish() {
472
- if (this.settled)
473
- return;
474
- this.settled = true;
475
- this.playState = 'finished';
476
- this.dispatchEvent(new Event('finish'));
477
- this.onfinish?.();
478
- }
479
- play() {
480
- return undefined;
481
- }
482
- pause() {
483
- return undefined;
484
- }
485
- reverse() {
486
- return undefined;
487
- }
488
- commitStyles() {
489
- return undefined;
490
- }
491
- }
492
408
  const fakeMediaQueryList = (query) => ({
493
- matches: false,
494
- media: query,
495
- onchange: null,
496
- addListener: () => undefined,
497
- removeListener: () => undefined,
498
- addEventListener: () => undefined,
499
- removeEventListener: () => undefined,
500
- dispatchEvent: () => false,
409
+ matches: false,
410
+ media: query,
411
+ onchange: null,
412
+ addListener: () => void 0,
413
+ removeListener: () => void 0,
414
+ addEventListener: () => void 0,
415
+ removeEventListener: () => void 0,
416
+ dispatchEvent: () => false
501
417
  });
502
418
  const defineMissing = (target, key, value) => {
503
- if (Reflect.get(target, key))
504
- return;
505
- Object.defineProperty(target, key, { configurable: true, value, writable: true });
419
+ if (Reflect.get(target, key)) return;
420
+ Object.defineProperty(target, key, {
421
+ configurable: true,
422
+ value,
423
+ writable: true
424
+ });
506
425
  };
507
- /**
508
- * Installs the browser APIs jsdom lacks and `@ethlete/components` needs to render: `ResizeObserver`,
509
- * `IntersectionObserver`, `matchMedia`, `Element.prototype.animate` and `Element.prototype.scroll`. Call it once in a test setup file;
510
- * an API that already exists is left alone. The fakes never report: observers never fire, no media query
511
- * matches, and an animation finishes on the next microtask.
512
- */
513
426
  const setupComponentsTestEnvironment = () => {
514
- defineMissing(globalThis, 'ResizeObserver', ResizeObserverFake);
515
- defineMissing(globalThis, 'IntersectionObserver', IntersectionObserverFake);
516
- defineMissing(globalThis, 'matchMedia', fakeMediaQueryList);
517
- defineMissing(Element.prototype, 'animate', () => new AnimationFake());
518
- defineMissing(Element.prototype, 'scroll', () => undefined);
427
+ defineMissing(globalThis, "ResizeObserver", ResizeObserverFake);
428
+ defineMissing(globalThis, "IntersectionObserver", IntersectionObserverFake);
429
+ defineMissing(globalThis, "matchMedia", fakeMediaQueryList);
430
+ defineMissing(Element.prototype, "animate", () => new AnimationFake());
431
+ defineMissing(Element.prototype, "scroll", () => void 0);
519
432
  };
520
-
521
- /**
522
- * Generated bundle index. Do not edit.
523
- */
524
-
525
433
  export { blurField, createControlDriver, createDatePickerDriver, createFieldControlDriver, createOverlayControlDriver, createSelectDriver, createTabBarDriver, createTestOverlayRef, directiveAt, flushFrames, focusEvent, focusField, hostDirective, hostElement, latestPane, pasteInto, pointerDownOutside, pointerEnter, pointerEvent, pressKey, provideTestOverlayRef, query, queryAll, resetOverlays, setInputValue, setupComponentsTestEnvironment, textOf, tick, typeChars, typeInField, typeMasked };
526
- //# sourceMappingURL=ethlete-components-testing.mjs.map
434
+
435
+ //# sourceMappingURL=ethlete-components-testing.mjs.map