diff --git a/.all-contributorsrc b/.all-contributorsrc index 198cd09c..07a9a931 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -410,6 +410,33 @@ "code", "test" ] + }, + { + "login": "Arthie", + "name": "Arthur Petrie", + "avatar_url": "https://avatars.githubusercontent.com/u/16376476?v=4", + "profile": "https://arthurpetrie.com", + "contributions": [ + "code" + ] + }, + { + "login": "FabienDehopre", + "name": "Fabien Dehopré", + "avatar_url": "https://avatars.githubusercontent.com/u/97023?v=4", + "profile": "https://github.com/FabienDehopre", + "contributions": [ + "code" + ] + }, + { + "login": "jvereecken", + "name": "Jamie Vereecken", + "avatar_url": "https://avatars.githubusercontent.com/u/108937550?v=4", + "profile": "https://github.com/jvereecken", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index db976dcf..ac9d248f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. { "name": "angular-testing-library", - "image": "mcr.microsoft.com/devcontainers/typescript-node:0-20-bullseye", + "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bullseye", // Features to add to the dev container. More info: https://containers.dev/features. "features": { @@ -13,7 +13,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm i", + "postCreateCommand": "npm install --force", "onCreateCommand": "sudo cp .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt", "waitFor": "postCreateCommand", diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3c3629e6..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 0a96094f..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "root": true, - "ignorePatterns": ["**/*"], - "plugins": ["@nx", "testing-library"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": { - "@nx/enforce-module-boundaries": [ - "error", - { - "enforceBuildableLibDependency": true, - "allow": [], - "depConstraints": [ - { - "sourceTag": "*", - "onlyDependOnLibsWithTags": ["*"] - } - ] - } - ] - } - }, - { - "files": ["*.ts", "*.tsx"], - "extends": ["plugin:@nx/typescript"], - "rules": { - "@typescript-eslint/no-extra-semi": "error", - "no-extra-semi": "off" - } - }, - { - "files": ["*.js", "*.jsx"], - "extends": ["plugin:@nx/javascript"], - "rules": { - "@typescript-eslint/no-extra-semi": "error", - "no-extra-semi": "off" - } - }, - { - "files": ["*.ts"], - "plugins": ["eslint-plugin-import", "@angular-eslint/eslint-plugin", "@typescript-eslint"], - "rules": { - "@typescript-eslint/consistent-type-definitions": "error", - "@typescript-eslint/dot-notation": "off", - "@typescript-eslint/naming-convention": "error", - "@typescript-eslint/no-shadow": [ - "error", - { - "hoist": "all" - } - ], - "@typescript-eslint/no-unused-expressions": "error", - "@typescript-eslint/prefer-function-type": "error", - "@typescript-eslint/quotes": "off", - "@typescript-eslint/type-annotation-spacing": "error", - "@typescript-eslint/no-explicit-any": "off", - "arrow-body-style": "off", - "brace-style": ["error", "1tbs"], - "curly": "error", - "eol-last": "error", - "eqeqeq": ["error", "smart"], - "guard-for-in": "error", - "id-blacklist": "off", - "id-match": "off", - "import/no-deprecated": "warn", - "no-bitwise": "error", - "no-caller": "error", - "no-console": [ - "error", - { - "allow": [ - "log", - "warn", - "dir", - "timeLog", - "assert", - "clear", - "count", - "countReset", - "group", - "groupEnd", - "table", - "dirxml", - "error", - "groupCollapsed", - "Console", - "profile", - "profileEnd", - "timeStamp", - "context" - ] - } - ], - "no-empty": "off", - "no-eval": "error", - "no-new-wrappers": "error", - "no-throw-literal": "error", - "no-undef-init": "error", - "no-underscore-dangle": "off", - "radix": "error", - "spaced-comment": [ - "error", - "always", - { - "markers": ["/"] - } - ] - } - }, - { - "files": ["*.html"], - "rules": {} - }, - { - "files": ["*.ts", "*.js"], - "extends": ["prettier"] - }, - { - "files": ["*.spec.ts"], - "extends": ["plugin:testing-library/angular"], - "rules": { - "testing-library/prefer-explicit-assert": "error" - } - } - ] -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5820814f..215ef7d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: - node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[20]' || '[18, 20]') }} + node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[22]' || '[18, 20, 22]') }} os: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '["ubuntu-latest"]' || '["ubuntu-latest", "windows-latest"]') }} runs-on: ${{ matrix.os }} @@ -38,6 +38,8 @@ jobs: run: npm run build -- --skip-nx-cache - name: test run: npm run test + - name: lint + run: npm run lint - name: Release if: github.repository == 'testing-library/angular-testing-library' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') run: npx semantic-release diff --git a/.gitignore b/.gitignore index 215c8cba..d16a75b3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ /.angular/cache .angular .nx +migrations.json .cache /.sass-cache /connect.lock diff --git a/.node-version b/.node-version index 914d1a73..8fdd954d 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.9 \ No newline at end of file +22 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 2bdc4f98..03ff48d9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -54,4 +54,5 @@ deployment.yaml .DS_Store Thumbs.db -/.nx/cache \ No newline at end of file +/.nx/cache +/.nx/workspace-data \ No newline at end of file diff --git a/README.md b/README.md index 17bffcec..028f7213 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ counter.component.ts ```ts @Component({ - selector: 'app-counter', + selector: 'atl-counter', template: ` {{ hello() }} @@ -179,6 +179,7 @@ You may also be interested in installing `jest-dom` so you can use | Angular | Angular Testing Library | | ------- | ---------------------------- | +| 19.x | 17.x, 16.x, 15.x, 14.x, 13.x | | 18.x | 17.x, 16.x, 15.x, 14.x, 13.x | | 17.x | 17.x, 16.x, 15.x, 14.x, 13.x | | 16.x | 14.x, 13.x | @@ -270,6 +271,11 @@ Thanks goes to these people ([emoji key][emojis]): Daniel Ramírez Barrientos
Daniel Ramírez Barrientos

💻 Mahdi Lazraq
Mahdi Lazraq

💻 ⚠️ + + Arthur Petrie
Arthur Petrie

💻 + Fabien Dehopré
Fabien Dehopré

💻 + Jamie Vereecken
Jamie Vereecken

💻 + diff --git a/apps/example-app-karma/.eslintrc.json b/apps/example-app-karma/.eslintrc.json deleted file mode 100644 index 404aa664..00000000 --- a/apps/example-app-karma/.eslintrc.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "extends": "../../.eslintrc.json", - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts"], - "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], - "parserOptions": { - "project": ["apps/example-app-karma/tsconfig.*?.json"] - }, - "rules": { - "@angular-eslint/directive-selector": [ - "error", - { - "type": "attribute", - "prefix": "app", - "style": "camelCase" - } - ], - "@angular-eslint/component-selector": [ - "error", - { - "type": "element", - "prefix": "app", - "style": "kebab-case" - } - ] - } - }, - { - "files": ["*.spec.ts"], - "env": { - "jasmine": true - }, - "plugins": ["jasmine"], - "extends": ["plugin:jasmine/recommended"] - }, - { - "files": ["*.html"], - "extends": ["plugin:@nx/angular-template"], - "rules": {} - } - ] -} diff --git a/apps/example-app-karma/eslint.config.cjs b/apps/example-app-karma/eslint.config.cjs new file mode 100644 index 00000000..9e951e7a --- /dev/null +++ b/apps/example-app-karma/eslint.config.cjs @@ -0,0 +1,7 @@ +// @ts-check + +// TODO - https://github.com/nrwl/nx/issues/22576 + +/** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */ +const config = (async () => (await import('./eslint.config.mjs')).default)(); +module.exports = config; diff --git a/apps/example-app-karma/eslint.config.mjs b/apps/example-app-karma/eslint.config.mjs new file mode 100644 index 00000000..8f627dbf --- /dev/null +++ b/apps/example-app-karma/eslint.config.mjs @@ -0,0 +1,8 @@ +// @ts-check + +import tseslint from "typescript-eslint"; +import rootConfig from "../../eslint.config.mjs"; + +export default tseslint.config( + ...rootConfig, +); diff --git a/apps/example-app-karma/jasmine-dom.d.ts b/apps/example-app-karma/jasmine-dom.d.ts index f8fa4a7f..54d79038 100644 --- a/apps/example-app-karma/jasmine-dom.d.ts +++ b/apps/example-app-karma/jasmine-dom.d.ts @@ -1,5 +1,4 @@ declare module '@testing-library/jasmine-dom' { - // eslint-disable-next-line @typescript-eslint/naming-convention const JasmineDOM: any; export default JasmineDOM; } diff --git a/apps/example-app-karma/src/app/examples/login-form.spec.ts b/apps/example-app-karma/src/app/examples/login-form.spec.ts index 9c510652..a0282341 100644 --- a/apps/example-app-karma/src/app/examples/login-form.spec.ts +++ b/apps/example-app-karma/src/app/examples/login-form.spec.ts @@ -29,7 +29,7 @@ it('should display invalid message and submit button must be disabled', async () }); @Component({ - selector: 'app-login', + selector: 'atl-login', standalone: true, imports: [ReactiveFormsModule, NgIf], template: ` @@ -51,7 +51,7 @@ class LoginComponent { }); constructor(private fb: FormBuilder) {} - + get email(): FormControl { return this.form.get('email') as FormControl; } diff --git a/apps/example-app-karma/src/app/issues/issue-491.spec.ts b/apps/example-app-karma/src/app/issues/issue-491.spec.ts new file mode 100644 index 00000000..7da4d6d4 --- /dev/null +++ b/apps/example-app-karma/src/app/issues/issue-491.spec.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { render, screen, waitForElementToBeRemoved } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; + +it('test click event with router.navigate', async () => { + const user = userEvent.setup(); + await render(``, { + routes: [ + { + path: '', + component: LoginComponent, + }, + { + path: 'logged-in', + component: LoggedInComponent, + }, + ], + }); + + expect(await screen.findByRole('heading', { name: 'Login' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'submit' })).toBeInTheDocument(); + + const email = screen.getByRole('textbox', { name: 'email' }); + const password = screen.getByLabelText('password'); + + await user.type(email, 'user@example.com'); + await user.type(password, 'with_valid_password'); + + expect(screen.getByRole('button', { name: 'submit' })).toBeEnabled(); + + await user.click(screen.getByRole('button', { name: 'submit' })); + + await waitForElementToBeRemoved(() => screen.queryByRole('heading', { name: 'Login' })); + + expect(await screen.findByRole('heading', { name: 'Logged In' })).toBeVisible(); +}); + +@Component({ + template: ` +

