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 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/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/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/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/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/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 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-data-test.gts b/test-app/tests/integration/components/headless-form-data-test.gts index bd7808b..22621cc 100644 --- a/test-app/tests/integration/components/headless-form-data-test.gts +++ b/test-app/tests/integration/components/headless-form-data-test.gts @@ -1,5 +1,3 @@ - - import { tracked } from '@glimmer/tracking'; import { fn } from '@ember/helper'; import { on } from '@ember/modifier'; @@ -19,6 +17,9 @@ import { setupRenderingTest } from 'test-app/tests/helpers'; import { HeadlessForm } from '@universal-ember/form'; +import type Store from '@ember-data/store'; +import type UserModel from 'test-app/models/user'; + module('Integration Component HeadlessForm > Data', function (hooks) { setupRenderingTest(hooks); @@ -36,69 +37,71 @@ module('Integration Component HeadlessForm > Data', function (hooks) { age: 21, }; - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -116,7 +119,16 @@ module('Integration Component HeadlessForm > Data', function (hooks) { test('class object', async function (assert) { class MyData { - constructor(public firstName: string, public lastName: string, public gender: 'male' | 'female' | 'other', public likes: string[], public country: string, public comments: string, public acceptTerms: boolean, public age: number) {} + constructor( + public firstName: string, + public lastName: string, + public gender: 'male' | 'female' | 'other', + public likes: string[], + public country: string, + public comments: string, + public acceptTerms: boolean, + public age: number, + ) {} } const data = new MyData( @@ -130,100 +142,8 @@ module('Integration Component HeadlessForm > Data', function (hooks) { 21, ); - await render(); - - assert.dom('input[data-test-first-name]').hasValue('Tony'); - assert.dom('input[data-test-last-name]').hasValue('Ward'); - assert.dom('input[data-test-gender-male]').isChecked(); - assert.dom('input[data-test-gender-female]').isNotChecked(); - assert.dom('input[data-test-gender-other]').isNotChecked(); - assert.dom('input[data-test-likes-red]').isChecked(); - assert.dom('input[data-test-likes-blue]').isChecked(); - assert.dom('input[data-test-likes-green]').isNotChecked(); - assert.dom('input[data-test-age]').hasValue('21'); - assert.dom('select[data-test-country]').hasValue('USA'); - assert.dom('textarea[data-test-comments]').hasValue('lorem ipsum'); - assert.dom('input[data-test-terms]').isChecked(); - }); - - if (macroCondition(getOwnConfig<{ supportsEmberData: boolean }>().supportsEmberData)) { - test('ember-data object', async function (assert) { - const store = this.owner.lookup('service:store'); - - const data = store.createRecord('user', { - firstName: 'Tony', - lastName: 'Ward', - gender: 'male', - likes: ['red', 'blue'], - country: 'USA', - comments: 'lorem ipsum', - acceptTerms: true, - age: 21, - }); - - await render(, + ); + + assert.dom('input[data-test-first-name]').hasValue('Tony'); + assert.dom('input[data-test-last-name]').hasValue('Ward'); + assert.dom('input[data-test-gender-male]').isChecked(); + assert.dom('input[data-test-gender-female]').isNotChecked(); + assert.dom('input[data-test-gender-other]').isNotChecked(); + assert.dom('input[data-test-likes-red]').isChecked(); + assert.dom('input[data-test-likes-blue]').isChecked(); + assert.dom('input[data-test-likes-green]').isNotChecked(); + assert.dom('input[data-test-age]').hasValue('21'); + assert.dom('select[data-test-country]').hasValue('USA'); + assert.dom('textarea[data-test-comments]').hasValue('lorem ipsum'); + assert.dom('input[data-test-terms]').isChecked(); + }); + + if ( + macroCondition( + getOwnConfig<{ supportsEmberData: boolean }>().supportsEmberData, + ) + ) { + test('ember-data object', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + + const data = store.createRecord('user', { + firstName: 'Tony', + lastName: 'Ward', + gender: 'male', + likes: ['red', 'blue'], + country: 'USA', + comments: 'lorem ipsum', + acceptTerms: true, + age: 21, + }) as UserModel; + + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -306,16 +326,18 @@ module('Integration Component HeadlessForm > Data', function (hooks) { test('value is yielded from field component', async function (assert) { const data = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); assert.dom('[data-test-first-name]').hasText('Tony'); assert.dom('[data-test-last-name]').hasText('Ward'); @@ -334,18 +356,20 @@ module('Integration Component HeadlessForm > Data', function (hooks) { ctx.data = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -367,18 +391,20 @@ module('Integration Component HeadlessForm > Data', function (hooks) { const data = new DummyData(); - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -401,18 +427,20 @@ module('Integration Component HeadlessForm > Data', function (hooks) { const data = new DummyData(); - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -431,25 +459,27 @@ module('Integration Component HeadlessForm > Data', function (hooks) { test('data is not mutated', async function (assert) { const data = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); await fillIn('input[data-test-first-name]', 'Preston'); assert.dom('input[data-test-first-name]').hasValue('Preston'); assert.strictEqual( data.firstName, 'Tony', - 'data object is not mutated after entering data' + 'data object is not mutated after entering data', ); await triggerEvent('form', 'submit'); @@ -457,12 +487,12 @@ module('Integration Component HeadlessForm > Data', function (hooks) { assert.strictEqual( data.firstName, 'Tony', - 'data object is not mutated after submitting' + 'data object is not mutated after submitting', ); }); }); module('actions up', function () { - module('onSubmit is called with user data', function() { + module('onSubmit is called with user data', function () { test('POJO', async function (assert) { const data = { firstName: 'Tony', @@ -476,70 +506,72 @@ module('Integration Component HeadlessForm > Data', function (hooks) { }; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -569,7 +601,7 @@ module('Integration Component HeadlessForm > Data', function (hooks) { acceptTerms: false, age: 21, }, - 'original data is not mutated' + 'original data is not mutated', ); assert.true( @@ -583,13 +615,22 @@ module('Integration Component HeadlessForm > Data', function (hooks) { acceptTerms: true, age: 20, }), - 'new data is passed to submit handler' + 'new data is passed to submit handler', ); }); test('class object', async function (assert) { class MyData { - constructor(public firstName: string, public lastName: string, public gender: 'male' | 'female' | 'other', public likes: string[], public country: string, public comments: string, public acceptTerms: boolean, public age: number) {} + constructor( + public firstName: string, + public lastName: string, + public gender: 'male' | 'female' | 'other', + public likes: string[], + public country: string, + public comments: string, + public acceptTerms: boolean, + public age: number, + ) {} } const data = new MyData( @@ -604,32 +645,33 @@ module('Integration Component HeadlessForm > Data', function (hooks) { ); const submitHandler = sinon.spy(); - await render(, + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -697,7 +740,7 @@ module('Integration Component HeadlessForm > Data', function (hooks) { acceptTerms: false, age: 21, }, - 'original data is not mutated' + 'original data is not mutated', ); assert.true( @@ -711,13 +754,17 @@ module('Integration Component HeadlessForm > Data', function (hooks) { acceptTerms: true, age: 20, }), - 'new data is passed to submit handler' + 'new data is passed to submit handler', ); }); - if (macroCondition(getOwnConfig<{ supportsEmberData: boolean }>().supportsEmberData)) { + if ( + macroCondition( + getOwnConfig<{ supportsEmberData: boolean }>().supportsEmberData, + ) + ) { test('ember-data object', async function (assert) { - const store = this.owner.lookup('service:store'); + const store = this.owner.lookup('service:store') as Store; const data = store.createRecord('user', { firstName: 'Tony', @@ -728,73 +775,79 @@ module('Integration Component HeadlessForm > Data', function (hooks) { comments: 'lorem ipsum', acceptTerms: false, age: 21, - }); + }) as UserModel; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -832,7 +885,7 @@ module('Integration Component HeadlessForm > Data', function (hooks) { acceptTerms: true, age: 20, }), - 'new data is passed to submit handler' + 'new data is passed to submit handler', ); }); } @@ -845,23 +898,25 @@ module('Integration Component HeadlessForm > Data', function (hooks) { }; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); assert.dom('input[data-test-last-name]').hasValue('Ward'); @@ -876,33 +931,35 @@ module('Integration Component HeadlessForm > Data', function (hooks) { firstName: 'Nicole', lastName: 'Chung', }), - 'new data is passed to submit handler' + 'new data is passed to submit handler', ); }); test('setValue yielded from field sets internal value', async function (assert) { const data = { firstName: 'Tony' }; - await render(); + await render( + , + ); assert.dom('input[data-test-first-name]').hasValue('Tony'); @@ -917,25 +974,27 @@ module('Integration Component HeadlessForm > Data', function (hooks) { test('mutates passed @data when form fields are updated', async function (assert) { const data = { firstName: 'Tony', lastName: 'Ward' }; - await render(); + await render( + , + ); await fillIn('input[data-test-first-name]', 'Preston'); assert.dom('input[data-test-first-name]').hasValue('Preston'); assert.strictEqual( data.firstName, 'Preston', - 'data object is mutated after entering data' + 'data object is mutated after entering data', ); }); @@ -943,24 +1002,26 @@ module('Integration Component HeadlessForm > Data', function (hooks) { const data = { firstName: 'Tony', lastName: 'Ward' }; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await fillIn('input[data-test-first-name]', 'Preston'); await click('[data-test-submit]'); @@ -968,7 +1029,7 @@ module('Integration Component HeadlessForm > Data', function (hooks) { assert.strictEqual( submitHandler.firstCall.firstArg, data, - '@OnSubmit is called with same instance of @data, not a copy' + '@OnSubmit is called with same instance of @data, not a copy', ); }); }); 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-native-validation-test.gts b/test-app/tests/integration/components/headless-form-native-validation-test.gts index 3ed8bd5..b501712 100644 --- a/test-app/tests/integration/components/headless-form-native-validation-test.gts +++ b/test-app/tests/integration/components/headless-form-native-validation-test.gts @@ -1,5 +1,3 @@ - - import { blur, click, fillIn, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -24,15 +22,17 @@ module( const data: TestFormData = {}; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); assert.dom('form').hasAttribute('novalidate'); }); @@ -41,21 +41,23 @@ module( const data: TestFormData = {}; const submitHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert.false( submitHandler.called, - '@onSubmit is not called when required field in empty' + '@onSubmit is not called when required field in empty', ); }); @@ -63,21 +65,23 @@ module( const data: TestFormData = {}; const invalidHandler = sinon.spy(); - await render(); + await render( + , + ); await click('[data-test-submit]'); assert.true( invalidHandler.calledOnce, - '@onInvalid was called when required field in empty' + '@onInvalid was called when required field in empty', ); }); @@ -85,19 +89,21 @@ module( 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'); @@ -109,31 +115,33 @@ module( test('validation errors are revalidated on submit', 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' + '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]'); @@ -144,23 +152,25 @@ module( 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 renders all error messages in non-block mode', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -173,30 +183,32 @@ module( test('field.Errors yields errors in block mode', async function (assert) { const data: TestFormData = {}; - await render(, + ); await click('[data-test-submit]'); @@ -207,17 +219,17 @@ module( 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('native'); 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]', ) .hasNoText(); 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]', ) .hasAnyText(); // validation error message is browser and locale dependant, so testing against actual message would be very brittle. }); @@ -225,33 +237,35 @@ module( test('works with setCustomValidity', async function (this: RenderingTestContext, assert) { const data: TestFormData = {}; - await render(, + ); const input = this.element.querySelector( - '[data-test-first-name]' + '[data-test-first-name]', ) as HTMLInputElement; input.setCustomValidity('This is a custom error message'); @@ -265,17 +279,17 @@ module( 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('native'); 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]', ) .hasNoText(); 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('This is a custom error message'); }); @@ -283,15 +297,17 @@ module( test('validation errors mark the control as invalid', async function (assert) { const data: TestFormData = {}; - await render(); + await render( + , + ); await click('[data-test-submit]'); @@ -323,43 +339,49 @@ module( ] : undefined; - await render(); + {{/each}} + + + + Last Name + + + + + + , + ); 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', ); }); }); }); - } + }, ); 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/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. 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;