diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5a97feac..c60fd019 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -195,7 +195,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/4863062?v=4",
"profile": "https://github.com/the-ult",
"contributions": [
- "code"
+ "code",
+ "maintenance"
]
},
{
@@ -254,6 +255,217 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "MatanBobi",
+ "name": "Matan Borenkraout",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12711091?v=4",
+ "profile": "https://matan.io",
+ "contributions": [
+ "maintenance"
+ ]
+ },
+ {
+ "login": "mleimer",
+ "name": "mleimer",
+ "avatar_url": "https://avatars.githubusercontent.com/u/14271564?v=4",
+ "profile": "https://github.com/mleimer",
+ "contributions": [
+ "doc",
+ "test"
+ ]
+ },
+ {
+ "login": "meirka",
+ "name": "MeIr",
+ "avatar_url": "https://avatars.githubusercontent.com/u/750901?v=4",
+ "profile": "https://github.com/meirka",
+ "contributions": [
+ "bug",
+ "test"
+ ]
+ },
+ {
+ "login": "jadengis",
+ "name": "John Dengis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13421336?v=4",
+ "profile": "https://github.com/jadengis",
+ "contributions": [
+ "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,
@@ -261,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
new file mode 100755
index 00000000..f0f7d343
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+npm run pre-commit
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e85298ba..b35c5abb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,33 +4,46 @@ on:
push:
branches:
- 'main'
+ - 'beta'
pull_request: {}
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
build_test_release:
+ permissions:
+ actions: write
+ contents: write
+
strategy:
matrix:
- node-version: ${{ fromJSON(github.ref == 'refs/heads/main' && '[16]' || '[12,14,16]') }}
- os: ${{ fromJSON(github.ref == 'refs/heads/main' && '["ubuntu-latest"]' || '["ubuntu-latest", "windows-latest"]') }}
+ 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@v2
+ - uses: actions/checkout@v4
- name: use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
- uses: actions/setup-node@v2
+ 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'
+ if: github.repository == 'testing-library/angular-testing-library' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta')
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CI: true
- HUSKY: 0
diff --git a/.gitignore b/.gitignore
index 82985ca5..22faaca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,11 @@
!.vscode/extensions.json
# misc
+/.angular/cache
+.angular
+.nx
+migrations.json
+.cache
/.sass-cache
/connect.lock
/coverage
@@ -39,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/.husky/.gitignore b/.husky/.gitignore
deleted file mode 100644
index 31354ec1..00000000
--- a/.husky/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_
diff --git a/.husky/pre-commit b/.husky/pre-commit
deleted file mode 100644
index d0612ad3..00000000
--- a/.husky/pre-commit
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "0ドル")/_/husky.sh"
-
-npm run pre-commit
diff --git a/.node-version b/.node-version
new file mode 100644
index 00000000..8fdd954d
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+22
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
index d2722898..1df2a6d8 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,2 +1,2 @@
-registry=http://registry.npmjs.org/
+registry=https://registry.npmjs.org/
package-lock=false
diff --git a/.nxignore b/.nxignore
new file mode 100644
index 00000000..aeb6b7f1
--- /dev/null
+++ b/.nxignore
@@ -0,0 +1 @@
+/projects/vscode-atl-render
diff --git a/.prettierignore b/.prettierignore
index 2cbf2c26..03ff48d9 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -35,6 +35,8 @@ CHANGELOG.md
!.vscode/extensions.json
# misc
+.cache
+.angular
/.sass-cache
/connect.lock
/coverage
@@ -51,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 029f501a..848aed06 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,12 @@
@testing-library/angular
-
- hedgehog
-
+[
画像:Octopus with the Angular logo]
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,40 +217,70 @@ Thanks goes to these people ([emoji key][emojis]):
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -234,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`.
@@ -264,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 45bf7eca..00000000
--- a/angular.json
+++ /dev/null
@@ -1,292 +0,0 @@
-{
- "version": 1,
- "cli": {
- "analytics": false
- },
- "defaultProject": "example-app",
- "projects": {
- "example-app": {
- "projectType": "application",
- "root": "apps/example-app",
- "sourceRoot": "apps/example-app/src",
- "prefix": "app",
- "schematics": {},
- "architect": {
- "build": {
- "builder": "@angular-devkit/build-angular:browser",
- "options": {
- "outputPath": "dist/apps/example-app",
- "index": "apps/example-app/src/index.html",
- "main": "apps/example-app/src/main.ts",
- "polyfills": "apps/example-app/src/polyfills.ts",
- "tsConfig": "apps/example-app/tsconfig.app.json",
- "assets": ["apps/example-app/src/favicon.ico", "apps/example-app/src/assets"],
- "styles": ["apps/example-app/src/styles.css"],
- "scripts": [],
- "vendorChunk": true,
- "extractLicenses": false,
- "buildOptimizer": false,
- "sourceMap": true,
- "optimization": false,
- "namedChunks": true
- },
- "configurations": {
- "production": {
- "budgets": [
- {
- "type": "anyComponentStyle",
- "maximumWarning": "6kb"
- }
- ],
- "fileReplacements": [
- {
- "replace": "apps/example-app/src/environments/environment.ts",
- "with": "apps/example-app/src/environments/environment.prod.ts"
- }
- ],
- "optimization": true,
- "outputHashing": "all",
- "sourceMap": false,
- "namedChunks": false,
- "extractLicenses": true,
- "vendorChunk": false,
- "buildOptimizer": true
- }
- },
- "outputs": ["{options.outputPath}"]
- },
- "serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
- "options": {
- "browserTarget": "example-app:build"
- },
- "configurations": {
- "production": {
- "browserTarget": "example-app:build:production"
- }
- }
- },
- "extract-i18n": {
- "builder": "@angular-devkit/build-angular:extract-i18n",
- "options": {
- "browserTarget": "example-app:build"
- }
- },
- "lint": {
- "builder": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": [
- "apps/example-app/**/*.ts",
- "apps/example-app/**/*.html",
- "apps/example-app/src/**/*.html",
- "apps/example-app/src/**/*.html",
- "apps/example-app/src/**/*.html"
- ]
- }
- },
- "test": {
- "builder": "@nrwl/jest:jest",
- "options": {
- "jestConfig": "apps/example-app/jest.config.js"
- },
- "outputs": ["coverage/"]
- }
- }
- },
- "example-app-karma": {
- "projectType": "application",
- "root": "apps/example-app-karma",
- "sourceRoot": "apps/example-app-karma/src",
- "prefix": "app",
- "schematics": {},
- "architect": {
- "build": {
- "builder": "@angular-devkit/build-angular:browser",
- "options": {
- "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": [],
- "scripts": [],
- "vendorChunk": true,
- "extractLicenses": false,
- "buildOptimizer": false,
- "sourceMap": true,
- "optimization": false,
- "namedChunks": true
- },
- "configurations": {
- "production": {
- "budgets": [
- {
- "type": "anyComponentStyle",
- "maximumWarning": "6kb"
- }
- ],
- "fileReplacements": [
- {
- "replace": "apps/example-app-karma/src/environments/environment.ts",
- "with": "apps/example-app-karma/src/environments/environment.prod.ts"
- }
- ],
- "optimization": true,
- "outputHashing": "all",
- "sourceMap": false,
- "namedChunks": false,
- "extractLicenses": true,
- "vendorChunk": false,
- "buildOptimizer": true
- }
- },
- "outputs": ["{options.outputPath}"]
- },
- "serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
- "options": {
- "browserTarget": "example-app-karma:build"
- },
- "configurations": {
- "production": {
- "browserTarget": "example-app-karma:build:production"
- }
- }
- },
- "lint": {
- "builder": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": [
- "apps/example-app-karma/**/*.ts",
- "apps/example-app-karma/**/*.html",
- "apps/example-app-karma/src/**/*.html",
- "apps/example-app-karma/src/**/*.html",
- "apps/example-app-karma/src/**/*.html"
- ]
- }
- },
- "test": {
- "builder": "@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",
- "styles": [],
- "scripts": [],
- "assets": []
- }
- }
- }
- },
- "testing-library": {
- "root": "projects/testing-library",
- "sourceRoot": "projects/testing-library/src",
- "projectType": "library",
- "prefix": "lib",
- "architect": {
- "build-package": {
- "builder": "@angular-devkit/build-angular:ng-packagr",
- "options": {
- "tsConfig": "projects/testing-library/tsconfig.lib.json",
- "project": "projects/testing-library/ng-package.json"
- },
- "configurations": {
- "production": {
- "project": "projects/testing-library/ng-package.json",
- "tsConfig": "projects/testing-library/tsconfig.lib.json"
- }
- }
- },
- "lint": {
- "builder": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": [
- "projects/testing-library/**/*.ts",
- "projects/testing-library/**/*.html",
- "projects/testing-library/src/**/*.html",
- "projects/testing-library/src/**/*.html",
- "projects/testing-library/src/**/*.html"
- ]
- }
- },
- "build": {
- "builder": "@nrwl/workspace:run-commands",
- "options": {
- "parallel": false,
- "commands": [
- {
- "command": "ng run testing-library:build-package"
- },
- {
- "command": "npm run build:schematics"
- },
- {
- "command": "cpy ./README.md ./dist/@testing-library/angular"
- }
- ]
- }
- },
- "test": {
- "builder": "@nrwl/jest:jest",
- "options": {
- "jestConfig": "projects/testing-library/jest.config.js"
- },
- "outputs": ["coverage/projects/testing-library"]
- }
- }
- },
- "jest-utils": {
- "root": "projects/jest-utils",
- "sourceRoot": "projects/jest-utils/src",
- "projectType": "library",
- "prefix": "lib",
- "architect": {
- "build-package": {
- "builder": "@angular-devkit/build-angular:ng-packagr",
- "options": {
- "tsConfig": "projects/jest-utils/tsconfig.lib.json",
- "project": "projects/jest-utils/ng-package.json"
- },
- "configurations": {
- "production": {
- "project": "projects/jest-utils/ng-package.json",
- "tsConfig": "projects/jest-utils/tsconfig.lib.json"
- }
- }
- },
- "lint": {
- "builder": "@nrwl/linter:eslint",
- "options": {
- "lintFilePatterns": [
- "projects/jest-utils/**/*.ts",
- "projects/jest-utils/**/*.html",
- "projects/jest-utils/src/**/*.html",
- "projects/jest-utils/src/**/*.html",
- "projects/jest-utils/src/**/*.html"
- ]
- }
- },
- "build": {
- "builder": "@nrwl/workspace:run-commands",
- "options": {
- "parallel": false,
- "commands": [
- {
- "command": "ng run jest-utils:build-package"
- }
- ]
- }
- },
- "test": {
- "builder": "@nrwl/jest:jest",
- "options": {
- "jestConfig": "projects/jest-utils/jest.config.js"
- },
- "outputs": ["coverage/projects/jest-utils"]
- }
- }
- }
- }
-}
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
new file mode 100644
index 00000000..54d79038
--- /dev/null
+++ b/apps/example-app-karma/jasmine-dom.d.ts
@@ -0,0 +1,4 @@
+declare module '@testing-library/jasmine-dom' {
+ const JasmineDOM: any;
+ export default JasmineDOM;
+}
diff --git a/apps/example-app-karma/project.json b/apps/example-app-karma/project.json
new file mode 100644
index 00000000..27c4cbd4
--- /dev/null
+++ b/apps/example-app-karma/project.json
@@ -0,0 +1,68 @@
+{
+ "name": "example-app-karma",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/example-app-karma/src",
+ "prefix": "app",
+ "tags": [],
+ "generators": {},
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/example-app-karma",
+ "index": "apps/example-app-karma/src/index.html",
+ "main": "apps/example-app-karma/src/main.ts",
+ "tsConfig": "apps/example-app-karma/tsconfig.app.json",
+ "assets": ["apps/example-app-karma/src/favicon.ico", "apps/example-app-karma/src/assets"],
+ "styles": [],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "6kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "example-app-karma:build:production"
+ },
+ "development": {
+ "buildTarget": "example-app-karma:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "lint": {
+ "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",
+ "karmaConfig": "apps/example-app-karma/karma.conf.js"
+ }
+ }
+ }
+}
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 740d8184..00000000
--- a/apps/example-app-karma/src/app/issues/issue-222.spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { render, screen } from '@testing-library/angular';
-
-it('https://github.com/testing-library/angular-testing-library/issues/222', async () => {
- const { rerender } = await render(`Hello {{ name}}
`, {
- componentProperties: {
- name: 'Sarah',
- },
- });
-
- expect(screen.getByText('Hello Sarah')).toBeTruthy();
- rerender({ 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/jasmine-matchers.spec.ts b/apps/example-app-karma/src/app/issues/jasmine-matchers.spec.ts
new file mode 100644
index 00000000..0f6e3fd2
--- /dev/null
+++ b/apps/example-app-karma/src/app/issues/jasmine-matchers.spec.ts
@@ -0,0 +1,11 @@
+import { render, screen } from '@testing-library/angular';
+
+it('can use jasmine matchers', async () => {
+ await render(`Hello {{ name}}
`, {
+ componentProperties: {
+ name: 'Sarah',
+ },
+ });
+
+ expect(screen.getByText('Hello Sarah')).toBeVisible();
+});
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 85db3caf..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 `angular.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 bb20fec0..00000000
--- a/apps/example-app-karma/src/polyfills.ts
+++ /dev/null
@@ -1,75 +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 NgClass support on SVG elements */
-// import 'classlist.js'; // Run `npm install --save classlist.js`.
-
-/** 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.
-
-/**
- * Web Animations `@angular/platform-browser/animations`
- * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
- * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
- **/
-// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
-
-/**
- * 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 bd5e2db8..e6bf956d 100644
--- a/apps/example-app-karma/src/test.ts
+++ b/apps/example-app-karma/src/test.ts
@@ -1,13 +1,13 @@
-// 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';
-declare const require: any;
+// Install custom matchers from jasmine-dom
+beforeEach(() => {
+ jasmine.addMatchers(JasmineDOM);
+});
// 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);
+getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {});
diff --git a/apps/example-app-karma/tsconfig.app.json b/apps/example-app-karma/tsconfig.app.json
index 629fd434..46150c25 100644
--- a/apps/example-app-karma/tsconfig.app.json
+++ b/apps/example-app-karma/tsconfig.app.json
@@ -3,7 +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 e1d5ba47..9453a196 100644
--- a/apps/example-app-karma/tsconfig.json
+++ b/apps/example-app-karma/tsconfig.json
@@ -1,8 +1,10 @@
{
- "extends": "../../tsconfig.json",
+ "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
new file mode 100644
index 00000000..1cf90ac4
--- /dev/null
+++ b/apps/example-app/project.json
@@ -0,0 +1,75 @@
+{
+ "name": "example-app",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/example-app/src",
+ "prefix": "app",
+ "tags": [],
+ "generators": {},
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/example-app",
+ "index": "apps/example-app/src/index.html",
+ "main": "apps/example-app/src/main.ts",
+ "polyfills": "apps/example-app/src/polyfills.ts",
+ "tsConfig": "apps/example-app/tsconfig.app.json",
+ "assets": ["apps/example-app/src/favicon.ico", "apps/example-app/src/assets"],
+ "styles": ["apps/example-app/src/styles.css"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "6kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "example-app:build:production"
+ },
+ "development": {
+ "buildTarget": "example-app:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "example-app:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "options": {
+ "jestConfig": "apps/example-app/jest.config.ts",
+ "passWithNoTests": false
+ },
+ "outputs": ["{workspaceRoot}/coverage/"]
+ }
+ }
+}
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 1cfa2f59..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 { MasterComponent, 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: MasterComponent,
- 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 fb4ccaf7..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 { MasterComponent, 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,
- MasterComponent,
- 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 5b0faeb2..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,29 +1,33 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
- selector: 'app-button',
+ standalone: true,
+ selector: 'atl-button',
template: ' ',
})
export class NestedButtonComponent {
- @Input() name: string;
+ @Input() name = '';
@Output() raise = new EventEmitter();
}
@Component({
- selector: 'app-value',
+ standalone: true,
+ selector: 'atl-value',
template: ' {{ value }} ',
})
export class NestedValueComponent {
- @Input() value: number;
+ @Input() value?: number;
}
@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 663d63e0..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(InputOutputComponent, {
- template: '',
+ 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 6be38a45..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.queryByText('score must be lesser than 10')).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 03fa74f3..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 { FormBuilder, Validators, ValidationErrors } 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;
+let originalConfig: Config;
beforeEach(() => {
// Grab the existing configuration so we can restore
// it at the end of the test
configure((existingConfig) => {
- originalConfig = existingConfig;
+ originalConfig = existingConfig as Config;
// Don't change the existing config
return {};
});
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 49e9094e..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,17 +51,20 @@ 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),
- switchMap((q) => this.entitiesService.fetchAll().pipe(map((ent) => ent.filter((e) => e.name.includes(q))))),
+ switchMap((q) =>
+ this.entitiesService.fetchAll().pipe(map((ent: any) => ent.filter((e: any) => e.name.includes(q)))),
+ ),
startWith(entities),
);
- constructor(private entitiesService: EntitiesService, private modalService: ModalService) {}
-
newEntityClicked() {
this.modalService.open('new entity');
}
@@ -53,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,
@@ -84,11 +91,11 @@ const entities = [
},
];
-it('renders the table', async () => {
+async function setup() {
jest.useFakeTimers();
+ const user = userEvent.setup();
await render(EntitiesComponent, {
- declarations: [TableComponent],
providers: [
{
provide: EntitiesService,
@@ -104,31 +111,53 @@ it('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();
+});
+
+test.skip('finds the cell', async () => {
+ const { user } = await setup();
- userEvent.type(await screen.findByRole('textbox', { name: /Search entities/i }), 'Entity 2', {});
+ 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,
}),
);
- waitFor(() => expect(modalMock.open).toHaveBeenCalledWith('edit entity', 'Entity 2'));
+ await waitFor(() => expect(modalMock.open).toHaveBeenCalledWith('edit entity', 'Entity 2'));
});
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-188.spec.ts b/projects/testing-library/tests/issues/issue-188.spec.ts
index 8077358a..b150dacc 100644
--- a/projects/testing-library/tests/issues/issue-188.spec.ts
+++ b/projects/testing-library/tests/issues/issue-188.spec.ts
@@ -6,9 +6,9 @@ import { render, screen } from '../../src/public_api';
template: `Hello {{ formattedName }}
`,
})
class BugOnChangeComponent implements OnChanges {
- @Input() name: string;
+ @Input() name?: string;
- formattedName: string;
+ formattedName?: string;
ngOnChanges(changes: SimpleChanges) {
if (changes.name) {
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
new file mode 100644
index 00000000..ea230e78
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-280.spec.ts
@@ -0,0 +1,59 @@
+import { Location } from '@angular/common';
+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
+ `,
+ imports: [RouterOutlet],
+})
+class MainComponent {}
+
+@Component({
+ template: ` first page
+ go to second`,
+ imports: [RouterLink],
+})
+class FirstComponent {}
+
+@Component({
+ template: `second page
+ `,
+})
+class SecondComponent {
+ private location = inject(Location);
+ goBack() {
+ this.location.back();
+ }
+}
+
+const routes: Routes = [
+ { path: '', redirectTo: '/first', pathMatch: 'full' },
+ { path: 'first', component: FirstComponent },
+ { path: 'second', component: SecondComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule],
+})
+class AppRoutingModule {}
+
+test('navigate to second page and back', async () => {
+ await render(MainComponent, { imports: [AppRoutingModule, RouterTestingModule] });
+
+ expect(await screen.findByText('Navigate')).toBeInTheDocument();
+ expect(await screen.findByText('first page')).toBeInTheDocument();
+
+ await userEvent.click(await screen.findByText('go to second'));
+
+ expect(await screen.findByText('second page')).toBeInTheDocument();
+ expect(await screen.findByText('navigate back')).toBeInTheDocument();
+
+ 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 60c2a074..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
@@ -62,18 +62,9 @@ test('the component renders', async () => {
expect(screen.getByText('Hello Angular!')).toBeInTheDocument();
});
-test('the directive renders (compatibility with the deprecated signature)', async () => {
- const view = await render(OnOffDirective, {
- template: '',
- });
-
- // eslint-disable-next-line testing-library/no-container
- expect(view.container.querySelector('[onoff]')).toBeInTheDocument();
-});
-
test('uses the default props', async () => {
await render('', {
- declarations: [OnOffDirective],
+ imports: [OnOffDirective],
});
fireEvent.click(screen.getByText('init'));
@@ -83,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'));
@@ -94,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',
},
@@ -109,7 +100,7 @@ test('overrides output properties', async () => {
const clicked = jest.fn();
await render('', {
- declarations: [OnOffDirective],
+ imports: [OnOffDirective],
componentProperties: {
clicked,
},
@@ -125,7 +116,7 @@ test('overrides output properties', async () => {
describe('removeAngularAttributes', () => {
it('should remove angular attributes', async () => {
await render('', {
- declarations: [OnOffDirective],
+ imports: [OnOffDirective],
removeAngularAttributes: true,
});
@@ -135,7 +126,7 @@ describe('removeAngularAttributes', () => {
it('is disabled by default', async () => {
await render('', {
- declarations: [OnOffDirective],
+ imports: [OnOffDirective],
});
expect(document.querySelector('[ng-version]')).not.toBeNull();
@@ -144,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 678da79c..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, screen } 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, screen } 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 c98bd1bc..04b8185a 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -1,66 +1,188 @@
-import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } 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: ` {{ name }} `,
+ template: ` {{ firstName }} {{ lastName }} `,
})
-class FixtureComponent {
- @Input() name = 'Sarah';
+class FixtureComponent implements OnChanges {
+ @Input() firstName = 'Sarah';
+ @Input() lastName?: string;
+ ngOnChanges(changes: SimpleChanges): void {
+ ngOnChangesSpy(changes);
+ }
}
-test('will rerender the component with updated props', async () => {
+beforeEach(() => {
+ ngOnChangesSpy = jest.fn();
+});
+
+test('rerenders the component with updated props', async () => {
const { rerender } = await render(FixtureComponent);
expect(screen.getByText('Sarah')).toBeInTheDocument();
- const name = 'Mark';
- rerender({ name });
+ const firstName = 'Mark';
+ await rerender({ componentProperties: { firstName } });
- expect(screen.getByText(name)).toBeInTheDocument();
+ expect(screen.getByText(firstName)).toBeInTheDocument();
});
-@Component({
- selector: 'atl-fixture',
- template: ` {{ name }} `,
-})
-class FixtureWithNgOnChangesComponent implements OnChanges {
- @Input() name = 'Sarah';
- @Input() nameChanged: (name: string, isFirstChange: boolean) => void;
-
- ngOnChanges(changes: SimpleChanges) {
- if (changes.name && this.nameChanged) {
- this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
- }
- }
-}
+test('rerenders without props', async () => {
+ const { rerender } = await render(FixtureComponent);
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ await rerender();
-test('will call ngOnChanges on rerender', async () => {
- const nameChanged = jest.fn();
- const componentProperties = { nameChanged };
- const { rerender } = await render(FixtureWithNgOnChangesComponent, { componentProperties });
expect(screen.getByText('Sarah')).toBeInTheDocument();
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); // one time initially and one time for rerender
+});
- const name = 'Mark';
- rerender({ name });
+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(name)).toBeInTheDocument();
- expect(nameChanged).toHaveBeenCalledWith(name, false);
+ expect(screen.getByText(firstName)).toBeInTheDocument();
});
-@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
- selector: 'atl-fixture',
- template: ` Number
`,
-})
-class FixtureWithOnPushComponent {
- @Input() activeField: string;
-}
+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, {
+ componentProperties: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
-test('update properties on rerender', async () => {
- const { rerender } = await render(FixtureWithOnPushComponent);
- const numberHtmlElementRef = screen.queryByTestId('number');
+ const firstName2 = 'Chris';
+ await rerender({ componentProperties: { 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,
+ },
+ });
+});
- expect(numberHtmlElementRef).not.toHaveClass('active');
- rerender({ activeField: 'number' });
- expect(numberHtmlElementRef).toHaveClass('active');
+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
new file mode 100644
index 00000000..21a2b8ef
--- /dev/null
+++ b/projects/testing-library/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.lib.prod.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "compilerOptions": {
+ "target": "es2020"
+ },
+ "angularCompilerOptions": {
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true,
+ "flatModuleId": "AUTOGENERATED",
+ "flatModuleOutFile": "AUTOGENERATED"
+ }
+}
diff --git a/projects/testing-library/tsconfig.lib.json b/projects/testing-library/tsconfig.lib.json
index 8506c888..0938741e 100644
--- a/projects/testing-library/tsconfig.lib.json
+++ b/projects/testing-library/tsconfig.lib.json
@@ -1,28 +1,14 @@
{
- "extends": "../../tsconfig.json",
+ "extends": "./tsconfig.json",
"compilerOptions": {
- "outDir": "../../out-tsc/lib",
- "declarationMap": false,
- "target": "es2015",
- "module": "ES2015",
- "moduleResolution": "node",
+ "outDir": "../../dist/out-tsc",
"declaration": true,
- "sourceMap": true,
+ "declarationMap": true,
"inlineSources": true,
- "emitDecoratorMetadata": true,
- "experimentalDecorators": true,
- "importHelpers": false,
"types": ["node", "jest"],
- "lib": ["dom", "es2018"]
+ "target": "ES2022",
+ "useDefineForClassFields": false
},
- "angularCompilerOptions": {
- "enableIvy": false,
- "skipTemplateCodegen": true,
- "strictMetadataEmit": true,
- "fullTemplateTypeCheck": true,
- "strictInjectionParameters": true,
- "flatModuleId": "AUTOGENERATED",
- "flatModuleOutFile": "AUTOGENERATED"
- },
- "exclude": ["src/test.ts", "**/*.spec.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
new file mode 100644
index 00000000..752ed5ea
--- /dev/null
+++ b/projects/testing-library/tsconfig.lib.prod.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "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 91de9574..c0118513 100644
--- a/projects/testing-library/tsconfig.schematics.json
+++ b/projects/testing-library/tsconfig.schematics.json
@@ -1,17 +1,18 @@
{
- "extends": "../../tsconfig.json",
+ "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 091c5cf3..9fee53b3 100644
--- a/projects/testing-library/tsconfig.spec.json
+++ b/projects/testing-library/tsconfig.spec.json
@@ -1,9 +1,9 @@
{
- "extends": "../../tsconfig.json",
+ "extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"types": ["node", "jest", "@testing-library/jest-dom"]
},
"files": ["test-setup.ts"],
- "include": ["**/*.spec.ts", "**/*.d.ts"]
+ "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
}
diff --git a/projects/vscode-atl-render/.vscode/launch.json b/projects/vscode-atl-render/.vscode/launch.json
index 0e191b59..4058e694 100644
--- a/projects/vscode-atl-render/.vscode/launch.json
+++ b/projects/vscode-atl-render/.vscode/launch.json
@@ -3,15 +3,13 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Extension",
- "type": "extensionHost",
- "request": "launch",
- "args": [
- "--extensionDevelopmentPath=${workspaceFolder}"
- ]
- }
- ]
-}
\ No newline at end of file
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Extension",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
+ }
+ ]
+}
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
new file mode 100644
index 00000000..b75283e1
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,30 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "lib": ["es2018", "dom"],
+ "module": "esnext",
+ "moduleResolution": "node",
+ "outDir": "./dist/out-tsc",
+ "sourceMap": true,
+ "target": "ES2020",
+ "typeRoots": ["node_modules/@types"],
+ "strict": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitOverride": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "paths": {
+ "@testing-library/angular": ["projects/testing-library"],
+ "@testing-library/angular/jest-utils": ["projects/testing-library/jest-utils"]
+ }
+ },
+ "exclude": ["node_modules", "tmp"]
+}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 69eb7ed5..00000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "compileOnSave": false,
- "compilerOptions": {
- "baseUrl": "./",
- "importHelpers": true,
- "module": "esnext",
- "outDir": "./dist/out-tsc",
- "sourceMap": true,
- "declaration": false,
- "moduleResolution": "node",
- "emitDecoratorMetadata": true,
- "experimentalDecorators": true,
- "target": "es2015",
- "typeRoots": ["node_modules/@types"],
- "lib": ["es2017", "dom"],
- "paths": {
- "@testing-library/angular": ["projects/testing-library"],
- "@testing-library/angular/jest-utils": ["projects/jest-utils"]
- }
- }
-}