@assure-one/design-system 1.35.0 → 1.36.0
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/README.md +52 -0
- package/codemods/README.md +102 -12
- package/codemods/lib/registry.mjs +3 -0
- package/codemods/transforms/cm-07-input-size.mjs +104 -0
- package/codemods/transforms/cm-08-search-select-to-combobox.mjs +236 -0
- package/codemods/transforms/cm-10-date-picker-value-change.mjs +416 -0
- package/codemods/transforms/cm-14-hidden-mirrors.mjs +2 -2
- package/dist/css/components.css +1 -1
- package/dist/design-system-provider-cPUklDJv.d.ts +220 -0
- package/dist/icons/index.d.ts +2 -2
- package/dist/{index-B3OiMlQv.d.ts → index-CQwzTm0v.d.ts} +1 -1
- package/dist/index.d.ts +2011 -399
- package/dist/index.js +8859 -5262
- package/dist/index.js.map +1 -1
- package/dist/next/index.d.ts +20 -0
- package/dist/next/index.js +16 -0
- package/dist/next/index.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/{system-lyhOUTpa.d.ts → system-BDU18fVg.d.ts} +1 -1
- package/dist/testing/index.cjs +133 -11
- package/dist/testing/index.d.cts +98 -2
- package/dist/testing/index.d.ts +98 -2
- package/dist/testing/index.js +132 -12
- package/dist/tokens/index.d.ts +1 -1
- package/docs/components.md +601 -0
- package/docs/components.registry.json +1586 -0
- package/docs/for-ai-agents.md +153 -0
- package/package.json +15 -3
|
@@ -556,4 +556,4 @@ declare const systemTokens: {
|
|
|
556
556
|
};
|
|
557
557
|
type SystemTokens = typeof systemTokens;
|
|
558
558
|
|
|
559
|
-
export { type ColorName as C, type ReferenceTokens as R, type SystemTokens as S, reference as a,
|
|
559
|
+
export { type ColorName as C, type ReferenceTokens as R, type SystemTokens as S, reference as a, spacing as b, colors as c, surfaces as d, systemTokens as e, breakpoints as f, iconSizes as i, layout as l, motion as m, overlays as o, radii as r, shadows as s, typography as t, zIndex as z };
|
package/dist/testing/index.cjs
CHANGED
|
@@ -312,14 +312,28 @@ function accessibleText(element) {
|
|
|
312
312
|
function isDisabled(element) {
|
|
313
313
|
return element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true" || element.hasAttribute("data-disabled");
|
|
314
314
|
}
|
|
315
|
+
function isQueries(target) {
|
|
316
|
+
return typeof target.getByRole === "function";
|
|
317
|
+
}
|
|
315
318
|
function press(target, key) {
|
|
316
319
|
const view = target.ownerDocument.defaultView ?? window;
|
|
317
320
|
const init = { key, code: key === " " ? "Space" : key, bubbles: true, cancelable: true };
|
|
318
321
|
target.dispatchEvent(new view.KeyboardEvent("keydown", init));
|
|
319
322
|
target.dispatchEvent(new view.KeyboardEvent("keyup", init));
|
|
320
323
|
}
|
|
324
|
+
async function run(fn) {
|
|
325
|
+
const actEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
|
|
326
|
+
if (actEnvironment) {
|
|
327
|
+
await react.act(async () => {
|
|
328
|
+
await fn();
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await fn();
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
334
|
+
}
|
|
321
335
|
async function flush() {
|
|
322
|
-
await
|
|
336
|
+
await run(async () => {
|
|
323
337
|
await Promise.resolve();
|
|
324
338
|
});
|
|
325
339
|
}
|
|
@@ -332,7 +346,7 @@ async function waitUntil(find, maxFlushes) {
|
|
|
332
346
|
return null;
|
|
333
347
|
}
|
|
334
348
|
function resolveTrigger(target, name, role) {
|
|
335
|
-
if (
|
|
349
|
+
if (isQueries(target)) {
|
|
336
350
|
if (name === void 0)
|
|
337
351
|
throw new Error(`A ${role} name is required when passing a query object.`);
|
|
338
352
|
return target.getByRole(role, { name });
|
|
@@ -350,8 +364,33 @@ function popupOf(trigger, role) {
|
|
|
350
364
|
}
|
|
351
365
|
return null;
|
|
352
366
|
}
|
|
367
|
+
function isTextControl(element) {
|
|
368
|
+
const tag = element.tagName;
|
|
369
|
+
return tag === "TEXTAREA" || tag === "INPUT" && !/^(checkbox|radio|file|button|submit|reset|image|hidden)$/i.test(
|
|
370
|
+
element.type
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
async function typeInto(control, text2, commit) {
|
|
374
|
+
const view = control.ownerDocument.defaultView ?? window;
|
|
375
|
+
const proto = control.tagName === "TEXTAREA" ? view.HTMLTextAreaElement.prototype : view.HTMLInputElement.prototype;
|
|
376
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
377
|
+
await run(() => {
|
|
378
|
+
control.focus();
|
|
379
|
+
if (setter) setter.call(control, text2);
|
|
380
|
+
else control.value = text2;
|
|
381
|
+
control.dispatchEvent(new view.Event("input", { bubbles: true }));
|
|
382
|
+
});
|
|
383
|
+
if (commit) {
|
|
384
|
+
await run(() => {
|
|
385
|
+
control.blur();
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
await flush();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/testing/interactions.ts
|
|
353
392
|
async function selectOption(target, ...rest) {
|
|
354
|
-
const byQuery =
|
|
393
|
+
const byQuery = isQueries(target);
|
|
355
394
|
const label = byQuery ? rest[0] : void 0;
|
|
356
395
|
const option = byQuery ? rest[1] : rest[0];
|
|
357
396
|
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
@@ -363,7 +402,7 @@ async function selectOption(target, ...rest) {
|
|
|
363
402
|
);
|
|
364
403
|
}
|
|
365
404
|
if (trigger.getAttribute("aria-expanded") !== "true") {
|
|
366
|
-
await
|
|
405
|
+
await run(() => {
|
|
367
406
|
trigger.focus();
|
|
368
407
|
press(trigger, "Enter");
|
|
369
408
|
});
|
|
@@ -378,16 +417,16 @@ async function selectOption(target, ...rest) {
|
|
|
378
417
|
const wanted = all.find((el) => matches(accessibleText(el), option));
|
|
379
418
|
if (!wanted) {
|
|
380
419
|
const available = all.map((el) => JSON.stringify(accessibleText(el))).join(", ");
|
|
381
|
-
await
|
|
420
|
+
await run(() => press(listbox, "Escape"));
|
|
382
421
|
throw new Error(
|
|
383
422
|
`selectOption: no option matches ${describe(option)}. Options: ${available || "(none)"}.`
|
|
384
423
|
);
|
|
385
424
|
}
|
|
386
425
|
if (isDisabled(wanted)) {
|
|
387
|
-
await
|
|
426
|
+
await run(() => press(listbox, "Escape"));
|
|
388
427
|
throw new Error(`selectOption: the option ${describe(option)} is disabled.`);
|
|
389
428
|
}
|
|
390
|
-
await
|
|
429
|
+
await run(() => {
|
|
391
430
|
wanted.focus();
|
|
392
431
|
press(wanted, "Enter");
|
|
393
432
|
});
|
|
@@ -396,7 +435,7 @@ async function selectOption(target, ...rest) {
|
|
|
396
435
|
throw new Error(`selectOption: the list stayed open after choosing ${describe(option)}.`);
|
|
397
436
|
}
|
|
398
437
|
async function openMenu(target, ...rest) {
|
|
399
|
-
const byQuery =
|
|
438
|
+
const byQuery = isQueries(target);
|
|
400
439
|
const name = byQuery ? rest[0] : void 0;
|
|
401
440
|
const settings = (byQuery ? rest[1] : rest[0]) ?? {};
|
|
402
441
|
const maxFlushes = settings.maxFlushes ?? 20;
|
|
@@ -407,7 +446,7 @@ async function openMenu(target, ...rest) {
|
|
|
407
446
|
);
|
|
408
447
|
}
|
|
409
448
|
if (trigger.getAttribute("aria-expanded") !== "true") {
|
|
410
|
-
await
|
|
449
|
+
await run(() => {
|
|
411
450
|
trigger.focus();
|
|
412
451
|
press(trigger, "ArrowDown");
|
|
413
452
|
});
|
|
@@ -445,21 +484,104 @@ async function openMenu(target, ...rest) {
|
|
|
445
484
|
const item = getItem(itemName);
|
|
446
485
|
if (isDisabled(item))
|
|
447
486
|
throw new Error(`openMenu: the item ${describe(itemName)} is disabled.`);
|
|
448
|
-
await
|
|
487
|
+
await run(() => {
|
|
449
488
|
item.focus();
|
|
450
489
|
press(item, "Enter");
|
|
451
490
|
});
|
|
452
491
|
await waitUntil(() => menu.isConnected ? null : true, maxFlushes);
|
|
453
492
|
},
|
|
454
493
|
async close() {
|
|
455
|
-
await
|
|
494
|
+
await run(() => press(menu, "Escape"));
|
|
456
495
|
await waitClosed("Escape");
|
|
457
496
|
}
|
|
458
497
|
};
|
|
459
498
|
}
|
|
460
499
|
|
|
500
|
+
// src/testing/helpers/fill-field.ts
|
|
501
|
+
var TEXT_ROLES = ["textbox", "spinbutton", "combobox"];
|
|
502
|
+
function resolveByLabel(queries, label) {
|
|
503
|
+
const errors = [];
|
|
504
|
+
for (const role of TEXT_ROLES) {
|
|
505
|
+
try {
|
|
506
|
+
return queries.getByRole(role, { name: label });
|
|
507
|
+
} catch (error) {
|
|
508
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
throw new Error(
|
|
512
|
+
`fillField: no text control is labelled ${describe(label)} (looked for roles ${TEXT_ROLES.join(", ")}).
|
|
513
|
+
${errors[0] ?? ""}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
async function fillField(target, ...rest) {
|
|
517
|
+
const byQuery = isQueries(target);
|
|
518
|
+
const label = byQuery ? rest[0] : void 0;
|
|
519
|
+
const value = byQuery ? rest[1] : rest[0];
|
|
520
|
+
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
521
|
+
const control = byQuery ? resolveByLabel(target, label) : target;
|
|
522
|
+
const name = label ?? accessibleText(control);
|
|
523
|
+
if (!isTextControl(control)) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`fillField: ${describe(name)} is a <${control.tagName.toLowerCase()}>, not a text input or textarea. Use selectOption for a Select and pickDate for a DatePicker.`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (isDisabled(control)) throw new Error(`fillField: the control ${describe(name)} is disabled.`);
|
|
529
|
+
if (control.readOnly || control.getAttribute("aria-readonly") === "true") {
|
|
530
|
+
throw new Error(`fillField: the control ${describe(name)} is read-only.`);
|
|
531
|
+
}
|
|
532
|
+
await typeInto(control, value, settings.commit ?? true);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/testing/helpers/pick-date.ts
|
|
536
|
+
var ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
537
|
+
var DEFAULT_PATTERN = "MM/DD/YYYY";
|
|
538
|
+
function looksLikePattern(text2) {
|
|
539
|
+
return !!text2 && /YYYY/.test(text2) && /MM/.test(text2) === /DD/.test(text2);
|
|
540
|
+
}
|
|
541
|
+
function formatIso(iso, pattern) {
|
|
542
|
+
const match = ISO.exec(iso);
|
|
543
|
+
if (!match) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`pickDate: expected an ISO date (YYYY-MM-DD), received ${JSON.stringify(iso)}.`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const [, year, month, day] = match;
|
|
549
|
+
return pattern.replace("YYYY", year).replace("MM", month).replace("DD", day);
|
|
550
|
+
}
|
|
551
|
+
async function pickDate(target, ...rest) {
|
|
552
|
+
const byQuery = isQueries(target);
|
|
553
|
+
const label = byQuery ? rest[0] : void 0;
|
|
554
|
+
const iso = byQuery ? rest[1] : rest[0];
|
|
555
|
+
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
556
|
+
const input = byQuery ? target.getByRole("textbox", { name: label }) : target;
|
|
557
|
+
const name = label ?? accessibleText(input);
|
|
558
|
+
if (!isTextControl(input) || input.tagName !== "INPUT") {
|
|
559
|
+
throw new Error(
|
|
560
|
+
`pickDate: ${describe(name)} is not a DatePicker text input (a <${input.tagName.toLowerCase()}>).`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (isDisabled(input))
|
|
564
|
+
throw new Error(`pickDate: the date picker ${describe(name)} is disabled.`);
|
|
565
|
+
if (input.readOnly) throw new Error(`pickDate: the date picker ${describe(name)} is read-only.`);
|
|
566
|
+
const placeholder = input.getAttribute("placeholder");
|
|
567
|
+
const pattern = settings.dateFormat ?? (looksLikePattern(placeholder) ? placeholder : DEFAULT_PATTERN);
|
|
568
|
+
const yearOnly = !/MM|DD/.test(pattern);
|
|
569
|
+
let display = "";
|
|
570
|
+
if (iso !== "") {
|
|
571
|
+
display = yearOnly ? formatIso(iso, "YYYY") : formatIso(iso, pattern);
|
|
572
|
+
}
|
|
573
|
+
await typeInto(input, display, true);
|
|
574
|
+
if (input.value !== display) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`pickDate: typed ${JSON.stringify(display)} into ${describe(name)} but it shows ${JSON.stringify(input.value)}. The picker's pattern is probably not ${JSON.stringify(pattern)}; pass { dateFormat } with the pattern the picker uses.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
461
581
|
exports.createToastRecorder = createToastRecorder;
|
|
582
|
+
exports.fillField = fillField;
|
|
462
583
|
exports.installDomPolyfills = installDomPolyfills;
|
|
463
584
|
exports.openMenu = openMenu;
|
|
585
|
+
exports.pickDate = pickDate;
|
|
464
586
|
exports.selectOption = selectOption;
|
|
465
587
|
exports.withAssureDesignSystem = withAssureDesignSystem;
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -182,11 +182,22 @@ interface RoleQueries {
|
|
|
182
182
|
}
|
|
183
183
|
/** Text matcher: exact text (after trimming and collapsing whitespace) or a pattern. @experimental */
|
|
184
184
|
type TextMatch = string | RegExp;
|
|
185
|
-
/** Options for
|
|
185
|
+
/** Options for the interaction helpers. @experimental */
|
|
186
186
|
interface InteractionOptions {
|
|
187
187
|
/** How many render passes to wait for the popup to appear or close. Default 20. */
|
|
188
188
|
maxFlushes?: number;
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Interaction helpers that drive design-system components the way a keyboard
|
|
193
|
+
* user does, so tests do not encode Radix internals (ADR-014).
|
|
194
|
+
*
|
|
195
|
+
* They use only the DOM (roles, `aria-*`, keyboard events) and React's `act`,
|
|
196
|
+
* so they work with any renderer and any query library. This repository tests
|
|
197
|
+
* them against the real components; if a component's markup changes, those
|
|
198
|
+
* tests fail here rather than in a consumer.
|
|
199
|
+
*/
|
|
200
|
+
|
|
190
201
|
/**
|
|
191
202
|
* Open a design-system `Select` and choose an option by its visible text.
|
|
192
203
|
*
|
|
@@ -250,4 +261,89 @@ declare function openMenu(trigger: HTMLElement, options?: InteractionOptions): P
|
|
|
250
261
|
*/
|
|
251
262
|
declare function openMenu(queries: RoleQueries, name: TextMatch, options?: InteractionOptions): Promise<MenuHandle>;
|
|
252
263
|
|
|
253
|
-
|
|
264
|
+
/**
|
|
265
|
+
* `fillField` — type a value into a design-system text control found by its
|
|
266
|
+
* Field label (ADR-014, W4-26).
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/** Options for `fillField`. @experimental */
|
|
270
|
+
interface FillFieldOptions {
|
|
271
|
+
/**
|
|
272
|
+
* Blur the control after typing so controls that commit on blur
|
|
273
|
+
* (`NumberInput` formatting) settle. Default `true`; pass `false` to keep
|
|
274
|
+
* focus, for example before pressing Enter yourself.
|
|
275
|
+
*/
|
|
276
|
+
commit?: boolean;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Type `value` into a text control — `Input`, `Textarea`, `SearchInput`,
|
|
280
|
+
* `NumberInput`, a `DatePicker`'s text input — replacing what it holds.
|
|
281
|
+
*
|
|
282
|
+
* ```ts
|
|
283
|
+
* await fillField(screen, "Full name", "Ada Lovelace"); // control named by its Field label
|
|
284
|
+
* await fillField(screen.getByRole("textbox", { name: "Notes" }), "Late filing");
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* The control is found by its accessible name, which inside a `<Field>` is
|
|
288
|
+
* the `Label` text. React's `onChange` fires once with the new value; the
|
|
289
|
+
* control's own masking or formatting applies as it would for a user. Throws
|
|
290
|
+
* when the control is disabled or read-only, or when nothing is labelled
|
|
291
|
+
* `label`.
|
|
292
|
+
*
|
|
293
|
+
* @experimental
|
|
294
|
+
*/
|
|
295
|
+
declare function fillField(control: HTMLElement, value: string, options?: FillFieldOptions): Promise<void>;
|
|
296
|
+
/**
|
|
297
|
+
* Type `value` into the text control whose accessible name is `label`, found
|
|
298
|
+
* through `queries` (for example Testing Library's `screen`).
|
|
299
|
+
*
|
|
300
|
+
* @experimental
|
|
301
|
+
*/
|
|
302
|
+
declare function fillField(queries: RoleQueries, label: TextMatch, value: string, options?: FillFieldOptions): Promise<void>;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `pickDate` — set a design-system `DatePicker` to an ISO date by typing into
|
|
306
|
+
* its masked text input (ADR-014, W4-26).
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/** Options for `pickDate`. @experimental */
|
|
310
|
+
interface PickDateOptions {
|
|
311
|
+
/**
|
|
312
|
+
* The picker's display pattern — `MM`, `DD` and `YYYY` around literal
|
|
313
|
+
* separators, as passed to `DatePicker`'s `dateFormat` — when the helper
|
|
314
|
+
* cannot read it from the input's placeholder (a custom `placeholder`).
|
|
315
|
+
* Default: the placeholder, else `"MM/DD/YYYY"`.
|
|
316
|
+
*/
|
|
317
|
+
dateFormat?: string;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Set a `DatePicker` to `iso` (`YYYY-MM-DD`) by typing the date into its text
|
|
321
|
+
* input in the picker's own display pattern. `""` clears it.
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* await pickDate(screen, "Due date", "2025-03-14"); // input named by its Field label
|
|
325
|
+
* await pickDate(screen.getByRole("textbox", { name: "Due date" }), "2025-03-14");
|
|
326
|
+
* await pickDate(screen, "Fällig am", "2025-03-14", { dateFormat: "DD.MM.YYYY" });
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* The pattern is read from the input's placeholder, which `DatePicker` sets to
|
|
330
|
+
* its pattern (`MM/DD/YYYY` for `en-US`, `DD.MM.YYYY` for `de-DE`, a custom
|
|
331
|
+
* `dateFormat`); pass `dateFormat` when the picker has its own placeholder. A
|
|
332
|
+
* year picker (`picker="year"`, placeholder `YYYY`) receives the year. The
|
|
333
|
+
* helper checks that the input shows the typed date afterwards, so a picker
|
|
334
|
+
* that masked it differently — a pattern mismatch — throws instead of
|
|
335
|
+
* submitting a wrong value. `onValueChange` / `onChange` fire and a `name`d
|
|
336
|
+
* picker updates its form value, as for a real user.
|
|
337
|
+
*
|
|
338
|
+
* @experimental
|
|
339
|
+
*/
|
|
340
|
+
declare function pickDate(input: HTMLElement, iso: string, options?: PickDateOptions): Promise<void>;
|
|
341
|
+
/**
|
|
342
|
+
* Set the `DatePicker` whose text input has the accessible name `label`
|
|
343
|
+
* (found through `queries`, for example Testing Library's `screen`) to `iso`.
|
|
344
|
+
*
|
|
345
|
+
* @experimental
|
|
346
|
+
*/
|
|
347
|
+
declare function pickDate(queries: RoleQueries, label: TextMatch, iso: string, options?: PickDateOptions): Promise<void>;
|
|
348
|
+
|
|
349
|
+
export { type AssureJestOptions, type DomPolyfill, type DomPolyfillOptions, type FillFieldOptions, type InteractionOptions, type JestConfigInput, type JestConfigLike, type MenuHandle, type PickDateOptions, type RecordedToast, type RecordedToastVariant, type RoleQueries, type TextMatch, type ToastRecorder, type ToastRecorderOptions, type WithAssureDesignSystem, createToastRecorder, fillField, installDomPolyfills, openMenu, pickDate, selectOption, withAssureDesignSystem };
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -182,11 +182,22 @@ interface RoleQueries {
|
|
|
182
182
|
}
|
|
183
183
|
/** Text matcher: exact text (after trimming and collapsing whitespace) or a pattern. @experimental */
|
|
184
184
|
type TextMatch = string | RegExp;
|
|
185
|
-
/** Options for
|
|
185
|
+
/** Options for the interaction helpers. @experimental */
|
|
186
186
|
interface InteractionOptions {
|
|
187
187
|
/** How many render passes to wait for the popup to appear or close. Default 20. */
|
|
188
188
|
maxFlushes?: number;
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Interaction helpers that drive design-system components the way a keyboard
|
|
193
|
+
* user does, so tests do not encode Radix internals (ADR-014).
|
|
194
|
+
*
|
|
195
|
+
* They use only the DOM (roles, `aria-*`, keyboard events) and React's `act`,
|
|
196
|
+
* so they work with any renderer and any query library. This repository tests
|
|
197
|
+
* them against the real components; if a component's markup changes, those
|
|
198
|
+
* tests fail here rather than in a consumer.
|
|
199
|
+
*/
|
|
200
|
+
|
|
190
201
|
/**
|
|
191
202
|
* Open a design-system `Select` and choose an option by its visible text.
|
|
192
203
|
*
|
|
@@ -250,4 +261,89 @@ declare function openMenu(trigger: HTMLElement, options?: InteractionOptions): P
|
|
|
250
261
|
*/
|
|
251
262
|
declare function openMenu(queries: RoleQueries, name: TextMatch, options?: InteractionOptions): Promise<MenuHandle>;
|
|
252
263
|
|
|
253
|
-
|
|
264
|
+
/**
|
|
265
|
+
* `fillField` — type a value into a design-system text control found by its
|
|
266
|
+
* Field label (ADR-014, W4-26).
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/** Options for `fillField`. @experimental */
|
|
270
|
+
interface FillFieldOptions {
|
|
271
|
+
/**
|
|
272
|
+
* Blur the control after typing so controls that commit on blur
|
|
273
|
+
* (`NumberInput` formatting) settle. Default `true`; pass `false` to keep
|
|
274
|
+
* focus, for example before pressing Enter yourself.
|
|
275
|
+
*/
|
|
276
|
+
commit?: boolean;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Type `value` into a text control — `Input`, `Textarea`, `SearchInput`,
|
|
280
|
+
* `NumberInput`, a `DatePicker`'s text input — replacing what it holds.
|
|
281
|
+
*
|
|
282
|
+
* ```ts
|
|
283
|
+
* await fillField(screen, "Full name", "Ada Lovelace"); // control named by its Field label
|
|
284
|
+
* await fillField(screen.getByRole("textbox", { name: "Notes" }), "Late filing");
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* The control is found by its accessible name, which inside a `<Field>` is
|
|
288
|
+
* the `Label` text. React's `onChange` fires once with the new value; the
|
|
289
|
+
* control's own masking or formatting applies as it would for a user. Throws
|
|
290
|
+
* when the control is disabled or read-only, or when nothing is labelled
|
|
291
|
+
* `label`.
|
|
292
|
+
*
|
|
293
|
+
* @experimental
|
|
294
|
+
*/
|
|
295
|
+
declare function fillField(control: HTMLElement, value: string, options?: FillFieldOptions): Promise<void>;
|
|
296
|
+
/**
|
|
297
|
+
* Type `value` into the text control whose accessible name is `label`, found
|
|
298
|
+
* through `queries` (for example Testing Library's `screen`).
|
|
299
|
+
*
|
|
300
|
+
* @experimental
|
|
301
|
+
*/
|
|
302
|
+
declare function fillField(queries: RoleQueries, label: TextMatch, value: string, options?: FillFieldOptions): Promise<void>;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `pickDate` — set a design-system `DatePicker` to an ISO date by typing into
|
|
306
|
+
* its masked text input (ADR-014, W4-26).
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/** Options for `pickDate`. @experimental */
|
|
310
|
+
interface PickDateOptions {
|
|
311
|
+
/**
|
|
312
|
+
* The picker's display pattern — `MM`, `DD` and `YYYY` around literal
|
|
313
|
+
* separators, as passed to `DatePicker`'s `dateFormat` — when the helper
|
|
314
|
+
* cannot read it from the input's placeholder (a custom `placeholder`).
|
|
315
|
+
* Default: the placeholder, else `"MM/DD/YYYY"`.
|
|
316
|
+
*/
|
|
317
|
+
dateFormat?: string;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Set a `DatePicker` to `iso` (`YYYY-MM-DD`) by typing the date into its text
|
|
321
|
+
* input in the picker's own display pattern. `""` clears it.
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* await pickDate(screen, "Due date", "2025-03-14"); // input named by its Field label
|
|
325
|
+
* await pickDate(screen.getByRole("textbox", { name: "Due date" }), "2025-03-14");
|
|
326
|
+
* await pickDate(screen, "Fällig am", "2025-03-14", { dateFormat: "DD.MM.YYYY" });
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* The pattern is read from the input's placeholder, which `DatePicker` sets to
|
|
330
|
+
* its pattern (`MM/DD/YYYY` for `en-US`, `DD.MM.YYYY` for `de-DE`, a custom
|
|
331
|
+
* `dateFormat`); pass `dateFormat` when the picker has its own placeholder. A
|
|
332
|
+
* year picker (`picker="year"`, placeholder `YYYY`) receives the year. The
|
|
333
|
+
* helper checks that the input shows the typed date afterwards, so a picker
|
|
334
|
+
* that masked it differently — a pattern mismatch — throws instead of
|
|
335
|
+
* submitting a wrong value. `onValueChange` / `onChange` fire and a `name`d
|
|
336
|
+
* picker updates its form value, as for a real user.
|
|
337
|
+
*
|
|
338
|
+
* @experimental
|
|
339
|
+
*/
|
|
340
|
+
declare function pickDate(input: HTMLElement, iso: string, options?: PickDateOptions): Promise<void>;
|
|
341
|
+
/**
|
|
342
|
+
* Set the `DatePicker` whose text input has the accessible name `label`
|
|
343
|
+
* (found through `queries`, for example Testing Library's `screen`) to `iso`.
|
|
344
|
+
*
|
|
345
|
+
* @experimental
|
|
346
|
+
*/
|
|
347
|
+
declare function pickDate(queries: RoleQueries, label: TextMatch, iso: string, options?: PickDateOptions): Promise<void>;
|
|
348
|
+
|
|
349
|
+
export { type AssureJestOptions, type DomPolyfill, type DomPolyfillOptions, type FillFieldOptions, type InteractionOptions, type JestConfigInput, type JestConfigLike, type MenuHandle, type PickDateOptions, type RecordedToast, type RecordedToastVariant, type RoleQueries, type TextMatch, type ToastRecorder, type ToastRecorderOptions, type WithAssureDesignSystem, createToastRecorder, fillField, installDomPolyfills, openMenu, pickDate, selectOption, withAssureDesignSystem };
|
package/dist/testing/index.js
CHANGED
|
@@ -310,14 +310,28 @@ function accessibleText(element) {
|
|
|
310
310
|
function isDisabled(element) {
|
|
311
311
|
return element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true" || element.hasAttribute("data-disabled");
|
|
312
312
|
}
|
|
313
|
+
function isQueries(target) {
|
|
314
|
+
return typeof target.getByRole === "function";
|
|
315
|
+
}
|
|
313
316
|
function press(target, key) {
|
|
314
317
|
const view = target.ownerDocument.defaultView ?? window;
|
|
315
318
|
const init = { key, code: key === " " ? "Space" : key, bubbles: true, cancelable: true };
|
|
316
319
|
target.dispatchEvent(new view.KeyboardEvent("keydown", init));
|
|
317
320
|
target.dispatchEvent(new view.KeyboardEvent("keyup", init));
|
|
318
321
|
}
|
|
322
|
+
async function run(fn) {
|
|
323
|
+
const actEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
|
|
324
|
+
if (actEnvironment) {
|
|
325
|
+
await act(async () => {
|
|
326
|
+
await fn();
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
await fn();
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
332
|
+
}
|
|
319
333
|
async function flush() {
|
|
320
|
-
await
|
|
334
|
+
await run(async () => {
|
|
321
335
|
await Promise.resolve();
|
|
322
336
|
});
|
|
323
337
|
}
|
|
@@ -330,7 +344,7 @@ async function waitUntil(find, maxFlushes) {
|
|
|
330
344
|
return null;
|
|
331
345
|
}
|
|
332
346
|
function resolveTrigger(target, name, role) {
|
|
333
|
-
if (
|
|
347
|
+
if (isQueries(target)) {
|
|
334
348
|
if (name === void 0)
|
|
335
349
|
throw new Error(`A ${role} name is required when passing a query object.`);
|
|
336
350
|
return target.getByRole(role, { name });
|
|
@@ -348,8 +362,33 @@ function popupOf(trigger, role) {
|
|
|
348
362
|
}
|
|
349
363
|
return null;
|
|
350
364
|
}
|
|
365
|
+
function isTextControl(element) {
|
|
366
|
+
const tag = element.tagName;
|
|
367
|
+
return tag === "TEXTAREA" || tag === "INPUT" && !/^(checkbox|radio|file|button|submit|reset|image|hidden)$/i.test(
|
|
368
|
+
element.type
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async function typeInto(control, text2, commit) {
|
|
372
|
+
const view = control.ownerDocument.defaultView ?? window;
|
|
373
|
+
const proto = control.tagName === "TEXTAREA" ? view.HTMLTextAreaElement.prototype : view.HTMLInputElement.prototype;
|
|
374
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
375
|
+
await run(() => {
|
|
376
|
+
control.focus();
|
|
377
|
+
if (setter) setter.call(control, text2);
|
|
378
|
+
else control.value = text2;
|
|
379
|
+
control.dispatchEvent(new view.Event("input", { bubbles: true }));
|
|
380
|
+
});
|
|
381
|
+
if (commit) {
|
|
382
|
+
await run(() => {
|
|
383
|
+
control.blur();
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
await flush();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/testing/interactions.ts
|
|
351
390
|
async function selectOption(target, ...rest) {
|
|
352
|
-
const byQuery =
|
|
391
|
+
const byQuery = isQueries(target);
|
|
353
392
|
const label = byQuery ? rest[0] : void 0;
|
|
354
393
|
const option = byQuery ? rest[1] : rest[0];
|
|
355
394
|
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
@@ -361,7 +400,7 @@ async function selectOption(target, ...rest) {
|
|
|
361
400
|
);
|
|
362
401
|
}
|
|
363
402
|
if (trigger.getAttribute("aria-expanded") !== "true") {
|
|
364
|
-
await
|
|
403
|
+
await run(() => {
|
|
365
404
|
trigger.focus();
|
|
366
405
|
press(trigger, "Enter");
|
|
367
406
|
});
|
|
@@ -376,16 +415,16 @@ async function selectOption(target, ...rest) {
|
|
|
376
415
|
const wanted = all.find((el) => matches(accessibleText(el), option));
|
|
377
416
|
if (!wanted) {
|
|
378
417
|
const available = all.map((el) => JSON.stringify(accessibleText(el))).join(", ");
|
|
379
|
-
await
|
|
418
|
+
await run(() => press(listbox, "Escape"));
|
|
380
419
|
throw new Error(
|
|
381
420
|
`selectOption: no option matches ${describe(option)}. Options: ${available || "(none)"}.`
|
|
382
421
|
);
|
|
383
422
|
}
|
|
384
423
|
if (isDisabled(wanted)) {
|
|
385
|
-
await
|
|
424
|
+
await run(() => press(listbox, "Escape"));
|
|
386
425
|
throw new Error(`selectOption: the option ${describe(option)} is disabled.`);
|
|
387
426
|
}
|
|
388
|
-
await
|
|
427
|
+
await run(() => {
|
|
389
428
|
wanted.focus();
|
|
390
429
|
press(wanted, "Enter");
|
|
391
430
|
});
|
|
@@ -394,7 +433,7 @@ async function selectOption(target, ...rest) {
|
|
|
394
433
|
throw new Error(`selectOption: the list stayed open after choosing ${describe(option)}.`);
|
|
395
434
|
}
|
|
396
435
|
async function openMenu(target, ...rest) {
|
|
397
|
-
const byQuery =
|
|
436
|
+
const byQuery = isQueries(target);
|
|
398
437
|
const name = byQuery ? rest[0] : void 0;
|
|
399
438
|
const settings = (byQuery ? rest[1] : rest[0]) ?? {};
|
|
400
439
|
const maxFlushes = settings.maxFlushes ?? 20;
|
|
@@ -405,7 +444,7 @@ async function openMenu(target, ...rest) {
|
|
|
405
444
|
);
|
|
406
445
|
}
|
|
407
446
|
if (trigger.getAttribute("aria-expanded") !== "true") {
|
|
408
|
-
await
|
|
447
|
+
await run(() => {
|
|
409
448
|
trigger.focus();
|
|
410
449
|
press(trigger, "ArrowDown");
|
|
411
450
|
});
|
|
@@ -443,17 +482,98 @@ async function openMenu(target, ...rest) {
|
|
|
443
482
|
const item = getItem(itemName);
|
|
444
483
|
if (isDisabled(item))
|
|
445
484
|
throw new Error(`openMenu: the item ${describe(itemName)} is disabled.`);
|
|
446
|
-
await
|
|
485
|
+
await run(() => {
|
|
447
486
|
item.focus();
|
|
448
487
|
press(item, "Enter");
|
|
449
488
|
});
|
|
450
489
|
await waitUntil(() => menu.isConnected ? null : true, maxFlushes);
|
|
451
490
|
},
|
|
452
491
|
async close() {
|
|
453
|
-
await
|
|
492
|
+
await run(() => press(menu, "Escape"));
|
|
454
493
|
await waitClosed("Escape");
|
|
455
494
|
}
|
|
456
495
|
};
|
|
457
496
|
}
|
|
458
497
|
|
|
459
|
-
|
|
498
|
+
// src/testing/helpers/fill-field.ts
|
|
499
|
+
var TEXT_ROLES = ["textbox", "spinbutton", "combobox"];
|
|
500
|
+
function resolveByLabel(queries, label) {
|
|
501
|
+
const errors = [];
|
|
502
|
+
for (const role of TEXT_ROLES) {
|
|
503
|
+
try {
|
|
504
|
+
return queries.getByRole(role, { name: label });
|
|
505
|
+
} catch (error) {
|
|
506
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
throw new Error(
|
|
510
|
+
`fillField: no text control is labelled ${describe(label)} (looked for roles ${TEXT_ROLES.join(", ")}).
|
|
511
|
+
${errors[0] ?? ""}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
async function fillField(target, ...rest) {
|
|
515
|
+
const byQuery = isQueries(target);
|
|
516
|
+
const label = byQuery ? rest[0] : void 0;
|
|
517
|
+
const value = byQuery ? rest[1] : rest[0];
|
|
518
|
+
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
519
|
+
const control = byQuery ? resolveByLabel(target, label) : target;
|
|
520
|
+
const name = label ?? accessibleText(control);
|
|
521
|
+
if (!isTextControl(control)) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`fillField: ${describe(name)} is a <${control.tagName.toLowerCase()}>, not a text input or textarea. Use selectOption for a Select and pickDate for a DatePicker.`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (isDisabled(control)) throw new Error(`fillField: the control ${describe(name)} is disabled.`);
|
|
527
|
+
if (control.readOnly || control.getAttribute("aria-readonly") === "true") {
|
|
528
|
+
throw new Error(`fillField: the control ${describe(name)} is read-only.`);
|
|
529
|
+
}
|
|
530
|
+
await typeInto(control, value, settings.commit ?? true);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/testing/helpers/pick-date.ts
|
|
534
|
+
var ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
535
|
+
var DEFAULT_PATTERN = "MM/DD/YYYY";
|
|
536
|
+
function looksLikePattern(text2) {
|
|
537
|
+
return !!text2 && /YYYY/.test(text2) && /MM/.test(text2) === /DD/.test(text2);
|
|
538
|
+
}
|
|
539
|
+
function formatIso(iso, pattern) {
|
|
540
|
+
const match = ISO.exec(iso);
|
|
541
|
+
if (!match) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`pickDate: expected an ISO date (YYYY-MM-DD), received ${JSON.stringify(iso)}.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const [, year, month, day] = match;
|
|
547
|
+
return pattern.replace("YYYY", year).replace("MM", month).replace("DD", day);
|
|
548
|
+
}
|
|
549
|
+
async function pickDate(target, ...rest) {
|
|
550
|
+
const byQuery = isQueries(target);
|
|
551
|
+
const label = byQuery ? rest[0] : void 0;
|
|
552
|
+
const iso = byQuery ? rest[1] : rest[0];
|
|
553
|
+
const settings = (byQuery ? rest[2] : rest[1]) ?? {};
|
|
554
|
+
const input = byQuery ? target.getByRole("textbox", { name: label }) : target;
|
|
555
|
+
const name = label ?? accessibleText(input);
|
|
556
|
+
if (!isTextControl(input) || input.tagName !== "INPUT") {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`pickDate: ${describe(name)} is not a DatePicker text input (a <${input.tagName.toLowerCase()}>).`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (isDisabled(input))
|
|
562
|
+
throw new Error(`pickDate: the date picker ${describe(name)} is disabled.`);
|
|
563
|
+
if (input.readOnly) throw new Error(`pickDate: the date picker ${describe(name)} is read-only.`);
|
|
564
|
+
const placeholder = input.getAttribute("placeholder");
|
|
565
|
+
const pattern = settings.dateFormat ?? (looksLikePattern(placeholder) ? placeholder : DEFAULT_PATTERN);
|
|
566
|
+
const yearOnly = !/MM|DD/.test(pattern);
|
|
567
|
+
let display = "";
|
|
568
|
+
if (iso !== "") {
|
|
569
|
+
display = yearOnly ? formatIso(iso, "YYYY") : formatIso(iso, pattern);
|
|
570
|
+
}
|
|
571
|
+
await typeInto(input, display, true);
|
|
572
|
+
if (input.value !== display) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`pickDate: typed ${JSON.stringify(display)} into ${describe(name)} but it shows ${JSON.stringify(input.value)}. The picker's pattern is probably not ${JSON.stringify(pattern)}; pass { dateFormat } with the pattern the picker uses.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export { createToastRecorder, fillField, installDomPolyfills, openMenu, pickDate, selectOption, withAssureDesignSystem };
|