diff --git a/.all-contributorsrc b/.all-contributorsrc
index 54c827ec..c60fd019 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -370,6 +370,102 @@
"code",
"test"
]
+ },
+ {
+ "login": "mumenthalers",
+ "name": "S. Mumenthaler",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3604424?v=4",
+ "profile": "https://github.com/mumenthalers",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "andreialecu",
+ "name": "Andrei Alecu",
+ "avatar_url": "https://avatars.githubusercontent.com/u/697707?v=4",
+ "profile": "https://lets.poker/",
+ "contributions": [
+ "code",
+ "ideas",
+ "doc"
+ ]
+ },
+ {
+ "login": "Hyperxq",
+ "name": "Daniel Ramírez Barrientos",
+ "avatar_url": "https://avatars.githubusercontent.com/u/22332354?v=4",
+ "profile": "https://github.com/Hyperxq",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mlz11",
+ "name": "Mahdi Lazraq",
+ "avatar_url": "https://avatars.githubusercontent.com/u/94069699?v=4",
+ "profile": "https://github.com/mlz11",
+ "contributions": [
+ "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"
+ ]
+ },
+ {
+ "login": "Christian24",
+ "name": "Christian24",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2406635?v=4",
+ "profile": "https://github.com/Christian24",
+ "contributions": [
+ "code",
+ "review"
+ ]
+ },
+ {
+ "login": "mikeshtro",
+ "name": "Michal Štrajt",
+ "avatar_url": "https://avatars.githubusercontent.com/u/93714867?v=4",
+ "profile": "https://github.com/mikeshtro",
+ "contributions": [
+ "code",
+ "bug"
+ ]
+ },
+ {
+ "login": "jdegand",
+ "name": "J. Degand",
+ "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
+ "profile": "https://github.com/jdegand",
+ "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..b35c5abb 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]' || '[20, 22, 24]') }}
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..22faaca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@
/.angular/cache
.angular
.nx
+migrations.json
.cache
/.sass-cache
/connect.lock
@@ -43,3 +44,6 @@ yarn.lock
# System Files
.DS_Store
Thumbs.db
+.cursor/rules/nx-rules.mdc
+.github/instructions/nx.instructions.md
+.history
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 ce2e906b..848aed06 100644
--- a/README.md
+++ b/README.md
@@ -98,22 +98,24 @@ counter.component.ts
```ts
@Component({
- selector: 'app-counter',
+ selector: 'atl-counter',
template: `
+ {{ hello() }}
- Current Count: {{ counter }}
+ Current Count: {{ counter() }}
`,
})
export class CounterComponent {
- @Input() counter = 0;
+ counter = model(0);
+ hello = input('Hi', { alias: 'greeting' });
increment() {
- this.counter += 1;
+ this.counter.set(this.counter() + 1);
}
decrement() {
- this.counter -= 1;
+ this.counter.set(this.counter() - 1);
}
}
```
@@ -121,23 +123,30 @@ export class CounterComponent {
counter.component.spec.ts
```typescript
-import { render, screen, fireEvent } from '@testing-library/angular';
+import { render, screen, fireEvent, aliasedInput } from '@testing-library/angular';
import { CounterComponent } from './counter.component';
describe('Counter', () => {
- test('should render counter', async () => {
- await render(CounterComponent, { componentProperties: { counter: 5 } });
-
- expect(screen.getByText('Current Count: 5'));
+ it('should render counter', async () => {
+ await render(CounterComponent, {
+ inputs: {
+ counter: 5,
+ // aliases need to be specified this way
+ ...aliasedInput('greeting', 'Hello Alias!'),
+ },
+ });
+
+ expect(screen.getByText('Current Count: 5')).toBeVisible();
+ expect(screen.getByText('Hello Alias!')).toBeVisible();
});
- test('should increment the counter on click', async () => {
- await render(CounterComponent, { componentProperties: { counter: 5 } });
+ it('should increment the counter on click', async () => {
+ await render(CounterComponent, { inputs: { counter: 5 } });
const incrementButton = screen.getByRole('button', { name: '+' });
fireEvent.click(incrementButton);
- expect(screen.getByText('Current Count: 6'));
+ expect(screen.getByText('Current Count: 6')).toBeVisible();
});
});
```
@@ -168,14 +177,16 @@ You may also be interested in installing `jest-dom` so you can use
## Version compatibility
-| Angular | Angular Testing Library |
-| ------- | ---------------------------- |
-| 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 |
-| >= 15.1 | 14.x, 13.x |
-| < 15.1 | 12.x, 11.x |
-| 14.x | 12.x, 11.x |
+| Angular | Angular Testing Library |
+| ------- | ---------------------------------- |
+| 20.x | 18.x, 17.x, 16.x, 15.x, 14.x, 13.x |
+| 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 |
+| >= 15.1 | 14.x, 13.x |
+| < 15.1 | 12.x, 11.x |
+| 14.x | 12.x, 11.x |
## Guiding Principles
@@ -256,6 +267,18 @@ Thanks goes to these people ([emoji key][emojis]):
 Florian Pabst 💻 |
 Mark Goho 🚧 📖 |
 Jan-Willem Baart 💻 ⚠️ |
+  S. Mumenthaler 💻 ⚠️ |
+  Andrei Alecu 💻 🤔 📖 |
+  Daniel Ramírez Barrientos 💻 |
+  Mahdi Lazraq 💻 ⚠️ |
+
+
+  Arthur Petrie 💻 |
+  Fabien Dehopré 💻 |
+  Jamie Vereecken 💻 |
+  Christian24 💻 👀 |
+  Michal Štrajt 💻 🐛 |
+  J. Degand 💻 |
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..bd9b42bf
--- /dev/null
+++ b/apps/example-app-karma/eslint.config.mjs
@@ -0,0 +1,6 @@
+// @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/project.json b/apps/example-app-karma/project.json
index 00820e35..27c4cbd4 100644
--- a/apps/example-app-karma/project.json
+++ b/apps/example-app-karma/project.json
@@ -50,7 +50,8 @@
"buildTarget": "example-app-karma:build:development"
}
},
- "defaultConfiguration": "development"
+ "defaultConfiguration": "development",
+ "continuous": true
},
"lint": {
"executor": "@nx/eslint:lint"
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..d019e069 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
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/angular';
@@ -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: `
@@ -45,13 +45,13 @@ it('should display invalid message and submit button must be disabled', async ()
`,
})
class LoginComponent {
+ private fb = inject(FormBuilder);
+
form: FormGroup = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
- 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..9c967710
--- /dev/null
+++ b/apps/example-app-karma/src/app/issues/issue-491.spec.ts
@@ -0,0 +1,55 @@
+import { Component, inject } from '@angular/core';
+import { Router } from '@angular/router';
+import { render, screen } 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' }));
+
+ expect(await screen.findByRole('heading', { name: 'Logged In' })).toBeVisible();
+});
+
+@Component({
+ template: `
+ Login
+
+
+
+ `,
+})
+class LoginComponent {
+ private readonly router = inject(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..bd9b42bf
--- /dev/null
+++ b/apps/example-app/eslint.config.mjs
@@ -0,0 +1,6 @@
+// @ts-check
+
+import tseslint from 'typescript-eslint';
+import rootConfig from '../../eslint.config.mjs';
+
+export default tseslint.config(...rootConfig);
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/project.json b/apps/example-app/project.json
index ecbadfcb..1cf90ac4 100644
--- a/apps/example-app/project.json
+++ b/apps/example-app/project.json
@@ -51,7 +51,8 @@
"buildTarget": "example-app:build:development"
}
},
- "defaultConfiguration": "development"
+ "defaultConfiguration": "development",
+ "continuous": true
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
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 c193d3e5..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
@@ -8,7 +8,63 @@ test('is possible to set input and listen for output', async () => {
const sendValue = jest.fn();
await render(InputOutputComponent, {
- componentInputs: {
+ inputs: {
+ value: 47,
+ },
+ on: {
+ sendValue,
+ },
+ });
+
+ const incrementControl = screen.getByRole('button', { name: /increment/i });
+ const sendControl = screen.getByRole('button', { name: /send/i });
+ const valueControl = screen.getByTestId('value');
+
+ expect(valueControl).toHaveTextContent('47');
+
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ expect(valueControl).toHaveTextContent('50');
+
+ await user.click(sendControl);
+ expect(sendValue).toHaveBeenCalledTimes(1);
+ expect(sendValue).toHaveBeenCalledWith(50);
+});
+
+test.skip('is possible to set input and listen for output with the template syntax', async () => {
+ const user = userEvent.setup();
+ const sendSpy = jest.fn();
+
+ await render('', {
+ imports: [InputOutputComponent],
+ on: {
+ sendValue: sendSpy,
+ },
+ });
+
+ const incrementControl = screen.getByRole('button', { name: /increment/i });
+ const sendControl = screen.getByRole('button', { name: /send/i });
+ const valueControl = screen.getByTestId('value');
+
+ expect(valueControl).toHaveTextContent('47');
+
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ expect(valueControl).toHaveTextContent('50');
+
+ await user.click(sendControl);
+ expect(sendSpy).toHaveBeenCalledTimes(1);
+ expect(sendSpy).toHaveBeenCalledWith(50);
+});
+
+test('is possible to set input and listen for output (deprecated)', async () => {
+ const user = userEvent.setup();
+ const sendValue = jest.fn();
+
+ await render(InputOutputComponent, {
+ inputs: {
value: 47,
},
componentOutputs: {
@@ -34,11 +90,11 @@ test('is possible to set input and listen for output', async () => {
expect(sendValue).toHaveBeenCalledWith(50);
});
-test('is possible to set input and listen for output with the template syntax', async () => {
+test('is possible to set input and listen for output with the template syntax (deprecated)', async () => {
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..c1e48c23 100644
--- a/apps/example-app/src/app/examples/03-forms.ts
+++ b/apps/example-app/src/app/examples/03-forms.ts
@@ -1,10 +1,10 @@
import { NgForOf, NgIf } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
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 {
+ private formBuilder = inject(FormBuilder);
form = this.formBuilder.group({
name: [''],
});
-
- constructor(private formBuilder: FormBuilder) {}
}
let originalConfig: Config;
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/defer-blocks.spec.ts b/projects/testing-library/tests/defer-blocks.spec.ts
index 7405a4dd..ffd5e95b 100644
--- a/projects/testing-library/tests/defer-blocks.spec.ts
+++ b/projects/testing-library/tests/defer-blocks.spec.ts
@@ -33,7 +33,6 @@ test('renders a defer block in different states using DeferBlockBehavior.Playthr
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
- expect(await screen.findByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/Defer block content/i)).toBeInTheDocument();
});
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..70d0169c 100644
--- a/projects/testing-library/tests/integration.spec.ts
+++ b/projects/testing-library/tests/integration.spec.ts
@@ -1,9 +1,10 @@
-import { Component, EventEmitter, Injectable, Input, Output } from '@angular/core';
+import { Component, EventEmitter, inject, Injectable, Input, Output } from '@angular/core';
import { TestBed } from '@angular/core/testing';
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,8 +51,11 @@ class ModalService {
`,
+ imports: [TableComponent, AsyncPipe],
})
class EntitiesComponent {
+ private entitiesService = inject(EntitiesService);
+ private modalService = inject(ModalService);
query = new BehaviorSubject('');
readonly entities = this.query.pipe(
debounceTime(DEBOUNCE_TIME),
@@ -42,8 +65,6 @@ class EntitiesComponent {
startWith(entities),
);
- constructor(private entitiesService: EntitiesService, private modalService: ModalService) {}
-
newEntityClicked() {
this.modalService.open('new entity');
}
@@ -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/integrations/ng-mocks.spec.ts b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
index 63584850..8886fb3f 100644
--- a/projects/testing-library/tests/integrations/ng-mocks.spec.ts
+++ b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
@@ -8,7 +8,7 @@ import { NgIf } from '@angular/common';
test('sends the correct value to the child input', async () => {
const utils = await render(TargetComponent, {
imports: [MockComponent(ChildComponent)],
- componentInputs: { value: 'foo' },
+ inputs: { value: 'foo' },
});
const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
@@ -21,7 +21,7 @@ test('sends the correct value to the child input', async () => {
test('sends the correct value to the child input 2', async () => {
const utils = await render(TargetComponent, {
imports: [MockComponent(ChildComponent)],
- componentInputs: { value: 'bar' },
+ inputs: { value: 'bar' },
});
const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
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..ea230e78 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 { Component, inject, NgModule } from '@angular/core';
+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 {}
@@ -22,7 +24,7 @@ class FirstComponent {}
`,
})
class SecondComponent {
- constructor(private location: Location) {}
+ private location = inject(Location);
goBack() {
this.location.back();
}
@@ -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-318.spec.ts b/projects/testing-library/tests/issues/issue-318.spec.ts
index 3f1430e8..1cfe5b85 100644
--- a/projects/testing-library/tests/issues/issue-318.spec.ts
+++ b/projects/testing-library/tests/issues/issue-318.spec.ts
@@ -1,21 +1,20 @@
-import {Component, OnDestroy, OnInit} from '@angular/core';
-import {Router} from '@angular/router';
-import {RouterTestingModule} from '@angular/router/testing';
-import {Subject, takeUntil} from 'rxjs';
-import {render} from "@testing-library/angular";
+import { Component, inject, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Subject, takeUntil } from 'rxjs';
+import { render } from '@testing-library/angular';
@Component({
selector: 'atl-app-fixture',
template: '',
})
class FixtureComponent implements OnInit, OnDestroy {
+ private readonly router = inject(Router);
unsubscribe$ = new Subject();
- constructor(private router: Router) {}
-
ngOnInit(): void {
this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => {
- this.eventReceived(evt)
+ this.eventReceived(evt);
});
}
@@ -29,15 +28,13 @@ class FixtureComponent implements OnInit, OnDestroy {
}
}
-
test('it does not invoke router events on init', async () => {
const eventReceived = jest.fn();
await render(FixtureComponent, {
imports: [RouterTestingModule],
componentProperties: {
- eventReceived
- }
+ eventReceived,
+ },
});
expect(eventReceived).not.toHaveBeenCalled();
});
-
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-397-directive-overrides-component-input.spec.ts b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
index c2a02a8c..c34e1304 100644
--- a/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
+++ b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
@@ -1,4 +1,4 @@
-import { Component, Directive, Input, OnInit } from '@angular/core';
+import { Component, Directive, inject, Input, OnInit } from '@angular/core';
import { render, screen } from '../../src/public_api';
test('the value set in the directive constructor is overriden by the input binding', async () => {
@@ -48,7 +48,8 @@ class FixtureComponent {
standalone: true,
})
class InputOverrideViaConstructorDirective {
- constructor(private fixture: FixtureComponent) {
+ private readonly fixture = inject(FixtureComponent);
+ constructor() {
this.fixture.input = 'set by directive constructor';
}
}
@@ -59,7 +60,7 @@ class InputOverrideViaConstructorDirective {
standalone: true,
})
class InputOverrideViaOnInitDirective implements OnInit {
- constructor(private fixture: FixtureComponent) {}
+ private readonly fixture = inject(FixtureComponent);
ngOnInit(): void {
this.fixture.input = 'set by directive ngOnInit';
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..6dd5bc0c 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
@@ -1,4 +1,4 @@
-import { Component, ElementRef } from '@angular/core';
+import { Component, ElementRef, inject } from '@angular/core';
import { NgIf } from '@angular/common';
import { render } from '../../src/public_api';
@@ -9,7 +9,8 @@ test('declaration specific dependencies should be available for components', asy
template: `Test
`,
})
class TestComponent {
- constructor(_elementRef: ElementRef) {}
+ // @ts-expect-error - testing purpose
+ private _el = inject(ElementRef);
}
await expect(async () => await render(TestComponent)).not.toThrow();
diff --git a/projects/testing-library/tests/issues/issue-435.spec.ts b/projects/testing-library/tests/issues/issue-435.spec.ts
index e1e420f9..2982319b 100644
--- a/projects/testing-library/tests/issues/issue-435.spec.ts
+++ b/projects/testing-library/tests/issues/issue-435.spec.ts
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
-import { Component, Inject, Injectable } from '@angular/core';
+import { Component, inject, Injectable } from '@angular/core';
import { screen, render } from '../../src/public_api';
// Service
@@ -23,7 +23,7 @@ class DemoService {
`,
})
class DemoComponent {
- constructor(@Inject(DemoService) public demoService: DemoService) {}
+ protected readonly demoService = inject(DemoService);
}
test('issue #435', async () => {
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..00a39b37
--- /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, inject, 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 {
+ // @ts-expect-error - testing purpose
+ private _http = inject(HttpClient);
+ value = input.required();
+}
diff --git a/projects/testing-library/tests/providers/component-provider.spec.ts b/projects/testing-library/tests/providers/component-provider.spec.ts
index 3c3ec0cf..b774064e 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 { inject, 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() {
@@ -55,5 +73,5 @@ class Service {
providers: [Service],
})
class FixtureComponent {
- constructor(public service: Service) {}
+ protected readonly service = inject(Service);
}
diff --git a/projects/testing-library/tests/providers/module-provider.spec.ts b/projects/testing-library/tests/providers/module-provider.spec.ts
index bd39b81b..80710291 100644
--- a/projects/testing-library/tests/providers/module-provider.spec.ts
+++ b/projects/testing-library/tests/providers/module-provider.spec.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Component } from '@angular/core';
import { render, screen } from '../../src/public_api';
@@ -64,5 +64,5 @@ class Service {
template: '{{service.foo()}}',
})
class FixtureComponent {
- constructor(public service: Service) {}
+ protected readonly service = inject(Service);
}
diff --git a/projects/testing-library/tests/render-template.spec.ts b/projects/testing-library/tests/render-template.spec.ts
index a6892dbc..cddc28a1 100644
--- a/projects/testing-library/tests/render-template.spec.ts
+++ b/projects/testing-library/tests/render-template.spec.ts
@@ -1,4 +1,4 @@
-import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component } from '@angular/core';
+import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component, inject } from '@angular/core';
import { render, fireEvent, screen } from '../src/public_api';
@@ -7,11 +7,12 @@ import { render, fireEvent, screen } from '../src/public_api';
selector: '[onOff]',
})
class OnOffDirective {
+ private el = inject(ElementRef);
@Input() on = 'on';
@Input() off = 'off';
@Output() clicked = new EventEmitter();
- constructor(private el: ElementRef) {
+ constructor() {
this.el.nativeElement.textContent = 'init';
}
@@ -26,12 +27,11 @@ class OnOffDirective {
selector: '[update]',
})
class UpdateInputDirective {
+ private readonly el = inject(ElementRef);
@Input()
set update(value: any) {
this.el.nativeElement.textContent = value;
}
-
- constructor(private el: ElementRef) {}
}
@Component({
@@ -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 56f4608f..243a5e81 100644
--- a/projects/testing-library/tests/render.spec.ts
+++ b/projects/testing-library/tests/render.spec.ts
@@ -10,12 +10,17 @@ import {
Injectable,
EventEmitter,
Output,
+ ElementRef,
+ inject,
+ output,
+ input,
+ model,
} from '@angular/core';
-import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { outputFromObservable } from '@angular/core/rxjs-interop';
import { TestBed } from '@angular/core/testing';
-import { render, fireEvent, screen } from '../src/public_api';
+import { render, fireEvent, screen, OutputRefKeysWithCallback, aliasedInput } from '../src/public_api';
import { ActivatedRoute, Resolve, RouterModule } from '@angular/router';
-import { map } from 'rxjs';
+import { fromEvent, map } from 'rxjs';
import { AsyncPipe, NgIf } from '@angular/common';
@Component({
@@ -31,7 +36,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
@@ -41,34 +46,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 {}
@@ -77,18 +79,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();
@@ -112,17 +113,15 @@ describe('childComponentOverrides', () => {
@Component({
selector: 'atl-child-fixture',
template: `{{ simpleService.value }}`,
- standalone: true,
providers: [MySimpleService],
})
class NestedChildFixtureComponent {
- public constructor(public simpleService: MySimpleService) {}
+ protected simpleService = inject(MySimpleService);
}
@Component({
selector: 'atl-parent-fixture',
template: ``,
- standalone: true,
imports: [NestedChildFixtureComponent],
})
class ParentFixtureComponent {}
@@ -183,35 +182,151 @@ describe('componentOutputs', () => {
});
});
-describe('animationModule', () => {
- @NgModule({
- declarations: [FixtureComponent],
- })
- class FixtureModule {}
- describe('excludeComponentDeclaration', () => {
- it('does not throw if component is declared in an imported module', async () => {
- await render(FixtureComponent, {
- imports: [FixtureModule],
- excludeComponentDeclaration: true,
- });
+describe('on', () => {
+ @Component({ template: `` })
+ class TestFixtureWithEventEmitterComponent {
+ @Output() readonly event = new EventEmitter();
+ }
+
+ @Component({ template: `` })
+ class TestFixtureWithDerivedEventComponent {
+ @Output() readonly event = fromEvent(inject(ElementRef).nativeElement, 'click');
+ }
+
+ @Component({ template: `` })
+ class TestFixtureWithFunctionalOutputComponent {
+ readonly event = output();
+ }
+
+ @Component({ template: `` })
+ class TestFixtureWithFunctionalDerivedEventComponent {
+ readonly event = outputFromObservable(fromEvent(inject(ElementRef).nativeElement, 'click'));
+ }
+
+ it('should subscribe passed listener to the component EventEmitter', async () => {
+ const spy = jest.fn();
+ const { fixture } = await render(TestFixtureWithEventEmitterComponent, { on: { event: spy } });
+ fixture.componentInstance.event.emit();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should unsubscribe on rerender without listener', async () => {
+ const spy = jest.fn();
+ const { fixture, rerender } = await render(TestFixtureWithEventEmitterComponent, {
+ on: { event: spy },
});
+
+ await rerender({});
+
+ fixture.componentInstance.event.emit();
+ expect(spy).not.toHaveBeenCalled();
});
- it('adds NoopAnimationsModule by default', async () => {
- await render(FixtureComponent);
- const noopAnimationsModule = TestBed.inject(NoopAnimationsModule);
- expect(noopAnimationsModule).toBeDefined();
+ it('should not unsubscribe when same listener function is used on rerender', async () => {
+ const spy = jest.fn();
+ const { fixture, rerender } = await render(TestFixtureWithEventEmitterComponent, {
+ on: { event: spy },
+ });
+
+ await rerender({ on: { event: spy } });
+
+ fixture.componentInstance.event.emit();
+ expect(spy).toHaveBeenCalled();
});
- it('does not add NoopAnimationsModule if BrowserAnimationsModule is an import', async () => {
- await render(FixtureComponent, {
- imports: [BrowserAnimationsModule],
+ it('should unsubscribe old and subscribe new listener function on rerender', async () => {
+ const firstSpy = jest.fn();
+ const { fixture, rerender } = await render(TestFixtureWithEventEmitterComponent, {
+ on: { event: firstSpy },
});
- const browserAnimationsModule = TestBed.inject(BrowserAnimationsModule);
- expect(browserAnimationsModule).toBeDefined();
+ const newSpy = jest.fn();
+ await rerender({ on: { event: newSpy } });
- expect(() => TestBed.inject(NoopAnimationsModule)).toThrow();
+ fixture.componentInstance.event.emit();
+
+ expect(firstSpy).not.toHaveBeenCalled();
+ expect(newSpy).toHaveBeenCalled();
+ });
+
+ it('should subscribe passed listener to a derived component output', async () => {
+ const spy = jest.fn();
+ const { fixture } = await render(TestFixtureWithDerivedEventComponent, {
+ on: { event: spy },
+ });
+ fireEvent.click(fixture.nativeElement);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should subscribe passed listener to a functional component output', async () => {
+ const spy = jest.fn();
+ const { fixture } = await render(TestFixtureWithFunctionalOutputComponent, {
+ on: { event: spy },
+ });
+ fixture.componentInstance.event.emit('test');
+ expect(spy).toHaveBeenCalledWith('test');
+ });
+
+ it('should subscribe passed listener to a functional derived component output', async () => {
+ const spy = jest.fn();
+ const { fixture } = await render(TestFixtureWithFunctionalDerivedEventComponent, {
+ on: { event: spy },
+ });
+ fireEvent.click(fixture.nativeElement);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('OutputRefKeysWithCallback is correctly typed', () => {
+ const fnWithVoidArg = (_: void) => void 0;
+ const fnWithNumberArg = (_: number) => void 0;
+ const fnWithStringArg = (_: string) => void 0;
+ const fnWithMouseEventArg = (_: MouseEvent) => void 0;
+
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ function _test(_on: OutputRefKeysWithCallback) {}
+
+ // @ts-expect-error wrong event type
+ _test({ event: fnWithNumberArg });
+ _test({ event: fnWithVoidArg });
+
+ // @ts-expect-error wrong event type
+ _test({ event: fnWithNumberArg });
+ _test({ event: fnWithMouseEventArg });
+
+ // @ts-expect-error wrong event type
+ _test({ event: fnWithNumberArg });
+ _test({ event: fnWithStringArg });
+
+ // @ts-expect-error wrong event type
+ _test({ event: fnWithNumberArg });
+ _test({ event: fnWithMouseEventArg });
+
+ // add a statement so the test succeeds
+ expect(true).toBeTruthy();
+ });
+});
+
+describe('excludeComponentDeclaration', () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: `
+
+
+ `,
+ standalone: false,
+ })
+ class NotStandaloneFixtureComponent {}
+
+ @NgModule({
+ declarations: [NotStandaloneFixtureComponent],
+ })
+ class FixtureModule {}
+
+ it('does not throw if component is declared in an imported module', async () => {
+ await render(NotStandaloneFixtureComponent, {
+ imports: [FixtureModule],
+ excludeComponentDeclaration: true,
+ });
});
});
@@ -256,11 +371,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);
});
@@ -272,11 +387,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);
});
@@ -328,14 +443,12 @@ describe('DebugElement', () => {
describe('initialRoute', () => {
@Component({
- standalone: true,
selector: 'atl-fixture2',
template: ``,
})
class SecondaryFixtureComponent {}
@Component({
- standalone: true,
selector: 'atl-router-fixture',
template: ``,
imports: [RouterModule],
@@ -372,13 +485,12 @@ 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],
})
class QueryParamFixtureComponent {
- constructor(public route: ActivatedRoute) {}
+ private readonly route = inject(ActivatedRoute);
paramPresent$ = this.route.queryParams.pipe(map((queryParams) => (queryParams?.param ? 'present' : 'missing')));
}
@@ -405,3 +517,117 @@ describe('configureTestBed', () => {
expect(configureTestBedFn).toHaveBeenCalledTimes(1);
});
});
+
+describe('inputs and signals', () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: `{{ myName() }} {{ myJob() }}`,
+ })
+ class InputComponent {
+ myName = input('foo');
+
+ myJob = input('bar', { alias: 'job' });
+ }
+
+ it('should set the input component', async () => {
+ await render(InputComponent, {
+ inputs: {
+ myName: 'Bob',
+ ...aliasedInput('job', 'Builder'),
+ },
+ });
+
+ expect(screen.getByText('Bob')).toBeInTheDocument();
+ expect(screen.getByText('Builder')).toBeInTheDocument();
+ });
+
+ it('should typecheck correctly', async () => {
+ // we only want to check the types here
+ // so we are purposely not calling render
+
+ const typeTests = [
+ async () => {
+ // OK:
+ await render(InputComponent, {
+ inputs: {
+ myName: 'OK',
+ },
+ });
+ },
+ async () => {
+ // @ts-expect-error - myName is a string
+ await render(InputComponent, {
+ inputs: {
+ myName: 123,
+ },
+ });
+ },
+ async () => {
+ // OK:
+ await render(InputComponent, {
+ inputs: {
+ ...aliasedInput('job', 'OK'),
+ },
+ });
+ },
+ async () => {
+ // @ts-expect-error - job is not using aliasedInput
+ await render(InputComponent, {
+ inputs: {
+ job: 'not used with aliasedInput',
+ },
+ });
+ },
+ ];
+
+ // add a statement so the test succeeds
+ expect(typeTests).toBeTruthy();
+ });
+});
+
+describe('README examples', () => {
+ describe('Counter', () => {
+ @Component({
+ selector: 'atl-counter',
+ template: `
+ {{ hello() }}
+
+ Current Count: {{ counter() }}
+
+ `,
+ })
+ class CounterComponent {
+ counter = model(0);
+ hello = input('Hi', { alias: 'greeting' });
+
+ increment() {
+ this.counter.set(this.counter() + 1);
+ }
+
+ decrement() {
+ this.counter.set(this.counter() - 1);
+ }
+ }
+
+ it('should render counter', async () => {
+ await render(CounterComponent, {
+ inputs: {
+ counter: 5,
+ ...aliasedInput('greeting', 'Hello Alias!'),
+ },
+ });
+
+ expect(screen.getByText('Current Count: 5')).toBeVisible();
+ expect(screen.getByText('Hello Alias!')).toBeVisible();
+ });
+
+ it('should increment the counter on click', async () => {
+ await render(CounterComponent, { inputs: { counter: 5 } });
+
+ const incrementButton = screen.getByRole('button', { name: '+' });
+ fireEvent.click(incrementButton);
+
+ expect(screen.getByText('Current Count: 6')).toBeVisible();
+ });
+ });
+});
diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts
index 571d6429..04b8185a 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -43,7 +43,7 @@ test('rerenders the component with updated inputs', async () => {
expect(screen.getByText('Sarah')).toBeInTheDocument();
const firstName = 'Mark';
- await rerender({ componentInputs: { firstName } });
+ await rerender({ inputs: { firstName } });
expect(screen.getByText(firstName)).toBeInTheDocument();
});
@@ -52,7 +52,7 @@ test('rerenders the component with updated inputs and resets other props', async
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
- componentInputs: {
+ inputs: {
firstName,
lastName,
},
@@ -61,7 +61,7 @@ test('rerenders the component with updated inputs and resets other props', async
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
const firstName2 = 'Chris';
- await rerender({ componentInputs: { firstName: firstName2 } });
+ await rerender({ inputs: { firstName: firstName2 } });
expect(screen.getByText(firstName2)).toBeInTheDocument();
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
@@ -87,7 +87,7 @@ test('rerenders the component with updated inputs and keeps other props when par
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
- componentInputs: {
+ inputs: {
firstName,
lastName,
},
@@ -96,7 +96,7 @@ test('rerenders the component with updated inputs and keeps other props when par
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
const firstName2 = 'Chris';
- await rerender({ componentInputs: { firstName: firstName2 }, partialUpdate: true });
+ await rerender({ inputs: { firstName: firstName2 }, partialUpdate: true });
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
@@ -181,7 +181,7 @@ test('change detection gets not called if `detectChangesOnRender` is set to fals
expect(screen.getByText('Sarah')).toBeInTheDocument();
const firstName = 'Mark';
- await rerender({ componentInputs: { firstName }, detectChangesOnRender: false });
+ await rerender({ inputs: { firstName }, detectChangesOnRender: false });
expect(screen.getByText('Sarah')).toBeInTheDocument();
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
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;