Login

+ + + + `, +}) +class LoginComponent { + constructor(private router: Router) {} + onSubmit(): void { + this.router.navigate(['logged-in']); + } +} + +@Component({ + template: `

Logged In

`, +}) +class LoggedInComponent {} diff --git a/apps/example-app-karma/src/app/issues/rerender.spec.ts b/apps/example-app-karma/src/app/issues/rerender.spec.ts index 9b044d1f..324e8a16 100644 --- a/apps/example-app-karma/src/app/issues/rerender.spec.ts +++ b/apps/example-app-karma/src/app/issues/rerender.spec.ts @@ -7,9 +7,9 @@ it('can rerender component', async () => { }, }); - expect(screen.getByText('Hello Sarah')).toBeTruthy(); + expect(screen.getByText('Hello Sarah')).toBeInTheDocument(); await rerender({ componentProperties: { name: 'Mark' } }); - expect(screen.getByText('Hello Mark')).toBeTruthy(); + expect(screen.getByText('Hello Mark')).toBeInTheDocument(); }); diff --git a/apps/example-app/.eslintrc.json b/apps/example-app/.eslintrc.json deleted file mode 100644 index ed5e4d11..00000000 --- a/apps/example-app/.eslintrc.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "extends": "../../.eslintrc.json", - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts"], - "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], - "parserOptions": { - "project": ["apps/example-app/tsconfig.*?.json"] - }, - "rules": { - "@angular-eslint/directive-selector": [ - "error", - { - "type": "attribute", - "prefix": "app", - "style": "camelCase" - } - ], - "@angular-eslint/component-selector": [ - "error", - { - "type": "element", - "prefix": "app", - "style": "kebab-case" - } - ] - } - }, - { - "files": ["*.spec.ts"], - "env": { - "jest": true - }, - "extends": ["plugin:jest/recommended", "plugin:jest/style", "plugin:jest-dom/recommended"], - "rules": { - "jest/consistent-test-it": ["error"], - "jest/expect-expect": "off" - } - }, - { - "files": ["*.html"], - "extends": ["plugin:@nx/angular-template"], - "rules": {} - } - ] -} diff --git a/apps/example-app/eslint.config.cjs b/apps/example-app/eslint.config.cjs new file mode 100644 index 00000000..9e951e7a --- /dev/null +++ b/apps/example-app/eslint.config.cjs @@ -0,0 +1,7 @@ +// @ts-check + +// TODO - https://github.com/nrwl/nx/issues/22576 + +/** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */ +const config = (async () => (await import('./eslint.config.mjs')).default)(); +module.exports = config; diff --git a/apps/example-app/eslint.config.mjs b/apps/example-app/eslint.config.mjs new file mode 100644 index 00000000..01625848 --- /dev/null +++ b/apps/example-app/eslint.config.mjs @@ -0,0 +1,8 @@ +// @ts-check + +import tseslint from "typescript-eslint"; +import rootConfig from "../../eslint.config.mjs"; + +export default tseslint.config( + ...rootConfig, +); \ No newline at end of file diff --git a/apps/example-app/jest.config.ts b/apps/example-app/jest.config.ts index 4b0c248c..e0ea9c2d 100644 --- a/apps/example-app/jest.config.ts +++ b/apps/example-app/jest.config.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ export default { displayName: { name: 'Example App', diff --git a/apps/example-app/src/app/examples/00-single-component.ts b/apps/example-app/src/app/examples/00-single-component.ts index 7c132c2f..4a092390 100644 --- a/apps/example-app/src/app/examples/00-single-component.ts +++ b/apps/example-app/src/app/examples/00-single-component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-fixture', + selector: 'atl-fixture', standalone: true, template: ` diff --git a/apps/example-app/src/app/examples/01-nested-component.ts b/apps/example-app/src/app/examples/01-nested-component.ts index 645ce966..fd0d0c0e 100644 --- a/apps/example-app/src/app/examples/01-nested-component.ts +++ b/apps/example-app/src/app/examples/01-nested-component.ts @@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ standalone: true, - selector: 'app-button', + selector: 'atl-button', template: ' ', }) export class NestedButtonComponent { @@ -12,7 +12,7 @@ export class NestedButtonComponent { @Component({ standalone: true, - selector: 'app-value', + selector: 'atl-value', template: ' {{ value }} ', }) export class NestedValueComponent { @@ -21,11 +21,11 @@ export class NestedValueComponent { @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: ` - - - + + + `, imports: [NestedButtonComponent, NestedValueComponent], }) diff --git a/apps/example-app/src/app/examples/02-input-output.spec.ts b/apps/example-app/src/app/examples/02-input-output.spec.ts index 847f6e14..5a55bd57 100644 --- a/apps/example-app/src/app/examples/02-input-output.spec.ts +++ b/apps/example-app/src/app/examples/02-input-output.spec.ts @@ -36,7 +36,7 @@ test.skip('is possible to set input and listen for output with the template synt const user = userEvent.setup(); const sendSpy = jest.fn(); - await render('', { + await render('', { imports: [InputOutputComponent], on: { sendValue: sendSpy, @@ -94,7 +94,7 @@ test('is possible to set input and listen for output with the template syntax (d const user = userEvent.setup(); const sendSpy = jest.fn(); - await render('', { + await render('', { imports: [InputOutputComponent], componentProperties: { sendValue: sendSpy, diff --git a/apps/example-app/src/app/examples/02-input-output.ts b/apps/example-app/src/app/examples/02-input-output.ts index 5bf70abb..3d7f9796 100644 --- a/apps/example-app/src/app/examples/02-input-output.ts +++ b/apps/example-app/src/app/examples/02-input-output.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: ` {{ value }} diff --git a/apps/example-app/src/app/examples/03-forms.ts b/apps/example-app/src/app/examples/03-forms.ts index 49756dca..a62d8650 100644 --- a/apps/example-app/src/app/examples/03-forms.ts +++ b/apps/example-app/src/app/examples/03-forms.ts @@ -4,7 +4,7 @@ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', imports: [ReactiveFormsModule, NgForOf, NgIf], template: `
diff --git a/apps/example-app/src/app/examples/04-forms-with-material.ts b/apps/example-app/src/app/examples/04-forms-with-material.ts index ef80493e..cf117a51 100644 --- a/apps/example-app/src/app/examples/04-forms-with-material.ts +++ b/apps/example-app/src/app/examples/04-forms-with-material.ts @@ -18,7 +18,7 @@ import { MatNativeDateModule } from '@angular/material/core'; NgForOf, NgIf, ], - selector: 'app-fixture', + selector: 'atl-fixture', template: ` diff --git a/apps/example-app/src/app/examples/05-component-provider.ts b/apps/example-app/src/app/examples/05-component-provider.ts index 1f345b94..2d66b070 100644 --- a/apps/example-app/src/app/examples/05-component-provider.ts +++ b/apps/example-app/src/app/examples/05-component-provider.ts @@ -21,7 +21,7 @@ export class CounterService { @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: ` {{ counter.value() }} diff --git a/apps/example-app/src/app/examples/06-with-ngrx-store.ts b/apps/example-app/src/app/examples/06-with-ngrx-store.ts index b1db1d46..8702843c 100644 --- a/apps/example-app/src/app/examples/06-with-ngrx-store.ts +++ b/apps/example-app/src/app/examples/06-with-ngrx-store.ts @@ -18,7 +18,7 @@ const selectValue = createSelector( @Component({ standalone: true, imports: [AsyncPipe], - selector: 'app-fixture', + selector: 'atl-fixture', template: ` {{ value | async }} diff --git a/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts b/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts index 7754bf1c..915a88d8 100644 --- a/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts +++ b/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts @@ -10,10 +10,12 @@ export const selectItems = createSelector( @Component({ standalone: true, imports: [AsyncPipe, NgForOf], - selector: 'app-fixture', + selector: 'atl-fixture', template: `
    -
  • {{ item }}
  • +
  • + +
`, }) diff --git a/apps/example-app/src/app/examples/08-directive.spec.ts b/apps/example-app/src/app/examples/08-directive.spec.ts index 8b70b38c..28a41e98 100644 --- a/apps/example-app/src/app/examples/08-directive.spec.ts +++ b/apps/example-app/src/app/examples/08-directive.spec.ts @@ -6,7 +6,7 @@ import { SpoilerDirective } from './08-directive'; test('it is possible to test directives with container component', async () => { @Component({ - template: `
`, + template: `
`, imports: [SpoilerDirective], standalone: true, }) @@ -32,7 +32,7 @@ test('it is possible to test directives with container component', async () => { test('it is possible to test directives', async () => { const user = userEvent.setup(); - await render('
', { + await render('
', { imports: [SpoilerDirective], }); @@ -55,7 +55,7 @@ test('it is possible to test directives with props', async () => { const hidden = 'SPOILER ALERT'; const visible = 'There is nothing to see here ...'; - await render('
', { + await render('
', { imports: [SpoilerDirective], componentProperties: { hidden, @@ -80,7 +80,7 @@ test('it is possible to test directives with props in template', async () => { const hidden = 'SPOILER ALERT'; const visible = 'There is nothing to see here ...'; - await render(``, { + await render(``, { imports: [SpoilerDirective], }); diff --git a/apps/example-app/src/app/examples/08-directive.ts b/apps/example-app/src/app/examples/08-directive.ts index 40548b1e..63efe415 100644 --- a/apps/example-app/src/app/examples/08-directive.ts +++ b/apps/example-app/src/app/examples/08-directive.ts @@ -2,7 +2,7 @@ import { Directive, HostListener, ElementRef, Input, OnInit } from '@angular/cor @Directive({ standalone: true, - selector: '[appSpoiler]', + selector: '[atlSpoiler]', }) export class SpoilerDirective implements OnInit { @Input() hidden = 'SPOILER'; diff --git a/apps/example-app/src/app/examples/09-router.ts b/apps/example-app/src/app/examples/09-router.ts index 888d7fdf..e46773be 100644 --- a/apps/example-app/src/app/examples/09-router.ts +++ b/apps/example-app/src/app/examples/09-router.ts @@ -6,7 +6,7 @@ import { map } from 'rxjs/operators'; @Component({ standalone: true, imports: [RouterLink, RouterOutlet], - selector: 'app-main', + selector: 'atl-main', template: ` Load one | Load two | Load three | @@ -21,7 +21,7 @@ export class RootComponent {} @Component({ standalone: true, imports: [RouterLink, AsyncPipe], - selector: 'app-detail', + selector: 'atl-detail', template: `

Detail {{ id | async }}

@@ -40,7 +40,7 @@ export class DetailComponent { @Component({ standalone: true, - selector: 'app-detail-hidden', + selector: 'atl-detail-hidden', template: ' You found the treasure! ', }) export class HiddenDetailComponent {} diff --git a/apps/example-app/src/app/examples/10-inject-token-dependency.ts b/apps/example-app/src/app/examples/10-inject-token-dependency.ts index 6a17d538..f7b2f661 100644 --- a/apps/example-app/src/app/examples/10-inject-token-dependency.ts +++ b/apps/example-app/src/app/examples/10-inject-token-dependency.ts @@ -4,7 +4,7 @@ export const DATA = new InjectionToken<{ text: string }>('Components Data'); @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: ' {{ data.text }} ', }) export class DataInjectedComponent { diff --git a/apps/example-app/src/app/examples/11-ng-content.spec.ts b/apps/example-app/src/app/examples/11-ng-content.spec.ts index 2f910252..468a3f29 100644 --- a/apps/example-app/src/app/examples/11-ng-content.spec.ts +++ b/apps/example-app/src/app/examples/11-ng-content.spec.ts @@ -5,7 +5,7 @@ import { CellComponent } from './11-ng-content'; test('it is possible to test ng-content without selector', async () => { const projection = 'it should be showed into a p element!'; - await render(`${projection}`, { + await render(`${projection}`, { imports: [CellComponent], }); diff --git a/apps/example-app/src/app/examples/11-ng-content.ts b/apps/example-app/src/app/examples/11-ng-content.ts index d4446839..0dd668bc 100644 --- a/apps/example-app/src/app/examples/11-ng-content.ts +++ b/apps/example-app/src/app/examples/11-ng-content.ts @@ -2,7 +2,7 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: `

diff --git a/apps/example-app/src/app/examples/12-service-component.ts b/apps/example-app/src/app/examples/12-service-component.ts index 2aed1657..1746eb2c 100644 --- a/apps/example-app/src/app/examples/12-service-component.ts +++ b/apps/example-app/src/app/examples/12-service-component.ts @@ -19,7 +19,7 @@ export class CustomersService { @Component({ standalone: true, imports: [AsyncPipe, NgForOf], - selector: 'app-fixture', + selector: 'atl-fixture', template: `

  • diff --git a/apps/example-app/src/app/examples/13-scrolling.component.ts b/apps/example-app/src/app/examples/13-scrolling.component.ts index 7d7b2e7c..6a36ed8f 100644 --- a/apps/example-app/src/app/examples/13-scrolling.component.ts +++ b/apps/example-app/src/app/examples/13-scrolling.component.ts @@ -4,7 +4,7 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; @Component({ standalone: true, imports: [ScrollingModule], - selector: 'app-cdk-virtual-scroll-overview-example', + selector: 'atl-cdk-virtual-scroll-overview-example', template: `
    {{ item }}
    diff --git a/apps/example-app/src/app/examples/14-async-component.spec.ts b/apps/example-app/src/app/examples/14-async-component.spec.ts index b54740a2..5cfd3e0e 100644 --- a/apps/example-app/src/app/examples/14-async-component.spec.ts +++ b/apps/example-app/src/app/examples/14-async-component.spec.ts @@ -3,7 +3,6 @@ import { render, screen, fireEvent } from '@testing-library/angular'; import { AsyncComponent } from './14-async-component'; -// eslint-disable-next-line jest/no-disabled-tests test.skip('can use fakeAsync utilities', fakeAsync(async () => { await render(AsyncComponent); diff --git a/apps/example-app/src/app/examples/14-async-component.ts b/apps/example-app/src/app/examples/14-async-component.ts index f87732ad..64d7aaa2 100644 --- a/apps/example-app/src/app/examples/14-async-component.ts +++ b/apps/example-app/src/app/examples/14-async-component.ts @@ -6,7 +6,7 @@ import { delay, filter, mapTo } from 'rxjs/operators'; @Component({ standalone: true, imports: [AsyncPipe, NgIf], - selector: 'app-fixture', + selector: 'atl-fixture', template: `
    {{ data }}
    diff --git a/apps/example-app/src/app/examples/15-dialog.component.spec.ts b/apps/example-app/src/app/examples/15-dialog.component.spec.ts index 97ad829a..017afdcf 100644 --- a/apps/example-app/src/app/examples/15-dialog.component.spec.ts +++ b/apps/example-app/src/app/examples/15-dialog.component.spec.ts @@ -38,7 +38,7 @@ test('closes the dialog via the backdrop', async () => { const dialogTitleControl = await screen.findByRole('heading', { name: /dialog title/i }); expect(dialogTitleControl).toBeInTheDocument(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, testing-library/no-node-access + // eslint-disable-next-line testing-library/no-node-access await user.click(document.querySelector('.cdk-overlay-backdrop')!); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); diff --git a/apps/example-app/src/app/examples/15-dialog.component.ts b/apps/example-app/src/app/examples/15-dialog.component.ts index f6c89703..029ee64e 100644 --- a/apps/example-app/src/app/examples/15-dialog.component.ts +++ b/apps/example-app/src/app/examples/15-dialog.component.ts @@ -4,7 +4,7 @@ import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dial @Component({ standalone: true, imports: [MatDialogModule], - selector: 'app-dialog-overview-example', + selector: 'atl-dialog-overview-example', template: '', }) export class DialogComponent { @@ -18,7 +18,7 @@ export class DialogComponent { @Component({ standalone: true, imports: [MatDialogModule], - selector: 'app-dialog-overview-example-dialog', + selector: 'atl-dialog-overview-example-dialog', template: `

    Dialog Title

    Dialog content
    diff --git a/apps/example-app/src/app/examples/16-input-getter-setter.ts b/apps/example-app/src/app/examples/16-input-getter-setter.ts index a9097a48..4c189000 100644 --- a/apps/example-app/src/app/examples/16-input-getter-setter.ts +++ b/apps/example-app/src/app/examples/16-input-getter-setter.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; @Component({ standalone: true, - selector: 'app-fixture', + selector: 'atl-fixture', template: ` {{ derivedValue }} {{ value }} diff --git a/apps/example-app/src/app/examples/17-component-with-attribute-selector.spec.ts b/apps/example-app/src/app/examples/17-component-with-attribute-selector.spec.ts index ba69f70a..f33dee3e 100644 --- a/apps/example-app/src/app/examples/17-component-with-attribute-selector.spec.ts +++ b/apps/example-app/src/app/examples/17-component-with-attribute-selector.spec.ts @@ -5,7 +5,7 @@ import { ComponentWithAttributeSelectorComponent } from './17-component-with-att // for components with attribute selectors! test('is possible to set input of component with attribute selector through template', async () => { await render( - ``, + ``, { imports: [ComponentWithAttributeSelectorComponent], }, diff --git a/apps/example-app/src/app/examples/17-component-with-attribute-selector.ts b/apps/example-app/src/app/examples/17-component-with-attribute-selector.ts index ac2a25d3..930032c4 100644 --- a/apps/example-app/src/app/examples/17-component-with-attribute-selector.ts +++ b/apps/example-app/src/app/examples/17-component-with-attribute-selector.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; @Component({ standalone: true, - selector: 'app-fixture-component-with-attribute-selector[value]', + selector: 'atl-fixture-component-with-attribute-selector[value]', template: ` {{ value }} `, }) export class ComponentWithAttributeSelectorComponent { diff --git a/apps/example-app/src/app/examples/19-standalone-component.ts b/apps/example-app/src/app/examples/19-standalone-component.ts index efcf0884..95eae3d5 100644 --- a/apps/example-app/src/app/examples/19-standalone-component.ts +++ b/apps/example-app/src/app/examples/19-standalone-component.ts @@ -1,17 +1,17 @@ import { Component, Input } from '@angular/core'; @Component({ - selector: 'app-standalone', + selector: 'atl-standalone', template: `
    Standalone Component
    `, standalone: true, }) export class StandaloneComponent {} @Component({ - selector: 'app-standalone-with-child', + selector: 'atl-standalone-with-child', template: `

    Hi {{ name }}

    This has a child

    - `, + `, standalone: true, imports: [StandaloneComponent], }) diff --git a/apps/example-app/src/app/examples/20-test-harness.spec.ts b/apps/example-app/src/app/examples/20-test-harness.spec.ts index 6e4d9e3a..4a88a580 100644 --- a/apps/example-app/src/app/examples/20-test-harness.spec.ts +++ b/apps/example-app/src/app/examples/20-test-harness.spec.ts @@ -6,9 +6,8 @@ import userEvent from '@testing-library/user-event'; import { HarnessComponent } from './20-test-harness'; -// eslint-disable-next-line jest/no-disabled-tests test.skip('can be used with TestHarness', async () => { - const view = await render(``, { + const view = await render(``, { imports: [HarnessComponent], }); const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture); @@ -21,7 +20,6 @@ test.skip('can be used with TestHarness', async () => { expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i); }); -// eslint-disable-next-line jest/no-disabled-tests test.skip('can be used in combination with TestHarness', async () => { const user = userEvent.setup(); diff --git a/apps/example-app/src/app/examples/20-test-harness.ts b/apps/example-app/src/app/examples/20-test-harness.ts index 08d6afd7..8e5e4071 100644 --- a/apps/example-app/src/app/examples/20-test-harness.ts +++ b/apps/example-app/src/app/examples/20-test-harness.ts @@ -3,7 +3,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ - selector: 'app-harness', + selector: 'atl-harness', standalone: true, imports: [MatButtonModule, MatSnackBarModule], template: ` diff --git a/apps/example-app/src/app/examples/21-deferable-view.component.ts b/apps/example-app/src/app/examples/21-deferable-view.component.ts index ce47a586..7b66d85a 100644 --- a/apps/example-app/src/app/examples/21-deferable-view.component.ts +++ b/apps/example-app/src/app/examples/21-deferable-view.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-deferable-view-child', + selector: 'atl-deferable-view-child', template: `

    Hello from deferred child component

    `, standalone: true, }) @@ -10,7 +10,7 @@ export class DeferableViewChildComponent {} @Component({ template: ` @defer (on timer(2s)) { - + } @placeholder {

    Hello from placeholder

    } @loading { diff --git a/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts b/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts index 470e639d..355e8ae4 100644 --- a/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts +++ b/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts @@ -60,7 +60,7 @@ test('output emits a value', async () => { age: '45', }, on: { - submit: submitFn, + submitValue: submitFn, }, }); diff --git a/apps/example-app/src/app/examples/22-signal-inputs.component.ts b/apps/example-app/src/app/examples/22-signal-inputs.component.ts index dfe6bd03..27ed23b7 100644 --- a/apps/example-app/src/app/examples/22-signal-inputs.component.ts +++ b/apps/example-app/src/app/examples/22-signal-inputs.component.ts @@ -2,7 +2,7 @@ import { Component, computed, input, model, numberAttribute, output } from '@ang import { FormsModule } from '@angular/forms'; @Component({ - selector: 'app-signal-input', + selector: 'atl-signal-input', template: `
    {{ greetings() }} {{ name() }} of {{ age() }} years old
    {{ greetingMessage() }}
    @@ -18,11 +18,11 @@ export class SignalInputComponent { }); age = input.required({ transform: numberAttribute }); name = model.required(); - submit = output(); + submitValue = output(); greetingMessage = computed(() => `${this.greetings()} ${this.name()} of ${this.age()} years old`); submitName() { - this.submit.emit(this.name()); + this.submitValue.emit(this.name()); } } diff --git a/apps/example-app/src/app/examples/23-host-directive.spec.ts b/apps/example-app/src/app/examples/23-host-directive.spec.ts new file mode 100644 index 00000000..32892992 --- /dev/null +++ b/apps/example-app/src/app/examples/23-host-directive.spec.ts @@ -0,0 +1,22 @@ +import { aliasedInput, render, screen } from '@testing-library/angular'; +import { HostDirectiveComponent } from './23-host-directive'; + +test('can set input properties of host directives using aliasedInput', async () => { + await render(HostDirectiveComponent, { + inputs: { + ...aliasedInput('atlText', 'Hello world'), + }, + }); + + expect(screen.getByText(/hello world/i)).toBeInTheDocument(); +}); + +test('can set input properties of host directives using componentInputs', async () => { + await render(HostDirectiveComponent, { + componentInputs: { + atlText: 'Hello world', + }, + }); + + expect(screen.getByText(/hello world/i)).toBeInTheDocument(); +}); diff --git a/apps/example-app/src/app/examples/23-host-directive.ts b/apps/example-app/src/app/examples/23-host-directive.ts new file mode 100644 index 00000000..3e201c75 --- /dev/null +++ b/apps/example-app/src/app/examples/23-host-directive.ts @@ -0,0 +1,21 @@ +import { Component, Directive, ElementRef, input, OnInit } from '@angular/core'; + +@Directive({ + selector: '[atlText]', +}) +export class TextDirective implements OnInit { + atlText = input(''); + + constructor(private el: ElementRef) {} + + ngOnInit() { + this.el.nativeElement.textContent = this.atlText(); + } +} + +@Component({ + selector: 'atl-host-directive', + template: ``, + hostDirectives: [{ directive: TextDirective, inputs: ['atlText'] }], +}) +export class HostDirectiveComponent {} diff --git a/apps/example-app/src/test-setup.ts b/apps/example-app/src/test-setup.ts index 0da94a0a..96bfd347 100644 --- a/apps/example-app/src/test-setup.ts +++ b/apps/example-app/src/test-setup.ts @@ -1,2 +1,4 @@ -import 'jest-preset-angular/setup-jest'; +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; import '@testing-library/jest-dom'; + +setupZoneTestEnv(); diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 00000000..9e951e7a --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,7 @@ +// @ts-check + +// TODO - https://github.com/nrwl/nx/issues/22576 + +/** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */ +const config = (async () => (await import('./eslint.config.mjs')).default)(); +module.exports = config; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..95e031ac --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,66 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import angular from "angular-eslint"; +import jestDom from 'eslint-plugin-jest-dom'; +import testingLibrary from 'eslint-plugin-testing-library'; + +export default tseslint.config( + { + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + ...angular.configs.tsRecommended, + ], + processor: angular.processInlineTemplates, + rules: { + "@angular-eslint/directive-selector": [ + "error", + { + type: "attribute", + prefix: "atl", + style: "camelCase", + }, + ], + "@angular-eslint/component-selector": [ + "error", + { + type: "element", + prefix: "atl", + style: "kebab-case", + }, + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + // These are needed for test cases + "@angular-eslint/prefer-standalone": "off", + "@angular-eslint/no-input-rename": "off", + "@angular-eslint/no-input-rename": "off", + }, + }, + { + files: ["**/*.spec.ts"], + extends: [ + jestDom.configs["flat/recommended"], + testingLibrary.configs["flat/angular"], + ], + }, + { + files: ["**/*.html"], + extends: [ + ...angular.configs.templateRecommended, + ...angular.configs.templateAccessibility, + ], + rules: {}, + } +); diff --git a/jest.config.ts b/jest.config.ts index 0830aab5..f5c10f47 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,5 @@ -const { getJestProjects } = require('@nx/jest'); +const { getJestProjectsAsync } = require('@nx/jest'); -export default { - projects: getJestProjects(), -}; +export default async () => ({ + projects: await getJestProjectsAsync(), +}); diff --git a/nx.json b/nx.json index df534f76..a308e678 100644 --- a/nx.json +++ b/nx.json @@ -82,7 +82,7 @@ } }, "@nx/eslint:lint": { - "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], + "inputs": ["default", "{workspaceRoot}/eslint.config.cjs"], "cache": true } }, @@ -96,7 +96,7 @@ "!{projectRoot}/karma.conf.js", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/jest.config.[jt]s", - "!{projectRoot}/.eslintrc.json", + "!{projectRoot}/eslint.config.cjs", "!{projectRoot}/src/test-setup.[jt]s" ] }, diff --git a/package.json b/package.json index 235d0eca..bbf0e3c8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "nx run-many --target=build --projects=testing-library", "build:schematics": "tsc -p ./projects/testing-library/tsconfig.schematics.json", "test": "nx run-many --target=test --all --parallel=1", - "lint": "nx workspace-lint && nx lint", + "lint": "nx run-many --all --target=lint", "e2e": "nx e2e", "affected:apps": "nx affected:apps", "affected:libs": "nx affected:libs", @@ -27,83 +27,82 @@ "prepare": "git config core.hookspath .githooks" }, "dependencies": { - "@angular/animations": "18.0.0", - "@angular/cdk": "18.0.0", - "@angular/common": "18.0.0", - "@angular/compiler": "18.0.0", - "@angular/core": "18.0.0", - "@angular/material": "18.0.0", - "@angular/platform-browser": "18.0.0", - "@angular/platform-browser-dynamic": "18.0.0", - "@angular/router": "18.0.0", - "@ngrx/store": "18.0.0-beta.1", - "@nx/angular": "19.1.0", - "@testing-library/dom": "^10.0.0", + "@angular/animations": "19.0.1", + "@angular/cdk": "19.0.1", + "@angular/common": "19.0.1", + "@angular/compiler": "19.0.1", + "@angular/core": "19.0.1", + "@angular/material": "19.0.1", + "@angular/platform-browser": "19.0.1", + "@angular/platform-browser-dynamic": "19.0.1", + "@angular/router": "19.0.1", + "@ngrx/store": "19.0.0", + "@nx/angular": "20.3.0", + "@testing-library/dom": "^10.4.0", "rxjs": "7.8.0", - "tslib": "~2.3.1", - "zone.js": "0.14.2" + "tslib": "~2.8.1", + "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "18.0.1", - "@angular-devkit/core": "18.0.1", - "@angular-devkit/schematics": "18.0.1", - "@angular-eslint/builder": "17.3.0", - "@angular-eslint/eslint-plugin": "17.3.0", - "@angular-eslint/eslint-plugin-template": "17.3.0", - "@angular-eslint/schematics": "17.5.1", - "@angular-eslint/template-parser": "17.3.0", - "@angular/cli": "~18.0.0", - "@angular/compiler-cli": "18.0.0", - "@angular/forms": "18.0.0", - "@angular/language-service": "18.0.0", - "@nx/eslint": "19.1.0", - "@nx/eslint-plugin": "19.1.0", - "@nx/jest": "19.1.0", - "@nx/node": "19.1.0", - "@nx/plugin": "19.1.0", - "@nx/workspace": "19.1.0", - "@schematics/angular": "18.0.1", - "@testing-library/jasmine-dom": "^1.2.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/user-event": "^14.4.3", + "@angular-devkit/build-angular": "19.0.1", + "@angular-devkit/core": "19.0.1", + "@angular-devkit/schematics": "19.0.1", + "@angular-eslint/builder": "19.0.2", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "@angular-eslint/schematics": "19.0.2", + "@angular-eslint/template-parser": "19.0.2", + "@angular/cli": "~19.0.6", + "@angular/compiler-cli": "19.0.1", + "@angular/forms": "19.0.1", + "@angular/language-service": "19.0.1", + "@eslint/eslintrc": "^2.1.1", + "@nx/eslint": "20.3.0", + "@nx/eslint-plugin": "20.3.0", + "@nx/jest": "20.3.0", + "@nx/node": "20.3.0", + "@nx/plugin": "20.3.0", + "@nx/workspace": "20.3.0", + "@schematics/angular": "18.2.9", + "@testing-library/jasmine-dom": "^1.3.3", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.5.2", "@types/jasmine": "4.3.1", - "@types/jest": "29.5.1", - "@types/node": "18.16.9", - "@types/testing-library__jasmine-dom": "^1.3.0", - "@typescript-eslint/eslint-plugin": "7.3.0", - "@typescript-eslint/parser": "7.3.0", - "autoprefixer": "^10.4.0", - "cpy-cli": "^3.1.1", - "eslint": "8.57.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-import": "~2.25.4", - "eslint-plugin-jasmine": "~4.1.3", - "eslint-plugin-jest": "^27.6.3", - "eslint-plugin-jest-dom": "~4.0.1", - "eslint-plugin-testing-library": "~5.0.1", + "@types/jest": "29.5.14", + "@types/node": "22.10.1", + "@types/testing-library__jasmine-dom": "^1.3.4", + "@typescript-eslint/types": "^8.19.0", + "@typescript-eslint/utils": "^8.19.0", + "angular-eslint": "^19.0.2", + "autoprefixer": "^10.4.20", + "cpy-cli": "^5.0.0", + "eslint": "^9.8.0", + "eslint-plugin-jest-dom": "~5.5.0", + "eslint-plugin-testing-library": "~7.1.1", "jasmine-core": "4.2.0", "jasmine-spec-reporter": "7.0.0", "jest": "29.7.0", - "jest-environment-jsdom": "29.5.0", - "jest-preset-angular": "14.1.0", + "jest-environment-jsdom": "29.7.0", + "jest-preset-angular": "14.4.2", "karma": "6.4.0", - "karma-chrome-launcher": "^3.1.0", + "karma-chrome-launcher": "^3.2.0", "karma-coverage": "^2.2.1", "karma-jasmine": "5.1.0", "karma-jasmine-html-reporter": "2.0.0", - "lint-staged": "^12.1.6", - "ng-mocks": "^14.11.0", - "ng-packagr": "18.0.0", - "nx": "19.1.0", - "postcss": "^8.4.5", + "lint-staged": "^15.3.0", + "ng-mocks": "^14.13.1", + "ng-packagr": "19.0.1", + "nx": "20.3.0", + "postcss": "^8.4.49", "postcss-import": "14.1.0", "postcss-preset-env": "7.5.0", "postcss-url": "10.1.3", "prettier": "2.6.2", - "rimraf": "^3.0.2", - "semantic-release": "^18.0.0", + "rimraf": "^5.0.10", + "semantic-release": "^24.2.1", "ts-jest": "29.1.0", "ts-node": "10.9.1", - "typescript": "5.4.5" + "typescript": "5.6.2", + "typescript-eslint": "^8.19.0" } } diff --git a/projects/testing-library/.eslintrc.json b/projects/testing-library/.eslintrc.json deleted file mode 100644 index 5a9d6905..00000000 --- a/projects/testing-library/.eslintrc.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "extends": "../../.eslintrc.json", - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts"], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ] - } - }, - { - "files": ["*.ts"], - "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], - "parserOptions": { - "project": ["projects/testing-library/tsconfig.*?.json"] - }, - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@angular-eslint/directive-selector": [ - "error", - { - "type": "attribute", - "prefix": "atl", - "style": "camelCase" - } - ], - "@angular-eslint/component-selector": [ - "error", - { - "type": "element", - "prefix": "atl", - "style": "kebab-case" - } - ] - } - }, - { - "files": ["*.spec.ts"], - "env": { - "jest": true - }, - "extends": ["plugin:jest/recommended", "plugin:jest/style", "plugin:jest-dom/recommended"], - "rules": { - "jest/consistent-test-it": ["error"], - "jest/expect-expect": "off" - } - }, - { - "files": ["*.html"], - "extends": ["plugin:@nx/angular-template"], - "rules": {} - } - ] -} diff --git a/projects/testing-library/eslint.config.cjs b/projects/testing-library/eslint.config.cjs new file mode 100644 index 00000000..9e951e7a --- /dev/null +++ b/projects/testing-library/eslint.config.cjs @@ -0,0 +1,7 @@ +// @ts-check + +// TODO - https://github.com/nrwl/nx/issues/22576 + +/** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */ +const config = (async () => (await import('./eslint.config.mjs')).default)(); +module.exports = config; diff --git a/projects/testing-library/eslint.config.mjs b/projects/testing-library/eslint.config.mjs new file mode 100644 index 00000000..8f627dbf --- /dev/null +++ b/projects/testing-library/eslint.config.mjs @@ -0,0 +1,8 @@ +// @ts-check + +import tseslint from "typescript-eslint"; +import rootConfig from "../../eslint.config.mjs"; + +export default tseslint.config( + ...rootConfig, +); diff --git a/projects/testing-library/jest.config.ts b/projects/testing-library/jest.config.ts index 189e52f8..bc5a665d 100644 --- a/projects/testing-library/jest.config.ts +++ b/projects/testing-library/jest.config.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ export default { displayName: { name: 'ATL', diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json index 2852d027..0c3abd6c 100644 --- a/projects/testing-library/package.json +++ b/projects/testing-library/package.json @@ -29,6 +29,7 @@ "migrations": "./schematics/migrations/migrations.json" }, "peerDependencies": { + "@angular/animations": ">= 17.0.0", "@angular/common": ">= 17.0.0", "@angular/platform-browser": ">= 17.0.0", "@angular/router": ">= 17.0.0", diff --git a/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts b/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts index a3c0fd1e..ebc3922a 100644 --- a/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts +++ b/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts @@ -16,7 +16,6 @@ test('adds DTL to devDependencies', async () => { }); test('ignores if DTL is already listed as a dev dependency', async () => { - // eslint-disable-next-line @typescript-eslint/naming-convention const tree = await setup({ devDependencies: { '@testing-library/dom': '^9.0.0' } }); const pkg = tree.readContent('package.json'); @@ -24,7 +23,6 @@ test('ignores if DTL is already listed as a dev dependency', async () => { }); test('ignores if DTL is already listed as a dependency', async () => { - // eslint-disable-next-line @typescript-eslint/naming-convention const tree = await setup({ dependencies: { '@testing-library/dom': '^11.0.0' } }); const pkg = tree.readContent('package.json'); diff --git a/projects/testing-library/schematics/ng-add/index.ts b/projects/testing-library/schematics/ng-add/index.ts index d961e150..868d2031 100644 --- a/projects/testing-library/schematics/ng-add/index.ts +++ b/projects/testing-library/schematics/ng-add/index.ts @@ -32,9 +32,9 @@ function addDependency(packageName: string, version: string, dependencyType: Nod }; } -export function installDependencies(packageManager = 'npm') { +export function installDependencies() { return (_tree: Tree, context: SchematicContext) => { - context.addTask(new NodePackageInstallTask({ packageManager })); + context.addTask(new NodePackageInstallTask()); context.logger.info( `Correctly installed @testing-library/angular. diff --git a/projects/testing-library/schematics/ng-add/schema.ts b/projects/testing-library/schematics/ng-add/schema.ts index dc146334..b0dcd227 100644 --- a/projects/testing-library/schematics/ng-add/schema.ts +++ b/projects/testing-library/schematics/ng-add/schema.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Schema { installJestDom: boolean; installUserEvent: boolean; diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 19567281..159ac414 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -215,7 +215,7 @@ export interface RenderComponentOptions | { [alias: string]: unknown }; + componentInputs?: Partial | Record; /** * @description @@ -254,10 +254,11 @@ export interface RenderComponentOptions; @@ -292,7 +293,7 @@ export interface RenderComponentOptions { ... } * await render(AppComponent, { * on: { - * send: (_v:any) => void + * send: (value) => sendValue(value) * } * }) */ @@ -465,13 +466,14 @@ export interface ComponentOverride { providers: any[]; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface RenderTemplateOptions extends RenderComponentOptions { /** * @description * An Angular component to wrap the component in. * The template will be overridden with the `template` option. + * NOTE: A standalone component cannot be used as a wrapper. * * @default * `WrapperComponent`, an empty component that strips the `ng-version` attribute diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index fbe94f21..f498a893 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -2,7 +2,6 @@ import { ApplicationInitStatus, ChangeDetectorRef, Component, - isStandalone, NgZone, OnChanges, OutputRef, @@ -10,6 +9,7 @@ import { SimpleChange, SimpleChanges, Type, + isStandalone, } from '@angular/core'; import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed, tick } from '@angular/core/testing'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -27,14 +27,15 @@ import { waitForOptions as dtlWaitForOptions, within as dtlWithin, } from '@testing-library/dom'; +import { getConfig } from './config'; import { ComponentOverride, + OutputRefKeysWithCallback, RenderComponentOptions, RenderResult, RenderTemplateOptions, - OutputRefKeysWithCallback, + Config, } from './models'; -import { getConfig } from './config'; type SubscribedOutput = readonly [key: keyof T, callback: (v: any) => void, subscription: OutputRefSubscription]; @@ -71,7 +72,7 @@ export async function render( on = {}, componentProviders = [], childComponentOverrides = [], - componentImports: componentImports, + componentImports, excludeComponentDeclaration = false, routes = [], removeAngularAttributes = false, @@ -82,7 +83,9 @@ export async function render( configureTestBed = () => { /* noop*/ }, - } = { ...globalConfig, ...renderOptions }; + } = { ...globalConfig, ...renderOptions } as RenderComponentOptions & + RenderTemplateOptions & + Config; dtlConfigure({ eventWrapper: (cb) => { @@ -116,12 +119,10 @@ export async function render( await TestBed.compileComponents(); - componentProviders - .reduce((acc, provider) => acc.concat(provider), [] as any[]) - .forEach((p: any) => { - const { provide, ...provider } = p; - TestBed.overrideProvider(provide, provider); - }); + // Angular supports nested arrays of providers, so we need to flatten them to emulate the same behavior. + for (const { provide, ...provider } of componentProviders.flat(Infinity)) { + TestBed.overrideProvider(provide, provider); + } const componentContainer = createComponentFixture(sut, wrapper); @@ -158,7 +159,9 @@ export async function render( let result; if (zone) { - await zone.run(() => (result = doNavigate())); + await zone.run(() => { + result = doNavigate(); + }); } else { result = doNavigate(); } @@ -199,7 +202,7 @@ export async function render( if (removeAngularAttributes) { createdFixture.nativeElement.removeAttribute('ng-version'); const idAttribute = createdFixture.nativeElement.getAttribute('id'); - if (idAttribute && idAttribute.startsWith('root')) { + if (idAttribute?.startsWith('root')) { createdFixture.nativeElement.removeAttribute('id'); } } @@ -207,7 +210,9 @@ export async function render( mountedFixtures.add(createdFixture); let isAlive = true; - createdFixture.componentRef.onDestroy(() => (isAlive = false)); + createdFixture.componentRef.onDestroy(() => { + isAlive = false; + }); if (hasOnChangesHook(createdFixture.componentInstance) && Object.keys(properties).length > 0) { const changes = getChangesObj(null, componentProperties); @@ -227,7 +232,7 @@ export async function render( return createdFixture; }; - const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on); + const fixture = await renderFixture(componentProperties, allInputs as any, componentOutputs, on); if (deferBlockStates) { if (Array.isArray(deferBlockStates)) { @@ -318,10 +323,15 @@ export async function render( }, debugElement: fixture.debugElement, container: fixture.nativeElement, - debug: (element = fixture.nativeElement, maxLength, options) => - Array.isArray(element) - ? element.forEach((e) => console.log(dtlPrettyDOM(e, maxLength, options))) - : console.log(dtlPrettyDOM(element, maxLength, options)), + debug: (element = fixture.nativeElement, maxLength, options) => { + if (Array.isArray(element)) { + for (const e of element) { + console.log(dtlPrettyDOM(e, maxLength, options)); + } + } else { + console.log(dtlPrettyDOM(element, maxLength, options)); + } + }, ...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)), }; } @@ -423,9 +433,11 @@ function overrideComponentImports(sut: Type | string, imports: } function overrideChildComponentProviders(componentOverrides: ComponentOverride[]) { - componentOverrides?.forEach(({ component, providers }) => { - TestBed.overrideComponent(component, { set: { providers } }); - }); + if (componentOverrides) { + for (const { component, providers } of componentOverrides) { + TestBed.overrideComponent(component, { set: { providers } }); + } + } } function hasOnChangesHook(componentInstance: SutType): componentInstance is SutType & OnChanges { @@ -439,13 +451,10 @@ function hasOnChangesHook(componentInstance: SutType): componentInstanc function getChangesObj(oldProps: Record | null, newProps: Record) { const isFirstChange = oldProps === null; - return Object.keys(newProps).reduce( - (changes, key) => ({ - ...changes, - [key]: new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange), - }), - {} as Record, - ); + return Object.keys(newProps).reduce((changes, key) => { + changes[key] = new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange); + return changes; + }, {} as Record); } function update( @@ -461,10 +470,12 @@ function update( const componentInstance = fixture.componentInstance as Record; const simpleChanges: SimpleChanges = {}; - for (const key of prevRenderedKeys) { - if (!partialUpdate && !Object.prototype.hasOwnProperty.call(newValues, key)) { - simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false); - delete componentInstance[key]; + if (!partialUpdate) { + for (const key of prevRenderedKeys) { + if (!Object.prototype.hasOwnProperty.call(newValues, key)) { + simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false); + delete componentInstance[key]; + } } } @@ -487,12 +498,16 @@ function addAutoDeclarations( wrapper, }: Pick, 'declarations' | 'excludeComponentDeclaration' | 'wrapper'>, ) { + const nonStandaloneDeclarations = declarations?.filter((d) => !isStandalone(d)); if (typeof sut === 'string') { - return [...declarations, wrapper]; + if (wrapper && isStandalone(wrapper)) { + return nonStandaloneDeclarations; + } + return [...nonStandaloneDeclarations, wrapper]; } const components = () => (excludeComponentDeclaration || isStandalone(sut) ? [] : [sut]); - return [...declarations, ...components()]; + return [...nonStandaloneDeclarations, ...components()]; } function addAutoImports( @@ -545,7 +560,7 @@ async function waitForWrapper( let inFakeAsync = true; try { tick(0); - } catch (err) { + } catch { inFakeAsync = false; } @@ -616,7 +631,7 @@ if (typeof process === 'undefined' || !process.env?.ATL_SKIP_AUTO_CLEANUP) { } } -@Component({ selector: 'atl-wrapper-component', template: '' }) +@Component({ selector: 'atl-wrapper-component', template: '', standalone: false }) class WrapperComponent {} /** @@ -643,7 +658,7 @@ function replaceFindWithFindAndDetectChanges>(orig * Call detectChanges for all fixtures */ function detectChangesForMountedFixtures() { - mountedFixtures.forEach((fixture) => { + for (const fixture of mountedFixtures) { try { fixture.detectChanges(); } catch (err: any) { @@ -651,7 +666,7 @@ function detectChangesForMountedFixtures() { throw err; } } - }); + } } /** diff --git a/projects/testing-library/test-setup.ts b/projects/testing-library/test-setup.ts index 600d0857..be311bfe 100644 --- a/projects/testing-library/test-setup.ts +++ b/projects/testing-library/test-setup.ts @@ -1,6 +1,7 @@ -import 'jest-preset-angular/setup-jest'; +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; -// eslint-disable-next-line @typescript-eslint/naming-convention +setupZoneTestEnv(); + Object.assign(global, { TextDecoder, TextEncoder }); diff --git a/projects/testing-library/tests/config.spec.ts b/projects/testing-library/tests/config.spec.ts index bb8c61fc..041d991a 100644 --- a/projects/testing-library/tests/config.spec.ts +++ b/projects/testing-library/tests/config.spec.ts @@ -13,6 +13,7 @@ import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; `, + standalone: false, }) class FormsComponent { form = this.formBuilder.group({ diff --git a/projects/testing-library/tests/debug.spec.ts b/projects/testing-library/tests/debug.spec.ts index e1ad1dff..63ab7e67 100644 --- a/projects/testing-library/tests/debug.spec.ts +++ b/projects/testing-library/tests/debug.spec.ts @@ -14,11 +14,11 @@ test('debug', async () => { jest.spyOn(console, 'log').mockImplementation(); const { debug } = await render(FixtureComponent); - // eslint-disable-next-line testing-library/no-debug + // eslint-disable-next-line testing-library/no-debugging-utils debug(); expect(console.log).toHaveBeenCalledWith(expect.stringContaining('rawr')); - (console.log).mockRestore(); + (console.log as any).mockRestore(); }); test('debug allows to be called with an element', async () => { @@ -26,10 +26,10 @@ test('debug allows to be called with an element', async () => { const { debug } = await render(FixtureComponent); const btn = screen.getByTestId('btn'); - // eslint-disable-next-line testing-library/no-debug + // eslint-disable-next-line testing-library/no-debugging-utils debug(btn); expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('rawr')); expect(console.log).toHaveBeenCalledWith(expect.stringContaining(`I'm a button`)); - (console.log).mockRestore(); + (console.log as any).mockRestore(); }); diff --git a/projects/testing-library/tests/find-by.spec.ts b/projects/testing-library/tests/find-by.spec.ts index 9d499fda..30f11ee3 100644 --- a/projects/testing-library/tests/find-by.spec.ts +++ b/projects/testing-library/tests/find-by.spec.ts @@ -2,10 +2,12 @@ import { Component } from '@angular/core'; import { timer } from 'rxjs'; import { render, screen } from '../src/public_api'; import { mapTo } from 'rxjs/operators'; +import { AsyncPipe } from '@angular/common'; @Component({ selector: 'atl-fixture', template: `
    {{ result | async }}
    `, + imports: [AsyncPipe], }) class FixtureComponent { result = timer(30).pipe(mapTo('I am visible')); diff --git a/projects/testing-library/tests/integration.spec.ts b/projects/testing-library/tests/integration.spec.ts index eedec0e9..02ca2902 100644 --- a/projects/testing-library/tests/integration.spec.ts +++ b/projects/testing-library/tests/integration.spec.ts @@ -4,6 +4,7 @@ import { of, BehaviorSubject } from 'rxjs'; import { debounceTime, switchMap, map, startWith } from 'rxjs/operators'; import { render, screen, waitFor, waitForElementToBeRemoved, within } from '../src/lib/testing-library'; import userEvent from '@testing-library/user-event'; +import { AsyncPipe, NgForOf } from '@angular/common'; const DEBOUNCE_TIME = 1_000; @@ -21,6 +22,25 @@ class ModalService { } } +@Component({ + selector: 'atl-table', + template: ` + + + + + +
    {{ entity.name }} + +
    + `, + imports: [NgForOf], +}) +class TableComponent { + @Input() entities: any[] = []; + @Output() edit = new EventEmitter(); +} + @Component({ template: `

    Entities Title

    @@ -31,6 +51,7 @@ class ModalService { `, + imports: [TableComponent, AsyncPipe], }) class EntitiesComponent { query = new BehaviorSubject(''); @@ -55,22 +76,6 @@ class EntitiesComponent { } } -@Component({ - selector: 'atl-table', - template: ` - - - - - -
    {{ entity.name }}
    - `, -}) -class TableComponent { - @Input() entities: any[] = []; - @Output() edit = new EventEmitter(); -} - const entities = [ { id: 1, @@ -91,7 +96,6 @@ async function setup() { const user = userEvent.setup(); await render(EntitiesComponent, { - declarations: [TableComponent], providers: [ { provide: EntitiesService, diff --git a/projects/testing-library/tests/issues/issue-230.spec.ts b/projects/testing-library/tests/issues/issue-230.spec.ts index fe004b62..8df58f66 100644 --- a/projects/testing-library/tests/issues/issue-230.spec.ts +++ b/projects/testing-library/tests/issues/issue-230.spec.ts @@ -1,8 +1,10 @@ import { Component } from '@angular/core'; import { render, waitFor, screen } from '../../src/public_api'; +import { NgClass } from '@angular/common'; @Component({ template: ` `, + imports: [NgClass], }) class LoopComponent { get classes() { @@ -17,7 +19,7 @@ test('wait does not end up in a loop', async () => { await expect( waitFor(() => { - expect(true).toEqual(false); + expect(true).toBe(false); }), ).rejects.toThrow(); }); diff --git a/projects/testing-library/tests/issues/issue-280.spec.ts b/projects/testing-library/tests/issues/issue-280.spec.ts index 19f644ef..711cbec3 100644 --- a/projects/testing-library/tests/issues/issue-280.spec.ts +++ b/projects/testing-library/tests/issues/issue-280.spec.ts @@ -1,19 +1,21 @@ import { Location } from '@angular/common'; import { Component, NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { RouterLink, RouterModule, RouterOutlet, Routes } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import userEvent from '@testing-library/user-event'; import { render, screen } from '../../src/public_api'; @Component({ - template: `
    Navigate
    + template: `
    Navigate
    `, + imports: [RouterOutlet], }) class MainComponent {} @Component({ - template: `
    first page
    + template: `
    first page
    go to second`, + imports: [RouterLink], }) class FirstComponent {} @@ -35,7 +37,6 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [FirstComponent, SecondComponent], imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) @@ -47,12 +48,12 @@ test('navigate to second page and back', async () => { expect(await screen.findByText('Navigate')).toBeInTheDocument(); expect(await screen.findByText('first page')).toBeInTheDocument(); - userEvent.click(await screen.findByText('go to second')); + await userEvent.click(await screen.findByText('go to second')); expect(await screen.findByText('second page')).toBeInTheDocument(); expect(await screen.findByText('navigate back')).toBeInTheDocument(); - userEvent.click(await screen.findByText('navigate back')); + await userEvent.click(await screen.findByText('navigate back')); expect(await screen.findByText('first page')).toBeInTheDocument(); }); diff --git a/projects/testing-library/tests/issues/issue-389.spec.ts b/projects/testing-library/tests/issues/issue-389.spec.ts index 03f25f74..626d3889 100644 --- a/projects/testing-library/tests/issues/issue-389.spec.ts +++ b/projects/testing-library/tests/issues/issue-389.spec.ts @@ -6,7 +6,6 @@ import { render, screen } from '../../src/public_api'; template: `Hello {{ name }}`, }) class TestComponent { - // eslint-disable-next-line @angular-eslint/no-input-rename @Input('aliasName') name = ''; } diff --git a/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts b/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts index 2da43b32..7be9913e 100644 --- a/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts +++ b/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts @@ -42,7 +42,6 @@ class ChildComponent {} selector: 'atl-child', template: `Hello from stub`, standalone: true, - // eslint-disable-next-line @angular-eslint/no-host-metadata-property, @typescript-eslint/naming-convention host: { 'collision-id': StubComponent.name }, }) class StubComponent {} diff --git a/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts b/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts index 4508d642..c775a2ab 100644 --- a/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts +++ b/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts @@ -15,9 +15,7 @@ test('should re-create the app', async () => { selector: 'atl-fixture', standalone: true, template: '

    My title

    ', - // eslint-disable-next-line @angular-eslint/no-host-metadata-property host: { - // eslint-disable-next-line @typescript-eslint/naming-convention '[attr.id]': 'null', // this breaks the cleaning up of tests }, }) diff --git a/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts b/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts index 05e6e11a..c4fa7a37 100644 --- a/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts +++ b/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts @@ -9,6 +9,7 @@ test('declaration specific dependencies should be available for components', asy template: `
    Test
    `, }) class TestComponent { + // eslint-disable-next-line @typescript-eslint/no-empty-function constructor(_elementRef: ElementRef) {} } diff --git a/projects/testing-library/tests/issues/issue-437.spec.ts b/projects/testing-library/tests/issues/issue-437.spec.ts index 2d0e7c51..dbf2506b 100644 --- a/projects/testing-library/tests/issues/issue-437.spec.ts +++ b/projects/testing-library/tests/issues/issue-437.spec.ts @@ -24,7 +24,6 @@ test('issue #437', async () => { { imports: [MatSidenavModule] }, ); - // eslint-disable-next-line testing-library/prefer-explicit-assert await screen.findByTestId('test-button'); await user.click(screen.getByTestId('test-button')); @@ -51,7 +50,6 @@ test('issue #437 with fakeTimers', async () => { { imports: [MatSidenavModule] }, ); - // eslint-disable-next-line testing-library/prefer-explicit-assert await screen.findByTestId('test-button'); await user.click(screen.getByTestId('test-button')); diff --git a/projects/testing-library/tests/issues/issue-492.spec.ts b/projects/testing-library/tests/issues/issue-492.spec.ts new file mode 100644 index 00000000..a1e44b09 --- /dev/null +++ b/projects/testing-library/tests/issues/issue-492.spec.ts @@ -0,0 +1,48 @@ +import { AsyncPipe } from '@angular/common'; +import { Component, inject, Injectable } from '@angular/core'; +import { render, screen } from '../../src/public_api'; +import { Observable, BehaviorSubject, map } from 'rxjs'; + +test('displays username', async () => { + // stubbed user service using a Subject + const user = new BehaviorSubject({ name: 'username 1' }); + const userServiceStub: Partial = { + getName: () => user.asObservable().pipe(map((u) => u.name)), + }; + + // render the component with injection of the stubbed service + await render(UserComponent, { + componentProviders: [ + { + provide: UserService, + useValue: userServiceStub, + }, + ], + }); + + // assert first username emitted is rendered + expect(await screen.findByRole('heading', { name: 'username 1' })).toBeInTheDocument(); + + // emitting a second username + user.next({ name: 'username 2' }); + + // assert the second username is rendered + expect(await screen.findByRole('heading', { name: 'username 2' })).toBeInTheDocument(); +}); + +@Component({ + selector: 'atl-user', + standalone: true, + template: `

    {{ username$ | async }}

    `, + imports: [AsyncPipe], +}) +class UserComponent { + readonly username$: Observable = inject(UserService).getName(); +} + +@Injectable() +class UserService { + getName(): Observable { + throw new Error('Not implemented'); + } +} diff --git a/projects/testing-library/tests/issues/issue-493.spec.ts b/projects/testing-library/tests/issues/issue-493.spec.ts new file mode 100644 index 00000000..5d0e1237 --- /dev/null +++ b/projects/testing-library/tests/issues/issue-493.spec.ts @@ -0,0 +1,27 @@ +import { HttpClient, provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { Component, input } from '@angular/core'; +import { render, screen } from '../../src/public_api'; + +test('succeeds', async () => { + await render(DummyComponent, { + inputs: { + value: 'test', + }, + providers: [provideHttpClientTesting(), provideHttpClient()], + }); + + expect(screen.getByText('test')).toBeVisible(); +}); + +@Component({ + selector: 'atl-dummy', + standalone: true, + imports: [], + template: '

    {{ value() }}

    ', +}) +class DummyComponent { + value = input.required(); + // @ts-expect-error http is unused but needed for the test + constructor(private http: HttpClient) {} +} diff --git a/projects/testing-library/tests/providers/component-provider.spec.ts b/projects/testing-library/tests/providers/component-provider.spec.ts index 3c3ec0cf..9290d5bd 100644 --- a/projects/testing-library/tests/providers/component-provider.spec.ts +++ b/projects/testing-library/tests/providers/component-provider.spec.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Provider } from '@angular/core'; import { Component } from '@angular/core'; import { render, screen } from '../../src/public_api'; @@ -42,6 +42,24 @@ test('shows the provided service value with template syntax', async () => { expect(screen.getByText('bar')).toBeInTheDocument(); }); +test('flatten the nested array of component providers', async () => { + const provideService = (): Provider => [ + { + provide: Service, + useValue: { + foo() { + return 'bar'; + }, + }, + }, + ]; + await render(FixtureComponent, { + componentProviders: [provideService()], + }); + + expect(screen.getByText('bar')).toBeInTheDocument(); +}); + @Injectable() class Service { foo() { diff --git a/projects/testing-library/tests/render-template.spec.ts b/projects/testing-library/tests/render-template.spec.ts index a6892dbc..e185f702 100644 --- a/projects/testing-library/tests/render-template.spec.ts +++ b/projects/testing-library/tests/render-template.spec.ts @@ -45,7 +45,7 @@ class GreetingComponent { test('the directive renders', async () => { const view = await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); // eslint-disable-next-line testing-library/no-container @@ -54,7 +54,7 @@ test('the directive renders', async () => { test('the component renders', async () => { const view = await render('', { - declarations: [GreetingComponent], + imports: [GreetingComponent], }); // eslint-disable-next-line testing-library/no-container @@ -64,7 +64,7 @@ test('the component renders', async () => { test('uses the default props', async () => { await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); fireEvent.click(screen.getByText('init')); @@ -74,7 +74,7 @@ test('uses the default props', async () => { test('overrides input properties', async () => { await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); fireEvent.click(screen.getByText('init')); @@ -85,7 +85,7 @@ test('overrides input properties', async () => { test('overrides input properties via a wrapper', async () => { // `bar` will be set as a property on the wrapper component, the property will be used to pass to the directive await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], componentProperties: { bar: 'hello', }, @@ -100,7 +100,7 @@ test('overrides output properties', async () => { const clicked = jest.fn(); await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], componentProperties: { clicked, }, @@ -116,7 +116,7 @@ test('overrides output properties', async () => { describe('removeAngularAttributes', () => { it('should remove angular attributes', async () => { await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], removeAngularAttributes: true, }); @@ -126,7 +126,7 @@ describe('removeAngularAttributes', () => { it('is disabled by default', async () => { await render('
    ', { - declarations: [OnOffDirective], + imports: [OnOffDirective], }); expect(document.querySelector('[ng-version]')).not.toBeNull(); @@ -136,7 +136,7 @@ describe('removeAngularAttributes', () => { test('updates properties and invokes change detection', async () => { const view = await render<{ value: string }>('
    ', { - declarations: [UpdateInputDirective], + imports: [UpdateInputDirective], componentProperties: { value: 'value1', }, diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 59e0f75b..dc54ac5d 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -37,7 +37,7 @@ describe('DTL functionality', () => { it('creates queries and events', async () => { const view = await render(FixtureComponent); - /// We wish to test the utility function from `render` here. + // We wish to test the utility function from `render` here. // eslint-disable-next-line testing-library/prefer-screen-queries fireEvent.input(view.getByTestId('input'), { target: { value: 'a super awesome input' } }); // eslint-disable-next-line testing-library/prefer-screen-queries @@ -47,34 +47,31 @@ describe('DTL functionality', () => { }); }); -describe('standalone', () => { +describe('components', () => { @Component({ selector: 'atl-fixture', template: ` {{ name }} `, }) - class StandaloneFixtureComponent { + class FixtureWithInputComponent { @Input() name = ''; } - it('renders standalone component', async () => { - await render(StandaloneFixtureComponent, { componentProperties: { name: 'Bob' } }); + it('renders component', async () => { + await render(FixtureWithInputComponent, { componentProperties: { name: 'Bob' } }); expect(screen.getByText('Bob')).toBeInTheDocument(); }); }); -describe('standalone with child', () => { +describe('component with child', () => { @Component({ selector: 'atl-child-fixture', template: `A child fixture`, - standalone: true, }) class ChildFixtureComponent {} @Component({ selector: 'atl-child-fixture', template: `A mock child fixture`, - standalone: true, - // eslint-disable-next-line @angular-eslint/no-host-metadata-property, @typescript-eslint/naming-convention host: { 'collision-id': MockChildFixtureComponent.name }, }) class MockChildFixtureComponent {} @@ -83,18 +80,17 @@ describe('standalone with child', () => { selector: 'atl-parent-fixture', template: `

    Parent fixture

    `, - standalone: true, imports: [ChildFixtureComponent], }) class ParentFixtureComponent {} - it('renders the standalone component with a mocked child', async () => { + it('renders the component with a mocked child', async () => { await render(ParentFixtureComponent, { componentImports: [MockChildFixtureComponent] }); expect(screen.getByText('Parent fixture')).toBeInTheDocument(); expect(screen.getByText('A mock child fixture')).toBeInTheDocument(); }); - it('renders the standalone component with child', async () => { + it('renders the component with child', async () => { await render(ParentFixtureComponent); expect(screen.getByText('Parent fixture')).toBeInTheDocument(); expect(screen.getByText('A child fixture')).toBeInTheDocument(); @@ -118,7 +114,6 @@ describe('childComponentOverrides', () => { @Component({ selector: 'atl-child-fixture', template: `{{ simpleService.value }}`, - standalone: true, providers: [MySimpleService], }) class NestedChildFixtureComponent { @@ -128,7 +123,6 @@ describe('childComponentOverrides', () => { @Component({ selector: 'atl-parent-fixture', template: ``, - standalone: true, imports: [NestedChildFixtureComponent], }) class ParentFixtureComponent {} @@ -190,22 +184,22 @@ describe('componentOutputs', () => { }); describe('on', () => { - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithEventEmitterComponent { @Output() readonly event = new EventEmitter(); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithDerivedEventComponent { @Output() readonly event = fromEvent(inject(ElementRef).nativeElement, 'click'); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithFunctionalOutputComponent { readonly event = output(); } - @Component({ template: ``, standalone: true }) + @Component({ template: `` }) class TestFixtureWithFunctionalDerivedEventComponent { readonly event = outputFromObservable(fromEvent(inject(ElementRef).nativeElement, 'click')); } @@ -292,19 +286,19 @@ describe('on', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function function _test(_on: OutputRefKeysWithCallback) {} - // @ts-expect-error + // @ts-expect-error wrong event type _test({ event: fnWithNumberArg }); _test({ event: fnWithVoidArg }); - // @ts-expect-error + // @ts-expect-error wrong event type _test({ event: fnWithNumberArg }); _test({ event: fnWithMouseEventArg }); - // @ts-expect-error + // @ts-expect-error wrong event type _test({ event: fnWithNumberArg }); _test({ event: fnWithStringArg }); - // @ts-expect-error + // @ts-expect-error wrong event type _test({ event: fnWithNumberArg }); _test({ event: fnWithMouseEventArg }); @@ -313,20 +307,31 @@ describe('on', () => { }); }); -describe('animationModule', () => { +describe('excludeComponentDeclaration', () => { + @Component({ + selector: 'atl-fixture', + template: ` + + + `, + standalone: false, + }) + class NotStandaloneFixtureComponent {} + @NgModule({ - declarations: [FixtureComponent], + declarations: [NotStandaloneFixtureComponent], }) class FixtureModule {} - describe('excludeComponentDeclaration', () => { - it('does not throw if component is declared in an imported module', async () => { - await render(FixtureComponent, { - imports: [FixtureModule], - excludeComponentDeclaration: true, - }); + + it('does not throw if component is declared in an imported module', async () => { + await render(NotStandaloneFixtureComponent, { + imports: [FixtureModule], + excludeComponentDeclaration: true, }); }); +}); +describe('animationModule', () => { it('adds NoopAnimationsModule by default', async () => { await render(FixtureComponent); const noopAnimationsModule = TestBed.inject(NoopAnimationsModule); @@ -386,11 +391,11 @@ describe('Angular component life-cycle hooks', () => { const view = await render(FixtureWithNgOnChangesComponent, { componentProperties }); - /// We wish to test the utility function from `render` here. + // We wish to test the utility function from `render` here. // eslint-disable-next-line testing-library/prefer-screen-queries expect(view.getByText('Sarah')).toBeInTheDocument(); expect(nameChanged).toHaveBeenCalledWith('Sarah', true); - /// expect `nameChanged` to be called before `nameInitialized` + // expect `nameChanged` to be called before `nameInitialized` expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); expect(nameChanged).toHaveBeenCalledTimes(1); }); @@ -402,11 +407,11 @@ describe('Angular component life-cycle hooks', () => { const view = await render(FixtureWithNgOnChangesComponent, { componentInputs: componentInput }); - /// We wish to test the utility function from `render` here. + // We wish to test the utility function from `render` here. // eslint-disable-next-line testing-library/prefer-screen-queries expect(view.getByText('Sarah')).toBeInTheDocument(); expect(nameChanged).toHaveBeenCalledWith('Sarah', true); - /// expect `nameChanged` to be called before `nameInitialized` + // expect `nameChanged` to be called before `nameInitialized` expect(nameChanged.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]); expect(nameChanged).toHaveBeenCalledTimes(1); }); @@ -458,14 +463,12 @@ describe('DebugElement', () => { describe('initialRoute', () => { @Component({ - standalone: true, selector: 'atl-fixture2', template: ``, }) class SecondaryFixtureComponent {} @Component({ - standalone: true, selector: 'atl-router-fixture', template: ``, imports: [RouterModule], @@ -502,7 +505,6 @@ describe('initialRoute', () => { it('allows initially rendering a specific route with query parameters', async () => { @Component({ - standalone: true, selector: 'atl-query-param-fixture', template: `

    paramPresent$: {{ paramPresent$ | async }}

    `, imports: [NgIf, AsyncPipe], diff --git a/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts index 5c16a539..64d6c356 100644 --- a/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts +++ b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts @@ -1,10 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { render, screen, waitForElementToBeRemoved } from '../src/public_api'; import { timer } from 'rxjs'; +import { NgIf } from '@angular/common'; @Component({ selector: 'atl-fixture', template: `
    👋
    `, + imports: [NgIf], }) class FixtureComponent implements OnInit { visible = true;