@redrockswebdevelopment/formstack-form-testing 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -290,6 +290,368 @@ function matchesPattern(value, pattern) {
290
290
  function isIgnored(value, patterns) {
291
291
  return patterns?.some((pattern) => matchesPattern(value, pattern)) ?? false;
292
292
  }
293
+ function liveFormTargetMatches(source, event) {
294
+ if (source.internalLabel && event.internalLabel)
295
+ return source.internalLabel === event.internalLabel;
296
+ if (source.internalLabel && event.target.internalLabel)
297
+ return source.internalLabel === event.target.internalLabel;
298
+ if (source.fieldId != null && event.fieldId != null)
299
+ return String(source.fieldId) === String(event.fieldId);
300
+ if (source.fieldId != null && event.target.fieldId != null)
301
+ return String(source.fieldId) === String(event.target.fieldId);
302
+ return false;
303
+ }
304
+ function createLiveFormEffectTransactionId(prefix = "fspt-effect") {
305
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
306
+ }
307
+ export function createLiveFormEffectController(adapter, options = {}) {
308
+ const startedAt = adapter.now?.() ?? Date.now();
309
+ const now = () => adapter.now?.() ?? Date.now();
310
+ const effects = new Map();
311
+ const traces = [];
312
+ const runCounts = new Map();
313
+ const latestSequenceByEffect = new Map();
314
+ const maxRunsPerTransaction = options.maxRunsPerTransaction ?? 25;
315
+ const emitTrace = (kind, detail, effectId, transactionId) => {
316
+ const event = {
317
+ kind,
318
+ at: new Date().toISOString(),
319
+ elapsedMs: Math.round(now() - startedAt),
320
+ ...(effectId ? { effectId } : {}),
321
+ ...(transactionId ? { transactionId } : {}),
322
+ detail,
323
+ };
324
+ if (options.trace !== false)
325
+ traces.push(event);
326
+ options.onTrace?.(event);
327
+ };
328
+ const runEffect = async (effect, event) => {
329
+ const transactionId = event.transactionId ?? createLiveFormEffectTransactionId();
330
+ if (effect.ignoreTransactions?.includes(transactionId)) {
331
+ emitTrace("effect-skip", { reason: "ignored-transaction" }, effect.id, transactionId);
332
+ return;
333
+ }
334
+ const runKey = `${transactionId}:${effect.id}`;
335
+ const runIndex = (runCounts.get(runKey) ?? 0) + 1;
336
+ runCounts.set(runKey, runIndex);
337
+ if (runIndex > maxRunsPerTransaction) {
338
+ emitTrace("effect-skip", { reason: "max-runs-exceeded", runIndex, maxRunsPerTransaction }, effect.id, transactionId);
339
+ return;
340
+ }
341
+ const sequence = (latestSequenceByEffect.get(effect.id) ?? 0) + 1;
342
+ latestSequenceByEffect.set(effect.id, sequence);
343
+ const waitForPhase = async () => {
344
+ if (effect.phase === "afterQuiet" || effect.phase === "afterValidation")
345
+ await adapter.waitForQuiet?.();
346
+ if (effect.phase === "afterValidation")
347
+ await adapter.validate?.({ validate: true });
348
+ };
349
+ emitTrace("effect-start", { phase: effect.phase ?? "afterValue", runIndex }, effect.id, transactionId);
350
+ try {
351
+ await waitForPhase();
352
+ if (effect.latestOnly !== false && latestSequenceByEffect.get(effect.id) !== sequence) {
353
+ emitTrace("effect-skip", { reason: "stale-run", sequence }, effect.id, transactionId);
354
+ return;
355
+ }
356
+ const context = {
357
+ event,
358
+ effectId: effect.id,
359
+ transactionId,
360
+ runIndex,
361
+ readField: (target) => adapter.readField(target),
362
+ writeField: async (target, value, writeOptions = {}) => {
363
+ const writeTransactionId = writeOptions.transactionId ?? transactionId;
364
+ emitTrace("write", {
365
+ target,
366
+ value,
367
+ source: writeOptions.source ?? effect.id,
368
+ suppressEffects: writeOptions.suppressEffects ?? false,
369
+ }, effect.id, writeTransactionId);
370
+ await adapter.writeField(target, value, {
371
+ ...writeOptions,
372
+ source: writeOptions.source ?? effect.id,
373
+ transactionId: writeTransactionId,
374
+ });
375
+ },
376
+ waitForQuiet: async (quietOptions) => {
377
+ await adapter.waitForQuiet?.(quietOptions);
378
+ },
379
+ validate: async (validationOptions) => adapter.validate?.(validationOptions),
380
+ trace: (kind, detail = {}) => emitTrace(kind, detail, effect.id, transactionId),
381
+ };
382
+ await effect.handler(context);
383
+ emitTrace("effect-complete", { runIndex }, effect.id, transactionId);
384
+ }
385
+ catch (error) {
386
+ emitTrace("effect-error", { message: error instanceof Error ? error.message : String(error) }, effect.id, transactionId);
387
+ throw error;
388
+ }
389
+ };
390
+ const emit = async (event) => {
391
+ emitTrace("change", {
392
+ target: event.target,
393
+ fieldId: event.fieldId ?? null,
394
+ internalLabel: event.internalLabel ?? null,
395
+ source: event.source ?? null,
396
+ }, undefined, event.transactionId);
397
+ const matching = [...effects.values()].filter((effect) => effect.sources.some((source) => liveFormTargetMatches(source, event)));
398
+ for (const effect of matching)
399
+ await runEffect(effect, event);
400
+ };
401
+ const unsubscribe = adapter.onFieldChange((event) => {
402
+ void emit(event);
403
+ });
404
+ return {
405
+ register: (effect) => {
406
+ if (effects.has(effect.id))
407
+ throw new Error(`Live form effect "${effect.id}" is already registered.`);
408
+ effects.set(effect.id, effect);
409
+ emitTrace("registered", { sources: effect.sources, phase: effect.phase ?? "afterValue" }, effect.id);
410
+ return () => {
411
+ effects.delete(effect.id);
412
+ };
413
+ },
414
+ emit,
415
+ writeField: async (target, value, writeOptions = {}) => {
416
+ const transactionId = writeOptions.transactionId ?? createLiveFormEffectTransactionId();
417
+ emitTrace("write", { target, value, source: writeOptions.source ?? "controller", suppressEffects: writeOptions.suppressEffects ?? false }, undefined, transactionId);
418
+ await adapter.writeField(target, value, { ...writeOptions, transactionId });
419
+ },
420
+ readField: (target) => adapter.readField(target),
421
+ waitForQuiet: async (quietOptions) => {
422
+ await adapter.waitForQuiet?.(quietOptions);
423
+ },
424
+ validate: async (validationOptions) => adapter.validate?.(validationOptions),
425
+ traces: () => [...traces],
426
+ dispose: () => {
427
+ effects.clear();
428
+ unsubscribe();
429
+ },
430
+ };
431
+ }
432
+ export function createFormstackLiveFormEffectAdapter(options = {}) {
433
+ const readApi = () => {
434
+ const globalObject = globalThis;
435
+ const factory = globalObject.window && typeof globalObject.window === "object"
436
+ ? Reflect.get(globalObject.window, "fsApi")
437
+ : Reflect.get(globalObject, "fsApi");
438
+ const api = typeof factory === "function" ? Reflect.apply(factory, globalObject.window ?? globalObject, []) : null;
439
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
440
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
441
+ return Array.isArray(forms) ? forms[0] ?? null : null;
442
+ };
443
+ const waitForForm = async () => {
444
+ const timeoutMs = options.waitForApiTimeoutMs ?? 10_000;
445
+ const deadline = Date.now() + timeoutMs;
446
+ let form = readApi();
447
+ while ((!form || typeof form !== "object") && Date.now() < deadline) {
448
+ await delay(50);
449
+ form = readApi();
450
+ }
451
+ if (!form || typeof form !== "object")
452
+ throw new Error("Formstack Live Form API did not become available.");
453
+ return form;
454
+ };
455
+ const resolveField = async (target) => {
456
+ const form = await waitForForm();
457
+ const getField = Reflect.get(form, "getField");
458
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
459
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function") {
460
+ const field = Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
461
+ if (field)
462
+ return field;
463
+ }
464
+ if (target.fieldId != null && typeof getField === "function")
465
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
466
+ return null;
467
+ };
468
+ const readFieldValue = (field) => {
469
+ if (!field || typeof field !== "object")
470
+ return null;
471
+ const getValue = Reflect.get(field, "getValue");
472
+ return typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
473
+ };
474
+ const readFieldId = (field) => {
475
+ if (!field || typeof field !== "object")
476
+ return null;
477
+ const getId = Reflect.get(field, "getId");
478
+ if (typeof getId === "function")
479
+ return Reflect.apply(getId, field, []);
480
+ return Reflect.get(field, "id") ?? null;
481
+ };
482
+ const listeners = new Set();
483
+ let formListenerDispose = null;
484
+ const ensureFormListener = async () => {
485
+ if (formListenerDispose)
486
+ return;
487
+ const form = await waitForForm();
488
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
489
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
490
+ if (typeof registerFormEventListener !== "function")
491
+ return;
492
+ const formEventListener = {
493
+ type: "change",
494
+ onFormEvent: async (event) => {
495
+ const eventObject = event && typeof event === "object" ? event : {};
496
+ const data = Reflect.get(eventObject, "data");
497
+ const dataObject = data && typeof data === "object" ? data : {};
498
+ const fieldId = Reflect.get(dataObject, "fieldId") ?? null;
499
+ const field = fieldId == null ? null : typeof Reflect.get(form, "getField") === "function"
500
+ ? Reflect.apply(Reflect.get(form, "getField"), form, [String(fieldId)])
501
+ : null;
502
+ const currentValue = readFieldValue(field);
503
+ const internalLabel = field && typeof field === "object"
504
+ ? Reflect.get(field, "internalLabel") ?? null
505
+ : null;
506
+ const changeEvent = {
507
+ target: fieldId == null ? {} : { fieldId },
508
+ fieldId,
509
+ internalLabel,
510
+ currentValue,
511
+ source: Reflect.get(dataObject, "type") ?? "formstack-change",
512
+ };
513
+ for (const listener of listeners)
514
+ listener(changeEvent);
515
+ },
516
+ };
517
+ Reflect.apply(registerFormEventListener, form, [formEventListener]);
518
+ formListenerDispose = () => {
519
+ if (typeof unregisterFormEventListener === "function")
520
+ Reflect.apply(unregisterFormEventListener, form, [formEventListener]);
521
+ formListenerDispose = null;
522
+ };
523
+ };
524
+ return {
525
+ readField: async (target) => readFieldValue(await resolveField(target)),
526
+ writeField: async (target, value, writeOptions = {}) => {
527
+ const form = await waitForForm();
528
+ const field = await resolveField(target);
529
+ if (!field || typeof field !== "object")
530
+ throw new Error(`Unable to resolve Formstack field ${target.internalLabel ?? target.fieldId ?? "unknown"}.`);
531
+ const setValue = Reflect.get(field, "setValue");
532
+ if (typeof setValue !== "function")
533
+ throw new Error("Resolved Formstack field does not support setValue().");
534
+ const previousValue = readFieldValue(field);
535
+ await Promise.resolve(Reflect.apply(setValue, field, [value]));
536
+ if (options.notifyWrites ?? true) {
537
+ const notifyFormEventListeners = Reflect.get(form, "notifyFormEventListeners");
538
+ if (typeof notifyFormEventListeners === "function") {
539
+ await Promise.resolve(Reflect.apply(notifyFormEventListeners, form, [
540
+ "change",
541
+ {
542
+ fieldId: readFieldId(field),
543
+ type: writeOptions.source ?? "effect-write",
544
+ transactionId: writeOptions.transactionId,
545
+ suppressEffects: writeOptions.suppressEffects ?? false,
546
+ value,
547
+ },
548
+ ]));
549
+ }
550
+ }
551
+ if (!writeOptions.suppressEffects) {
552
+ const currentValue = readFieldValue(field);
553
+ const event = {
554
+ target,
555
+ fieldId: readFieldId(field),
556
+ internalLabel: target.internalLabel ?? null,
557
+ previousValue,
558
+ currentValue,
559
+ source: writeOptions.source ?? "effect-write",
560
+ ...(writeOptions.transactionId ? { transactionId: writeOptions.transactionId } : {}),
561
+ };
562
+ for (const listener of listeners)
563
+ listener(event);
564
+ }
565
+ },
566
+ onFieldChange: (listener) => {
567
+ listeners.add(listener);
568
+ void ensureFormListener();
569
+ return () => {
570
+ listeners.delete(listener);
571
+ if (listeners.size === 0)
572
+ formListenerDispose?.();
573
+ };
574
+ },
575
+ waitForQuiet: async (quietOptions = {}) => {
576
+ await delay(quietOptions.quietMs ?? options.quietMs ?? 250);
577
+ },
578
+ validate: async (validationOptions = {}) => {
579
+ const form = await waitForForm();
580
+ if (!validationOptions.validate)
581
+ return null;
582
+ const validateForm = Reflect.get(form, "validateForm");
583
+ return typeof validateForm === "function" ? Reflect.apply(validateForm, form, []) : null;
584
+ },
585
+ now: () => Date.now(),
586
+ };
587
+ }
588
+ function liveFormEffectValuesEqual(left, right) {
589
+ if (Object.is(left, right))
590
+ return true;
591
+ try {
592
+ return JSON.stringify(left) === JSON.stringify(right);
593
+ }
594
+ catch {
595
+ return false;
596
+ }
597
+ }
598
+ export function deriveLiveFormField(options) {
599
+ return {
600
+ id: options.id,
601
+ sources: options.sources,
602
+ phase: options.phase ?? "afterQuiet",
603
+ latestOnly: options.latestOnly ?? true,
604
+ handler: async (context) => {
605
+ const values = await Promise.all(options.sources.map((source) => context.readField(source)));
606
+ const nextValue = await options.compute(values, context);
607
+ const currentValue = await context.readField(options.target);
608
+ const shouldWrite = options.shouldWrite?.(nextValue, currentValue) ?? !liveFormEffectValuesEqual(nextValue, currentValue);
609
+ if (!shouldWrite) {
610
+ context.trace("effect-skip", { reason: "derived-value-unchanged", target: options.target });
611
+ return;
612
+ }
613
+ await context.writeField(options.target, nextValue, options.suppressEffects === undefined ? {} : { suppressEffects: options.suppressEffects });
614
+ },
615
+ };
616
+ }
617
+ export function resetLiveFormDependents(options) {
618
+ return {
619
+ id: options.id,
620
+ sources: options.sources,
621
+ phase: options.phase ?? "afterQuiet",
622
+ handler: async (context) => {
623
+ for (const dependent of options.dependents) {
624
+ await context.writeField(dependent, options.emptyValue ?? "", {
625
+ suppressEffects: options.suppressEffects ?? true,
626
+ });
627
+ }
628
+ },
629
+ };
630
+ }
631
+ export function gateLiveFormSubmit(options) {
632
+ return {
633
+ id: options.id,
634
+ sources: options.sources,
635
+ phase: options.phase ?? "afterQuiet",
636
+ latestOnly: true,
637
+ handler: async (context) => {
638
+ const values = await Promise.all(options.sources.map((source) => context.readField(source)));
639
+ const ready = await options.isReady(values, context);
640
+ const selector = options.submitSelector ?? ".fsSubmitButton, [type=submit]";
641
+ const documentObject = globalThis.document;
642
+ if (!documentObject?.querySelectorAll) {
643
+ context.trace("effect-skip", { reason: "document-unavailable", ready, selector });
644
+ return;
645
+ }
646
+ for (const element of Array.from(documentObject.querySelectorAll(selector))) {
647
+ element.toggleAttribute("disabled", !ready);
648
+ element.setAttribute("aria-disabled", ready ? "false" : "true");
649
+ element.dataset.fsptSubmitReady = ready ? "true" : "false";
650
+ }
651
+ context.trace("effect-complete", { kind: "submit-gate", ready, selector });
652
+ },
653
+ };
654
+ }
293
655
  function defaultDiagnosticsOptions(options = {}) {
294
656
  return {
295
657
  failOnConsoleTypes: options.failOnConsoleTypes ?? ["error"],
@@ -1566,6 +1928,157 @@ export async function waitForWindowFlag(page, options) {
1566
1928
  };
1567
1929
  }, options);
