From 00daab139c9d1e91893b3a15b9b16880c737d998 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:08:55 -0400 Subject: [PATCH 1/4] Upgrade ember-template-imports to v4; wire up ember-data preview types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ETI v3's babel-plugin ordering left every `precompileTemplate` emitted by gts test files uncompiled, so all 150+ rendering tests failed at runtime with 'Attempted to call precompileTemplate at runtime'. v4 preprocesses gjs/gts via content-tag before babel runs, so ember-cli-htmlbars' template-compilation plugin sees the output within the same pass. Also: - add prettier-plugin-ember-template-tag to the addon package (it runs prettier against the shared root config, which requires the plugin to resolve from the package under pnpm's strict layout) - add @ember-data/model + @ember-data/store devDeps and their unstable-preview-types to test-app's tsconfig so the ember-data 5.8 imports typecheck - verifyDepsBeforeRun: false — pnpm 11 re-verifies (and re-installs) before every run script; with injected workspace deps + dedupeInjectedDeps the regenerated lockfile trips a false-positive ERR_PNPM_OUTDATED_LOCKFILE in the floating-deps CI job Co-Authored-By: Claude Fable 5 --- docs-app/package.json | 13 +++++----- packages/ember-headless-form/package.json | 6 +++-- pnpm-lock.yaml | 29 +++++++++++++++++++---- pnpm-workspace.yaml | 6 +++++ test-app/package.json | 15 +++++++----- test-app/tsconfig.json | 8 ++++++- 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docs-app/package.json b/docs-app/package.json index 01d1f27..1d75d2d 100644 --- a/docs-app/package.json +++ b/docs-app/package.json @@ -30,6 +30,7 @@ "@crowdstrike/tailwind-toucan-base": "^5.0.0", "@docfy/core": "^0.9.0", "@docfy/ember": "^0.8.5", + "@ember/app-tsconfig": "^1.0.0", "@ember/optional-features": "^2.0.0", "@ember/string": "^3.1.1", "@ember/test-helpers": "^3.2.0", @@ -41,8 +42,8 @@ "@glint/ember-tsc": "^1.0.3", "@glint/template": "^1.6.1", "@glint/tsserver-plugin": "^2.0.0", + "@nullvoxpopuli/eslint-configs": "^6.0.0", "@tailwindcss/typography": "^0.5.7", - "@ember/app-tsconfig": "^1.0.0", "@types/ember-qunit": "^6.0.0", "@types/ember-resolver": "^9.0.0", "@types/qunit": "^2.19.3", @@ -66,9 +67,9 @@ "ember-qunit": "^8.0.0", "ember-resolver": "^13.1.0", "ember-source": "~5.12.0", - "ember-template-imports": "^3.1.2", + "ember-template-imports": "^4.4.0", "ember-template-lint": "^4.14.0", - "eslint": "^9.0.0","@nullvoxpopuli/eslint-configs": "^6.0.0", + "eslint": "^9.0.0", "loader.js": "^4.7.0", "postcss": "^8.4.17", "postcss-import": "^16.0.0", @@ -91,12 +92,12 @@ "@crowdstrike/ember-oss-docs": "^1.1.8", "@ember/test-waiters": "^3.0.2 || ^4.0.0", "@embroider/router": "^2.1.6", - "ember-browser-services": "^5.0.0", - "ember-changeset": "^4.1.2", - "ember-changeset-validations": "^4.1.1", "@universal-ember/form": "workspace:*", "@universal-ember/form-changeset": "workspace:*", "@universal-ember/form-yup": "workspace:*", + "ember-browser-services": "^5.0.0", + "ember-changeset": "^4.1.2", + "ember-changeset-validations": "^4.1.1", "ember-modifier": "^4.0.0", "ember-resources": "^7.0.0", "highlight.js": "^11.6.0", diff --git a/packages/ember-headless-form/package.json b/packages/ember-headless-form/package.json index 66b8bb4..4255eb8 100644 --- a/packages/ember-headless-form/package.json +++ b/packages/ember-headless-form/package.json @@ -45,19 +45,21 @@ "@babel/plugin-proposal-decorators": "^7.23.6", "@babel/plugin-syntax-decorators": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@ember/library-tsconfig": "^1.0.0", "@embroider/addon-dev": "^7.0.0", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "@glint/ember-tsc": "^1.0.3", "@glint/template": "^1.6.1", "@glint/tsserver-plugin": "^2.0.0", - "@ember/library-tsconfig": "^1.0.0", + "@nullvoxpopuli/eslint-configs": "^6.0.0", "concurrently": "^9.0.0", "ember-source": "^5.0.0", "ember-template-imports": "^3.4.1", "ember-template-lint": "^4.0.0", - "eslint": "^9.0.0","@nullvoxpopuli/eslint-configs": "^6.0.0", + "eslint": "^9.0.0", "prettier": "^3.8.1", + "prettier-plugin-ember-template-tag": "^2.1.7", "rollup": "^4.0.0", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-glimmer-template-tag": "^0.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c63158..5cb5781 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,8 +211,8 @@ importers: specifier: ~5.12.0 version: 5.12.0(627b12eebd4b8157321ad813eae7fb14) ember-template-imports: - specifier: ^3.1.2 - version: 3.4.2(supports-color@8.1.1) + specifier: ^4.4.0 + version: 4.4.0(supports-color@8.1.1) ember-template-lint: specifier: ^4.14.0 version: 4.18.2(supports-color@8.1.1) @@ -413,6 +413,9 @@ importers: prettier: specifier: ^3.8.1 version: 3.9.5 + prettier-plugin-ember-template-tag: + specifier: ^2.1.7 + version: 2.1.7(prettier@3.9.5)(supports-color@8.1.1) rollup: specifier: ^4.0.0 version: 4.62.2 @@ -523,6 +526,12 @@ importers: '@universal-ember/form-yup': injected: true devDependencies: + '@ember-data/model': + specifier: ^5.8.2 + version: 5.8.2(441692fd1e7dbc4a7b3cba7430ac7990) + '@ember-data/store': + specifier: ^5.8.2 + version: 5.8.2(5f2fde677cb60ab7cf61ce31646930bf) '@ember/app-tsconfig': specifier: ^1.0.0 version: 1.0.3 @@ -659,8 +668,8 @@ importers: specifier: ^3.0.0 version: 3.0.0(encoding@0.1.13) ember-template-imports: - specifier: ^3.4.0 - version: 3.4.2(supports-color@8.1.1) + specifier: ^4.4.0 + version: 4.4.0(supports-color@8.1.1) ember-template-lint: specifier: ^5.2.0 version: 5.13.0(supports-color@8.1.1) @@ -4540,6 +4549,10 @@ packages: resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} engines: {node: 12.* || >= 14} + ember-template-imports@4.4.0: + resolution: {integrity: sha512-HNOHabTEMbRluci1uScvh3ljMDo9E46dHHNcJAIf5yjOhIQ/zN4Y0DVDWrRfcbihlHvt4v/iF69G+8tffC1YkA==} + engines: {node: 16.* || >= 18} + ember-template-lint@4.18.2: resolution: {integrity: sha512-yI8kQ8IQ2x5HVq0tQAISXABOHr0Is5sAg6rwceO6M8CYozq7HMxUPEj0VbdcbyIE70SWw/8d24M1rBI4km544Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -14513,6 +14526,14 @@ snapshots: transitivePeerDependencies: - supports-color + ember-template-imports@4.4.0(supports-color@8.1.1): + dependencies: + broccoli-stew: 3.0.0(supports-color@8.1.1) + content-tag: 4.2.0 + ember-cli-version-checker: 5.1.2(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + ember-template-lint@4.18.2(supports-color@8.1.1): dependencies: '@lint-todo/utils': 13.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fde96bf..a73e7c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,6 +33,12 @@ injectWorkspacePackages: true # not ensuring folks are actually compatible with declared ranges. resolutionMode: highest +# pnpm 11 re-checks (and re-runs) install before every `pnpm run`. +# With injected workspace deps + dedupeInjectedDeps the regenerated +# lockfile trips a false-positive ERR_PNPM_OUTDATED_LOCKFILE in CI +# (e.g. the floating-deps job), so run scripts against what's installed. +verifyDepsBeforeRun: false + # Force browserslist-generator v3+ so that rollup-plugin-ts loads under Node 24. # v2.x uses the removed `assert { type: 'json' }` import assertion syntax; # v3.x uses the standard `with { type: 'json' }` import attribute syntax. diff --git a/test-app/package.json b/test-app/package.json index 99a4af1..13e8b71 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -27,6 +27,9 @@ "typecheck": "pnpm tsc -v; pnpm ember-tsc --version; pnpm lint:types" }, "devDependencies": { + "@ember-data/model": "^5.8.2", + "@ember-data/store": "^5.8.2", + "@ember/app-tsconfig": "^1.0.0", "@ember/optional-features": "^2.0.0", "@ember/string": "^3.1.1", "@ember/test-helpers": "^3.2.1", @@ -37,13 +40,16 @@ "@glint/ember-tsc": "^1.0.3", "@glint/template": "^1.6.1", "@glint/tsserver-plugin": "^2.0.0", - "@ember/app-tsconfig": "^1.0.0", + "@nullvoxpopuli/eslint-configs": "^6.0.0", "@types/babel__traverse": "^7.18.3", "@types/ember-qunit": "^6.1.1", "@types/ember-resolver": "^9.0.0", "@types/qunit": "^2.19.4", "@types/rsvp": "^4.0.4", "@types/sinon": "^17.0.0", + "@universal-ember/form": "workspace:*", + "@universal-ember/form-changeset": "workspace:*", + "@universal-ember/form-yup": "workspace:*", "broccoli-asset-rev": "^3.0.0", "concurrently": "^9.0.0", "ember-a11y-testing": "^7.0.0", @@ -61,9 +67,6 @@ "ember-data": "^5.3.0", "ember-disable-prototype-extensions": "^1.1.3", "ember-fetch": "^8.1.2", - "@universal-ember/form": "workspace:*", - "@universal-ember/form-changeset": "workspace:*", - "@universal-ember/form-yup": "workspace:*", "ember-load-initializers": "^3.0.1", "ember-page-title": "^9.0.0", "ember-qunit": "^8.0.0", @@ -71,10 +74,10 @@ "ember-sinon-qunit": "^7.0.0", "ember-source": "~5.12.0", "ember-source-channel-url": "^3.0.0", - "ember-template-imports": "^3.4.0", + "ember-template-imports": "^4.4.0", "ember-template-lint": "^5.2.0", "ember-try": "^3.0.0", - "eslint": "^9.0.0","@nullvoxpopuli/eslint-configs": "^6.0.0", + "eslint": "^9.0.0", "loader.js": "^4.7.0", "prettier": "^3.8.1", "prettier-plugin-ember-template-tag": "^2.1.3", diff --git a/test-app/tsconfig.json b/test-app/tsconfig.json index 13acb26..6cac137 100644 --- a/test-app/tsconfig.json +++ b/test-app/tsconfig.json @@ -2,7 +2,13 @@ "extends": "@ember/app-tsconfig", "compilerOptions": { "ignoreDeprecations": "6.0", - "types": ["ember-source/types", "@glint/ember-tsc/types"], + "types": [ + "ember-source/types", + "@glint/ember-tsc/types", + "ember-data/unstable-preview-types", + "@ember-data/model/unstable-preview-types", + "@ember-data/store/unstable-preview-types" + ], // The combination of `baseUrl` with `paths` allows Ember's classic package // layout, which is not resolvable with the Node resolution algorithm, to // work with TypeScript. From 8834d00593afbffdebf5ee91336a8d46bfd0733a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:09:08 -0400 Subject: [PATCH 2/4] Fix type and lint errors across docs-app, test-app, and the addon - utils.ts: the @ts-expect-error had drifted away from the line it suppresses (an eslint-disable block sat between them), so it was both 'unused' and not suppressing the intended errors - field.gts: get() now returns DATA[KEY] on its own; drop the redundant cast - docs-app: assert owners before use, type the Router constructor via ConstructorParameters, and map @crowdstrike/ember-oss-docs/* through tsconfig paths (its exports map uses extensionless dist targets that bundler-mode resolution refuses for subpaths) - test-app: type the store lookups, cast createRecord results to UserModel (ember-data 5.8 returns opaque records without the warp-drive Type brand), and drop a stray 'required' attribute that field.Errors' element types rightly reject Co-Authored-By: Claude Fable 5 --- docs-app/app/helpers/service.ts | 3 + docs-app/app/router.ts | 2 +- docs-app/app/routes/application.ts | 7 +- docs-app/tsconfig.json | 5 + .../src/-private/components/field.gts | 35 +- .../ember-headless-form/src/-private/utils.ts | 8 +- .../components/headless-form-data-test.gts | 1015 +++++++++-------- .../headless-form-native-validation-test.gts | 764 +++++++------ 8 files changed, 975 insertions(+), 864 deletions(-) diff --git a/docs-app/app/helpers/service.ts b/docs-app/app/helpers/service.ts index c92fb6f..fae2e07 100644 --- a/docs-app/app/helpers/service.ts +++ b/docs-app/app/helpers/service.ts @@ -1,5 +1,6 @@ import { getOwner } from '@ember/application'; import Helper from '@ember/component/helper'; +import { assert } from '@ember/debug'; import type { Registry } from '@ember/service'; @@ -16,6 +17,8 @@ export default class GetService extends Helper< compute([name]: [Key]): Registry[Key] { const owner = getOwner(this); + assert('Expected the service helper to have an owner', owner); + return owner.lookup(`service:${name}`); } } diff --git a/docs-app/app/router.ts b/docs-app/app/router.ts index 3613ccc..d812763 100644 --- a/docs-app/app/router.ts +++ b/docs-app/app/router.ts @@ -8,7 +8,7 @@ export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL; - constructor(...args: [object]) { + constructor(...args: ConstructorParameters) { super(...args); const scroll = () => window.scrollTo(0, 0); diff --git a/docs-app/app/routes/application.ts b/docs-app/app/routes/application.ts index 0add931..14c5268 100644 --- a/docs-app/app/routes/application.ts +++ b/docs-app/app/routes/application.ts @@ -1,4 +1,5 @@ import { getOwner } from '@ember/application'; +import { assert } from '@ember/debug'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; @@ -13,6 +14,10 @@ export default class Application extends Route { } afterModel() { - void setupHLJS(getOwner(this)); + const owner = getOwner(this); + + assert('Expected the application route to have an owner', owner); + + void setupHLJS(owner); } } diff --git a/docs-app/tsconfig.json b/docs-app/tsconfig.json index 2fed2d8..34f386d 100644 --- a/docs-app/tsconfig.json +++ b/docs-app/tsconfig.json @@ -12,6 +12,11 @@ "paths": { "docs-app/tests/*": ["tests/*"], "docs-app/*": ["app/*"], + // ember-oss-docs' exports map points at extensionless `./dist/*` targets, + // which module: bundler resolution refuses to resolve for subpath imports + "@crowdstrike/ember-oss-docs/*": [ + "node_modules/@crowdstrike/ember-oss-docs/dist/*" + ], "*": ["types/*"] } }, diff --git a/packages/ember-headless-form/src/-private/components/field.gts b/packages/ember-headless-form/src/-private/components/field.gts index ca1a5a2..4bab49f 100644 --- a/packages/ember-headless-form/src/-private/components/field.gts +++ b/packages/ember-headless-form/src/-private/components/field.gts @@ -29,7 +29,7 @@ import type { ModifierLike, WithBoundArgs } from '@glint/template'; export interface HeadlessFormFieldComponentSignature< DATA extends UserData, - KEY extends FormKey> = FormKey> + KEY extends FormKey> = FormKey>, > { Args: { /** @@ -212,14 +212,14 @@ export interface HeadlessFormFieldComponentSignature< ModifierLike, 'event' | 'triggerValidation' >; - } + }, ]; }; } export default class HeadlessFormFieldComponent< DATA extends FormData, - KEY extends FormKey> = FormKey> + KEY extends FormKey> = FormKey>, > extends Component> { LabelComponent = LabelComponent; InputComponent = InputComponent; @@ -233,13 +233,13 @@ export default class HeadlessFormFieldComponent< constructor( owner: unknown, - args: HeadlessFormFieldComponentSignature['Args'] + args: HeadlessFormFieldComponentSignature['Args'], ) { super(owner, args); assert( 'Nested property paths in @name are not supported.', - typeof this.args.name !== 'string' || !this.args.name.includes('.') + typeof this.args.name !== 'string' || !this.args.name.includes('.'), ); this.args.registerField(this.args.name, { @@ -256,7 +256,7 @@ export default class HeadlessFormFieldComponent< get value(): DATA[KEY] { // when @mutableData is set, data is something we don't control, i.e. might require old-school get() to be on the safe side // we do not want to support nested property paths for now though, see the constructor assertion! - return get(this.args.data, this.args.name) as DATA[KEY]; + return get(this.args.data, this.args.name); } get errors(): ValidationError[] | undefined { @@ -270,9 +270,9 @@ export default class HeadlessFormFieldComponent< get valueAsString(): string | undefined { assert( `Only string values are expected for ${String( - this.args.name + this.args.name, )}, but you passed ${typeof this.value}`, - typeof this.value === 'undefined' || typeof this.value === 'string' + typeof this.value === 'undefined' || typeof this.value === 'string', ); return this.value; @@ -280,12 +280,10 @@ export default class HeadlessFormFieldComponent< get valueAllAsString(): string[] { assert( - `Only string values are expected for ${String( - this.args.name - )}`, + `Only string values are expected for ${String(this.args.name)}`, typeof this.value === 'undefined' || (Array.isArray(this.value) && - this.value.every((v) => typeof v === 'string')) + this.value.every((v) => typeof v === 'string')), ); return this.value ?? []; @@ -294,11 +292,11 @@ export default class HeadlessFormFieldComponent< get valueAsStringOrNumber(): string | number | undefined { assert( `Only string or number values are expected for ${String( - this.args.name + this.args.name, )}, but you passed ${typeof this.value}`, typeof this.value === 'undefined' || typeof this.value === 'string' || - typeof this.value === 'number' + typeof this.value === 'number', ); return this.value; @@ -307,9 +305,9 @@ export default class HeadlessFormFieldComponent< get valueAsBoolean(): boolean | undefined { assert( `Only boolean values are expected for ${String( - this.args.name + this.args.name, )}, but you passed ${typeof this.value}`, - typeof this.value === 'undefined' || typeof this.value === 'boolean' + typeof this.value === 'undefined' || typeof this.value === 'boolean', ); return this.value; @@ -322,10 +320,7 @@ export default class HeadlessFormFieldComponent< , + ); await click('[data-test-submit]'); @@ -369,49 +391,49 @@ module( assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]', ) .hasText('native'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]', ) .hasText('foo123'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]', ) .hasAnyText(); // validation error message is browser and locale dependant, so testing against actual message would be very brittle. assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-value]', ) .hasText('foo123'); assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-message]', ) .hasText('First name must be upper case!'); assert .dom( - '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-type]', ) .hasText('notFoo'); assert .dom( - '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-value]', ) .hasText('foo123'); assert .dom( - '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="2"] [data-test-error-message]', ) .hasText('Foo is an invalid first name!'); @@ -443,29 +465,35 @@ module( ] : undefined; - await render(); + + First Name + + + + + Last Name + + + + + + , + ); await click('[data-test-submit]'); @@ -488,40 +516,46 @@ module( } : undefined; - await render(, + ); await click('[data-test-submit]'); @@ -531,17 +565,17 @@ module( assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]', ) .hasText('john'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]', ) .hasText('First name must be upper case!'); @@ -553,39 +587,41 @@ module( test('validation errors are exposed as field.Errors on focusout', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before form is filled in' + 'validation errors are not rendered before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before form is filled in' + 'validation errors are not rendered before form is filled in', ); await fillIn('[data-test-first-name]', '123'); @@ -593,12 +629,12 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on focusout' + 'validation errors are not rendered before validation happens on focusout', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on focusout' + 'validation errors are not rendered before validation happens on focusout', ); await blur('[data-test-first-name]'); @@ -607,12 +643,12 @@ module( .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on focusout when validation fails' + 'validation errors appear on focusout when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered for untouched fields' + 'validation errors are not rendered for untouched fields', ); }); }); @@ -621,39 +657,41 @@ module( test('validation errors are exposed as field.Errors on change', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on change' + 'validation errors are not rendered before validation happens on change', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on change' + 'validation errors are not rendered before validation happens on change', ); await fillIn('[data-test-first-name]', '123'); @@ -662,12 +700,12 @@ module( .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on focusout when validation fails' + 'validation errors appear on focusout when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered for untouched fields' + 'validation errors are not rendered for untouched fields', ); }); }); @@ -678,39 +716,41 @@ module( test('validation errors are exposed as field.Errors on focusout', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before before form is filled in' + 'validation errors are not rendered before before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before before form is filled in' + 'validation errors are not rendered before before form is filled in', ); await fillIn('[data-test-first-name]', '123'); @@ -718,12 +758,12 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await blur('[data-test-first-name]'); @@ -731,12 +771,12 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await click('[data-test-submit]'); @@ -745,13 +785,13 @@ module( .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); await fillIn('[data-test-first-name]', 'Tony'); @@ -760,13 +800,13 @@ module( .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); await blur('[data-test-first-name]'); @@ -774,13 +814,13 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors disappear after successful revalidation on focusout' + 'validation errors disappear after successful revalidation on focusout', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); }); }); @@ -789,39 +829,41 @@ module( test('validation errors are revalidated on change', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); await fillIn('[data-test-first-name]', '123'); @@ -829,12 +871,12 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await blur('[data-test-first-name]'); @@ -842,12 +884,12 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await click('[data-test-submit]'); @@ -856,13 +898,13 @@ module( .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); await fillIn('[data-test-first-name]', 'Tony'); @@ -870,16 +912,16 @@ module( assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors disappear after successful revalidation on change' + 'validation errors disappear after successful revalidation on change', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on change' + 'validation errors do not disappear until revalidation happens on change', ); }); }); }); - } + }, ); From e760c11016f8a25bee81e7e60f22d4152c93650d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:09:08 -0400 Subject: [PATCH 3/4] CI: pin node 24, gate Cloudflare publish on secrets, drop changesets job - setup-node had no version pinned, so jobs ran node 22 against the workspace's engines ^24 - the Cloudflare publish step hard-failed when the API token secret is absent (it is not configured on this repo); skip publish + PR comment instead, while still building the docs - the changesets release job predates the fork; release-plan (plan-release.yml + publish.yml) owns releasing now Co-Authored-By: Claude Fable 5 --- .github/actions/pnpm/action.yml | 1 + .github/workflows/ci.yml | 39 +++++---------------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/.github/actions/pnpm/action.yml b/.github/actions/pnpm/action.yml index 09ede00..520f422 100644 --- a/.github/actions/pnpm/action.yml +++ b/.github/actions/pnpm/action.yml @@ -6,6 +6,7 @@ runs: - uses: pnpm/action-setup@v4.1.0 - uses: actions/setup-node@v4 with: + node-version: 24 cache: 'pnpm' - name: 'Install dependencies' shell: 'bash' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bf863e..7adda27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,39 +133,6 @@ jobs: working-directory: ./test-app run: node_modules/.bin/ember try:one ${{matrix.try-scenario}} --skip-cleanup --- pnpm turbo test - # https://github.com/changesets/action - release: - name: Release - timeout-minutes: 5 - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - needs: - - default_tests - - floating_tests - - typecheck - - try_scenarios - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: TurboRepo local server - uses: felixmosh/turborepo-gh-artifacts@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - uses: ./.github/actions/pnpm - - run: pnpm build:packages - - name: Create Release Pull Request or Publish to npm - id: changesets - uses: changesets/action@v1 - with: - # This expects you to have a script called release which does a build for your packages and calls changeset publish - publish: pnpm release - title: 'Release Preview' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - publishDocs: name: Publish Docs to Cloudflare Pages runs-on: ubuntu-latest @@ -175,6 +142,8 @@ jobs: contents: read deployments: write pull-requests: write + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.UX_OSS_CLOUDFLARE_API_TOKEN }} steps: - uses: actions/checkout@v4 with: @@ -183,6 +152,9 @@ jobs: - run: pnpm build:docs - name: Publish to Cloudflare Pages id: publishStep + # skip (rather than fail) when the Cloudflare secrets are not + # configured, e.g. on forks or PRs from forks + if: env.CLOUDFLARE_API_TOKEN != '' uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.UX_OSS_CLOUDFLARE_API_TOKEN }} @@ -191,6 +163,7 @@ jobs: directory: ./docs-app/dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} - uses: marocchino/sticky-pull-request-comment@v2 + if: env.CLOUDFLARE_API_TOKEN != '' && github.event_name == 'pull_request' with: message: |+ ## Preview URLs From 7a6f6f107aadf65523e40b67f4da589594814337 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli-ai-agent <268630448+NullVoxPopuli-ai-agent@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:09:08 -0400 Subject: [PATCH 4/4] Apply prettier formatting The prettier check never actually ran for the addon package before (the template-tag plugin could not resolve), and test-app had drifted. Mechanical `prettier -w` output only. Co-Authored-By: Claude Fable 5 --- .../components/control/checkbox-group.gts | 2 +- .../control/checkbox-group/checkbox.gts | 10 +- .../control/checkbox-group/label.gts | 4 +- .../src/-private/components/control/input.gts | 6 +- .../components/control/radio-group.gts | 2 +- .../components/control/radio-group/label.gts | 4 +- .../components/control/radio-group/radio.gts | 2 +- .../-private/components/control/select.gts | 2 +- .../src/-private/components/errors.gts | 1 - .../src/-private/modifiers/capture-events.ts | 2 +- .../ember-headless-form/src/-private/types.ts | 14 +- .../src/components/headless-form.gts | 69 +- test-app/app/controllers/yup-async-form.ts | 2 +- test-app/app/index.html | 2 +- test-app/tests/index.html | 2 +- .../integration/components/async-yup-test.gts | 246 +-- .../integration/components/changeset-test.gts | 196 +- .../components/headless-form-async-test.gts | 224 +- .../components/headless-form-basic-test.gts | 133 +- ...dless-form-control-checkbox-group-test.gts | 245 ++- .../headless-form-control-checkbox-test.gts | 36 +- .../headless-form-control-input-test.gts | 58 +- .../headless-form-control-radio-test.gts | 236 ++- .../headless-form-control-select-test.gts | 76 +- .../headless-form-control-textarea-test.gts | 20 +- .../headless-form-custom-controls-test.gts | 312 +-- .../components/headless-form-glint-test.gts | 68 +- .../components/headless-form-reset-test.gts | 298 +-- .../headless-form-validation-test.gts | 1824 +++++++++-------- .../tests/integration/components/yup-test.gts | 174 +- .../unit/helpers/validate-changeset-test.ts | 8 +- .../tests/unit/helpers/validate-yup-test.ts | 2 +- test-app/types/global.d.ts | 3 +- 33 files changed, 2264 insertions(+), 2019 deletions(-) diff --git a/packages/ember-headless-form/src/-private/components/control/checkbox-group.gts b/packages/ember-headless-form/src/-private/components/control/checkbox-group.gts index d2e349f..10579d8 100644 --- a/packages/ember-headless-form/src/-private/components/control/checkbox-group.gts +++ b/packages/ember-headless-form/src/-private/components/control/checkbox-group.gts @@ -52,7 +52,7 @@ export interface HeadlessFormControlCheckboxGroupComponentSignature { typeof HeadlessFormControlCheckboxGroupLabelComponent, 'id' >; - } + }, ]; }; } diff --git a/packages/ember-headless-form/src/-private/components/control/checkbox-group/checkbox.gts b/packages/ember-headless-form/src/-private/components/control/checkbox-group/checkbox.gts index 1667e57..efc9076 100644 --- a/packages/ember-headless-form/src/-private/components/control/checkbox-group/checkbox.gts +++ b/packages/ember-headless-form/src/-private/components/control/checkbox-group/checkbox.gts @@ -47,7 +47,7 @@ export interface HeadlessFormControlCheckboxComponentSignature { typeof HeadlessFormControlCheckboxInputComponent, 'fieldId' | 'value' | 'toggleValue' | 'checked' | 'name' >; - } + }, ]; }; } @@ -60,9 +60,11 @@ export default class HeadlessFormControlCheckboxComponent extends Component v !== this.args.value)]); + this.args.setValue([...this.args.selected, this.args.value]); + } else { + this.args.setValue([ + ...this.args.selected.filter((v) => v !== this.args.value), + ]); } } diff --git a/packages/ember-headless-form/src/-private/components/control/checkbox-group/label.gts b/packages/ember-headless-form/src/-private/components/control/checkbox-group/label.gts index 582c821..4480d45 100644 --- a/packages/ember-headless-form/src/-private/components/control/checkbox-group/label.gts +++ b/packages/ember-headless-form/src/-private/components/control/checkbox-group/label.gts @@ -16,6 +16,8 @@ export interface HeadlessFormControlCheckboxGroupLabelComponentSignature { } const HeadlessFormControlCheckboxGroupLabelComponent: TemplateOnlyComponent = - ; + ; export default HeadlessFormControlCheckboxGroupLabelComponent; diff --git a/packages/ember-headless-form/src/-private/components/control/input.gts b/packages/ember-headless-form/src/-private/components/control/input.gts index 4e34b45..18bc2f1 100644 --- a/packages/ember-headless-form/src/-private/components/control/input.gts +++ b/packages/ember-headless-form/src/-private/components/control/input.gts @@ -87,13 +87,13 @@ export interface HeadlessFormControlInputComponentSignature { export default class HeadlessFormControlInputComponent extends Component { constructor( owner: unknown, - args: HeadlessFormControlInputComponentSignature['Args'] + args: HeadlessFormControlInputComponentSignature['Args'], ) { assert( `input component does not support @type="${args.type}" as there is a dedicated component for this. Please use the \`field.${args.type}\` instead!`, args.type === undefined || // TS would guard us against using an unsupported `InputType`, but for JS consumers we add a dev-only runtime check here - !['checkbox', 'radio'].includes(args.type) + !['checkbox', 'radio'].includes(args.type), ); super(owner, args); @@ -107,7 +107,7 @@ export default class HeadlessFormControlInputComponent extends Component diff --git a/packages/ember-headless-form/src/-private/components/control/radio-group.gts b/packages/ember-headless-form/src/-private/components/control/radio-group.gts index 9a71408..2c6d0db 100644 --- a/packages/ember-headless-form/src/-private/components/control/radio-group.gts +++ b/packages/ember-headless-form/src/-private/components/control/radio-group.gts @@ -52,7 +52,7 @@ export interface HeadlessFormControlRadioGroupComponentSignature { typeof HeadlessFormControlRadioGroupLabelComponent, 'id' >; - } + }, ]; }; } diff --git a/packages/ember-headless-form/src/-private/components/control/radio-group/label.gts b/packages/ember-headless-form/src/-private/components/control/radio-group/label.gts index 9e17016..50d390d 100644 --- a/packages/ember-headless-form/src/-private/components/control/radio-group/label.gts +++ b/packages/ember-headless-form/src/-private/components/control/radio-group/label.gts @@ -16,6 +16,8 @@ export interface HeadlessFormControlRadioGroupLabelComponentSignature { } const HeadlessFormControlRadioGroupLabelComponent: TemplateOnlyComponent = - ; + ; export default HeadlessFormControlRadioGroupLabelComponent; diff --git a/packages/ember-headless-form/src/-private/components/control/radio-group/radio.gts b/packages/ember-headless-form/src/-private/components/control/radio-group/radio.gts index 9f7f876..10a0b6d 100644 --- a/packages/ember-headless-form/src/-private/components/control/radio-group/radio.gts +++ b/packages/ember-headless-form/src/-private/components/control/radio-group/radio.gts @@ -46,7 +46,7 @@ export interface HeadlessFormControlRadioComponentSignature { typeof HeadlessFormControlRadioInputComponent, 'fieldId' | 'value' | 'setValue' | 'checked' | 'name' >; - } + }, ]; }; } diff --git a/packages/ember-headless-form/src/-private/components/control/select.gts b/packages/ember-headless-form/src/-private/components/control/select.gts index 23def33..7f84a26 100644 --- a/packages/ember-headless-form/src/-private/components/control/select.gts +++ b/packages/ember-headless-form/src/-private/components/control/select.gts @@ -49,7 +49,7 @@ export interface HeadlessFormControlSelectComponentSignature { typeof HeadlessFormControlSelectOptionComponent, 'selected' >; - } + }, ]; }; } diff --git a/packages/ember-headless-form/src/-private/components/errors.gts b/packages/ember-headless-form/src/-private/components/errors.gts index 22b061d..46854f4 100644 --- a/packages/ember-headless-form/src/-private/components/errors.gts +++ b/packages/ember-headless-form/src/-private/components/errors.gts @@ -22,7 +22,6 @@ export interface HeadlessFormErrorsComponentSignature { }; } - export default class HeadlessFormErrorsComponent extends Component< HeadlessFormErrorsComponentSignature > { diff --git a/packages/ember-headless-form/src/-private/modifiers/capture-events.ts b/packages/ember-headless-form/src/-private/modifiers/capture-events.ts index ab64d7f..00e30a6 100644 --- a/packages/ember-headless-form/src/-private/modifiers/capture-events.ts +++ b/packages/ember-headless-form/src/-private/modifiers/capture-events.ts @@ -28,7 +28,7 @@ const CaptureEventsModifier = modifier( element.removeEventListener(event, handler); }; } - } + }, ); export default CaptureEventsModifier; diff --git a/packages/ember-headless-form/src/-private/types.ts b/packages/ember-headless-form/src/-private/types.ts index b1cbd5c..c33903b 100644 --- a/packages/ember-headless-form/src/-private/types.ts +++ b/packages/ember-headless-form/src/-private/types.ts @@ -26,7 +26,7 @@ export interface ValidationError { export type ErrorRecord< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > = Partial[]>>; /** @@ -34,7 +34,7 @@ export type ErrorRecord< */ export type FormValidateCallback = ( formData: DATA, - fields: Array> + fields: Array>, ) => undefined | ErrorRecord | Promise>; /** @@ -42,11 +42,11 @@ export type FormValidateCallback = ( */ export type FieldValidateCallback< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > = ( fieldValue: DATA[KEY], fieldName: KEY, - formData: DATA + formData: DATA, ) => | undefined | ValidationError[] @@ -58,7 +58,7 @@ export type FieldValidateCallback< */ export interface FieldRegistrationData< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > { validate?: FieldValidateCallback; } @@ -69,12 +69,12 @@ export interface FieldRegistrationData< */ export type RegisterFieldCallback< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > = (name: KEY, field: FieldRegistrationData) => void; export type UnregisterFieldCallback< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > = (name: KEY) => void; /** diff --git a/packages/ember-headless-form/src/components/headless-form.gts b/packages/ember-headless-form/src/components/headless-form.gts index ce8ad61..2c914d9 100644 --- a/packages/ember-headless-form/src/components/headless-form.gts +++ b/packages/ember-headless-form/src/components/headless-form.gts @@ -28,7 +28,7 @@ type ValidateOn = 'change' | 'focusout' | 'submit' | 'input'; export interface HeadlessFormComponentSignature< DATA extends UserData, - SUBMISSION_VALUE + SUBMISSION_VALUE, > { Element: HTMLFormElement; Args: { @@ -74,7 +74,7 @@ export interface HeadlessFormComponentSignature< * Called when the user has submitted the form and no validation errors have been determined. Receives the new form data, or in case of `@dataMode="mutable"` the original data object. */ onSubmit?: ( - data: FormData + data: FormData, ) => SUBMISSION_VALUE | Promise; /** @@ -82,7 +82,7 @@ export interface HeadlessFormComponentSignature< */ onInvalid?: ( data: FormData, - errors: ErrorRecord> + errors: ErrorRecord>, ) => void; }; Blocks: { @@ -141,7 +141,7 @@ export interface HeadlessFormComponentSignature< * The current form data that is yielded on the form itself and can be used within the template. */ data: DATA; - } + }, ]; }; } @@ -151,7 +151,7 @@ export interface HeadlessFormComponentSignature< */ class FieldData< DATA extends FormData, - KEY extends FormKey = FormKey + KEY extends FormKey = FormKey, > { constructor(fieldRegistration: FieldRegistrationData) { this.validate = fieldRegistration.validate; @@ -200,7 +200,7 @@ class FieldData< */ export default class HeadlessFormComponent< DATA extends UserData, - SUBMISSION_VALUE + SUBMISSION_VALUE, > extends Component> { FieldComponent = FieldComponent; @@ -242,7 +242,7 @@ export default class HeadlessFormComponent< getOwnPropertyDescriptor(target, prop) { return Reflect.getOwnPropertyDescriptor( prop in internalData ? internalData : target, - prop + prop, ); }, @@ -304,11 +304,11 @@ export default class HeadlessFormComponent< ? // no need for dynamic validation, as validation always happens on submit undefined : // when validation happens more frequently than revalidation, then we can ignore revalidation, because the validation handler will already cover us - validateOn === 'input' || - (validateOn === 'change' && revalidateOn === 'focusout') || - validateOn === revalidateOn - ? undefined - : revalidateOn; + validateOn === 'input' || + (validateOn === 'change' && revalidateOn === 'focusout') || + validateOn === revalidateOn + ? undefined + : revalidateOn; } /** @@ -320,7 +320,7 @@ export default class HeadlessFormComponent< // Only consider validation errors for which we actually have a field rendered return validationState?.isResolved ? Object.keys(validationState.value).some((name) => - this.fields.has(name as FormKey>) + this.fields.has(name as FormKey>), ) : false; } @@ -333,7 +333,7 @@ export default class HeadlessFormComponent< this.args.ignoreNativeValidation !== true ? this.validateNative() : {}; const customFormValidation = await this.args.validate?.( this.effectiveData, - Array.from(this.fields.keys()) + Array.from(this.fields.keys()), ); const customFieldValidations: ErrorRecord>[] = []; @@ -341,7 +341,7 @@ export default class HeadlessFormComponent< const fieldValidationResult = await field.validate?.( this.effectiveData[name], name, - this.effectiveData + this.effectiveData, ); if (fieldValidationResult) { @@ -354,7 +354,7 @@ export default class HeadlessFormComponent< return mergeErrorRecord( nativeValidation, customFormValidation, - ...customFieldValidations + ...customFieldValidations, ); } @@ -371,7 +371,7 @@ export default class HeadlessFormComponent< assert( 'Form element expected to be present. If you see this, please report it as a bug to @universal-ember/form!', - form + form, ); if (form.checkValidity()) { @@ -390,7 +390,7 @@ export default class HeadlessFormComponent< el instanceof HTMLButtonElement || el instanceof HTMLFieldSetElement || el instanceof HTMLObjectElement || - el instanceof HTMLOutputElement + el instanceof HTMLOutputElement, ); if (el.validity.valid) { @@ -410,7 +410,7 @@ export default class HeadlessFormComponent< } else { warn( `An invalid form element with name "${name}" was detected, but this name is not used as a form field. It will be ignored for validation. Make sure to apply the correct name to custom form elements that participate in form validation!`, - { id: 'headless-form.invalid-control-for-unknown-field' } + { id: 'headless-form.invalid-control-for-unknown-field' }, ); } } @@ -431,10 +431,10 @@ export default class HeadlessFormComponent< const visibleErrors: ErrorRecord> = {}; for (const [field, errors] of Object.entries( - this.validationState.value + this.validationState.value, ) as [ FormKey>, - ValidationError[FormKey>]>[] + ValidationError[FormKey>]>[], ][]) { if (this.showErrorsFor(field)) { visibleErrors[field] = errors; @@ -458,11 +458,10 @@ export default class HeadlessFormComponent< async onSubmit(e?: Event): Promise { e?.preventDefault(); - if(this.validationState?.isPending) { - warn( - 'Async validation is pending, form will not be submitted', - { id: 'headless-form.prevent-submit-pending-async-validation' }, - ); + if (this.validationState?.isPending) { + warn('Async validation is pending, form will not be submitted', { + id: 'headless-form.prevent-submit-pending-async-validation', + }); return; } @@ -473,14 +472,14 @@ export default class HeadlessFormComponent< if (!this.hasValidationErrors) { if (this.args.onSubmit) { this.submissionState = new TrackedAsyncData( - this.args.onSubmit(this.effectiveData) + this.args.onSubmit(this.effectiveData), ); } } else { assert( 'Validation errors expected to be present. If you see this, please report it as a bug to @universal-ember/form!', // Do *not* use optional chaining due to https://github.com/ember-cli/babel-plugin-debug-macros/issues/89 - this.validationState && this.validationState.isResolved + this.validationState && this.validationState.isResolved, ); this.args.onInvalid?.(this.effectiveData, this.validationState.value); } @@ -501,13 +500,13 @@ export default class HeadlessFormComponent< @action registerField( name: FormKey>, - field: FieldRegistrationData> + field: FieldRegistrationData>, ): void { assert( `You passed @name="${String( - name + name, )}" to the form field, but this is already in use. Names of form fields must be unique!`, - !this.fields.has(name) + !this.fields.has(name), ); this.fields.set(name, new FieldData(field)); } @@ -550,7 +549,7 @@ export default class HeadlessFormComponent< } else if (e instanceof Event) { warn( `An event of type "${e.type}" was received by headless-form, which is supposed to trigger validations for a certain field. But the name of that field could not be determined. Make sure that your control element has a \`name\` attribute matching the field, or use the yielded \`{{field.captureEvents}}\` to capture the events.`, - { id: 'headless-form.validation-event-for-unknown-field' } + { id: 'headless-form.validation-event-for-unknown-field' }, ); } } @@ -574,7 +573,7 @@ export default class HeadlessFormComponent< } else { warn( `An event of type "${e.type}" was received by headless-form, which is supposed to trigger validations for a certain field. But the name of that field could not be determined. Make sure that your control element has a \`name\` attribute matching the field, or use the yielded \`{{field.captureEvents}}\` to capture the events.`, - { id: 'headless-form.validation-event-for-unknown-field' } + { id: 'headless-form.validation-event-for-unknown-field' }, ); } } @@ -582,14 +581,14 @@ export default class HeadlessFormComponent< onValidation = elementModifier( ( el: HTMLFormElement, - [eventName, handler]: [string | undefined, (e: Event) => void] + [eventName, handler]: [string | undefined, (e: Event) => void], ) => { if (eventName) { el.addEventListener(eventName, handler); return () => el.removeEventListener(eventName, handler); } - } + }, ); , + ); await click('[data-test-validate]'); assert.true( validateCallback.calledWith('foo', 'custom', data), - '@validate is called with form data' + '@validate is called with form data', ); assert .dom('[data-test-date-errors]') .exists( { count: 1 }, - 'validation errors appear when validation fails' + 'validation errors appear when validation fails', ); }); @@ -232,23 +240,25 @@ module( }, ]); - await render(); + await render( + , + ); // the input that triggers the blur does *not* have a name that would allow headless-form to understand from which field this event is coming from // but applying {{captureEvents}} will make headless-form be able to assiciate it to the name of the field @@ -257,14 +267,14 @@ module( assert.true( validateCallback.calledWith('foo', 'custom', data), - '@validate is called with form data' + '@validate is called with form data', ); assert .dom('[data-test-date-errors]') .exists( { count: 1 }, - 'validation errors appear when validation fails' + 'validation errors appear when validation fails', ); }); @@ -278,23 +288,25 @@ module( }, ]); - await render(); + await render( + , + ); // the input that triggers the blur does *not* have a name that would allow headless-form to understand from which field this event is coming from // but applying {{captureEvents}} will make headless-form be able to assiciate it to the name of the field @@ -302,14 +314,14 @@ module( assert.true( validateCallback.calledWith('foo', 'custom', data), - '@validate is called with form data' + '@validate is called with form data', ); assert .dom('[data-test-date-errors]') .exists( { count: 1 }, - 'validation errors appear when validation fails' + 'validation errors appear when validation fails', ); }); }); @@ -320,34 +332,36 @@ module( { type: 'invalid-date', value: undefined, message: 'Invalid Date!' }, ]); - await render(); + + + + + + + , + ); await triggerEvent('[data-test-custom-control', 'change'); assert.false( validateCallback.called, - '@validate is not called until blur' + '@validate is not called until blur', ); assert .dom('[data-test-date-errors]') @@ -358,23 +372,23 @@ module( assert.true( validateCallback.calledWith('foo', 'custom', data), - '@validate is called with form data' + '@validate is called with form data', ); assert .dom('[data-test-date-errors]') .exists( { count: 1 }, - 'validation errors appear when validation fails' + 'validation errors appear when validation fails', ); await triggerEvent('[data-test-custom-control', 'change'); assert.true( validateCallback.calledTwice, - '@validate is called again on change for revalidation, after initial validation has happened' + '@validate is called again on change for revalidation, after initial validation has happened', ); }); }); - } + }, ); diff --git a/test-app/tests/integration/components/headless-form-glint-test.gts b/test-app/tests/integration/components/headless-form-glint-test.gts index e4e954a..b673101 100644 --- a/test-app/tests/integration/components/headless-form-glint-test.gts +++ b/test-app/tests/integration/components/headless-form-glint-test.gts @@ -1,5 +1,3 @@ - - import { render } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -21,14 +19,16 @@ module('Integration Component HeadlessForm > Glint', function (hooks) { // Note that we have only firstName here in the type that is passed to @data, no lastName! const data = { firstName: 'Simon' }; - await render(); + await render( + , + ); }); test('@name argument only expects keys of @data w/ partial data', async function (assert) { @@ -36,14 +36,16 @@ module('Integration Component HeadlessForm > Glint', function (hooks) { const data: { firstName?: string } = {}; - await render(); + await render( + , + ); }); test('@name argument w/ an untyped @data errors', async function (assert) { @@ -51,12 +53,14 @@ module('Integration Component HeadlessForm > Glint', function (hooks) { const data = {}; - await render(); + await render( + , + ); }); test('@name argument can only be used for string-types keys', async function (assert) { @@ -64,12 +68,14 @@ module('Integration Component HeadlessForm > Glint', function (hooks) { const data: { foo?: string; 0?: number } = {}; - await render(); + await render( + , + ); }); }); diff --git a/test-app/tests/integration/components/headless-form-reset-test.gts b/test-app/tests/integration/components/headless-form-reset-test.gts index ad26674..685ce3b 100644 --- a/test-app/tests/integration/components/headless-form-reset-test.gts +++ b/test-app/tests/integration/components/headless-form-reset-test.gts @@ -1,5 +1,3 @@ - - import { on } from '@ember/modifier'; import { click, fillIn, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -20,19 +18,21 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('dirty fields are resetted', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Nicole'); await click('[data-test-reset]'); @@ -44,25 +44,27 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('validation errors are cleared', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -84,24 +86,28 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('validation state is resetted', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-validation-state]') .doesNotExist( - 'form.validationState is not present until first validation' + 'form.validationState is not present until first validation', ); await click('[data-test-submit]'); @@ -121,24 +127,28 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { const data: TestFormData = {}; const submitHandler = () => 'ok'; - await render(); + await render( + , + ); assert .dom('[data-test-submission-state]') .doesNotExist( - 'form.submissionState is not present until first validation' + 'form.submissionState is not present until first validation', ); await click('[data-test-submit]'); @@ -159,23 +169,25 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('dirty fields are resetted', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Nicole'); await click('[data-test-reset]'); @@ -187,29 +199,31 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('validation errors are cleared', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -231,28 +245,32 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { test('validation state is resetted', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert .dom('[data-test-validation-state]') .doesNotExist( - 'form.validationState is not present until first validation' + 'form.validationState is not present until first validation', ); await click('[data-test-submit]'); @@ -272,28 +290,32 @@ module('Integration Component HeadlessForm > Reset', function (hooks) { const data: TestFormData = {}; const submitHandler = () => 'ok'; - await render(); + await render( + , + ); assert .dom('[data-test-submission-state]') .doesNotExist( - 'form.submissionState is not present until first validation' + 'form.submissionState is not present until first validation', ); await click('[data-test-submit]'); diff --git a/test-app/tests/integration/components/headless-form-validation-test.gts b/test-app/tests/integration/components/headless-form-validation-test.gts index 7fb8121..c68fd0b 100644 --- a/test-app/tests/integration/components/headless-form-validation-test.gts +++ b/test-app/tests/integration/components/headless-form-validation-test.gts @@ -1,5 +1,3 @@ - - import { tracked } from '@glimmer/tracking'; import { blur, click, fillIn, render, rerender } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -29,7 +27,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const validateFormCallbackSync: FormValidateCallback = ( data, - fields + fields, ) => { const errorRecord: ErrorRecord = {}; @@ -71,7 +69,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const validateFormCallbackAsync: FormValidateCallback = async ( data, - fields + fields, ) => { // intentionally adding a delay here, to make the validation behave truly async and assert that we are correctly waiting for it in tests await new Promise((resolve) => setTimeout(resolve, 10)); @@ -81,7 +79,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const validateFieldCallbackSync: FieldValidateCallback = ( value, - field + field, ) => { const errors = []; @@ -131,29 +129,31 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert.true( validateCallback.calledWith(data), - '@validate is called with form data' + '@validate is called with form data', ); }); @@ -161,20 +161,22 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -185,20 +187,22 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; const invalidHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -212,7 +216,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { }, ], }), - '@onInvalid was called' + '@onInvalid was called', ); }); @@ -220,24 +224,26 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = {}; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await fillIn('input[data-test-first-name]', 'Nicole'); await fillIn('input[data-test-last-name]', 'Chung'); @@ -248,42 +254,44 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { firstName: 'Nicole', lastName: 'Chung', }), - '@onSubmit has been called' + '@onSubmit has been called', ); }); test('validation errors are exposed as field.Errors on submit', async function (assert) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens' + 'validation errors are not rendered before validation happens', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens' + 'validation errors are not rendered before validation happens', ); await click('[data-test-submit]'); @@ -292,32 +300,34 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear when validation fails' + 'validation errors appear when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered when validation succeeds' + 'validation errors are not rendered when validation succeeds', ); }); test('field.Errors is associated to input', async function (this: RenderingTestContext, assert) { const data: TestFormData = { firstName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('input') @@ -334,7 +344,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { 'id', // copied from https://ihateregex.io/expr/uuid/ /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, - 'errors element has id with dynamically generated uuid' + 'errors element has id with dynamically generated uuid', ) .hasAria('live', 'assertive'); @@ -348,27 +358,29 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .hasAria( 'describedby', id, - 'errors are associated to invalid input via aria-describedby' + 'errors are associated to invalid input via aria-describedby', ); }); test('field.Errors renders all error messages in non-block mode', async function (assert) { const data: TestFormData = { firstName: 'foo' }; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -376,41 +388,43 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists({ count: 1 }) .hasText( - 'firstName must be upper case! Foo is an invalid firstName!' + 'firstName must be upper case! Foo is an invalid firstName!', ); }); test('field.Errors yields errors in block mode', async function (assert) { const data: TestFormData = { firstName: 'foo' }; - await render(, + ); await click('[data-test-submit]'); @@ -421,33 +435,33 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-message]', ) .hasText('firstName must be upper case!'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-type]', ) .hasText('notFoo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-message]', ) .hasText('Foo is an invalid firstName!'); }); @@ -478,42 +492,44 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const formState = new FormState(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert .dom('[data-test-first-name-errors]') .exists( - 'validation errors are shown for firstName while being visible' + 'validation errors are shown for firstName while being visible', ); assert .dom('[data-test-last-name-errors]') .exists( - 'validation errors are shown for lastName while being visible' + 'validation errors are shown for lastName while being visible', ); assert.false(submitHandler.called, '@onSubmit has not been called'); @@ -526,7 +542,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-last-name-errors]') .exists( - 'validation errors are shown for lastName while being visible' + 'validation errors are shown for lastName while being visible', ); assert.false(submitHandler.called, '@onSubmit has not been called'); @@ -539,7 +555,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert.dom('[data-test-last-name-errors]').doesNotExist(); assert.true( submitHandler.called, - '@onSubmit has been called when no invalid field is left' + '@onSubmit has been called when no invalid field is left', ); formState.showFirstName = true; @@ -548,39 +564,41 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .exists( - 'validation errors are shown for firstName when being visible again' + 'validation errors are shown for firstName when being visible again', ); await click('[data-test-submit]'); assert.true( submitHandler.calledOnce, - '@onSubmit has not been called again' + '@onSubmit has not been called again', ); }); test('validation errors mark the control as invalid', async function (assert) { const data: TestFormData = { firstName: 'Foo' }; - await render(); + await render( + , + ); await click('[data-test-submit]'); assert.dom('[data-test-first-name]').hasAria('invalid', 'true'); }); - }) + }), ); }); @@ -594,29 +612,31 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert.true( validateCallback.calledWith(data.firstName, 'firstName', data), - '@validate is called with form data' + '@validate is called with form data', ); }); @@ -624,19 +644,25 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; const submitHandler = sinon.spy(); - await render(); + + First Name + + + + + , + ); await click('[data-test-submit]'); @@ -646,25 +672,27 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { test('validation errors are exposed as field.Errors on submit', async function (assert) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -690,45 +718,51 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const formState = new FormState(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert .dom('[data-test-first-name-errors]') .exists( - 'validation errors are shown for firstName while being visible' + 'validation errors are shown for firstName while being visible', ); assert .dom('[data-test-last-name-errors]') .exists( - 'validation errors are shown for lastName while being visible' + 'validation errors are shown for lastName while being visible', ); assert.false(submitHandler.called, '@onSubmit has not been called'); @@ -741,7 +775,7 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-last-name-errors]') .exists( - 'validation errors are shown for lastName while being visible' + 'validation errors are shown for lastName while being visible', ); assert.false(submitHandler.called, '@onSubmit has not been called'); @@ -754,13 +788,17 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert.dom('[data-test-last-name-errors]').doesNotExist(); assert.true( submitHandler.called, - '@onSubmit has been called when no invalid field is left' + '@onSubmit has been called when no invalid field is left', ); }); test('field validation errors are merged with form validation errors', async function (assert) { const data = { firstName: 'foo', lastName: 'Smith' }; - const formValidateCallback = ({ firstName }: { firstName: string }) => + const formValidateCallback = ({ + firstName, + }: { + firstName: string; + }) => firstName.charAt(0).toUpperCase() !== firstName.charAt(0) ? { firstName: [ @@ -783,43 +821,45 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { ] : undefined; - await render(); + {{/each}} + + + + Last Name + + + + + + , + ); await click('[data-test-submit]'); @@ -829,39 +869,39 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="0"] [data-test-error-message]', ) .hasText('First name must be upper case!'); assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-type]', ) .hasText('notFoo'); assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error="1"] [data-test-error-message]', ) .hasText('Foo is an invalid first name!'); assert.dom('[data-test-last-name-errors]').doesNotExist(); }); - }) + }), ); }); @@ -871,44 +911,46 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data on focusout' + '@validate is called with form data on focusout', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); @@ -916,29 +958,31 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); @@ -948,50 +992,52 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { ...data, firstName: 'Foo', }), - '@validate is called with form data on focusout' + '@validate is called with form data on focusout', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); test('validation errors are exposed as field.Errors on focusout', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before form is filled in' + 'validation errors are not rendered before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before form is filled in' + 'validation errors are not rendered before form is filled in', ); await fillIn('[data-test-first-name]', 'Foo'); @@ -999,12 +1045,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on focusout' + 'validation errors are not rendered before validation happens on focusout', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on focusout' + 'validation errors are not rendered before validation happens on focusout', ); await blur('[data-test-first-name]'); @@ -1013,12 +1059,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on focusout when validation fails' + 'validation errors appear on focusout when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered for untouched fields' + 'validation errors are not rendered for untouched fields', ); }); }); @@ -1028,37 +1074,39 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data on change' + '@validate is called with form data on change', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); @@ -1066,23 +1114,25 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); @@ -1091,50 +1141,52 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { ...data, firstName: 'Foo', }), - '@validate is called with form data' + '@validate is called with form data', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); test('validation errors are exposed as field.Errors on change', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on change' + 'validation errors are not rendered before validation happens on change', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on change' + 'validation errors are not rendered before validation happens on change', ); await fillIn('[data-test-first-name]', 'Foo'); @@ -1143,12 +1195,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on focusout when validation fails' + 'validation errors appear on focusout when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered for untouched fields' + 'validation errors are not rendered for untouched fields', ); }); }); @@ -1158,37 +1210,39 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await input('[data-test-first-name]', 'Foo'); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data on input' + '@validate is called with form data on input', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); @@ -1196,23 +1250,25 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await input('[data-test-first-name]', 'Foo'); @@ -1221,50 +1277,52 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { ...data, firstName: 'Foo', }), - '@validate is called with form data' + '@validate is called with form data', ); await click('[data-test-submit]'); assert.true( validateCallback.calledTwice, - '@validate is called again on submit' + '@validate is called again on submit', ); }); test('validation errors are exposed as field.Errors on input', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on input' + 'validation errors are not rendered before validation happens on input', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens on input' + 'validation errors are not rendered before validation happens on input', ); await input('[data-test-first-name]', 'Foo'); @@ -1273,12 +1331,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on input when validation fails' + 'validation errors appear on input when validation fails', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered for untouched fields' + 'validation errors are not rendered for untouched fields', ); }); }); @@ -1290,68 +1348,70 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data on submit' + '@validate is called with form data on submit', ); await fillIn('[data-test-first-name]', 'Tony'); assert.false( validateCallback.calledTwice, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on focusout' + '@validate is called for revalidation on focusout', ); assert.true( validateCallback .getCall(1) .calledWith({ ...data, firstName: 'Tony' }), - '@validate is called with form data on focusout' + '@validate is called with form data on focusout', ); }); @@ -1359,107 +1419,111 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith('Foo', 'firstName', { ...data, firstName: 'Foo', }), - '@validate is called with form data' + '@validate is called with form data', ); await fillIn('[data-test-first-name]', 'Tony'); assert.false( validateCallback.calledTwice, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on focusout' + '@validate is called for revalidation on focusout', ); assert.true( validateCallback.getCall(1).calledWith('Tony', 'firstName', { ...data, firstName: 'Tony', }), - '@validate is called with form data' + '@validate is called with form data', + ); + }); + + test('validation errors are exposed as field.Errors on focusout', async function (assert) { + const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; + + await render( + , ); - }); - - test('validation errors are exposed as field.Errors on focusout', async function (assert) { - const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - - await render(); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); await fillIn('[data-test-first-name]', 'Foo'); @@ -1467,12 +1531,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await blur('[data-test-first-name]'); @@ -1480,12 +1544,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await click('[data-test-submit]'); @@ -1494,13 +1558,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); await fillIn('[data-test-first-name]', 'Tony'); @@ -1509,13 +1573,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); await blur('[data-test-first-name]'); @@ -1523,13 +1587,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors disappear after successful revalidation on focusout' + 'validation errors disappear after successful revalidation on focusout', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on focusout' + 'validation errors do not disappear until revalidation happens on focusout', ); }); }); @@ -1539,68 +1603,70 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data' + '@validate is called with form data', ); await fillIn('[data-test-first-name]', 'Tony'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on change' + '@validate is called for revalidation on change', ); assert.true( validateCallback .getCall(1) .calledWith({ ...data, firstName: 'Tony' }), - '@validate is called with form data' + '@validate is called with form data', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is not called again on focusout' + '@validate is not called again on focusout', ); }); @@ -1608,107 +1674,111 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith('Foo', 'firstName', { ...data, firstName: 'Foo', }), - '@validate is called with form data' + '@validate is called with form data', ); await fillIn('[data-test-first-name]', 'Tony'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on change' + '@validate is called for revalidation on change', ); assert.true( validateCallback.getCall(1).calledWith('Tony', 'firstName', { ...data, firstName: 'Tony', }), - '@validate is called with form data' + '@validate is called with form data', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on focusout' + '@validate is called for revalidation on focusout', ); }); test('validation errors are exposed as field.Errors on change', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); await fillIn('[data-test-first-name]', 'Foo'); @@ -1716,12 +1786,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await blur('[data-test-first-name]'); @@ -1729,12 +1799,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await click('[data-test-submit]'); @@ -1743,13 +1813,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); await fillIn('[data-test-first-name]', 'Tony'); @@ -1757,13 +1827,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors disappear after successful revalidation on change' + 'validation errors disappear after successful revalidation on change', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on change' + 'validation errors do not disappear until revalidation happens on change', ); }); }); @@ -1773,68 +1843,70 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith({ ...data, firstName: 'Foo' }), - '@validate is called with form data' + '@validate is called with form data', ); await input('[data-test-first-name]', 'Tony'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on input' + '@validate is called for revalidation on input', ); assert.true( validateCallback .getCall(1) .calledWith({ ...data, firstName: 'Tony' }), - '@validate is called with form data' + '@validate is called with form data', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is not called again on focusout' + '@validate is not called again on focusout', ); }); @@ -1842,107 +1914,111 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { const data: TestFormData = { firstName: 'Tony', lastName: 'Ward' }; const validateCallback = sinon.spy(); - await render(); + await render( + , + ); await fillIn('[data-test-first-name]', 'Foo'); assert.false( validateCallback.called, - '@validate is not called while typing' + '@validate is not called while typing', ); await blur('[data-test-first-name]'); assert.false( validateCallback.called, - '@validate is not called until submitting' + '@validate is not called until submitting', ); await click('[data-test-submit]'); assert.true( validateCallback.calledOnce, - '@validate is called when submitting' + '@validate is called when submitting', ); assert.true( validateCallback.calledWith('Foo', 'firstName', { ...data, firstName: 'Foo', }), - '@validate is called with form data' + '@validate is called with form data', ); await input('[data-test-first-name]', 'Tony'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on input' + '@validate is called for revalidation on input', ); assert.true( validateCallback.getCall(1).calledWith('Tony', 'firstName', { ...data, firstName: 'Tony', }), - '@validate is called with form data' + '@validate is called with form data', ); await blur('[data-test-first-name]'); assert.true( validateCallback.calledTwice, - '@validate is called for revalidation on focusout' + '@validate is called for revalidation on focusout', ); }); test('validation errors are exposed as field.Errors on input', async function (assert) { const data: TestFormData = { firstName: 'Tony', lastName: 'Foo' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens before form is filled in' + 'validation errors are not rendered before initial validation happens before form is filled in', ); await fillIn('[data-test-first-name]', 'Foo'); @@ -1950,12 +2026,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await blur('[data-test-first-name]'); @@ -1963,12 +2039,12 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before initial validation happens on submit' + 'validation errors are not rendered before initial validation happens on submit', ); await click('[data-test-submit]'); @@ -1977,13 +2053,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { .dom('[data-test-first-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors appear on submit when validation fails' + 'validation errors appear on submit when validation fails', ); await input('[data-test-first-name]', 'Tony'); @@ -1991,13 +2067,13 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors disappear after successful revalidation on input' + 'validation errors disappear after successful revalidation on input', ); assert .dom('[data-test-last-name-errors]') .exists( { count: 1 }, - 'validation errors do not disappear until revalidation happens on input' + 'validation errors do not disappear until revalidation happens on input', ); }); }); @@ -2007,22 +2083,24 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { test('form yields isInvalid', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert.dom('[data-test-invalid]').doesNotExist(); @@ -2039,41 +2117,43 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { test('form yields rawErrors', async function (assert) { const data: TestFormData = { firstName: 'foo' }; - await render(, + ); await click('[data-test-submit]'); @@ -2084,33 +2164,33 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom( - '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-type]' + '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-value]' + '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-message]' + '[data-test-errors="firstName"] [data-test-error]:first-child [data-test-error-message]', ) .hasText('firstName must be upper case!'); assert .dom( - '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-type]' + '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-type]', ) .hasText('notFoo'); assert .dom( - '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-value]' + '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-message]' + '[data-test-errors="firstName"] [data-test-error]:last-child [data-test-error-message]', ) .hasText('Foo is an invalid firstName!'); @@ -2121,17 +2201,17 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom( - '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-type]' + '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-type]', ) .hasText('required'); assert .dom( - '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-value]' + '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-value]', ) .hasNoText(); assert .dom( - '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-message]' + '[data-test-errors="lastName"] [data-test-error]:first-child [data-test-error-message]', ) .hasText('lastName is required!'); }); @@ -2139,22 +2219,24 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { test('field yields isInvalid', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); assert.dom('[data-test-invalid]').doesNotExist(); @@ -2171,34 +2253,36 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { test('field yields rawErrors', async function (assert) { const data: TestFormData = { firstName: 'foo' }; - await render(, + ); await click('[data-test-submit]'); @@ -2208,33 +2292,33 @@ module('Integration Component HeadlessForm > Validation', function (hooks) { assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-type]', ) .hasText('uppercase'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error]:first-child [data-test-error-message]', ) .hasText('firstName must be upper case!'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-type]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-type]', ) .hasText('notFoo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-value]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-value]', ) .hasText('foo'); assert .dom( - '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-message]' + '[data-test-first-name-errors] [data-test-error]:last-child [data-test-error-message]', ) .hasText('Foo is an invalid firstName!'); }); diff --git a/test-app/tests/integration/components/yup-test.gts b/test-app/tests/integration/components/yup-test.gts index ec48d8f..b6b1c39 100644 --- a/test-app/tests/integration/components/yup-test.gts +++ b/test-app/tests/integration/components/yup-test.gts @@ -1,5 +1,3 @@ - - import { click, fillIn, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -31,20 +29,22 @@ module('Integration Component HeadlessForm > yup', function (hooks) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -55,20 +55,22 @@ module('Integration Component HeadlessForm > yup', function (hooks) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; const invalidHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -82,7 +84,7 @@ module('Integration Component HeadlessForm > yup', function (hooks) { }, ], }), - '@onInvalid was called' + '@onInvalid was called', ); }); @@ -90,24 +92,26 @@ module('Integration Component HeadlessForm > yup', function (hooks) { const data: TestFormData = {}; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await fillIn('input[data-test-first-name]', 'Nicole'); await fillIn('input[data-test-last-name]', 'Chung'); @@ -118,38 +122,40 @@ module('Integration Component HeadlessForm > yup', function (hooks) { firstName: 'Nicole', lastName: 'Chung', }), - '@onSubmit has been called' + '@onSubmit has been called', ); }); test('validation errors are exposed as field.Errors on submit', async function (assert) { const data: TestFormData = { firstName: 'Foo', lastName: 'Smith' }; - await render(); + await render( + , + ); assert .dom('[data-test-first-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens' + 'validation errors are not rendered before validation happens', ); assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered before validation happens' + 'validation errors are not rendered before validation happens', ); await click('[data-test-submit]'); @@ -160,7 +166,7 @@ module('Integration Component HeadlessForm > yup', function (hooks) { assert .dom('[data-test-last-name-errors]') .doesNotExist( - 'validation errors are not rendered when validation succeeds' + 'validation errors are not rendered when validation succeeds', ); }); @@ -170,20 +176,22 @@ module('Integration Component HeadlessForm > yup', function (hooks) { const data: { foo?: string } = {}; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); }); }); diff --git a/test-app/tests/unit/helpers/validate-changeset-test.ts b/test-app/tests/unit/helpers/validate-changeset-test.ts index ad998a5..9a82af7 100644 --- a/test-app/tests/unit/helpers/validate-changeset-test.ts +++ b/test-app/tests/unit/helpers/validate-changeset-test.ts @@ -40,10 +40,14 @@ module('Unit | Helpers | validate-changeset', function () { test('it returns error record if validation fails', async function (assert) { const changeset = Changeset( { firstName: 'foo', lastName: 'Smith' }, - validator + validator, ); - const result = await validate(changeset, ['firstName', 'lastName', 'email']); + const result = await validate(changeset, [ + 'firstName', + 'lastName', + 'email', + ]); assert.deepEqual(result, { firstName: [ diff --git a/test-app/tests/unit/helpers/validate-yup-test.ts b/test-app/tests/unit/helpers/validate-yup-test.ts index 3f3ba3c..86f935a 100644 --- a/test-app/tests/unit/helpers/validate-yup-test.ts +++ b/test-app/tests/unit/helpers/validate-yup-test.ts @@ -29,7 +29,7 @@ module('Unit | Helpers | validate-yup', function () { test('it returns error record if validation fails', async function (assert) { const result = await validator( { firstName: 'Foo', lastName: 'Smith', email: 'bar' }, - ['firstName', 'lastName'] + ['firstName', 'lastName'], ); assert.deepEqual(result, { diff --git a/test-app/types/global.d.ts b/test-app/types/global.d.ts index 91d59b5..548eb82 100644 --- a/test-app/types/global.d.ts +++ b/test-app/types/global.d.ts @@ -12,8 +12,7 @@ import type HeadlessFormYupRegistry from '@universal-ember/form-yup/template-reg declare module '@glint/template/registry' { export default interface Registry - extends HeadlessFormRegistry, - HeadlessFormYupRegistry { + extends HeadlessFormRegistry, HeadlessFormYupRegistry { 'page-title': HelperLike<{ Args: { Positional: [title: string] }; Return: void;