@ethlete/components 1.0.0-next.75 → 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.
- package/CHANGELOG.md +14 -0
- package/fesm2022/ethlete-components-testing.mjs +384 -475
- package/fesm2022/ethlete-components-testing.mjs.map +1 -1
- package/fesm2022/ethlete-components.mjs +105736 -72746
- package/fesm2022/ethlete-components.mjs.map +1 -1
- package/package.json +2 -2
- package/src/lib/forms/form-field/form-field-control-suffix-styles.component.css +10 -1
- package/src/lib/overlay/strategies/sheet-styles.component.css +19 -0
- package/types/ethlete-components-testing.d.ts +225 -238
- package/types/ethlete-components.d.ts +29206 -21279
|
@@ -1,526 +1,435 @@
|
|
|
1
|
-
import { ApplicationRef, signal } from
|
|
2
|
-
import { TestBed } from
|
|
3
|
-
import { ReplaySubject, map } from
|
|
4
|
-
import { OVERLAY_REF, SelectDirective, TabBarDirective } from
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
30
|
+
target.dispatchEvent(new FocusEvent(type));
|
|
31
|
+
tick();
|
|
42
32
|
};
|
|
43
33
|
const pointerEnter = (target) => {
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
target.dispatchEvent(new PointerEvent("pointerenter", { bubbles: false }));
|
|
35
|
+
tick();
|
|
46
36
|
};
|
|
47
37
|
const pointerEvent = (target, type, init = {}) => {
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
target.dispatchEvent(new PointerEvent(type, {
|
|
39
|
+
bubbles: true,
|
|
40
|
+
...init
|
|
41
|
+
}));
|
|
42
|
+
tick();
|
|
50
43
|
};
|
|
51
44
|
const setInputValue = (input, value) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
input.value = value;
|
|
46
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
47
|
+
tick();
|
|
55
48
|
};
|
|
56
49
|
const focusField = (field) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
field.focus();
|
|
51
|
+
field.dispatchEvent(new FocusEvent("focus"));
|
|
52
|
+
tick();
|
|
60
53
|
};
|
|
61
54
|
const typeInField = (field, text) => {
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
field.blur();
|
|
73
|
+
field.dispatchEvent(new Event("blur"));
|
|
74
|
+
tick();
|
|
85
75
|
};
|
|
86
76
|
const pointerDownOutside = () => {
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
434
|
+
|
|
435
|
+
//# sourceMappingURL=ethlete-components-testing.mjs.map
|