@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.
@@ -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, breakpoints as b, colors as c, spacing as d, surfaces as e, systemTokens 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 };
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 };
@@ -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 react.act(async () => {
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 (typeof target.getByRole === "function") {
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 = typeof target.getByRole === "function";
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 react.act(async () => {
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 react.act(async () => press(listbox, "Escape"));
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 react.act(async () => press(listbox, "Escape"));
426
+ await run(() => press(listbox, "Escape"));
388
427
  throw new Error(`selectOption: the option ${describe(option)} is disabled.`);
389
428
  }
390
- await react.act(async () => {
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 = typeof target.getByRole === "function";
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 react.act(async () => {
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 react.act(async () => {
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 react.act(async () => press(menu, "Escape"));
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;
@@ -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 `selectOption` and `openMenu`. @experimental */
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
- export { type AssureJestOptions, type DomPolyfill, type DomPolyfillOptions, type InteractionOptions, type JestConfigInput, type JestConfigLike, type MenuHandle, type RecordedToast, type RecordedToastVariant, type RoleQueries, type TextMatch, type ToastRecorder, type ToastRecorderOptions, type WithAssureDesignSystem, createToastRecorder, installDomPolyfills, openMenu, selectOption, withAssureDesignSystem };
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 };
@@ -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 `selectOption` and `openMenu`. @experimental */
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
- export { type AssureJestOptions, type DomPolyfill, type DomPolyfillOptions, type InteractionOptions, type JestConfigInput, type JestConfigLike, type MenuHandle, type RecordedToast, type RecordedToastVariant, type RoleQueries, type TextMatch, type ToastRecorder, type ToastRecorderOptions, type WithAssureDesignSystem, createToastRecorder, installDomPolyfills, openMenu, selectOption, withAssureDesignSystem };
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 };
@@ -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 act(async () => {
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 (typeof target.getByRole === "function") {
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 = typeof target.getByRole === "function";
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 act(async () => {
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 act(async () => press(listbox, "Escape"));
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 act(async () => press(listbox, "Escape"));
424
+ await run(() => press(listbox, "Escape"));
386
425
  throw new Error(`selectOption: the option ${describe(option)} is disabled.`);
387
426
  }
388
- await act(async () => {
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 = typeof target.getByRole === "function";
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 act(async () => {
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 act(async () => {
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 act(async () => press(menu, "Escape"));
492
+ await run(() => press(menu, "Escape"));
454
493
  await waitClosed("Escape");
455
494
  }
456
495
  };
457
496
  }
458
497
 
459
- export { createToastRecorder, installDomPolyfills, openMenu, selectOption, withAssureDesignSystem };
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 };