{"version":3,"file":"form-internals-polyfill.cjs","names":[],"sources":["../../src/testing/form-internals-polyfill.ts"],"sourcesContent":["import { walkFlatTree } from './dom';\n\n/**\n * jsdom implements none of the `ElementInternals` form-association API — not `setFormValue`,\n * not `setValidity`, not the `checkValidity`/`reportValidity`/`validity`/`validationMessage`\n * mixin real browsers put on every `formAssociated: true` element, not even `FormData`\n * collecting a form-associated element's set value, or `<form>.reset()` invoking\n * `formResetCallback()`. Every one of those gaps is exercised by `useField()` (`@vielzeug/ore`),\n * so any package testing a form-associated component needs all of them, not just one.\n *\n * Opt-in via `install(afterEach, { formInternals: true })` (or call this directly) —\n * the patches are global monkey-patches, so suites without form-associated components\n * shouldn't pay for them. Returns an `uninstall()` that restores every patched global.\n *\n * Deliberately lives here rather than in each consuming package's own `vitest.setup.ts`: the\n * gap is in `ore`'s own form-association feature, not in any one consumer, so the fix belongs\n * with the feature. `walkFlatTree` handles form-associated custom elements across shadow\n * boundaries, so consumer packages do not need package-local copies of this polyfill.\n */\nexport const installFormInternalsPolyfill = (): (() => void) => {\n  // Checked *before* consuming the flag below — an environment without `ElementInternals`\n  // installs nothing and should stay eligible to install for real later (e.g. a differently\n  // configured environment in the same process), not get marked \"already installed\" for having\n  // done nothing.\n  if (typeof ElementInternals === 'undefined') return () => {};\n\n  const INSTALLED_FLAG = Symbol.for('vielzeug.ore.testing.formInternalsPolyfillInstalled');\n  const flagHost = globalThis as Record<symbol, boolean | undefined>;\n\n  if (flagHost[INSTALLED_FLAG]) return () => {};\n\n  flagHost[INSTALLED_FLAG] = true;\n\n  // Every patch pushes its inverse here, so uninstall() restores the environment exactly.\n  const restorers: Array<() => void> = [];\n\n  const proto = ElementInternals.prototype as unknown as Record<string, unknown>;\n  const validityState = new WeakMap<ElementInternals, { flags: ValidityStateFlags; message: string }>();\n  const formValueByHost = new WeakMap<HTMLElement, File | FormData | string | null>();\n  const hostByInternals = new WeakMap<ElementInternals, HTMLElement>();\n\n  const patchProto = (name: string, descriptor: PropertyDescriptor): void => {\n    if (name in proto) return;\n\n    Object.defineProperty(proto, name, descriptor);\n    restorers.push(() => delete proto[name]);\n  };\n\n  const isValid = (internals: ElementInternals): boolean => {\n    const state = validityState.get(internals);\n\n    return !state || !Object.values(state.flags).some(Boolean);\n  };\n\n  patchProto('setFormValue', {\n    configurable: true,\n    value: function (this: ElementInternals, value: File | FormData | string | null) {\n      const host = hostByInternals.get(this);\n\n      if (host) formValueByHost.set(host, value);\n    },\n    writable: true,\n  });\n\n  patchProto('setValidity', {\n    configurable: true,\n    // Matches the real platform contract: throws if any flag is true and message is empty.\n    // `useField()` itself already guards against triggering this — see forms/field.ts — but\n    // the polyfill enforcing it too means a test would still catch a *different* caller doing\n    // the same thing wrong, instead of that bug only surfacing in a real browser.\n    value: function (this: ElementInternals, flags: ValidityStateFlags = {}, message = '') {\n      if (Object.values(flags).some(Boolean) && !message) {\n        throw new TypeError(\n          \"Failed to execute 'setValidity' on 'ElementInternals': The second argument must not be empty if \" +\n            'one or more flags in the first argument are true.',\n        );\n      }\n\n      validityState.set(this, { flags, message });\n    },\n    writable: true,\n  });\n\n  patchProto('checkValidity', {\n    configurable: true,\n    value: function (this: ElementInternals) {\n      return isValid(this);\n    },\n    writable: true,\n  });\n\n  patchProto('reportValidity', {\n    configurable: true,\n    value: function (this: ElementInternals) {\n      return isValid(this);\n    },\n    writable: true,\n  });\n\n  patchProto('validationMessage', {\n    configurable: true,\n    get(this: ElementInternals) {\n      return isValid(this) ? '' : (validityState.get(this)?.message ?? '');\n    },\n  });\n\n  // A ValidityState-shaped view of the flags passed to setValidity — every known\n  // flag defaults to false, `valid` reflects whether any flag is set.\n  patchProto('validity', {\n    configurable: true,\n    get(this: ElementInternals): ValidityState {\n      const state = validityState.get(this);\n\n      return {\n        badInput: false,\n        customError: false,\n        patternMismatch: false,\n        rangeOverflow: false,\n        rangeUnderflow: false,\n        stepMismatch: false,\n        tooLong: false,\n        tooShort: false,\n        typeMismatch: false,\n        valid: isValid(this),\n        valueMissing: false,\n        ...(state?.flags ?? {}),\n      } as ValidityState;\n    },\n  });\n\n  patchProto('states', {\n    configurable: true,\n    get: function (this: ElementInternals & { _states?: Set<string> }) {\n      this._states ??= new Set();\n\n      return this._states;\n    },\n  });\n\n  // Real browsers mix `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto any\n  // `formAssociated: true` custom element itself (delegating to its internals) — not just onto\n  // `ElementInternals`. jsdom does neither; mirror it here so a component's own\n  // `checkValidity()`/`reportValidity()`, and a test asserting against the element directly,\n  // behave the same as they would in a real browser.\n  const originalAttachInternals = HTMLElement.prototype.attachInternals;\n\n  HTMLElement.prototype.attachInternals = function (this: HTMLElement, ...args: []): ElementInternals {\n    const internals = originalAttachInternals.apply(this, args);\n\n    hostByInternals.set(internals, this);\n\n    Object.defineProperties(this, {\n      checkValidity: { configurable: true, value: () => internals.checkValidity() },\n      reportValidity: { configurable: true, value: () => internals.reportValidity() },\n      validationMessage: { configurable: true, get: () => internals.validationMessage },\n      validity: { configurable: true, get: () => internals.validity },\n    });\n\n    return internals;\n  };\n  restorers.push(() => {\n    HTMLElement.prototype.attachInternals = originalAttachInternals;\n  });\n\n  const appendFormValue = (formData: FormData, name: string, value: File | FormData | string): void => {\n    if (value instanceof FormData) {\n      for (const [entryName, entryValue] of value.entries()) formData.append(entryName, entryValue);\n\n      return;\n    }\n\n    formData.append(name, value);\n  };\n\n  // jsdom's `FormData` constructor never collects a form-associated custom element's set form\n  // value — real browsers walk the flat tree for every form-associated element with a `name`.\n  const NativeFormData = globalThis.FormData;\n\n  globalThis.FormData = class FormDataWithFormAssociatedElements extends NativeFormData {\n    constructor(form?: HTMLFormElement, submitter?: HTMLElement | null) {\n      super(form as HTMLFormElement | undefined, submitter as HTMLElement | null | undefined);\n\n      if (!(form instanceof HTMLFormElement)) return;\n\n      walkFlatTree(form, (element) => {\n        const name = element.getAttribute('name');\n        const value = formValueByHost.get(element);\n\n        if (!name || value == null || element.hasAttribute('disabled')) return;\n\n        appendFormValue(this, name, value);\n      });\n    }\n  };\n  restorers.push(() => {\n    globalThis.FormData = NativeFormData;\n  });\n\n  // jsdom never invokes the native `formResetCallback` lifecycle method on form-associated\n  // custom elements when their form resets — patch `reset()` to do it (via the same flat-tree\n  // walk, since a reset field can be nested behind a shadow boundary too), so `onFormReset()`\n  // (see `@vielzeug/ore`'s runtime.ts) is testable against a real `<form>.reset()` call instead\n  // of only by calling `element.formResetCallback()` directly.\n  const originalReset = HTMLFormElement.prototype.reset;\n\n  HTMLFormElement.prototype.reset = function (this: HTMLFormElement, ...args: []) {\n    originalReset.apply(this, args);\n\n    walkFlatTree(this, (element) => {\n      (element as Partial<{ formResetCallback: () => void }>).formResetCallback?.();\n    });\n  };\n  restorers.push(() => {\n    HTMLFormElement.prototype.reset = originalReset;\n  });\n\n  return () => {\n    for (const restore of restorers) restore();\n\n    restorers.length = 0;\n    flagHost[INSTALLED_FLAG] = false;\n  };\n};\n"],"mappings":"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"}