diff --git a/.all-contributorsrc b/.all-contributorsrc
index 83afbd44..c60fd019 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -294,6 +294,178 @@
"code",
"test"
]
+ },
+ {
+ "login": "dzonatan",
+ "name": "Rokas Brazdลพionis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5166666?v=4",
+ "profile": "https://github.com/dzonatan",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mateusduraes",
+ "name": "Mateus Duraes",
+ "avatar_url": "https://avatars.githubusercontent.com/u/19319404?v=4",
+ "profile": "https://github.com/mateusduraes",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "JJosephttg",
+ "name": "Josh Joseph",
+ "avatar_url": "https://avatars.githubusercontent.com/u/23690250?v=4",
+ "profile": "https://github.com/JJosephttg",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "shaman-apprentice",
+ "name": "Torsten Knauf",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3596742?v=4",
+ "profile": "https://github.com/shaman-apprentice",
+ "contributions": [
+ "maintenance"
+ ]
+ },
+ {
+ "login": "antischematic",
+ "name": "antischematic",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12976684?v=4",
+ "profile": "https://github.com/antischematic",
+ "contributions": [
+ "bug",
+ "ideas"
+ ]
+ },
+ {
+ "login": "TrustNoOneElse",
+ "name": "Florian Pabst",
+ "avatar_url": "https://avatars.githubusercontent.com/u/25935352?v=4",
+ "profile": "https://github.com/TrustNoOneElse",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "markgoho",
+ "name": "Mark Goho",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9759954?v=4",
+ "profile": "https://rochesterparks.org",
+ "contributions": [
+ "maintenance",
+ "doc"
+ ]
+ },
+ {
+ "login": "jwbaart",
+ "name": "Jan-Willem Baart",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10973990?v=4",
+ "profile": "http://jwbaart.dev",
+ "contributions": [
+ "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,
@@ -301,5 +473,7 @@
"projectOwner": "testing-library",
"repoType": "github",
"repoHost": "https://github.com",
- "skipCi": true
+ "skipCi": true,
+ "commitConvention": "angular",
+ "commitType": "docs"
}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..ac9d248f
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,42 @@
+// For format details, see https://aka.ms/devcontainer.json.
+{
+ "name": "angular-testing-library",
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bullseye",
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ "features": {
+ "ghcr.io/devcontainers/features/github-cli:1": {},
+ "ghcr.io/devcontainers/features/sshd:1": {}
+ },
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "npm install --force",
+ "onCreateCommand": "sudo cp .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt",
+ "waitFor": "postCreateCommand",
+
+ // Configure tool-specific properties.
+ "customizations": {
+ // Configure properties specific to VS Code.
+ "vscode": {
+ "settings": {
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ },
+ "[md]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ },
+ "[json]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ }
+ },
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
+ }
+ }
+}
diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt
new file mode 100644
index 00000000..952d2c48
--- /dev/null
+++ b/.devcontainer/welcome-message.txt
@@ -0,0 +1,7 @@
+๐ Welcome to "Angular Testing Library" in GitHub Codespaces!
+
+๐ ๏ธ Your environment is fully setup with all the required software.
+
+๐ To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
+
+๐ Edit away, run your app as usual, and we'll automatically make it available for you to access.
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 40a33190..00000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "root": true,
- "ignorePatterns": ["**/*"],
- "plugins": ["@nrwl/nx", "testing-library"],
- "overrides": [
- {
- "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
- "rules": {
- "@nrwl/nx/enforce-module-boundaries": [
- "error",
- {
- "enforceBuildableLibDependency": true,
- "allow": [],
- "depConstraints": [
- {
- "sourceTag": "*",
- "onlyDependOnLibsWithTags": ["*"]
- }
- ]
- }
- ]
- }
- },
- {
- "files": ["*.ts", "*.tsx"],
- "extends": ["plugin:@nrwl/nx/typescript"],
- "rules": {}
- },
- {
- "files": ["*.js", "*.jsx"],
- "extends": ["plugin:@nrwl/nx/javascript"],
- "rules": {}
- },
- {
- "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/.githooks/pre-commit b/.githooks/pre-commit
old mode 100644
new mode 100755
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69e0fda3..b35c5abb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,6 +6,9 @@ on:
- 'main'
- 'beta'
pull_request: {}
+ workflow_dispatch:
+
+permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -13,24 +16,30 @@ concurrency:
jobs:
build_test_release:
+ permissions:
+ actions: write
+ contents: write
+
strategy:
matrix:
- node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[16]' || '[14,16]') }}
+ 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 }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: install
- run: npm install
+ run: npm install --force
- name: build
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 b1a42cd6..22faaca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,8 @@
# misc
/.angular/cache
.angular
+.nx
+migrations.json
.cache
/.sass-cache
/connect.lock
@@ -42,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 b6a7d89c..8fdd954d 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-16
+22
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
index 9aa8a3df..03ff48d9 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -53,3 +53,6 @@ deployment.yaml
# System Files
.DS_Store
Thumbs.db
+
+/.nx/cache
+/.nx/workspace-data
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8c53210b..d3ecd5a3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,7 +5,7 @@ Hi there, thanks for being willing to contribute!
## Setup
- Fork and clone the repository
-- Install dependencies via via `npm install`
+- Install dependencies via `npm install`
- Create a new feature branch via `git checkout -b feature-branch-name`
## Testing
diff --git a/README.md b/README.md
index eb47f898..848aed06 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,12 @@
@testing-library/angular
-
-
-
+
Simple and complete Angular testing utilities that encourage good testing
practices.
@@ -28,7 +26,7 @@ practices.
[![version][version-badge]][package] [![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]
-[](#contributors)
+[](#contributors)
[![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc]
[![Discord][discord-badge]][discord]
@@ -47,23 +45,29 @@ practices.
+[](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=137053739)
+
## Table of Contents
+- [Table of Contents](#table-of-contents)
- [The problem](#the-problem)
- [This solution](#this-solution)
- [Example](#example)
- [Installation](#installation)
+- [Version compatibility](#version-compatibility)
- [Guiding Principles](#guiding-principles)
- [Contributors](#contributors)
- [Docs](#docs)
- [FAQ](#faq)
+ - [I am using Reactive Forms and the `jest-dom` matcher `toHaveFormValues` always returns an empty object or there are missing fields. Why?](#i-am-using-reactive-forms-and-the-jest-dom-matcher-tohaveformvalues-always-returns-an-empty-object-or-there-are-missing-fields-why)
- [Issues](#issues)
- [๐ Bugs](#-bugs)
- [๐ก Feature Requests](#-feature-requests)
- [โ Questions](#-questions)
+- [Getting started with GitHub Codespaces](#getting-started-with-github-codespaces)
- [LICENSE](#license)
@@ -94,22 +98,24 @@ counter.component.ts
```ts
@Component({
- selector: '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);
}
}
```
@@ -117,23 +123,30 @@ export class CounterComponent {
counter.component.spec.ts
```typescript
-import { render, screen, fireEvent } from '@testing-library/angular';
-import { CounterComponent } from './counter.component.ts';
+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: /increment/i });
+ const incrementButton = screen.getByRole('button', { name: '+' });
fireEvent.click(incrementButton);
- expect(screen.getByText('Current Count: 6'));
+ expect(screen.getByText('Current Count: 6')).toBeVisible();
});
});
```
@@ -143,10 +156,18 @@ describe('Counter', () => {
## Installation
This module is distributed via [npm][npm] which is bundled with [node][node] and
-should be installed as one of your project's `devDependencies`:
+should be installed as one of your project's `devDependencies`.
+Starting from ATL version 17, you also need to install `@testing-library/dom`:
```bash
-npm install @testing-library/angular --save-dev
+npm install --save-dev @testing-library/angular @testing-library/dom
+```
+
+Or, you can use the `ng add` command.
+This sets up your project to use Angular Testing Library, which also includes the installation of `@testing-library/dom`.
+
+```bash
+ng add @testing-library/angular
```
You may also be interested in installing `jest-dom` so you can use
@@ -154,6 +175,19 @@ You may also be interested in installing `jest-dom` so you can use
> [**Docs**](https://testing-library.com/angular)
+## Version compatibility
+
+| 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
> [The more your tests resemble the way your software is used, the more
@@ -183,46 +217,70 @@ Thanks goes to these people ([emoji key][emojis]):
@@ -240,7 +298,7 @@ Contributions of any kind welcome!
## FAQ
-##### I am using Reactive Forms and the `jest-dom` matcher `toHaveFormValues` always returns an empty object or there are missing fields. Why?
+### I am using Reactive Forms and the `jest-dom` matcher `toHaveFormValues` always returns an empty object or there are missing fields. Why?
Only form elements with a `name` attribute will have their values passed to `toHaveFormsValues`.
@@ -270,6 +328,16 @@ instead of filing an issue on GitHub.
- [Discord][discord]
- [Stack Overflow][stackoverflow]
+## Getting started with GitHub Codespaces
+
+To get started, create a codespace for this repository by clicking this ๐
+
+[](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=137053739)
+
+A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with software needed for this project.
+
+**Note**: Dev containers is an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting).
+
## LICENSE
MIT
diff --git a/angular.json b/angular.json
deleted file mode 100644
index dfd6bcf1..00000000
--- a/angular.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "version": 2,
- "projects": {
- "example-app": "apps/example-app",
- "example-app-karma": "apps/example-app-karma",
- "jest-utils": "projects/jest-utils",
- "testing-library": "projects/testing-library"
- }
-}
diff --git a/apps/example-app-karma/.browserslistrc b/apps/example-app-karma/.browserslistrc
deleted file mode 100644
index 427441dc..00000000
--- a/apps/example-app-karma/.browserslistrc
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
-# For additional information regarding the format and rule options, please see:
-# https://github.com/browserslist/browserslist#queries
-
-# For the full list of supported browsers by the Angular framework, please see:
-# https://angular.io/guide/browser-support
-
-# You can see what browsers were selected by your queries by running:
-# npx browserslist
-
-last 1 Chrome version
-last 1 Firefox version
-last 2 Edge major versions
-last 2 Safari major versions
-last 2 iOS major versions
-Firefox ESR
-not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
diff --git a/apps/example-app-karma/.eslintrc.json b/apps/example-app-karma/.eslintrc.json
deleted file mode 100644
index f1a2cfb5..00000000
--- a/apps/example-app-karma/.eslintrc.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "extends": "../../.eslintrc.json",
- "ignorePatterns": ["!**/*"],
- "overrides": [
- {
- "files": ["*.ts"],
- "extends": ["plugin:@nrwl/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:@nrwl/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 94b4a463..27c4cbd4 100644
--- a/apps/example-app-karma/project.json
+++ b/apps/example-app-karma/project.json
@@ -1,8 +1,10 @@
{
+ "name": "example-app-karma",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
- "root": "apps/example-app-karma",
"sourceRoot": "apps/example-app-karma/src",
"prefix": "app",
+ "tags": [],
"generators": {},
"targets": {
"build": {
@@ -12,7 +14,6 @@
"outputPath": "dist/apps/example-app-karma",
"index": "apps/example-app-karma/src/index.html",
"main": "apps/example-app-karma/src/main.ts",
- "polyfills": "apps/example-app-karma/src/polyfills.ts",
"tsConfig": "apps/example-app-karma/tsconfig.app.json",
"assets": ["apps/example-app-karma/src/favicon.ico", "apps/example-app-karma/src/assets"],
"styles": [],
@@ -26,12 +27,6 @@
"maximumWarning": "6kb"
}
],
- "fileReplacements": [
- {
- "replace": "apps/example-app-karma/src/environments/environment.ts",
- "with": "apps/example-app-karma/src/environments/environment.prod.ts"
- }
- ],
"outputHashing": "all"
},
"development": {
@@ -49,34 +44,25 @@
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
- "browserTarget": "example-app-karma:build:production"
+ "buildTarget": "example-app-karma:build:production"
},
"development": {
- "browserTarget": "example-app-karma:build:development"
+ "buildTarget": "example-app-karma:build:development"
}
},
- "defaultConfiguration": "development"
+ "defaultConfiguration": "development",
+ "continuous": true
},
"lint": {
- "executor": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": [
- "apps/example-app-karma/**/*.ts",
- "apps/example-app-karma/**/*.html",
- "apps/example-app-karma/src/**/*.html"
- ]
- },
- "outputs": ["{options.outputFile}"]
+ "executor": "@nx/eslint:lint"
},
"test": {
"executor": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/example-app-karma/src/test.ts",
"tsConfig": "apps/example-app-karma/tsconfig.spec.json",
- "polyfills": "apps/example-app-karma/src/polyfills.ts",
"karmaConfig": "apps/example-app-karma/karma.conf.js"
}
}
- },
- "tags": []
+ }
}
diff --git a/apps/example-app-karma/src/app/app.module.ts b/apps/example-app-karma/src/app/app.module.ts
deleted file mode 100644
index e636d9eb..00000000
--- a/apps/example-app-karma/src/app/app.module.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { NgModule } from '@angular/core';
-
-@NgModule({
- declarations: [],
- imports: [BrowserModule, BrowserAnimationsModule],
-})
-export class AppModule {}
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
new file mode 100644
index 00000000..d019e069
--- /dev/null
+++ b/apps/example-app-karma/src/app/examples/login-form.spec.ts
@@ -0,0 +1,66 @@
+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';
+import { NgIf } from '@angular/common';
+
+it('should create a component with inputs and a button to submit', async () => {
+ await render(LoginComponent);
+
+ expect(screen.getByRole('textbox', { name: 'email' })).toBeInTheDocument();
+ expect(screen.getByLabelText('password')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'submit' })).toBeInTheDocument();
+});
+
+it('should display invalid message and submit button must be disabled', async () => {
+ const user = userEvent.setup();
+
+ await render(LoginComponent);
+
+ const email = screen.getByRole('textbox', { name: 'email' });
+ const password = screen.getByLabelText('password');
+
+ await user.type(email, 'foo');
+ await user.type(password, 's');
+
+ expect(screen.getAllByText(/is invalid/i).length).toBe(2);
+ expect(screen.getAllByRole('alert').length).toBe(2);
+ expect(screen.getByRole('button', { name: 'submit' })).toBeDisabled();
+});
+
+@Component({
+ selector: 'atl-login',
+ standalone: true,
+ imports: [ReactiveFormsModule, NgIf],
+ template: `
+ Login
+
+
+ `,
+})
+class LoginComponent {
+ private fb = inject(FormBuilder);
+
+ form: FormGroup = this.fb.group({
+ email: ['', [Validators.required, Validators.email]],
+ password: ['', [Validators.required, Validators.minLength(8)]],
+ });
+
+ get email(): FormControl {
+ return this.form.get('email') as FormControl;
+ }
+
+ get password(): FormControl {
+ return this.form.get('password') as FormControl;
+ }
+
+ onSubmit(_fg: FormGroup): void {
+ // do nothing
+ }
+}
diff --git a/apps/example-app-karma/src/app/issues/issue-222.spec.ts b/apps/example-app-karma/src/app/issues/issue-222.spec.ts
deleted file mode 100644
index b6ac5204..00000000
--- a/apps/example-app-karma/src/app/issues/issue-222.spec.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { render, screen } from '@testing-library/angular';
-
-it('https://github.com/testing-library/angular-testing-library/issues/222 with rerender', async () => {
- const { rerender } = await render(`Hello {{ name}}
`, {
- componentProperties: {
- name: 'Sarah',
- },
- });
-
- expect(screen.getByText('Hello Sarah')).toBeTruthy();
-
- await rerender({ name: 'Mark' });
-
- expect(screen.getByText('Hello Mark')).toBeTruthy();
-});
-
-it('https://github.com/testing-library/angular-testing-library/issues/222 with change', async () => {
- const { change } = await render(`Hello {{ name}}
`, {
- componentProperties: {
- name: 'Sarah',
- },
- });
-
- expect(screen.getByText('Hello Sarah')).toBeTruthy();
- await change({ name: 'Mark' });
-
- expect(screen.getByText('Hello Mark')).toBeTruthy();
-});
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
new file mode 100644
index 00000000..324e8a16
--- /dev/null
+++ b/apps/example-app-karma/src/app/issues/rerender.spec.ts
@@ -0,0 +1,15 @@
+import { render, screen } from '@testing-library/angular';
+
+it('can rerender component', async () => {
+ const { rerender } = await render(`Hello {{ name}}
`, {
+ componentProperties: {
+ name: 'Sarah',
+ },
+ });
+
+ expect(screen.getByText('Hello Sarah')).toBeInTheDocument();
+
+ await rerender({ componentProperties: { name: 'Mark' } });
+
+ expect(screen.getByText('Hello Mark')).toBeInTheDocument();
+});
diff --git a/apps/example-app-karma/src/assets/.gitkeep b/apps/example-app-karma/src/assets/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/apps/example-app-karma/src/environments/environment.prod.ts b/apps/example-app-karma/src/environments/environment.prod.ts
deleted file mode 100644
index c9669790..00000000
--- a/apps/example-app-karma/src/environments/environment.prod.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export const environment = {
- production: true,
-};
diff --git a/apps/example-app-karma/src/environments/environment.ts b/apps/example-app-karma/src/environments/environment.ts
deleted file mode 100644
index 64c3e6c7..00000000
--- a/apps/example-app-karma/src/environments/environment.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// This file can be replaced during build by using the `fileReplacements` array.
-// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
-// The list of file replacements can be found in `workspace.json`.
-
-export const environment = {
- production: false,
-};
-
-/*
- * In development mode, to ignore zone related error stack frames such as
- * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
- * import the following file, but please comment it out in production mode
- * because it will have performance impact when throw error
- */
-// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/apps/example-app-karma/src/favicon.ico b/apps/example-app-karma/src/favicon.ico
deleted file mode 100644
index 8081c7ce..00000000
Binary files a/apps/example-app-karma/src/favicon.ico and /dev/null differ
diff --git a/apps/example-app-karma/src/index.html b/apps/example-app-karma/src/index.html
deleted file mode 100644
index 930133fd..00000000
--- a/apps/example-app-karma/src/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- AngularTestingLibraryApp
-
-
-
-
-
-
-
-
-
diff --git a/apps/example-app-karma/src/main.ts b/apps/example-app-karma/src/main.ts
deleted file mode 100644
index 741c9eb8..00000000
--- a/apps/example-app-karma/src/main.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { enableProdMode } from '@angular/core';
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-
-import { AppModule } from './app/app.module';
-import { environment } from './environments/environment';
-
-if (environment.production) {
- enableProdMode();
-}
-
-platformBrowserDynamic()
- .bootstrapModule(AppModule)
- .catch((err) => console.log(err));
diff --git a/apps/example-app-karma/src/polyfills.ts b/apps/example-app-karma/src/polyfills.ts
deleted file mode 100644
index f84fd8a6..00000000
--- a/apps/example-app-karma/src/polyfills.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * This file includes polyfills needed by Angular and is loaded before the app.
- * You can add your own extra polyfills to this file.
- *
- * This file is divided into 2 sections:
- * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
- * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
- * file.
- *
- * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
- * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
- * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
- *
- * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
- */
-
-/** *************************************************************************************************
- * BROWSER POLYFILLS
- */
-
-/** IE9, IE10 and IE11 requires all of the following polyfills. **/
-// import 'core-js/es6/symbol';
-// import 'core-js/es6/object';
-// import 'core-js/es6/function';
-// import 'core-js/es6/parse-int';
-// import 'core-js/es6/parse-float';
-// import 'core-js/es6/number';
-// import 'core-js/es6/math';
-// import 'core-js/es6/string';
-// import 'core-js/es6/date';
-// import 'core-js/es6/array';
-// import 'core-js/es6/regexp';
-// import 'core-js/es6/map';
-// import 'core-js/es6/weak-map';
-// import 'core-js/es6/set';
-
-/** IE10 and IE11 requires the following for the Reflect API. */
-// import 'core-js/es6/reflect';
-
-/** Evergreen browsers require these. **/
-// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
-
-/**
- * By default, zone.js will patch all possible macroTask and DomEvents
- * user can disable parts of macroTask/DomEvents patch by setting following flags
- */
-
-// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
-// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
-// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
-
-/*
- * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
- * with the following flag, it will bypass `zone.js` patch for IE/Edge
- */
-// (window as any).__Zone_enable_cross_context_check = true;
-
-/** *************************************************************************************************
- * Zone JS is required by default for Angular itself.
- */
-import 'zone.js'; // Included with Angular CLI.
-
-/** *************************************************************************************************
- * APPLICATION IMPORTS
- */
diff --git a/apps/example-app-karma/src/test.ts b/apps/example-app-karma/src/test.ts
index c2ab726d..e6bf956d 100644
--- a/apps/example-app-karma/src/test.ts
+++ b/apps/example-app-karma/src/test.ts
@@ -1,5 +1,5 @@
-// This file is required by karma.conf.js and loads recursively all the .spec and framework files
-import 'zone.js/dist/zone-testing';
+import 'zone.js';
+import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import JasmineDOM from '@testing-library/jasmine-dom';
@@ -9,11 +9,5 @@ beforeEach(() => {
jasmine.addMatchers(JasmineDOM);
});
-declare const require: any;
-
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {});
-// Then we find all the tests.
-const context = require.context('./', true, /\.spec\.ts$/);
-// And load the modules.
-context.keys().map(context);
diff --git a/apps/example-app-karma/tsconfig.app.json b/apps/example-app-karma/tsconfig.app.json
index 4de7101b..46150c25 100644
--- a/apps/example-app-karma/tsconfig.app.json
+++ b/apps/example-app-karma/tsconfig.app.json
@@ -3,9 +3,11 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
- "allowJs": true
+ "allowJs": true,
+ "target": "ES2022",
+ "useDefineForClassFields": false
},
- "files": ["src/main.ts", "src/polyfills.ts"],
+ "files": ["src/main.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
diff --git a/apps/example-app-karma/tsconfig.json b/apps/example-app-karma/tsconfig.json
index 0cfe7c56..9453a196 100644
--- a/apps/example-app-karma/tsconfig.json
+++ b/apps/example-app-karma/tsconfig.json
@@ -2,7 +2,9 @@
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
- "compilerOptions": {},
+ "compilerOptions": {
+ "target": "es2020"
+ },
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
diff --git a/apps/example-app-karma/tsconfig.spec.json b/apps/example-app-karma/tsconfig.spec.json
index f4b0d715..0f4baec3 100644
--- a/apps/example-app-karma/tsconfig.spec.json
+++ b/apps/example-app-karma/tsconfig.spec.json
@@ -2,8 +2,10 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
- "types": ["jasmine", "node", "@testing-library/jasmine-dom"]
+ "types": ["jasmine", "node", "@testing-library/jasmine-dom"],
+ "target": "ES2022",
+ "useDefineForClassFields": false
},
- "files": ["src/test.ts", "src/polyfills.ts"],
+ "files": ["src/test.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
}
diff --git a/apps/example-app/.browserslistrc b/apps/example-app/.browserslistrc
deleted file mode 100644
index 427441dc..00000000
--- a/apps/example-app/.browserslistrc
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
-# For additional information regarding the format and rule options, please see:
-# https://github.com/browserslist/browserslist#queries
-
-# For the full list of supported browsers by the Angular framework, please see:
-# https://angular.io/guide/browser-support
-
-# You can see what browsers were selected by your queries by running:
-# npx browserslist
-
-last 1 Chrome version
-last 1 Firefox version
-last 2 Edge major versions
-last 2 Safari major versions
-last 2 iOS major versions
-Firefox ESR
-not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
diff --git a/apps/example-app/.eslintrc.json b/apps/example-app/.eslintrc.json
deleted file mode 100644
index 897bee00..00000000
--- a/apps/example-app/.eslintrc.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "extends": "../../.eslintrc.json",
- "ignorePatterns": ["!**/*"],
- "overrides": [
- {
- "files": ["*.ts"],
- "extends": ["plugin:@nrwl/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:@nrwl/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.js b/apps/example-app/jest.config.ts
similarity index 67%
rename from apps/example-app/jest.config.js
rename to apps/example-app/jest.config.ts
index 2be66c61..e0ea9c2d 100644
--- a/apps/example-app/jest.config.js
+++ b/apps/example-app/jest.config.ts
@@ -1,7 +1,6 @@
-module.exports = {
- name: 'Example App',
+export default {
displayName: {
- name: 'Example',
+ name: 'Example App',
color: 'blue',
},
preset: '../../jest.preset.js',
diff --git a/apps/example-app/project.json b/apps/example-app/project.json
index ebab97cb..1cf90ac4 100644
--- a/apps/example-app/project.json
+++ b/apps/example-app/project.json
@@ -1,8 +1,10 @@
{
+ "name": "example-app",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
- "root": "apps/example-app",
"sourceRoot": "apps/example-app/src",
"prefix": "app",
+ "tags": [],
"generators": {},
"targets": {
"build": {
@@ -26,12 +28,6 @@
"maximumWarning": "6kb"
}
],
- "fileReplacements": [
- {
- "replace": "apps/example-app/src/environments/environment.ts",
- "with": "apps/example-app/src/environments/environment.prod.ts"
- }
- ],
"outputHashing": "all"
},
"development": {
@@ -49,34 +45,31 @@
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
- "browserTarget": "example-app:build:production"
+ "buildTarget": "example-app:build:production"
},
"development": {
- "browserTarget": "example-app:build:development"
+ "buildTarget": "example-app:build:development"
}
},
- "defaultConfiguration": "development"
+ "defaultConfiguration": "development",
+ "continuous": true
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
- "browserTarget": "example-app:build"
+ "buildTarget": "example-app:build"
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": ["apps/example-app/**/*.ts", "apps/example-app/**/*.html", "apps/example-app/src/**/*.html"]
- },
- "outputs": ["{options.outputFile}"]
+ "executor": "@nx/eslint:lint"
},
"test": {
- "executor": "@nrwl/jest:jest",
+ "executor": "@nx/jest:jest",
"options": {
- "jestConfig": "apps/example-app/jest.config.js"
+ "jestConfig": "apps/example-app/jest.config.ts",
+ "passWithNoTests": false
},
- "outputs": ["coverage/"]
+ "outputs": ["{workspaceRoot}/coverage/"]
}
- },
- "tags": []
+ }
}
diff --git a/apps/example-app/src/app/app-routing.module.ts b/apps/example-app/src/app/app-routing.module.ts
deleted file mode 100644
index a553ba61..00000000
--- a/apps/example-app/src/app/app-routing.module.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { NgModule } from '@angular/core';
-import { Routes, RouterModule } from '@angular/router';
-
-import { SingleComponent } from './examples/00-single-component';
-import { NestedContainerComponent } from './examples/01-nested-component';
-import { InputOutputComponent } from './examples/02-input-output';
-import { FormsComponent } from './examples/03-forms';
-import { MaterialFormsComponent } from './examples/04-forms-with-material';
-import { ComponentWithProviderComponent } from './examples/05-component-provider';
-import { WithNgRxStoreComponent } from './examples/06-with-ngrx-store';
-import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
-import { RootComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';
-
-export const examples = [
- {
- path: 'single-component',
- component: SingleComponent,
- data: {
- name: 'Single component',
- },
- },
- {
- path: 'nested-component',
- component: NestedContainerComponent,
- data: {
- name: 'Nested components',
- },
- },
- {
- path: 'input-output',
- component: InputOutputComponent,
- data: {
- name: 'Input and Output',
- },
- },
- {
- path: 'forms',
- component: FormsComponent,
- data: {
- name: 'Form',
- },
- },
- {
- path: 'forms-material',
- component: MaterialFormsComponent,
- data: {
- name: 'Material form',
- },
- },
- {
- path: 'component-with-provider',
- component: ComponentWithProviderComponent,
- data: {
- name: 'With provider',
- },
- },
- {
- path: 'with-ngrx-store',
- component: WithNgRxStoreComponent,
- data: {
- name: 'With NgRx Store',
- },
- },
- {
- path: 'with-ngrx-mock-store',
- component: WithNgRxMockStoreComponent,
- data: {
- name: 'With NgRx MockStore',
- },
- },
- {
- path: 'with-router',
- component: RootComponent,
- data: {
- name: 'Router',
- },
- children: [
- {
- path: 'detail/:id',
- component: DetailComponent,
- },
- {
- path: 'hidden-detail',
- component: HiddenDetailComponent,
- },
- ],
- },
-];
-
-export const routes: Routes = [
- {
- path: '',
- children: examples,
- },
-];
-
-@NgModule({
- imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
- exports: [RouterModule],
-})
-export class AppRoutingModule {}
diff --git a/apps/example-app/src/app/app.component.css b/apps/example-app/src/app/app.component.css
deleted file mode 100644
index 6d3bc67b..00000000
--- a/apps/example-app/src/app/app.component.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.container {
- display: flex;
- flex-direction: column;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-}
-
-.sidenav {
- flex: 1;
-}
-
-.sidenav-container {
- padding: 10px;
-}
diff --git a/apps/example-app/src/app/app.component.html b/apps/example-app/src/app/app.component.html
deleted file mode 100644
index 04b5f476..00000000
--- a/apps/example-app/src/app/app.component.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
diff --git a/apps/example-app/src/app/app.component.ts b/apps/example-app/src/app/app.component.ts
deleted file mode 100644
index 5b20ef63..00000000
--- a/apps/example-app/src/app/app.component.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Component } from '@angular/core';
-import { examples as routes } from './app-routing.module';
-
-@Component({
- selector: 'app-root',
- templateUrl: './app.component.html',
- styleUrls: ['app.component.css'],
-})
-export class AppComponent {
- examples = routes;
-}
diff --git a/apps/example-app/src/app/app.module.ts b/apps/example-app/src/app/app.module.ts
deleted file mode 100644
index cb95434f..00000000
--- a/apps/example-app/src/app/app.module.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { NgModule } from '@angular/core';
-import { ReactiveFormsModule } from '@angular/forms';
-import { StoreModule } from '@ngrx/store';
-
-import { AppRoutingModule } from './app-routing.module';
-import { MaterialModule } from './material.module';
-import { MatIconModule } from '@angular/material/icon';
-import { MatListModule } from '@angular/material/list';
-import { MatSidenavModule } from '@angular/material/sidenav';
-import { MatToolbarModule } from '@angular/material/toolbar';
-
-import { AppComponent } from './app.component';
-import { SingleComponent } from './examples/00-single-component';
-import { NestedButtonComponent, NestedValueComponent, NestedContainerComponent } from './examples/01-nested-component';
-import { InputOutputComponent } from './examples/02-input-output';
-import { FormsComponent } from './examples/03-forms';
-import { MaterialFormsComponent } from './examples/04-forms-with-material';
-import { ComponentWithProviderComponent } from './examples/05-component-provider';
-import { WithNgRxStoreComponent, reducer } from './examples/06-with-ngrx-store';
-import { WithNgRxMockStoreComponent } from './examples/07-with-ngrx-mock-store';
-import { RootComponent, DetailComponent, HiddenDetailComponent } from './examples/09-router';
-import { ScrollingModule } from '@angular/cdk/scrolling';
-
-function reducerItems() {
- return ['One', 'Two', 'Three'];
-}
-
-@NgModule({
- declarations: [
- AppComponent,
- SingleComponent,
- NestedButtonComponent,
- NestedValueComponent,
- NestedContainerComponent,
- InputOutputComponent,
- FormsComponent,
- MaterialFormsComponent,
- ComponentWithProviderComponent,
- WithNgRxStoreComponent,
- WithNgRxMockStoreComponent,
- RootComponent,
- DetailComponent,
- HiddenDetailComponent,
- ],
- imports: [
- BrowserModule,
- ReactiveFormsModule,
- BrowserAnimationsModule,
- MaterialModule,
- MatIconModule,
- MatListModule,
- MatSidenavModule,
- MatToolbarModule,
- AppRoutingModule,
- ScrollingModule,
- StoreModule.forRoot({
- value: reducer,
- items: reducerItems,
- }),
- ],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
diff --git a/apps/example-app/src/app/examples/00-single-component.spec.ts b/apps/example-app/src/app/examples/00-single-component.spec.ts
index 73e429bb..44ad2500 100644
--- a/apps/example-app/src/app/examples/00-single-component.spec.ts
+++ b/apps/example-app/src/app/examples/00-single-component.spec.ts
@@ -1,8 +1,10 @@
-import { render, screen, fireEvent } from '@testing-library/angular';
+import { render, screen } from '@testing-library/angular';
+import userEvent from '@testing-library/user-event';
import { SingleComponent } from './00-single-component';
test('renders the current value and can increment and decrement', async () => {
+ const user = userEvent.setup();
await render(SingleComponent);
const incrementControl = screen.getByRole('button', { name: /increment/i });
@@ -11,10 +13,10 @@ test('renders the current value and can increment and decrement', async () => {
expect(valueControl).toHaveTextContent('0');
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
expect(valueControl).toHaveTextContent('2');
- fireEvent.click(decrementControl);
+ await user.click(decrementControl);
expect(valueControl).toHaveTextContent('1');
});
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 25001036..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,8 @@
import { Component } from '@angular/core';
@Component({
- selector: 'app-fixture',
+ selector: 'atl-fixture',
+ standalone: true,
template: `
{{ value }}
diff --git a/apps/example-app/src/app/examples/01-nested-component.spec.ts b/apps/example-app/src/app/examples/01-nested-component.spec.ts
index 8f3a242d..dfa3fe3f 100644
--- a/apps/example-app/src/app/examples/01-nested-component.spec.ts
+++ b/apps/example-app/src/app/examples/01-nested-component.spec.ts
@@ -1,11 +1,11 @@
-import { render, screen, fireEvent } from '@testing-library/angular';
+import { render, screen } from '@testing-library/angular';
+import userEvent from '@testing-library/user-event';
-import { NestedButtonComponent, NestedValueComponent, NestedContainerComponent } from './01-nested-component';
+import { NestedContainerComponent } from './01-nested-component';
test('renders the current value and can increment and decrement', async () => {
- await render(NestedContainerComponent, {
- declarations: [NestedButtonComponent, NestedValueComponent],
- });
+ const user = userEvent.setup();
+ await render(NestedContainerComponent);
const incrementControl = screen.getByRole('button', { name: /increment/i });
const decrementControl = screen.getByRole('button', { name: /decrement/i });
@@ -13,10 +13,10 @@ test('renders the current value and can increment and decrement', async () => {
expect(valueControl).toHaveTextContent('0');
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
expect(valueControl).toHaveTextContent('2');
- fireEvent.click(decrementControl);
+ await user.click(decrementControl);
expect(valueControl).toHaveTextContent('1');
});
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 51087b44..fd0d0c0e 100644
--- a/apps/example-app/src/app/examples/01-nested-component.ts
+++ b/apps/example-app/src/app/examples/01-nested-component.ts
@@ -1,7 +1,8 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
- selector: 'app-button',
+ standalone: true,
+ selector: 'atl-button',
template: ' ',
})
export class NestedButtonComponent {
@@ -10,7 +11,8 @@ export class NestedButtonComponent {
}
@Component({
- selector: 'app-value',
+ standalone: true,
+ selector: 'atl-value',
template: ' {{ value }} ',
})
export class NestedValueComponent {
@@ -18,12 +20,14 @@ export class NestedValueComponent {
}
@Component({
- selector: 'app-fixture',
+ standalone: true,
+ selector: 'atl-fixture',
template: `
-
-
-
+
+
+
`,
+ imports: [NestedButtonComponent, NestedValueComponent],
})
export class NestedContainerComponent {
value = 0;
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 f1bd4a1c..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
@@ -1,13 +1,73 @@
-import { render, screen, fireEvent } from '@testing-library/angular';
+import { render, screen } from '@testing-library/angular';
+import userEvent from '@testing-library/user-event';
import { InputOutputComponent } from './02-input-output';
test('is possible to set input and listen for output', async () => {
+ const user = userEvent.setup();
const sendValue = jest.fn();
await render(InputOutputComponent, {
- componentProperties: {
+ 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: {
sendValue: {
emit: sendValue,
} as any,
@@ -20,21 +80,22 @@ test('is possible to set input and listen for output', async () => {
expect(valueControl).toHaveTextContent('47');
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
expect(valueControl).toHaveTextContent('50');
- fireEvent.click(sendControl);
+ await user.click(sendControl);
expect(sendValue).toHaveBeenCalledTimes(1);
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('', {
- declarations: [InputOutputComponent],
+ await render('', {
+ imports: [InputOutputComponent],
componentProperties: {
sendValue: sendSpy,
},
@@ -46,12 +107,12 @@ test('is possible to set input and listen for output with the template syntax',
expect(valueControl).toHaveTextContent('47');
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
- fireEvent.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
+ await user.click(incrementControl);
expect(valueControl).toHaveTextContent('50');
- fireEvent.click(sendControl);
+ await user.click(sendControl);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(sendSpy).toHaveBeenCalledWith(50);
});
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 a7ef9ce4..3d7f9796 100644
--- a/apps/example-app/src/app/examples/02-input-output.ts
+++ b/apps/example-app/src/app/examples/02-input-output.ts
@@ -1,7 +1,8 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
- selector: 'app-fixture',
+ standalone: true,
+ selector: 'atl-fixture',
template: `
{{ value }}
diff --git a/apps/example-app/src/app/examples/03-forms.spec.ts b/apps/example-app/src/app/examples/03-forms.spec.ts
index 2a53a5f0..0e475834 100644
--- a/apps/example-app/src/app/examples/03-forms.spec.ts
+++ b/apps/example-app/src/app/examples/03-forms.spec.ts
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { FormsComponent } from './03-forms';
test('is possible to fill in a form and verify error messages (with the help of jest-dom https://testing-library.com/docs/ecosystem-jest-dom)', async () => {
+ const user = userEvent.setup();
await render(FormsComponent);
const nameControl = screen.getByRole('textbox', { name: /name/i });
@@ -16,19 +17,19 @@ test('is possible to fill in a form and verify error messages (with the help of
expect(errors).toContainElement(screen.queryByText('color is required'));
expect(nameControl).toBeInvalid();
- userEvent.type(nameControl, 'Tim');
- userEvent.clear(scoreControl);
- userEvent.type(scoreControl, '12');
+ await user.type(nameControl, 'Tim');
+ await user.clear(scoreControl);
+ await user.type(scoreControl, '12');
fireEvent.blur(scoreControl);
- userEvent.selectOptions(colorControl, 'G');
+ await user.selectOptions(colorControl, 'G');
expect(screen.queryByText('name is required')).not.toBeInTheDocument();
expect(screen.getByText('score must be lesser than 10')).toBeInTheDocument();
expect(screen.queryByText('color is required')).not.toBeInTheDocument();
expect(scoreControl).toBeInvalid();
- userEvent.clear(scoreControl);
- userEvent.type(scoreControl, '7');
+ await user.clear(scoreControl);
+ await user.type(scoreControl, '7');
fireEvent.blur(scoreControl);
expect(scoreControl).toBeValid();
diff --git a/apps/example-app/src/app/examples/03-forms.ts b/apps/example-app/src/app/examples/03-forms.ts
index df861a3c..c1e48c23 100644
--- a/apps/example-app/src/app/examples/03-forms.ts
+++ b/apps/example-app/src/app/examples/03-forms.ts
@@ -1,8 +1,11 @@
-import { Component } from '@angular/core';
-import { UntypedFormBuilder, Validators } from '@angular/forms';
+import { NgForOf, NgIf } from '@angular/common';
+import { Component, inject } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
- selector: 'app-fixture',
+ standalone: true,
+ 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
new file mode 100644
index 00000000..ffd5e95b
--- /dev/null
+++ b/projects/testing-library/tests/defer-blocks.spec.ts
@@ -0,0 +1,96 @@
+import { Component } from '@angular/core';
+import { DeferBlockBehavior, DeferBlockState } from '@angular/core/testing';
+import { render, screen, fireEvent } from '../src/public_api';
+
+test('renders a defer block in different states using the official API', async () => {
+ const { fixture } = await render(FixtureComponent);
+
+ const deferBlockFixture = (await fixture.getDeferBlocks())[0];
+
+ await deferBlockFixture.render(DeferBlockState.Loading);
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
+
+ await deferBlockFixture.render(DeferBlockState.Complete);
+ expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
+ expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
+});
+
+test('renders a defer block in different states using ATL', async () => {
+ const { renderDeferBlock } = await render(FixtureComponent);
+
+ await renderDeferBlock(DeferBlockState.Loading);
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
+
+ await renderDeferBlock(DeferBlockState.Complete, 0);
+ expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
+ expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
+});
+
+test('renders a defer block in different states using DeferBlockBehavior.Playthrough', async () => {
+ await render(FixtureComponent, {
+ deferBlockBehavior: DeferBlockBehavior.Playthrough,
+ });
+
+ expect(await screen.findByText(/Defer block content/i)).toBeInTheDocument();
+});
+
+test('renders a defer block in different states using DeferBlockBehavior.Playthrough event', async () => {
+ await render(FixtureComponentWithEventsComponent, {
+ deferBlockBehavior: DeferBlockBehavior.Playthrough,
+ });
+
+ const button = screen.getByRole('button', { name: /click/i });
+ fireEvent.click(button);
+
+ expect(screen.getByText(/empty defer block/i)).toBeInTheDocument();
+});
+
+test('renders a defer block initially in the loading state', async () => {
+ await render(FixtureComponent, {
+ deferBlockStates: DeferBlockState.Loading,
+ });
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
+});
+
+test('renders a defer block initially in the complete state', async () => {
+ await render(FixtureComponent, {
+ deferBlockStates: DeferBlockState.Complete,
+ });
+
+ expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
+ expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
+});
+
+test('renders a defer block in an initial state using the array syntax', async () => {
+ await render(FixtureComponent, {
+ deferBlockStates: [{ deferBlockState: DeferBlockState.Complete, deferBlockIndex: 0 }],
+ });
+
+ expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
+ expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
+});
+
+@Component({
+ template: `
+ @defer {
+ Defer block content
+ } @loading {
+ Loading...
+ }
+ `,
+})
+class FixtureComponent {}
+
+@Component({
+ template: `
+
+ @defer(on interaction(trigger)) {
+ empty defer block
+ }
+ `,
+})
+class FixtureComponentWithEventsComponent {}
diff --git a/projects/testing-library/tests/detect-changes.spec.ts b/projects/testing-library/tests/detect-changes.spec.ts
index 766bf31a..363cb402 100644
--- a/projects/testing-library/tests/detect-changes.spec.ts
+++ b/projects/testing-library/tests/detect-changes.spec.ts
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
-import { fakeAsync, tick } from '@angular/core/testing';
+import { fakeAsync } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { delay } from 'rxjs/operators';
import { render, fireEvent, screen } from '../src/public_api';
@@ -10,6 +10,8 @@ import { render, fireEvent, screen } from '../src/public_api';
`,
+ standalone: true,
+ imports: [ReactiveFormsModule],
})
class FixtureComponent implements OnInit {
inputControl = new FormControl();
@@ -22,7 +24,7 @@ class FixtureComponent implements OnInit {
describe('detectChanges', () => {
it('does not recognize change if execution is delayed', async () => {
- await render(FixtureComponent, { imports: [ReactiveFormsModule] });
+ await render(FixtureComponent);
fireEvent.input(screen.getByTestId('input'), {
target: {
@@ -33,9 +35,7 @@ describe('detectChanges', () => {
});
it('exposes detectChanges triggering a change detection cycle', fakeAsync(async () => {
- const { detectChanges } = await render(FixtureComponent, {
- imports: [ReactiveFormsModule],
- });
+ const { detectChanges } = await render(FixtureComponent);
fireEvent.input(screen.getByTestId('input'), {
target: {
@@ -43,14 +43,17 @@ describe('detectChanges', () => {
},
});
- tick(500);
+ // TODO: The code should be running in the fakeAsync zone to call this function ?
+ // tick(500);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
detectChanges();
expect(screen.getByTestId('button').innerHTML).toBe('Button updated after 400ms');
}));
it('does not throw on a destroyed fixture', async () => {
- const { fixture } = await render(FixtureComponent, { imports: [ReactiveFormsModule] });
+ const { fixture } = await render(FixtureComponent);
fixture.destroy();
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/fire-event.spec.ts b/projects/testing-library/tests/fire-event.spec.ts
index ebb85017..7b4a90bb 100644
--- a/projects/testing-library/tests/fire-event.spec.ts
+++ b/projects/testing-library/tests/fire-event.spec.ts
@@ -1,17 +1,47 @@
import { Component } from '@angular/core';
import { render, fireEvent, screen } from '../src/public_api';
+import { FormsModule } from '@angular/forms';
-@Component({
- selector: 'atl-fixture',
- template: ` `,
-})
-class FixtureComponent {}
+describe('fireEvent', () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: `
+ Hello {{ name }}
`,
+ standalone: true,
+ imports: [FormsModule],
+ })
+ class FixtureComponent {
+ name = '';
+ }
-test('does not call detect changes when fixture is destroyed', async () => {
- const { fixture } = await render(FixtureComponent);
+ it('automatically detect changes when event is fired', async () => {
+ await render(FixtureComponent);
- fixture.destroy();
+ fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } });
- // should otherwise throw
- fireEvent.input(screen.getByTestId('input'), { target: { value: 'Bonjour' } });
+ expect(screen.getByText('Hello Tim')).toBeInTheDocument();
+ });
+
+ it('can disable automatic detect changes when event is fired', async () => {
+ const { detectChanges } = await render(FixtureComponent, {
+ autoDetectChanges: false,
+ });
+
+ fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } });
+
+ expect(screen.queryByText('Hello Tim')).not.toBeInTheDocument();
+
+ detectChanges();
+
+ expect(screen.getByText('Hello Tim')).toBeInTheDocument();
+ });
+
+ it('does not call detect changes when fixture is destroyed', async () => {
+ const { fixture } = await render(FixtureComponent);
+
+ fixture.destroy();
+
+ // should otherwise throw
+ fireEvent.input(screen.getByTestId('input'), { target: { value: 'Bonjour' } });
+ });
});
diff --git a/projects/testing-library/tests/integration.spec.ts b/projects/testing-library/tests/integration.spec.ts
index 112177e1..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 userEvent from '@testing-library/user-event';
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,
@@ -86,11 +91,11 @@ const entities = [
},
];
-test('renders the table', async () => {
+async function setup() {
jest.useFakeTimers();
+ const user = userEvent.setup();
await render(EntitiesComponent, {
- declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
@@ -106,29 +111,50 @@ test('renders the table', async () => {
},
],
});
+
const modalMock = TestBed.inject(ModalService);
+ return {
+ modalMock,
+ user,
+ };
+}
+
+test('renders the heading', async () => {
+ await setup();
+
expect(await screen.findByRole('heading', { name: /Entities Title/i })).toBeInTheDocument();
+});
+
+test('renders the entities', async () => {
+ await setup();
expect(await screen.findByRole('cell', { name: /Entity 1/i })).toBeInTheDocument();
expect(await screen.findByRole('cell', { name: /Entity 2/i })).toBeInTheDocument();
expect(await screen.findByRole('cell', { name: /Entity 3/i })).toBeInTheDocument();
+});
- userEvent.type(await screen.findByRole('textbox', { name: /Search entities/i }), 'Entity 2', {});
+test.skip('finds the cell', async () => {
+ const { user } = await setup();
+
+ await user.type(await screen.findByRole('textbox', { name: /Search entities/i }), 'Entity 2', {});
jest.advanceTimersByTime(DEBOUNCE_TIME);
await waitForElementToBeRemoved(() => screen.queryByRole('cell', { name: /Entity 1/i }));
expect(await screen.findByRole('cell', { name: /Entity 2/i })).toBeInTheDocument();
+});
- userEvent.click(await screen.findByRole('button', { name: /New Entity/i }));
+test.skip('opens the modal', async () => {
+ const { modalMock, user } = await setup();
+ await user.click(await screen.findByRole('button', { name: /New Entity/i }));
expect(modalMock.open).toHaveBeenCalledWith('new entity');
const row = await screen.findByRole('row', {
name: /Entity 2/i,
});
- userEvent.click(
+ await user.click(
await within(row).findByRole('button', {
name: /edit/i,
}),
diff --git a/projects/testing-library/tests/integrations/ng-mocks.spec.ts b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
new file mode 100644
index 00000000..8886fb3f
--- /dev/null
+++ b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
@@ -0,0 +1,64 @@
+import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+import { MockComponent } from 'ng-mocks';
+import { render } from '../../src/public_api';
+import { NgIf } from '@angular/common';
+
+test('sends the correct value to the child input', async () => {
+ const utils = await render(TargetComponent, {
+ imports: [MockComponent(ChildComponent)],
+ inputs: { value: 'foo' },
+ });
+
+ const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
+ expect(children).toHaveLength(1);
+
+ const mockComponent = children[0].componentInstance;
+ expect(mockComponent.someInput).toBe('foo');
+});
+
+test('sends the correct value to the child input 2', async () => {
+ const utils = await render(TargetComponent, {
+ imports: [MockComponent(ChildComponent)],
+ inputs: { value: 'bar' },
+ });
+
+ const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
+ expect(children).toHaveLength(1);
+
+ const mockComponent = children[0].componentInstance;
+ expect(mockComponent.someInput).toBe('bar');
+});
+
+@Component({
+ selector: 'atl-child',
+ template: 'child',
+ standalone: true,
+ imports: [NgIf],
+})
+class ChildComponent {
+ @ContentChild('something')
+ public injectedSomething: TemplateRef | undefined;
+
+ @Input()
+ public someInput = '';
+
+ @Output()
+ public someOutput = new EventEmitter();
+
+ public childMockComponent() {
+ /* noop */
+ }
+}
+
+@Component({
+ selector: 'atl-target-mock-component',
+ template: ` `,
+ standalone: true,
+ imports: [ChildComponent],
+})
+class TargetComponent {
+ @Input() value = '';
+ public trigger = (obj: any) => obj;
+}
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
new file mode 100644
index 00000000..1cfe5b85
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-318.spec.ts
@@ -0,0 +1,40 @@
+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();
+
+ ngOnInit(): void {
+ this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => {
+ this.eventReceived(evt);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.unsubscribe$.next();
+ this.unsubscribe$.complete();
+ }
+
+ eventReceived(evt: any) {
+ console.log(evt);
+ }
+}
+
+test('it does not invoke router events on init', async () => {
+ const eventReceived = jest.fn();
+ await render(FixtureComponent, {
+ imports: [RouterTestingModule],
+ componentProperties: {
+ eventReceived,
+ },
+ });
+ expect(eventReceived).not.toHaveBeenCalled();
+});
diff --git a/projects/testing-library/tests/issues/issue-346.spec.ts b/projects/testing-library/tests/issues/issue-346.spec.ts
new file mode 100644
index 00000000..ef1b7a38
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-346.spec.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { render } from '../../src/public_api';
+
+test('issue 364 detectChangesOnRender', async () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: `{{ myObj.myProp }}`,
+ })
+ class MyComponent {
+ myObj: any = null;
+ }
+
+ // autoDetectChanges invokes change detection, which makes the test fail
+ await render(MyComponent, {
+ detectChangesOnRender: false,
+ });
+});
diff --git a/projects/testing-library/tests/issues/issue-386.spec.ts b/projects/testing-library/tests/issues/issue-386.spec.ts
new file mode 100644
index 00000000..b0c5613d
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-386.spec.ts
@@ -0,0 +1,35 @@
+import { Component } from '@angular/core';
+import { throwError } from 'rxjs';
+import { render, screen, fireEvent } from '../../src/public_api';
+
+@Component({
+ selector: 'atl-fixture',
+ template: ``,
+ styles: [],
+})
+class TestComponent {
+ onTest() {
+ throwError(() => new Error('myerror')).subscribe();
+ }
+}
+
+describe('TestComponent', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runAllTicks();
+ jest.useRealTimers();
+ });
+
+ it('does not fail', async () => {
+ await render(TestComponent);
+ fireEvent.click(screen.getByText('Test'));
+ });
+
+ it('fails because of the previous one', async () => {
+ await render(TestComponent);
+ fireEvent.click(screen.getByText('Test'));
+ });
+});
diff --git a/projects/testing-library/tests/issues/issue-389.spec.ts b/projects/testing-library/tests/issues/issue-389.spec.ts
new file mode 100644
index 00000000..626d3889
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-389.spec.ts
@@ -0,0 +1,15 @@
+import { Component, Input } from '@angular/core';
+import { render, screen } from '../../src/public_api';
+
+@Component({
+ selector: 'atl-fixture',
+ template: `Hello {{ name }}`,
+})
+class TestComponent {
+ @Input('aliasName') name = '';
+}
+
+test('allows you to set componentInputs using the name alias', async () => {
+ await render(TestComponent, { componentInputs: { aliasName: 'test' } });
+ expect(screen.getByText('Hello test')).toBeInTheDocument();
+});
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
new file mode 100644
index 00000000..7be9913e
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts
@@ -0,0 +1,55 @@
+import { Component } from '@angular/core';
+import { render, screen } from '../../src/public_api';
+
+test('stub', async () => {
+ await render(FixtureComponent, {
+ componentImports: [StubComponent],
+ });
+
+ expect(screen.getByText('Hello from stub')).toBeInTheDocument();
+});
+
+test('configure', async () => {
+ await render(FixtureComponent, {
+ configureTestBed: (testBed) => {
+ testBed.overrideComponent(FixtureComponent, {
+ add: {
+ imports: [StubComponent],
+ },
+ remove: {
+ imports: [ChildComponent],
+ },
+ });
+ },
+ });
+
+ expect(screen.getByText('Hello from stub')).toBeInTheDocument();
+});
+
+test('child', async () => {
+ await render(FixtureComponent);
+ expect(screen.getByText('Hello from child')).toBeInTheDocument();
+});
+
+@Component({
+ selector: 'atl-child',
+ template: `Hello from child`,
+ standalone: true,
+})
+class ChildComponent {}
+
+@Component({
+ selector: 'atl-child',
+ template: `Hello from stub`,
+ standalone: true,
+ host: { 'collision-id': StubComponent.name },
+})
+class StubComponent {}
+
+@Component({
+ selector: 'atl-fixture',
+ template: ``,
+ standalone: true,
+ imports: [ChildComponent],
+})
+class FixtureComponent {}
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
new file mode 100644
index 00000000..c34e1304
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
@@ -0,0 +1,68 @@
+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 () => {
+ await render(``, {
+ imports: [FixtureComponent, InputOverrideViaConstructorDirective],
+ });
+
+ expect(screen.getByText('set by test')).toBeInTheDocument();
+});
+
+test('the value set in the directive onInit is used instead of the input binding', async () => {
+ await render(``, {
+ imports: [FixtureComponent, InputOverrideViaOnInitDirective],
+ });
+
+ expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument();
+});
+
+test('the value set in the directive constructor is used instead of the input value', async () => {
+ await render(``, {
+ imports: [FixtureComponent, InputOverrideViaConstructorDirective],
+ });
+
+ expect(screen.getByText('set by directive constructor')).toBeInTheDocument();
+});
+
+test('the value set in the directive ngOnInit is used instead of the input value and the directive constructor', async () => {
+ await render(``, {
+ imports: [FixtureComponent, InputOverrideViaConstructorDirective, InputOverrideViaOnInitDirective],
+ });
+
+ expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument();
+});
+
+@Component({
+ standalone: true,
+ selector: 'atl-fixture',
+ template: `{{ input }}`,
+})
+class FixtureComponent {
+ @Input() public input = 'default value';
+}
+
+@Directive({
+ // eslint-disable-next-line @angular-eslint/directive-selector
+ selector: 'atl-fixture',
+ standalone: true,
+})
+class InputOverrideViaConstructorDirective {
+ private readonly fixture = inject(FixtureComponent);
+ constructor() {
+ this.fixture.input = 'set by directive constructor';
+ }
+}
+
+@Directive({
+ // eslint-disable-next-line @angular-eslint/directive-selector
+ selector: 'atl-fixture',
+ standalone: true,
+})
+class InputOverrideViaOnInitDirective implements OnInit {
+ 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
new file mode 100644
index 00000000..c775a2ab
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+import { render, screen } from '../../src/public_api';
+
+test('should create the app', async () => {
+ await render(FixtureComponent);
+ expect(screen.getByRole('heading')).toBeInTheDocument();
+});
+
+test('should re-create the app', async () => {
+ await render(FixtureComponent);
+ expect(screen.getByRole('heading')).toBeInTheDocument();
+});
+
+@Component({
+ selector: 'atl-fixture',
+ standalone: true,
+ template: 'My title
',
+ host: {
+ '[attr.id]': 'null', // this breaks the cleaning up of tests
+ },
+})
+class FixtureComponent {}
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
new file mode 100644
index 00000000..6dd5bc0c
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts
@@ -0,0 +1,29 @@
+import { Component, ElementRef, inject } from '@angular/core';
+import { NgIf } from '@angular/common';
+import { render } from '../../src/public_api';
+
+test('declaration specific dependencies should be available for components', async () => {
+ @Component({
+ selector: 'atl-test',
+ standalone: true,
+ template: `Test
`,
+ })
+ class TestComponent {
+ // @ts-expect-error - testing purpose
+ private _el = inject(ElementRef);
+ }
+
+ await expect(async () => await render(TestComponent)).not.toThrow();
+});
+
+test('standalone directives imported in standalone components', async () => {
+ @Component({
+ selector: 'atl-test',
+ standalone: true,
+ imports: [NgIf],
+ template: `Test
`,
+ })
+ class TestComponent {}
+
+ await render(TestComponent);
+});
diff --git a/projects/testing-library/tests/issues/issue-435.spec.ts b/projects/testing-library/tests/issues/issue-435.spec.ts
new file mode 100644
index 00000000..2982319b
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-435.spec.ts
@@ -0,0 +1,37 @@
+import { CommonModule } from '@angular/common';
+import { BehaviorSubject } from 'rxjs';
+import { Component, inject, Injectable } from '@angular/core';
+import { screen, render } from '../../src/public_api';
+
+// Service
+@Injectable()
+class DemoService {
+ buttonTitle = new BehaviorSubject('Click me');
+}
+
+// Component
+@Component({
+ selector: 'atl-issue-435',
+ standalone: true,
+ imports: [CommonModule],
+ providers: [DemoService],
+ template: `
+
+ `,
+})
+class DemoComponent {
+ protected readonly demoService = inject(DemoService);
+}
+
+test('issue #435', async () => {
+ await render(DemoComponent);
+
+ const button = screen.getByRole('button', {
+ name: /Click me/,
+ });
+
+ expect(button).toBeVisible();
+});
diff --git a/projects/testing-library/tests/issues/issue-437.spec.ts b/projects/testing-library/tests/issues/issue-437.spec.ts
new file mode 100644
index 00000000..dbf2506b
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-437.spec.ts
@@ -0,0 +1,56 @@
+import userEvent from '@testing-library/user-event';
+import { screen, render } from '../../src/public_api';
+import { MatSidenavModule } from '@angular/material/sidenav';
+
+afterEach(() => {
+ jest.useRealTimers();
+});
+
+test('issue #437', async () => {
+ const user = userEvent.setup();
+ await render(
+ `
+
+
+
+
+
+
+
+
+
+
+ `,
+ { imports: [MatSidenavModule] },
+ );
+
+ await screen.findByTestId('test-button');
+
+ await user.click(screen.getByTestId('test-button'));
+});
+
+test('issue #437 with fakeTimers', async () => {
+ jest.useFakeTimers();
+ const user = userEvent.setup({
+ advanceTimers: jest.advanceTimersByTime,
+ });
+ await render(
+ `
+
+
+
+
+
+
+
+
+
+
+ `,
+ { imports: [MatSidenavModule] },
+ );
+
+ 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 9dd20d01..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();
@@ -135,8 +135,8 @@ describe('removeAngularAttributes', () => {
});
test('updates properties and invokes change detection', async () => {
- const view = await render('', {
- declarations: [UpdateInputDirective],
+ const view = await render<{ value: string }>('', {
+ imports: [UpdateInputDirective],
componentProperties: {
value: 'value1',
},
diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts
index 8312bd4a..243a5e81 100644
--- a/projects/testing-library/tests/render.spec.ts
+++ b/projects/testing-library/tests/render.spec.ts
@@ -7,10 +7,21 @@ import {
SimpleChanges,
APP_INITIALIZER,
ApplicationInitStatus,
+ 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 } from '../src/public_api';
+import { render, fireEvent, screen, OutputRefKeysWithCallback, aliasedInput } from '../src/public_api';
+import { ActivatedRoute, Resolve, RouterModule } from '@angular/router';
+import { fromEvent, map } from 'rxjs';
+import { AsyncPipe, NgIf } from '@angular/common';
@Component({
selector: 'atl-fixture',
@@ -21,16 +32,112 @@ import { render, fireEvent } from '../src/public_api';
})
class FixtureComponent {}
-test('creates queries and events', async () => {
- const view = await render(FixtureComponent);
+describe('DTL functionality', () => {
+ it('creates queries and events', async () => {
+ const view = await render(FixtureComponent);
- /// 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
- expect(view.getByDisplayValue('a super awesome input')).toBeInTheDocument();
- // eslint-disable-next-line testing-library/prefer-screen-queries
- fireEvent.click(view.getByText('button'));
+ // 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
+ expect(view.getByDisplayValue('a super awesome input')).toBeInTheDocument();
+ // eslint-disable-next-line testing-library/prefer-screen-queries
+ fireEvent.click(view.getByText('button'));
+ });
+});
+
+describe('components', () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: ` {{ name }} `,
+ })
+ class FixtureWithInputComponent {
+ @Input() name = '';
+ }
+
+ it('renders component', async () => {
+ await render(FixtureWithInputComponent, { componentProperties: { name: 'Bob' } });
+ expect(screen.getByText('Bob')).toBeInTheDocument();
+ });
+});
+
+describe('component with child', () => {
+ @Component({
+ selector: 'atl-child-fixture',
+ template: `A child fixture`,
+ })
+ class ChildFixtureComponent {}
+
+ @Component({
+ selector: 'atl-child-fixture',
+ template: `A mock child fixture`,
+ host: { 'collision-id': MockChildFixtureComponent.name },
+ })
+ class MockChildFixtureComponent {}
+
+ @Component({
+ selector: 'atl-parent-fixture',
+ template: `Parent fixture
+ `,
+ imports: [ChildFixtureComponent],
+ })
+ class ParentFixtureComponent {}
+
+ 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 component with child', async () => {
+ await render(ParentFixtureComponent);
+ expect(screen.getByText('Parent fixture')).toBeInTheDocument();
+ expect(screen.getByText('A child fixture')).toBeInTheDocument();
+ });
+
+ it('rejects render of template with componentImports set', () => {
+ const view = render(``, {
+ imports: [ParentFixtureComponent],
+ componentImports: [MockChildFixtureComponent],
+ });
+ return expect(view).rejects.toMatchObject({ message: /Error while rendering/ });
+ });
+});
+
+describe('childComponentOverrides', () => {
+ @Injectable()
+ class MySimpleService {
+ public value = 'real';
+ }
+
+ @Component({
+ selector: 'atl-child-fixture',
+ template: `{{ simpleService.value }}`,
+ providers: [MySimpleService],
+ })
+ class NestedChildFixtureComponent {
+ protected simpleService = inject(MySimpleService);
+ }
+
+ @Component({
+ selector: 'atl-parent-fixture',
+ template: ``,
+ imports: [NestedChildFixtureComponent],
+ })
+ class ParentFixtureComponent {}
+
+ it('renders with overridden child service when specified', async () => {
+ await render(ParentFixtureComponent, {
+ childComponentOverrides: [
+ {
+ component: NestedChildFixtureComponent,
+ providers: [{ provide: MySimpleService, useValue: { value: 'fake' } }],
+ },
+ ],
+ });
+
+ expect(screen.getByText('fake')).toBeInTheDocument();
+ });
});
describe('removeAngularAttributes', () => {
@@ -53,35 +160,173 @@ describe('removeAngularAttributes', () => {
});
});
-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('componentOutputs', () => {
+ it('should set passed event emitter to the component', async () => {
+ @Component({ template: `` })
+ class TestFixtureComponent {
+ @Output() event = new EventEmitter();
+ emitEvent() {
+ this.event.emit();
+ }
+ }
+
+ const mockEmitter = new EventEmitter();
+ const spy = jest.spyOn(mockEmitter, 'emit');
+ const { fixture } = await render(TestFixtureComponent, {
+ componentOutputs: { event: mockEmitter },
});
+
+ fixture.componentInstance.emitEvent();
+
+ expect(spy).toHaveBeenCalled();
+ });
+});
+
+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('adds NoopAnimationsModule by default', async () => {
- await render(FixtureComponent);
- const noopAnimationsModule = TestBed.inject(NoopAnimationsModule);
- expect(noopAnimationsModule).toBeDefined();
+ 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('does not add NoopAnimationsModule if BrowserAnimationsModule is an import', async () => {
- await render(FixtureComponent, {
- imports: [BrowserAnimationsModule],
+ 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('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 newSpy = jest.fn();
+ await rerender({ on: { event: newSpy } });
+
+ 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 {}
- const browserAnimationsModule = TestBed.inject(BrowserAnimationsModule);
- expect(browserAnimationsModule).toBeDefined();
+ @NgModule({
+ declarations: [NotStandaloneFixtureComponent],
+ })
+ class FixtureModule {}
- expect(() => TestBed.inject(NoopAnimationsModule)).toThrow();
+ it('does not throw if component is declared in an imported module', async () => {
+ await render(NotStandaloneFixtureComponent, {
+ imports: [FixtureModule],
+ excludeComponentDeclaration: true,
+ });
});
});
@@ -102,13 +347,13 @@ describe('Angular component life-cycle hooks', () => {
}
ngOnChanges(changes: SimpleChanges) {
- if (changes.name && this.nameChanged) {
- this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
+ if (this.nameChanged) {
+ this.nameChanged(changes.name?.currentValue, changes.name?.isFirstChange());
}
}
}
- it('will call ngOnInit on initial render', async () => {
+ it('invokes ngOnInit on initial render', async () => {
const nameInitialized = jest.fn();
const componentProperties = { nameInitialized };
const view = await render(FixtureWithNgOnChangesComponent, { componentProperties });
@@ -119,35 +364,270 @@ describe('Angular component life-cycle hooks', () => {
expect(nameInitialized).toHaveBeenCalledWith('Initial');
});
- it('will call ngOnChanges on initial render before ngOnInit', async () => {
+ it('invokes ngOnChanges with componentProperties on initial render before ngOnInit', async () => {
const nameInitialized = jest.fn();
const nameChanged = jest.fn();
const componentProperties = { nameInitialized, nameChanged, name: 'Sarah' };
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.mock.invocationCallOrder[0]).toBeLessThan(nameInitialized.mock.invocationCallOrder[0]);
+ expect(nameChanged).toHaveBeenCalledTimes(1);
+ });
+
+ it('invokes ngOnChanges with componentInputs on initial render before ngOnInit', async () => {
+ const nameInitialized = jest.fn();
+ const nameChanged = jest.fn();
+ const componentInput = { nameInitialized, nameChanged, name: 'Sarah' };
+
+ const view = await render(FixtureWithNgOnChangesComponent, { componentInputs: componentInput });
+
+ // 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);
+ });
+
+ it('does not invoke ngOnChanges when no properties are provided', async () => {
+ @Component({ template: `` })
+ class TestFixtureComponent implements OnChanges {
+ ngOnChanges() {
+ throw new Error('should not be called');
+ }
+ }
+
+ const { fixture, detectChanges } = await render(TestFixtureComponent);
+ const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges');
+
+ detectChanges();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+});
+
+describe('initializer', () => {
+ it('waits for angular app initialization before rendering components', async () => {
+ const mock = jest.fn();
+
+ await render(FixtureComponent, {
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ useFactory: () => mock,
+ multi: true,
+ },
+ ],
+ });
+
+ expect(TestBed.inject(ApplicationInitStatus).done).toBe(true);
+ expect(mock).toHaveBeenCalled();
+ });
+});
+
+describe('DebugElement', () => {
+ it('gets the DebugElement', async () => {
+ const view = await render(FixtureComponent);
+
+ expect(view.debugElement).not.toBeNull();
+ expect(view.debugElement.componentInstance).toBeInstanceOf(FixtureComponent);
+ });
+});
+
+describe('initialRoute', () => {
+ @Component({
+ selector: 'atl-fixture2',
+ template: ``,
+ })
+ class SecondaryFixtureComponent {}
+
+ @Component({
+ selector: 'atl-router-fixture',
+ template: ``,
+ imports: [RouterModule],
+ })
+ class RouterFixtureComponent {}
+
+ @Injectable()
+ class FixtureResolver implements Resolve {
+ public isResolved = false;
+
+ public resolve() {
+ this.isResolved = true;
+ }
+ }
+
+ it('allows initially rendering a specific route to avoid triggering a resolver for the default route', async () => {
+ const initialRoute = 'initial-route';
+ const routes = [
+ { path: initialRoute, component: FixtureComponent },
+ { path: '**', resolve: { data: FixtureResolver }, component: SecondaryFixtureComponent },
+ ];
+
+ await render(RouterFixtureComponent, {
+ initialRoute,
+ routes,
+ providers: [FixtureResolver],
+ });
+ const resolver = TestBed.inject(FixtureResolver);
+
+ expect(resolver.isResolved).toBe(false);
+ expect(screen.queryByText('Secondary Component')).not.toBeInTheDocument();
+ expect(screen.getByText('button')).toBeInTheDocument();
+ });
+
+ it('allows initially rendering a specific route with query parameters', async () => {
+ @Component({
+ selector: 'atl-query-param-fixture',
+ template: `paramPresent$: {{ paramPresent$ | async }}
`,
+ imports: [NgIf, AsyncPipe],
+ })
+ class QueryParamFixtureComponent {
+ private readonly route = inject(ActivatedRoute);
+
+ paramPresent$ = this.route.queryParams.pipe(map((queryParams) => (queryParams?.param ? 'present' : 'missing')));
+ }
+
+ const initialRoute = 'initial-route?param=query';
+ const routes = [{ path: 'initial-route', component: QueryParamFixtureComponent }];
+
+ await render(RouterFixtureComponent, {
+ initialRoute,
+ routes,
+ });
+
+ expect(screen.getByText(/present/i)).toBeVisible();
});
});
-test('waits for angular app initialization before rendering components', async () => {
- const mock = jest.fn();
+describe('configureTestBed', () => {
+ it('invokes configureTestBed', async () => {
+ const configureTestBedFn = jest.fn();
+ await render(FixtureComponent, {
+ configureTestBed: configureTestBedFn,
+ });
+
+ 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
- await render(FixtureComponent, {
- providers: [
- {
- provide: APP_INITIALIZER,
- useFactory: () => mock,
- multi: true,
+ 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(TestBed.inject(ApplicationInitStatus).done).toEqual(true);
- expect(mock).toHaveBeenCalled();
+ 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 0edf69ea..04b8185a 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -1,26 +1,118 @@
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { render, screen } from '../src/public_api';
+let ngOnChangesSpy: jest.Mock;
@Component({
selector: 'atl-fixture',
template: ` {{ firstName }} {{ lastName }} `,
})
-class FixtureComponent {
+class FixtureComponent implements OnChanges {
@Input() firstName = 'Sarah';
@Input() lastName?: string;
+ ngOnChanges(changes: SimpleChanges): void {
+ ngOnChangesSpy(changes);
+ }
}
+beforeEach(() => {
+ ngOnChangesSpy = jest.fn();
+});
+
test('rerenders the component with updated props', async () => {
const { rerender } = await render(FixtureComponent);
expect(screen.getByText('Sarah')).toBeInTheDocument();
const firstName = 'Mark';
- await rerender({ firstName });
+ await rerender({ componentProperties: { firstName } });
expect(screen.getByText(firstName)).toBeInTheDocument();
});
-test('rerenders the component with updated props and resets other props', async () => {
+test('rerenders without props', async () => {
+ const { rerender } = await render(FixtureComponent);
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ await rerender();
+
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); // one time initially and one time for rerender
+});
+
+test('rerenders the component with updated inputs', async () => {
+ const { rerender } = await render(FixtureComponent);
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ const firstName = 'Mark';
+ await rerender({ inputs: { firstName } });
+
+ expect(screen.getByText(firstName)).toBeInTheDocument();
+});
+
+test('rerenders the component with updated inputs and resets other props', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { rerender } = await render(FixtureComponent, {
+ inputs: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+ const firstName2 = 'Chris';
+ await rerender({ inputs: { firstName: firstName2 } });
+
+ expect(screen.getByText(firstName2)).toBeInTheDocument();
+ expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+ expect(screen.queryByText(lastName)).not.toBeInTheDocument();
+
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+ const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+ expect(rerenderedChanges).toEqual({
+ lastName: {
+ previousValue: 'Peeters',
+ currentValue: undefined,
+ firstChange: false,
+ },
+ firstName: {
+ previousValue: 'Mark',
+ currentValue: 'Chris',
+ firstChange: false,
+ },
+ });
+});
+
+test('rerenders the component with updated inputs and keeps other props when partial is true', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { rerender } = await render(FixtureComponent, {
+ inputs: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+ const firstName2 = 'Chris';
+ await rerender({ inputs: { firstName: firstName2 }, partialUpdate: true });
+
+ expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+ expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
+
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+ const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+ expect(rerenderedChanges).toEqual({
+ firstName: {
+ previousValue: 'Mark',
+ currentValue: 'Chris',
+ firstChange: false,
+ },
+ });
+});
+
+test('rerenders the component with updated props and resets other props with componentProperties', async () => {
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
@@ -33,8 +125,64 @@ test('rerenders the component with updated props and resets other props', async
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
const firstName2 = 'Chris';
- rerender({ firstName: firstName2 });
+ await rerender({ componentProperties: { firstName: firstName2 } });
- expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument();
- expect(screen.queryByText(firstName2)).not.toBeInTheDocument();
+ expect(screen.getByText(firstName2)).toBeInTheDocument();
+ expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+ expect(screen.queryByText(lastName)).not.toBeInTheDocument();
+
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+ const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+ expect(rerenderedChanges).toEqual({
+ lastName: {
+ previousValue: 'Peeters',
+ currentValue: undefined,
+ firstChange: false,
+ },
+ firstName: {
+ previousValue: 'Mark',
+ currentValue: 'Chris',
+ firstChange: false,
+ },
+ });
+});
+
+test('rerenders the component with updated props keeps other props when partial is true', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { rerender } = await render(FixtureComponent, {
+ componentProperties: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+ const firstName2 = 'Chris';
+ await rerender({ componentProperties: { firstName: firstName2 }, partialUpdate: true });
+
+ expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+ expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
+
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+ const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+ expect(rerenderedChanges).toEqual({
+ firstName: {
+ previousValue: 'Mark',
+ currentValue: 'Chris',
+ firstChange: false,
+ },
+ });
+});
+
+test('change detection gets not called if `detectChangesOnRender` is set to false', async () => {
+ const { rerender } = await render(FixtureComponent);
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ const firstName = 'Mark';
+ 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 1f2f8eae..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;
@@ -16,7 +18,7 @@ class FixtureComponent implements OnInit {
test('waits for element to be removed (callback)', async () => {
await render(FixtureComponent);
- await waitForElementToBeRemoved(() => screen.getByTestId('im-here'));
+ await waitForElementToBeRemoved(() => screen.queryByTestId('im-here'));
expect(screen.queryByTestId('im-here')).not.toBeInTheDocument();
});
@@ -24,7 +26,7 @@ test('waits for element to be removed (callback)', async () => {
test('waits for element to be removed (element)', async () => {
await render(FixtureComponent);
- await waitForElementToBeRemoved(screen.getByTestId('im-here'));
+ await waitForElementToBeRemoved(screen.queryByTestId('im-here'));
expect(screen.queryByTestId('im-here')).not.toBeInTheDocument();
});
@@ -32,7 +34,7 @@ test('waits for element to be removed (element)', async () => {
test('allows to override options', async () => {
await render(FixtureComponent);
- await expect(waitForElementToBeRemoved(() => screen.getByTestId('im-here'), { timeout: 200 })).rejects.toThrow(
+ await expect(waitForElementToBeRemoved(() => screen.queryByTestId('im-here'), { timeout: 200 })).rejects.toThrow(
/Timed out in waitForElementToBeRemoved/i,
);
});
diff --git a/projects/testing-library/tests/wait-for.spec.ts b/projects/testing-library/tests/wait-for.spec.ts
index e963b0c4..8c6562f0 100644
--- a/projects/testing-library/tests/wait-for.spec.ts
+++ b/projects/testing-library/tests/wait-for.spec.ts
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { timer } from 'rxjs';
-import { render, screen, fireEvent, waitFor } from '../src/public_api';
+import { render, screen, waitFor, fireEvent } from '../src/public_api';
@Component({
selector: 'atl-fixture',
@@ -24,8 +24,7 @@ test('waits for assertion to become true', async () => {
fireEvent.click(screen.getByTestId('button'));
- await screen.findByText('Success');
- expect(screen.getByText('Success')).toBeInTheDocument();
+ expect(await screen.findByText('Success')).toBeInTheDocument();
});
test('allows to override options', async () => {
diff --git a/projects/testing-library/tsconfig.json b/projects/testing-library/tsconfig.json
index 24663f6f..21a2b8ef 100644
--- a/projects/testing-library/tsconfig.json
+++ b/projects/testing-library/tsconfig.json
@@ -13,7 +13,9 @@
"path": "./tsconfig.spec.json"
}
],
- "compilerOptions": {},
+ "compilerOptions": {
+ "target": "es2020"
+ },
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
diff --git a/projects/testing-library/tsconfig.lib.json b/projects/testing-library/tsconfig.lib.json
index 7d77d4c9..0938741e 100644
--- a/projects/testing-library/tsconfig.lib.json
+++ b/projects/testing-library/tsconfig.lib.json
@@ -5,8 +5,10 @@
"declaration": true,
"declarationMap": true,
"inlineSources": true,
- "types": ["node", "jest"]
+ "types": ["node", "jest"],
+ "target": "ES2022",
+ "useDefineForClassFields": false
},
- "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts"],
+ "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
"include": ["**/*.ts"]
}
diff --git a/projects/testing-library/tsconfig.lib.prod.json b/projects/testing-library/tsconfig.lib.prod.json
index 2a2faa88..752ed5ea 100644
--- a/projects/testing-library/tsconfig.lib.prod.json
+++ b/projects/testing-library/tsconfig.lib.prod.json
@@ -1,9 +1,12 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
- "declarationMap": false
+ "declarationMap": false,
+ "target": "ES2022",
+ "useDefineForClassFields": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
- }
+ },
+ "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"]
}
diff --git a/projects/testing-library/tsconfig.schematics.json b/projects/testing-library/tsconfig.schematics.json
index 0573a52a..c0118513 100644
--- a/projects/testing-library/tsconfig.schematics.json
+++ b/projects/testing-library/tsconfig.schematics.json
@@ -2,16 +2,17 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true,
- "target": "es6",
+ "target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
- "outDir": "../../dist/@testing-library/angular/schematics/ng-add",
+ "outDir": "../../dist/@testing-library/angular/schematics",
"removeComments": true,
"skipLibCheck": true,
"sourceMap": false
},
- "include": ["schematics/**/*.ts"]
+ "include": ["schematics/**/*.ts"],
+ "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"]
}
diff --git a/projects/testing-library/tsconfig.spec.json b/projects/testing-library/tsconfig.spec.json
index 73461f35..9fee53b3 100644
--- a/projects/testing-library/tsconfig.spec.json
+++ b/projects/testing-library/tsconfig.spec.json
@@ -5,5 +5,5 @@
"types": ["node", "jest", "@testing-library/jest-dom"]
},
"files": ["test-setup.ts"],
- "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"]
+ "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
}
diff --git a/projects/vscode-atl-render/package.json b/projects/vscode-atl-render/package.json
index a6462d92..d5bcbe7a 100644
--- a/projects/vscode-atl-render/package.json
+++ b/projects/vscode-atl-render/package.json
@@ -3,7 +3,7 @@
"displayName": "Angular Testing Library Render Highlighting",
"description": "HTML highlighting in ATL the render method",
"version": "0.0.3",
- "icon": "other/hedgehog.png",
+ "icon": "other/logo.png",
"publisher": "timdeschryver",
"license": "MIT",
"repository": {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 1e0de6e1..b75283e1 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -11,7 +11,7 @@
"moduleResolution": "node",
"outDir": "./dist/out-tsc",
"sourceMap": true,
- "target": "es2015",
+ "target": "ES2020",
"typeRoots": ["node_modules/@types"],
"strict": true,
"exactOptionalPropertyTypes": true,
@@ -23,7 +23,7 @@
"noUnusedParameters": true,
"paths": {
"@testing-library/angular": ["projects/testing-library"],
- "@testing-library/angular/jest-utils": ["projects/jest-utils"]
+ "@testing-library/angular/jest-utils": ["projects/testing-library/jest-utils"]
}
},
"exclude": ["node_modules", "tmp"]