1568
1930
  }
1931
+ export async function installLiveFormEffectTraceProbe(page, options) {
1932
+ if (!hasEvaluate(page)) {
1933
+ throw new Error("installLiveFormEffectTraceProbe requires a Playwright page with evaluate().");
1934
+ }
1935
+ return page.evaluate(async (probeOptions) => {
1936
+ const propertyName = probeOptions.propertyName ?? "__fsptLiveFormEffectProbe";
1937
+ const effectId = probeOptions.effectId ?? "fspt-effect-probe";
1938
+ const startedAt = Date.now();
1939
+ const traces = [];
1940
+ const record = (kind, detail, transactionId) => {
1941
+ traces.push({
1942
+ kind,
1943
+ at: new Date().toISOString(),
1944
+ elapsedMs: Date.now() - startedAt,
1945
+ effectId,
1946
+ ...(transactionId ? { transactionId } : {}),
1947
+ detail,
1948
+ });
1949
+ };
1950
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1951
+ const readApi = () => {
1952
+ const factory = Reflect.get(window, "fsApi");
1953
+ const api = typeof factory === "function" ? Reflect.apply(factory, window, []) : null;
1954
+ const getForms = api && typeof api === "object" ? Reflect.get(api, "getForms") : null;
1955
+ const forms = typeof getForms === "function" ? Reflect.apply(getForms, api, []) : [];
1956
+ return Array.isArray(forms) ? forms[0] ?? null : null;
1957
+ };
1958
+ const waitForForm = async () => {
1959
+ const deadline = Date.now() + 10_000;
1960
+ let form = readApi();
1961
+ while ((!form || typeof form !== "object") && Date.now() < deadline) {
1962
+ await wait(50);
1963
+ form = readApi();
1964
+ }
1965
+ if (!form || typeof form !== "object")
1966
+ throw new Error("Formstack Live Form API did not become available.");
1967
+ return form;
1968
+ };
1969
+ const fieldIdFromTarget = async (target) => {
1970
+ if (target.fieldId != null)
1971
+ return target.fieldId;
1972
+ const form = await waitForForm();
1973
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1974
+ const field = target.internalLabel && typeof getFieldByInternalLabel === "function"
1975
+ ? Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel])
1976
+ : null;
1977
+ if (!field || typeof field !== "object")
1978
+ return null;
1979
+ const getId = Reflect.get(field, "getId");
1980
+ return typeof getId === "function" ? Reflect.apply(getId, field, []) : Reflect.get(field, "id");
1981
+ };
1982
+ const resolveField = async (target) => {
1983
+ const form = await waitForForm();
1984
+ const getField = Reflect.get(form, "getField");
1985
+ const getFieldByInternalLabel = Reflect.get(form, "getFieldByInternalLabel");
1986
+ if (target.internalLabel && typeof getFieldByInternalLabel === "function")
1987
+ return Reflect.apply(getFieldByInternalLabel, form, [target.internalLabel]);
1988
+ if (target.fieldId != null && typeof getField === "function")
1989
+ return Reflect.apply(getField, form, [String(target.fieldId)]);
1990
+ return null;
1991
+ };
1992
+ const readValue = (field) => {
1993
+ if (!field || typeof field !== "object")
1994
+ return null;
1995
+ const getValue = Reflect.get(field, "getValue");
1996
+ return typeof getValue === "function" ? Reflect.apply(getValue, field, []) : null;
1997
+ };
1998
+ const writeValue = async (target, value, transactionId) => {
1999
+ const field = await resolveField(target);
2000
+ if (!field || typeof field !== "object")
2001
+ return;
2002
+ const setValue = Reflect.get(field, "setValue");
2003
+ if (typeof setValue !== "function")
2004
+ return;
2005
+ await Promise.resolve(Reflect.apply(setValue, field, [value]));
2006
+ record("write", { target, value }, transactionId);
2007
+ };
2008
+ const form = await waitForForm();
2009
+ const sourceFieldId = await fieldIdFromTarget(probeOptions.source);
2010
+ const registerFormEventListener = Reflect.get(form, "registerFormEventListener");
2011
+ const unregisterFormEventListener = Reflect.get(form, "unregisterFormEventListener");
2012
+ const listener = {
2013
+ type: "change",
2014
+ onFormEvent: async (event) => {
2015
+ const eventObject = event && typeof event === "object" ? event : {};
2016
+ const data = Reflect.get(eventObject, "data");
2017
+ const dataObject = data && typeof data === "object" ? data : {};
2018
+ const changedFieldId = Reflect.get(dataObject, "fieldId");
2019
+ if (sourceFieldId != null && String(changedFieldId ?? "") !== String(sourceFieldId))
2020
+ return;
2021
+ const transactionId = `probe-${Date.now().toString(36)}`;
2022
+ record("change", { fieldId: changedFieldId ?? null, data }, transactionId);
2023
+ record("effect-start", { phase: probeOptions.phase ?? "afterQuiet" }, transactionId);
2024
+ if ((probeOptions.phase ?? "afterQuiet") === "afterQuiet" || probeOptions.phase === "afterValidation")
2025
+ await wait(probeOptions.quietMs ?? 250);
2026
+ if (probeOptions.phase === "afterValidation") {
2027
+ const validateForm = Reflect.get(form, "validateForm");
2028
+ if (typeof validateForm === "function")
2029
+ await Promise.resolve(Reflect.apply(validateForm, form, []));
2030
+ }
2031
+ if (probeOptions.target) {
2032
+ const sourceField = await resolveField(probeOptions.source);
2033
+ await writeValue(probeOptions.target, probeOptions.writeValue ?? readValue(sourceField), transactionId);
2034
+ }
2035
+ record("effect-complete", { target: probeOptions.target ?? null }, transactionId);
2036
+ },
2037
+ };
2038
+ if (typeof registerFormEventListener !== "function")
2039
+ throw new Error("Formstack form event listener API is not available.");
2040
+ Reflect.apply(registerFormEventListener, form, [listener]);
2041
+ Object.defineProperty(window, propertyName, {
2042
+ configurable: true,
2043
+ value: {
2044
+ traces,
2045
+ dispose: () => {
2046
+ if (typeof unregisterFormEventListener === "function")
2047
+ Reflect.apply(unregisterFormEventListener, form, [listener]);
2048
+ },
2049
+ },
2050
+ });
2051
+ record("registered", { source: probeOptions.source, target: probeOptions.target ?? null });
2052
+ return { installed: true, propertyName, effectId };
2053
+ }, options);
2054
+ }
2055
+ export async function readLiveFormEffectTraces(page, options = {}) {
2056
+ if (!hasEvaluate(page)) {
2057
+ throw new Error("readLiveFormEffectTraces requires a Playwright page with evaluate().");
2058
+ }
2059
+ return page.evaluate((readOptions) => {
2060
+ const propertyName = readOptions.propertyName ?? "__fsptLiveFormEffectProbe";
2061
+ const probe = Reflect.get(window, propertyName);
2062
+ return Array.isArray(probe?.traces) ? [...probe.traces] : [];
2063
+ }, options);
2064
+ }
2065
+ export async function waitForLiveFormEffectTrace(page, options = {}) {
2066
+ const timeoutMs = options.timeoutMs ?? 5_000;
2067
+ const intervalMs = options.intervalMs ?? 100;
2068
+ const startedAt = Date.now();
2069
+ const expectedKinds = options.kinds ?? ["effect-complete"];
2070
+ let traces = await readLiveFormEffectTraces(page, options);
2071
+ const matches = () => expectedKinds.every((kind) => traces.some((trace) => trace.kind === kind && (options.effectId === undefined || trace.effectId === options.effectId)));
2072
+ while (!matches() && Date.now() - startedAt < timeoutMs) {
2073
+ await delay(intervalMs);
2074
+ traces = await readLiveFormEffectTraces(page, options);
2075
+ }
2076
+ return {
2077
+ matched: matches(),
2078
+ timedOut: !matches(),
2079
+ traces,
2080
+ };
2081
+ }
1569
2082
  export async function readLiveFormValidationState(page, options = {}) {
1570
2083
  if (!hasEvaluate(page)) {
1571
2084
  throw new Error("readLiveFormValidationState requires a Playwright page with evaluate().");