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 💻
Mahdi Lazraq 💻 ⚠️
+
+ Arthur Petrie 💻
+ Fabien Dehopré 💻
+ 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
+ submit
+
+
+ `,
+})
+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: `
Decrement
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: ' {{ name }} ',
})
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: `
Decrement
{{ 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: `
`,
+ 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 }}
+
+ Edit
+
+
+
+ `,
+ 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 }}
- Edit
-
-
- `,
-})
-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: ` Load `,
+ 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: `
+
+ button
+ `,
+ 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: `Secondary Component `,
})
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;