├── .all-contributorsrc
├── .browserslistrc
├── .devcontainer
├── devcontainer.json
└── welcome-message.txt
├── .editorconfig
├── .githooks
└── pre-commit
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .node-version
├── .npmrc
├── .nxignore
├── .prettierignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps
├── example-app-karma
│ ├── eslint.config.cjs
│ ├── eslint.config.mjs
│ ├── jasmine-dom.d.ts
│ ├── karma.conf.js
│ ├── project.json
│ ├── src
│ │ ├── app
│ │ │ ├── examples
│ │ │ │ └── login-form.spec.ts
│ │ │ └── issues
│ │ │ │ ├── issue-491.spec.ts
│ │ │ │ ├── jasmine-matchers.spec.ts
│ │ │ │ └── rerender.spec.ts
│ │ └── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.editor.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
└── example-app
│ ├── eslint.config.cjs
│ ├── eslint.config.mjs
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ ├── app
│ │ └── examples
│ │ │ ├── 00-single-component.spec.ts
│ │ │ ├── 00-single-component.ts
│ │ │ ├── 01-nested-component.spec.ts
│ │ │ ├── 01-nested-component.ts
│ │ │ ├── 02-input-output.spec.ts
│ │ │ ├── 02-input-output.ts
│ │ │ ├── 03-forms.spec.ts
│ │ │ ├── 03-forms.ts
│ │ │ ├── 04-forms-with-material.spec.ts
│ │ │ ├── 04-forms-with-material.ts
│ │ │ ├── 05-component-provider.spec.ts
│ │ │ ├── 05-component-provider.ts
│ │ │ ├── 06-with-ngrx-store.spec.ts
│ │ │ ├── 06-with-ngrx-store.ts
│ │ │ ├── 07-with-ngrx-mock-store.spec.ts
│ │ │ ├── 07-with-ngrx-mock-store.ts
│ │ │ ├── 08-directive.spec.ts
│ │ │ ├── 08-directive.ts
│ │ │ ├── 09-router.spec.ts
│ │ │ ├── 09-router.ts
│ │ │ ├── 10-inject-token-dependency.spec.ts
│ │ │ ├── 10-inject-token-dependency.ts
│ │ │ ├── 11-ng-content.spec.ts
│ │ │ ├── 11-ng-content.ts
│ │ │ ├── 12-service-component.spec.ts
│ │ │ ├── 12-service-component.ts
│ │ │ ├── 13-scrolling.component.spec.ts
│ │ │ ├── 13-scrolling.component.ts
│ │ │ ├── 14-async-component.spec.ts
│ │ │ ├── 14-async-component.ts
│ │ │ ├── 15-dialog.component.spec.ts
│ │ │ ├── 15-dialog.component.ts
│ │ │ ├── 16-input-getter-setter.spec.ts
│ │ │ ├── 16-input-getter-setter.ts
│ │ │ ├── 17-component-with-attribute-selector.spec.ts
│ │ │ ├── 17-component-with-attribute-selector.ts
│ │ │ ├── 18-html-as-input.spec.ts
│ │ │ ├── 19-standalone-component.spec.ts
│ │ │ ├── 19-standalone-component.ts
│ │ │ ├── 20-test-harness.spec.ts
│ │ │ ├── 20-test-harness.ts
│ │ │ ├── 21-deferable-view.component.ts
│ │ │ ├── 21-deferable-view.spec.ts
│ │ │ ├── 22-signal-inputs.component.spec.ts
│ │ │ ├── 22-signal-inputs.component.ts
│ │ │ ├── 23-host-directive.spec.ts
│ │ │ ├── 23-host-directive.ts
│ │ │ └── README.md
│ └── test-setup.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.editor.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── eslint.config.cjs
├── eslint.config.mjs
├── jest.config.ts
├── jest.preset.js
├── lint-staged.config.js
├── nx.json
├── other
├── logo-icon.svg
├── logo-transparent.svg
├── logo.jpg
├── logo.png
└── logo.svg
├── package.json
├── prettier.config.js
├── projects
├── testing-library
│ ├── eslint.config.cjs
│ ├── eslint.config.mjs
│ ├── index.ts
│ ├── jest-utils
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ ├── src
│ │ │ ├── lib
│ │ │ │ ├── create-mock.ts
│ │ │ │ └── index.ts
│ │ │ └── public_api.ts
│ │ └── tests
│ │ │ └── create-mock.spec.ts
│ ├── jest.config.ts
│ ├── ng-package.json
│ ├── package.json
│ ├── project.json
│ ├── schematics
│ │ ├── collection.json
│ │ ├── migrations
│ │ │ ├── dtl-as-dev-dependency
│ │ │ │ ├── index.spec.ts
│ │ │ │ └── index.ts
│ │ │ └── migrations.json
│ │ └── ng-add
│ │ │ ├── index.ts
│ │ │ ├── schema.json
│ │ │ └── schema.ts
│ ├── src
│ │ ├── lib
│ │ │ ├── config.ts
│ │ │ ├── models.ts
│ │ │ └── testing-library.ts
│ │ └── public_api.ts
│ ├── test-setup.ts
│ ├── tests
│ │ ├── auto-cleanup.spec.ts
│ │ ├── config.spec.ts
│ │ ├── debug.spec.ts
│ │ ├── defer-blocks.spec.ts
│ │ ├── detect-changes.spec.ts
│ │ ├── find-by.spec.ts
│ │ ├── fire-event.spec.ts
│ │ ├── integration.spec.ts
│ │ ├── integrations
│ │ │ └── ng-mocks.spec.ts
│ │ ├── issues
│ │ │ ├── issue-188.spec.ts
│ │ │ ├── issue-230.spec.ts
│ │ │ ├── issue-280.spec.ts
│ │ │ ├── issue-318.spec.ts
│ │ │ ├── issue-346.spec.ts
│ │ │ ├── issue-386.spec.ts
│ │ │ ├── issue-389.spec.ts
│ │ │ ├── issue-396-standalone-stub-child.spec.ts
│ │ │ ├── issue-397-directive-overrides-component-input.spec.ts
│ │ │ ├── issue-398-component-without-host-id.spec.ts
│ │ │ ├── issue-422-view-already-destroyed.spec.ts
│ │ │ ├── issue-435.spec.ts
│ │ │ ├── issue-437.spec.ts
│ │ │ ├── issue-492.spec.ts
│ │ │ ├── issue-493.spec.ts
│ │ │ └── issue-67.spec.ts
│ │ ├── navigate.spec.ts
│ │ ├── providers
│ │ │ ├── component-provider.spec.ts
│ │ │ └── module-provider.spec.ts
│ │ ├── render-template.spec.ts
│ │ ├── render.spec.ts
│ │ ├── rerender.spec.ts
│ │ ├── wait-for-element-to-be-removed.spec.ts
│ │ └── wait-for.spec.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.schematics.json
│ └── tsconfig.spec.json
└── vscode-atl-render
│ ├── .gitattributes
│ ├── .gitignore
│ ├── .vscode
│ └── launch.json
│ ├── .vscodeignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── language-configuration.json
│ ├── other
│ └── hedgehog.png
│ ├── package.json
│ └── syntaxes
│ └── atl-render.json
├── release.config.js
└── tsconfig.base.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "timdeschryver",
10 | "name": "Tim Deschryver",
11 | "avatar_url": "https://avatars1.githubusercontent.com/u/28659384?v=4",
12 | "profile": "http://timdeschryver.dev",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "infra",
17 | "test"
18 | ]
19 | },
20 | {
21 | "login": "MichaelDeBoey",
22 | "name": "Michaël De Boey",
23 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
24 | "profile": "https://michaeldeboey.be",
25 | "contributions": [
26 | "doc"
27 | ]
28 | },
29 | {
30 | "login": "flakolefluk",
31 | "name": "Ignacio Le Fluk",
32 | "avatar_url": "https://avatars0.githubusercontent.com/u/11986564?v=4",
33 | "profile": "https://github.com/flakolefluk",
34 | "contributions": [
35 | "code",
36 | "test"
37 | ]
38 | },
39 | {
40 | "login": "szabototo89",
41 | "name": "Tamás Szabó",
42 | "avatar_url": "https://avatars0.githubusercontent.com/u/3720079?v=4",
43 | "profile": "https://hu.linkedin.com/pub/tamas-szabo/57/a4b/242",
44 | "contributions": [
45 | "code"
46 | ]
47 | },
48 | {
49 | "login": "GregOnNet",
50 | "name": "Gregor Woiwode",
51 | "avatar_url": "https://avatars3.githubusercontent.com/u/444278?v=4",
52 | "profile": "https://medium.com/@gregor.woiwode",
53 | "contributions": [
54 | "code"
55 | ]
56 | },
57 | {
58 | "login": "tonivj5",
59 | "name": "Toni Villena",
60 | "avatar_url": "https://avatars2.githubusercontent.com/u/7110786?v=4",
61 | "profile": "https://github.com/tonivj5",
62 | "contributions": [
63 | "bug",
64 | "code",
65 | "doc",
66 | "test"
67 | ]
68 | },
69 | {
70 | "login": "ShPelles",
71 | "name": "ShPelles",
72 | "avatar_url": "https://avatars0.githubusercontent.com/u/43875468?v=4",
73 | "profile": "https://github.com/ShPelles",
74 | "contributions": [
75 | "doc"
76 | ]
77 | },
78 | {
79 | "login": "miluoshi",
80 | "name": "Miluoshi",
81 | "avatar_url": "https://avatars1.githubusercontent.com/u/1130547?v=4",
82 | "profile": "https://github.com/miluoshi",
83 | "contributions": [
84 | "code",
85 | "test"
86 | ]
87 | },
88 | {
89 | "login": "nickmccurdy",
90 | "name": "Nick McCurdy",
91 | "avatar_url": "https://avatars0.githubusercontent.com/u/927220?v=4",
92 | "profile": "https://nickmccurdy.com/",
93 | "contributions": [
94 | "doc"
95 | ]
96 | },
97 | {
98 | "login": "SrinivasanTarget",
99 | "name": "Srinivasan Sekar",
100 | "avatar_url": "https://avatars2.githubusercontent.com/u/8896549?v=4",
101 | "profile": "https://github.com/SrinivasanTarget",
102 | "contributions": [
103 | "doc"
104 | ]
105 | },
106 | {
107 | "login": "SerkanSipahi",
108 | "name": "Bitcollage",
109 | "avatar_url": "https://avatars2.githubusercontent.com/u/1880749?v=4",
110 | "profile": "https://www.linkedin.com/in/serkan-sipahi-59b20081/",
111 | "contributions": [
112 | "doc"
113 | ]
114 | },
115 | {
116 | "login": "krokofant",
117 | "name": "Emil Sundin",
118 | "avatar_url": "https://avatars0.githubusercontent.com/u/5908498?v=4",
119 | "profile": "https://github.com/krokofant",
120 | "contributions": [
121 | "code"
122 | ]
123 | },
124 | {
125 | "login": "Ombrax",
126 | "name": "Ombrax",
127 | "avatar_url": "https://avatars0.githubusercontent.com/u/7486723?v=4",
128 | "profile": "https://github.com/Ombrax",
129 | "contributions": [
130 | "code"
131 | ]
132 | },
133 | {
134 | "login": "rafaelss95",
135 | "name": "Rafael Santana",
136 | "avatar_url": "https://avatars0.githubusercontent.com/u/11965907?v=4",
137 | "profile": "https://github.com/rafaelss95",
138 | "contributions": [
139 | "code",
140 | "test",
141 | "bug"
142 | ]
143 | },
144 | {
145 | "login": "BBlackwo",
146 | "name": "Benjamin Blackwood",
147 | "avatar_url": "https://avatars0.githubusercontent.com/u/7598058?v=4",
148 | "profile": "https://twitter.com/B_Blackwo",
149 | "contributions": [
150 | "doc",
151 | "test"
152 | ]
153 | },
154 | {
155 | "login": "portothree",
156 | "name": "Gustavo Porto",
157 | "avatar_url": "https://avatars2.githubusercontent.com/u/3718120?v=4",
158 | "profile": "http://gustavoporto.dev",
159 | "contributions": [
160 | "doc"
161 | ]
162 | },
163 | {
164 | "login": "bovandersteene",
165 | "name": "Bo Vandersteene",
166 | "avatar_url": "https://avatars1.githubusercontent.com/u/1673799?v=4",
167 | "profile": "http://wwww.reibo.be",
168 | "contributions": [
169 | "code"
170 | ]
171 | },
172 | {
173 | "login": "jbchr",
174 | "name": "Janek",
175 | "avatar_url": "https://avatars1.githubusercontent.com/u/23141806?v=4",
176 | "profile": "https://github.com/jbchr",
177 | "contributions": [
178 | "code",
179 | "test"
180 | ]
181 | },
182 | {
183 | "login": "GlebIrovich",
184 | "name": "Gleb Irovich",
185 | "avatar_url": "https://avatars.githubusercontent.com/u/33176414?v=4",
186 | "profile": "https://github.com/GlebIrovich",
187 | "contributions": [
188 | "code",
189 | "test"
190 | ]
191 | },
192 | {
193 | "login": "the-ult",
194 | "name": "Arjen",
195 | "avatar_url": "https://avatars.githubusercontent.com/u/4863062?v=4",
196 | "profile": "https://github.com/the-ult",
197 | "contributions": [
198 | "code",
199 | "maintenance"
200 | ]
201 | },
202 | {
203 | "login": "lacolaco",
204 | "name": "Suguru Inatomi",
205 | "avatar_url": "https://avatars.githubusercontent.com/u/1529180?v=4",
206 | "profile": "https://lacolaco.net",
207 | "contributions": [
208 | "code",
209 | "ideas"
210 | ]
211 | },
212 | {
213 | "login": "amitmiran137",
214 | "name": "Amit Miran",
215 | "avatar_url": "https://avatars.githubusercontent.com/u/47772523?v=4",
216 | "profile": "https://github.com/amitmiran137",
217 | "contributions": [
218 | "infra"
219 | ]
220 | },
221 | {
222 | "login": "jwillebrands",
223 | "name": "Jan-Willem Willebrands",
224 | "avatar_url": "https://avatars.githubusercontent.com/u/8925?v=4",
225 | "profile": "https://github.com/jwillebrands",
226 | "contributions": [
227 | "code"
228 | ]
229 | },
230 | {
231 | "login": "rothsandro",
232 | "name": "Sandro",
233 | "avatar_url": "https://avatars.githubusercontent.com/u/16229645?v=4",
234 | "profile": "https://www.sandroroth.com",
235 | "contributions": [
236 | "code",
237 | "bug"
238 | ]
239 | },
240 | {
241 | "login": "michaelwestphal",
242 | "name": "Michael Westphal",
243 | "avatar_url": "https://avatars.githubusercontent.com/u/1829174?v=4",
244 | "profile": "https://github.com/michaelwestphal",
245 | "contributions": [
246 | "code",
247 | "test"
248 | ]
249 | },
250 | {
251 | "login": "Lukas-Kullmann",
252 | "name": "Lukas",
253 | "avatar_url": "https://avatars.githubusercontent.com/u/387547?v=4",
254 | "profile": "https://github.com/Lukas-Kullmann",
255 | "contributions": [
256 | "code"
257 | ]
258 | },
259 | {
260 | "login": "MatanBobi",
261 | "name": "Matan Borenkraout",
262 | "avatar_url": "https://avatars.githubusercontent.com/u/12711091?v=4",
263 | "profile": "https://matan.io",
264 | "contributions": [
265 | "maintenance"
266 | ]
267 | },
268 | {
269 | "login": "mleimer",
270 | "name": "mleimer",
271 | "avatar_url": "https://avatars.githubusercontent.com/u/14271564?v=4",
272 | "profile": "https://github.com/mleimer",
273 | "contributions": [
274 | "doc",
275 | "test"
276 | ]
277 | },
278 | {
279 | "login": "meirka",
280 | "name": "MeIr",
281 | "avatar_url": "https://avatars.githubusercontent.com/u/750901?v=4",
282 | "profile": "https://github.com/meirka",
283 | "contributions": [
284 | "bug",
285 | "test"
286 | ]
287 | },
288 | {
289 | "login": "jadengis",
290 | "name": "John Dengis",
291 | "avatar_url": "https://avatars.githubusercontent.com/u/13421336?v=4",
292 | "profile": "https://github.com/jadengis",
293 | "contributions": [
294 | "code",
295 | "test"
296 | ]
297 | },
298 | {
299 | "login": "dzonatan",
300 | "name": "Rokas Brazdžionis",
301 | "avatar_url": "https://avatars.githubusercontent.com/u/5166666?v=4",
302 | "profile": "https://github.com/dzonatan",
303 | "contributions": [
304 | "code"
305 | ]
306 | },
307 | {
308 | "login": "mateusduraes",
309 | "name": "Mateus Duraes",
310 | "avatar_url": "https://avatars.githubusercontent.com/u/19319404?v=4",
311 | "profile": "https://github.com/mateusduraes",
312 | "contributions": [
313 | "code"
314 | ]
315 | },
316 | {
317 | "login": "JJosephttg",
318 | "name": "Josh Joseph",
319 | "avatar_url": "https://avatars.githubusercontent.com/u/23690250?v=4",
320 | "profile": "https://github.com/JJosephttg",
321 | "contributions": [
322 | "code",
323 | "test"
324 | ]
325 | },
326 | {
327 | "login": "shaman-apprentice",
328 | "name": "Torsten Knauf",
329 | "avatar_url": "https://avatars.githubusercontent.com/u/3596742?v=4",
330 | "profile": "https://github.com/shaman-apprentice",
331 | "contributions": [
332 | "maintenance"
333 | ]
334 | },
335 | {
336 | "login": "antischematic",
337 | "name": "antischematic",
338 | "avatar_url": "https://avatars.githubusercontent.com/u/12976684?v=4",
339 | "profile": "https://github.com/antischematic",
340 | "contributions": [
341 | "bug",
342 | "ideas"
343 | ]
344 | },
345 | {
346 | "login": "TrustNoOneElse",
347 | "name": "Florian Pabst",
348 | "avatar_url": "https://avatars.githubusercontent.com/u/25935352?v=4",
349 | "profile": "https://github.com/TrustNoOneElse",
350 | "contributions": [
351 | "code"
352 | ]
353 | },
354 | {
355 | "login": "markgoho",
356 | "name": "Mark Goho",
357 | "avatar_url": "https://avatars.githubusercontent.com/u/9759954?v=4",
358 | "profile": "https://rochesterparks.org",
359 | "contributions": [
360 | "maintenance",
361 | "doc"
362 | ]
363 | },
364 | {
365 | "login": "jwbaart",
366 | "name": "Jan-Willem Baart",
367 | "avatar_url": "https://avatars.githubusercontent.com/u/10973990?v=4",
368 | "profile": "http://jwbaart.dev",
369 | "contributions": [
370 | "code",
371 | "test"
372 | ]
373 | },
374 | {
375 | "login": "mumenthalers",
376 | "name": "S. Mumenthaler",
377 | "avatar_url": "https://avatars.githubusercontent.com/u/3604424?v=4",
378 | "profile": "https://github.com/mumenthalers",
379 | "contributions": [
380 | "code",
381 | "test"
382 | ]
383 | },
384 | {
385 | "login": "andreialecu",
386 | "name": "Andrei Alecu",
387 | "avatar_url": "https://avatars.githubusercontent.com/u/697707?v=4",
388 | "profile": "https://lets.poker/",
389 | "contributions": [
390 | "code",
391 | "ideas",
392 | "doc"
393 | ]
394 | },
395 | {
396 | "login": "Hyperxq",
397 | "name": "Daniel Ramírez Barrientos",
398 | "avatar_url": "https://avatars.githubusercontent.com/u/22332354?v=4",
399 | "profile": "https://github.com/Hyperxq",
400 | "contributions": [
401 | "code"
402 | ]
403 | },
404 | {
405 | "login": "mlz11",
406 | "name": "Mahdi Lazraq",
407 | "avatar_url": "https://avatars.githubusercontent.com/u/94069699?v=4",
408 | "profile": "https://github.com/mlz11",
409 | "contributions": [
410 | "code",
411 | "test"
412 | ]
413 | },
414 | {
415 | "login": "Arthie",
416 | "name": "Arthur Petrie",
417 | "avatar_url": "https://avatars.githubusercontent.com/u/16376476?v=4",
418 | "profile": "https://arthurpetrie.com",
419 | "contributions": [
420 | "code"
421 | ]
422 | },
423 | {
424 | "login": "FabienDehopre",
425 | "name": "Fabien Dehopré",
426 | "avatar_url": "https://avatars.githubusercontent.com/u/97023?v=4",
427 | "profile": "https://github.com/FabienDehopre",
428 | "contributions": [
429 | "code"
430 | ]
431 | },
432 | {
433 | "login": "jvereecken",
434 | "name": "Jamie Vereecken",
435 | "avatar_url": "https://avatars.githubusercontent.com/u/108937550?v=4",
436 | "profile": "https://github.com/jvereecken",
437 | "contributions": [
438 | "code"
439 | ]
440 | }
441 | ],
442 | "contributorsPerLine": 7,
443 | "projectName": "angular-testing-library",
444 | "projectOwner": "testing-library",
445 | "repoType": "github",
446 | "repoHost": "https://github.com",
447 | "skipCi": true,
448 | "commitConvention": "angular",
449 | "commitType": "docs"
450 | }
451 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed
5 | > 0.5%
6 | last 2 versions
7 | Firefox ESR
8 | not dead
9 | # IE 9-11
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json.
2 | {
3 | "name": "angular-testing-library",
4 | "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bullseye",
5 |
6 | // Features to add to the dev container. More info: https://containers.dev/features.
7 | "features": {
8 | "ghcr.io/devcontainers/features/github-cli:1": {},
9 | "ghcr.io/devcontainers/features/sshd:1": {}
10 | },
11 |
12 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
13 | // "forwardPorts": [],
14 |
15 | // Use 'postCreateCommand' to run commands after the container is created.
16 | "postCreateCommand": "npm install --force",
17 | "onCreateCommand": "sudo cp .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt",
18 | "waitFor": "postCreateCommand",
19 |
20 | // Configure tool-specific properties.
21 | "customizations": {
22 | // Configure properties specific to VS Code.
23 | "vscode": {
24 | "settings": {
25 | "[typescript]": {
26 | "editor.defaultFormatter": "esbenp.prettier-vscode",
27 | "editor.formatOnSave": true
28 | },
29 | "[md]": {
30 | "editor.defaultFormatter": "esbenp.prettier-vscode",
31 | "editor.formatOnSave": true
32 | },
33 | "[json]": {
34 | "editor.defaultFormatter": "esbenp.prettier-vscode",
35 | "editor.formatOnSave": true
36 | }
37 | },
38 | // Add the IDs of extensions you want installed when the container is created.
39 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.devcontainer/welcome-message.txt:
--------------------------------------------------------------------------------
1 | 👋 Welcome to "Angular Testing Library" in GitHub Codespaces!
2 |
3 | 🛠️ Your environment is fully setup with all the required software.
4 |
5 | 🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
6 |
7 | 📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.githooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | npm run pre-commit
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | - 'beta'
8 | pull_request: {}
9 | workflow_dispatch:
10 |
11 | permissions: {}
12 |
13 | concurrency:
14 | group: ${{ github.workflow }}-${{ github.ref }}
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | build_test_release:
19 | permissions:
20 | actions: write
21 | contents: write
22 |
23 | strategy:
24 | matrix:
25 | node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[22]' || '[18, 20, 22]') }}
26 | os: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '["ubuntu-latest"]' || '["ubuntu-latest", "windows-latest"]') }}
27 | runs-on: ${{ matrix.os }}
28 |
29 | steps:
30 | - uses: actions/checkout@v4
31 | - name: use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
32 | uses: actions/setup-node@v4
33 | with:
34 | node-version: ${{ matrix.node-version }}
35 | - name: install
36 | run: npm install --force
37 | - name: build
38 | run: npm run build -- --skip-nx-cache
39 | - name: test
40 | run: npm run test
41 | - name: lint
42 | run: npm run lint
43 | - name: Release
44 | if: github.repository == 'testing-library/angular-testing-library' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta')
45 | run: npx semantic-release
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
49 | CI: true
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | **/coverage
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.angular/cache
30 | .angular
31 | .nx
32 | migrations.json
33 | .cache
34 | /.sass-cache
35 | /connect.lock
36 | /coverage
37 | /libpeerconnection.log
38 | npm-debug.log
39 | yarn-error.log
40 | testem.log
41 | /typings
42 | yarn.lock
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 22
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/.nxignore:
--------------------------------------------------------------------------------
1 | /projects/vscode-atl-render
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # NPM Files
2 | package-lock.json
3 | migrations.json
4 |
5 | CHANGELOG.md
6 |
7 | #Ignore specific file types
8 | *.svg
9 | *.xml
10 | *.png
11 | *.jpg
12 |
13 | # compiled output
14 | /dist
15 | /tmp
16 | /out-tsc
17 | # dependencies
18 | /node_modules
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/cSpell.json
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
36 |
37 | # misc
38 | .cache
39 | .angular
40 | /.sass-cache
41 | /connect.lock
42 | /coverage
43 | /libpeerconnection.log
44 | npm-debug.log
45 | testem.log
46 | /typings
47 | deployment.yaml
48 |
49 | # e2e
50 | /*e2e/*.js
51 | /*e2e/*.map
52 |
53 | # System Files
54 | .DS_Store
55 | Thumbs.db
56 |
57 | /.nx/cache
58 | /.nx/workspace-data
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The changelog is automatically updated using
4 | [semantic-release](https://github.com/semantic-release/semantic-release). You
5 | can see it on the [releases page](../../releases).
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | education, socio-economic status, nationality, personal appearance, race,
10 | religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Hi there, thanks for being willing to contribute!
4 |
5 | ## Setup
6 |
7 | - Fork and clone the repository
8 | - Install dependencies via `npm install`
9 | - Create a new feature branch via `git checkout -b feature-branch-name`
10 |
11 | ## Testing
12 |
13 | - Run `npm run test` to test the library and the example application
14 | - Run `npm run build` to build the library
15 |
16 | ## Push changes
17 |
18 | - Add the files you want to push via `git add filename`, or add everything via `git add .`
19 | - Commit these changes locally and give it a proper description via `git commit -m "my changes here"`
20 | - Push these changes to your fork via `git push`
21 | - Create a new pull request
22 |
23 | ## Need some guidance?
24 |
25 | - [GitHub help](https://help.github.com/)
26 | - [How to Contribute to an Open Source Project on GitHub - by Kent C. Dodds](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tim Deschryver
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/example-app-karma/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | // TODO - https://github.com/nrwl/nx/issues/22576
4 |
5 | /** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */
6 | const config = (async () => (await import('./eslint.config.mjs')).default)();
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/apps/example-app-karma/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import tseslint from "typescript-eslint";
4 | import rootConfig from "../../eslint.config.mjs";
5 |
6 | export default tseslint.config(
7 | ...rootConfig,
8 | );
9 |
--------------------------------------------------------------------------------
/apps/example-app-karma/jasmine-dom.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@testing-library/jasmine-dom' {
2 | const JasmineDOM: any;
3 | export default JasmineDOM;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/example-app-karma/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 | module.exports = function (config) {
4 | try {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('@angular-devkit/build-angular/plugins/karma'),
12 | ],
13 | client: {
14 | jasmine: {
15 | // you can add configuration options for Jasmine here
16 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
17 | // for example, you can disable the random execution with `random: false`
18 | // or set a specific seed with `seed: 4321`
19 | },
20 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
21 | },
22 | jasmineHtmlReporter: {
23 | suppressAll: true, // removes the duplicated traces
24 | },
25 | reporters: ['progress'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['ChromeHeadless'],
31 | singleRun: true,
32 | restartOnFileChange: true,
33 | });
34 | } catch (err) {
35 | console.log(err);
36 | throw err;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/apps/example-app-karma/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-app-karma",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "sourceRoot": "apps/example-app-karma/src",
6 | "prefix": "app",
7 | "tags": [],
8 | "generators": {},
9 | "targets": {
10 | "build": {
11 | "executor": "@angular-devkit/build-angular:browser",
12 | "outputs": ["{options.outputPath}"],
13 | "options": {
14 | "outputPath": "dist/apps/example-app-karma",
15 | "index": "apps/example-app-karma/src/index.html",
16 | "main": "apps/example-app-karma/src/main.ts",
17 | "tsConfig": "apps/example-app-karma/tsconfig.app.json",
18 | "assets": ["apps/example-app-karma/src/favicon.ico", "apps/example-app-karma/src/assets"],
19 | "styles": [],
20 | "scripts": []
21 | },
22 | "configurations": {
23 | "production": {
24 | "budgets": [
25 | {
26 | "type": "anyComponentStyle",
27 | "maximumWarning": "6kb"
28 | }
29 | ],
30 | "outputHashing": "all"
31 | },
32 | "development": {
33 | "buildOptimizer": false,
34 | "optimization": false,
35 | "vendorChunk": true,
36 | "extractLicenses": false,
37 | "sourceMap": true,
38 | "namedChunks": true
39 | }
40 | },
41 | "defaultConfiguration": "production"
42 | },
43 | "serve": {
44 | "executor": "@angular-devkit/build-angular:dev-server",
45 | "configurations": {
46 | "production": {
47 | "buildTarget": "example-app-karma:build:production"
48 | },
49 | "development": {
50 | "buildTarget": "example-app-karma:build:development"
51 | }
52 | },
53 | "defaultConfiguration": "development"
54 | },
55 | "lint": {
56 | "executor": "@nx/eslint:lint"
57 | },
58 | "test": {
59 | "executor": "@angular-devkit/build-angular:karma",
60 | "options": {
61 | "main": "apps/example-app-karma/src/test.ts",
62 | "tsConfig": "apps/example-app-karma/tsconfig.spec.json",
63 | "karmaConfig": "apps/example-app-karma/karma.conf.js"
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/apps/example-app-karma/src/app/examples/login-form.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FormBuilder, FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
3 | import userEvent from '@testing-library/user-event';
4 | import { render, screen } from '@testing-library/angular';
5 | import { NgIf } from '@angular/common';
6 |
7 | it('should create a component with inputs and a button to submit', async () => {
8 | await render(LoginComponent);
9 |
10 | expect(screen.getByRole('textbox', { name: 'email' })).toBeInTheDocument();
11 | expect(screen.getByLabelText('password')).toBeInTheDocument();
12 | expect(screen.getByRole('button', { name: 'submit' })).toBeInTheDocument();
13 | });
14 |
15 | it('should display invalid message and submit button must be disabled', async () => {
16 | const user = userEvent.setup();
17 |
18 | await render(LoginComponent);
19 |
20 | const email = screen.getByRole('textbox', { name: 'email' });
21 | const password = screen.getByLabelText('password');
22 |
23 | await user.type(email, 'foo');
24 | await user.type(password, 's');
25 |
26 | expect(screen.getAllByText(/is invalid/i).length).toBe(2);
27 | expect(screen.getAllByRole('alert').length).toBe(2);
28 | expect(screen.getByRole('button', { name: 'submit' })).toBeDisabled();
29 | });
30 |
31 | @Component({
32 | selector: 'atl-login',
33 | standalone: true,
34 | imports: [ReactiveFormsModule, NgIf],
35 | template: `
36 |
Login
37 |
38 |
45 | `,
46 | })
47 | class LoginComponent {
48 | form: FormGroup = this.fb.group({
49 | email: ['', [Validators.required, Validators.email]],
50 | password: ['', [Validators.required, Validators.minLength(8)]],
51 | });
52 |
53 | constructor(private fb: FormBuilder) {}
54 |
55 | get email(): FormControl {
56 | return this.form.get('email') as FormControl;
57 | }
58 |
59 | get password(): FormControl {
60 | return this.form.get('password') as FormControl;
61 | }
62 |
63 | onSubmit(_fg: FormGroup): void {
64 | // do nothing
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/apps/example-app-karma/src/app/issues/issue-491.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { render, screen, waitForElementToBeRemoved } from '@testing-library/angular';
4 | import userEvent from '@testing-library/user-event';
5 |
6 | it('test click event with router.navigate', async () => {
7 | const user = userEvent.setup();
8 | await render(``, {
9 | routes: [
10 | {
11 | path: '',
12 | component: LoginComponent,
13 | },
14 | {
15 | path: 'logged-in',
16 | component: LoggedInComponent,
17 | },
18 | ],
19 | });
20 |
21 | expect(await screen.findByRole('heading', { name: 'Login' })).toBeVisible();
22 | expect(screen.getByRole('button', { name: 'submit' })).toBeInTheDocument();
23 |
24 | const email = screen.getByRole('textbox', { name: 'email' });
25 | const password = screen.getByLabelText('password');
26 |
27 | await user.type(email, 'user@example.com');
28 | await user.type(password, 'with_valid_password');
29 |
30 | expect(screen.getByRole('button', { name: 'submit' })).toBeEnabled();
31 |
32 | await user.click(screen.getByRole('button', { name: 'submit' }));
33 |
34 | await waitForElementToBeRemoved(() => screen.queryByRole('heading', { name: 'Login' }));
35 |
36 | expect(await screen.findByRole('heading', { name: 'Logged In' })).toBeVisible();
37 | });
38 |
39 | @Component({
40 | template: `
41 | Login
42 |
43 |
44 |
45 | `,
46 | })
47 | class LoginComponent {
48 | constructor(private router: Router) {}
49 | onSubmit(): void {
50 | this.router.navigate(['logged-in']);
51 | }
52 | }
53 |
54 | @Component({
55 | template: ` Logged In
`,
56 | })
57 | class LoggedInComponent {}
58 |
--------------------------------------------------------------------------------
/apps/example-app-karma/src/app/issues/jasmine-matchers.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 |
3 | it('can use jasmine matchers', async () => {
4 | await render(`Hello {{ name}}
`, {
5 | componentProperties: {
6 | name: 'Sarah',
7 | },
8 | });
9 |
10 | expect(screen.getByText('Hello Sarah')).toBeVisible();
11 | });
12 |
--------------------------------------------------------------------------------
/apps/example-app-karma/src/app/issues/rerender.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 |
3 | it('can rerender component', async () => {
4 | const { rerender } = await render(`Hello {{ name}}
`, {
5 | componentProperties: {
6 | name: 'Sarah',
7 | },
8 | });
9 |
10 | expect(screen.getByText('Hello Sarah')).toBeInTheDocument();
11 |
12 | await rerender({ componentProperties: { name: 'Mark' } });
13 |
14 | expect(screen.getByText('Hello Mark')).toBeInTheDocument();
15 | });
16 |
--------------------------------------------------------------------------------
/apps/example-app-karma/src/test.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 | import 'zone.js/testing';
3 | import { getTestBed } from '@angular/core/testing';
4 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
5 | import JasmineDOM from '@testing-library/jasmine-dom';
6 |
7 | // Install custom matchers from jasmine-dom
8 | beforeEach(() => {
9 | jasmine.addMatchers(JasmineDOM);
10 | });
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {});
14 |
--------------------------------------------------------------------------------
/apps/example-app-karma/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [],
6 | "allowJs": true,
7 | "target": "ES2022",
8 | "useDefineForClassFields": false
9 | },
10 | "files": ["src/main.ts"],
11 | "include": ["src/**/*.d.ts"],
12 | "exclude": ["**/*.test.ts", "**/*.spec.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/example-app-karma/tsconfig.editor.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "types": ["jasmine", "node", "@testing-library/jasmine-dom"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/example-app-karma/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "compilerOptions": {
6 | "target": "es2020"
7 | },
8 | "angularCompilerOptions": {
9 | "strictInjectionParameters": true,
10 | "strictInputAccessModifiers": true,
11 | "strictTemplates": true
12 | },
13 | "references": [
14 | {
15 | "path": "./tsconfig.app.json"
16 | },
17 | {
18 | "path": "./tsconfig.spec.json"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/apps/example-app-karma/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": ["jasmine", "node", "@testing-library/jasmine-dom"],
6 | "target": "ES2022",
7 | "useDefineForClassFields": false
8 | },
9 | "files": ["src/test.ts"],
10 | "include": ["**/*.spec.ts", "**/*.d.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/example-app/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | // TODO - https://github.com/nrwl/nx/issues/22576
4 |
5 | /** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */
6 | const config = (async () => (await import('./eslint.config.mjs')).default)();
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/apps/example-app/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import tseslint from "typescript-eslint";
4 | import rootConfig from "../../eslint.config.mjs";
5 |
6 | export default tseslint.config(
7 | ...rootConfig,
8 | );
--------------------------------------------------------------------------------
/apps/example-app/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | displayName: {
3 | name: 'Example App',
4 | color: 'blue',
5 | },
6 | preset: '../../jest.preset.js',
7 | setupFilesAfterEnv: ['/src/test-setup.ts'],
8 | };
9 |
--------------------------------------------------------------------------------
/apps/example-app/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-app",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "sourceRoot": "apps/example-app/src",
6 | "prefix": "app",
7 | "tags": [],
8 | "generators": {},
9 | "targets": {
10 | "build": {
11 | "executor": "@angular-devkit/build-angular:browser",
12 | "outputs": ["{options.outputPath}"],
13 | "options": {
14 | "outputPath": "dist/apps/example-app",
15 | "index": "apps/example-app/src/index.html",
16 | "main": "apps/example-app/src/main.ts",
17 | "polyfills": "apps/example-app/src/polyfills.ts",
18 | "tsConfig": "apps/example-app/tsconfig.app.json",
19 | "assets": ["apps/example-app/src/favicon.ico", "apps/example-app/src/assets"],
20 | "styles": ["apps/example-app/src/styles.css"],
21 | "scripts": []
22 | },
23 | "configurations": {
24 | "production": {
25 | "budgets": [
26 | {
27 | "type": "anyComponentStyle",
28 | "maximumWarning": "6kb"
29 | }
30 | ],
31 | "outputHashing": "all"
32 | },
33 | "development": {
34 | "buildOptimizer": false,
35 | "optimization": false,
36 | "vendorChunk": true,
37 | "extractLicenses": false,
38 | "sourceMap": true,
39 | "namedChunks": true
40 | }
41 | },
42 | "defaultConfiguration": "production"
43 | },
44 | "serve": {
45 | "executor": "@angular-devkit/build-angular:dev-server",
46 | "configurations": {
47 | "production": {
48 | "buildTarget": "example-app:build:production"
49 | },
50 | "development": {
51 | "buildTarget": "example-app:build:development"
52 | }
53 | },
54 | "defaultConfiguration": "development"
55 | },
56 | "extract-i18n": {
57 | "executor": "@angular-devkit/build-angular:extract-i18n",
58 | "options": {
59 | "buildTarget": "example-app:build"
60 | }
61 | },
62 | "lint": {
63 | "executor": "@nx/eslint:lint"
64 | },
65 | "test": {
66 | "executor": "@nx/jest:jest",
67 | "options": {
68 | "jestConfig": "apps/example-app/jest.config.ts",
69 | "passWithNoTests": false
70 | },
71 | "outputs": ["{workspaceRoot}/coverage/"]
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/00-single-component.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { SingleComponent } from './00-single-component';
5 |
6 | test('renders the current value and can increment and decrement', async () => {
7 | const user = userEvent.setup();
8 | await render(SingleComponent);
9 |
10 | const incrementControl = screen.getByRole('button', { name: /increment/i });
11 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
12 | const valueControl = screen.getByTestId('value');
13 |
14 | expect(valueControl).toHaveTextContent('0');
15 |
16 | await user.click(incrementControl);
17 | await user.click(incrementControl);
18 | expect(valueControl).toHaveTextContent('2');
19 |
20 | await user.click(decrementControl);
21 | expect(valueControl).toHaveTextContent('1');
22 | });
23 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/00-single-component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'atl-fixture',
5 | standalone: true,
6 | template: `
7 |
8 | {{ value }}
9 |
10 | `,
11 | })
12 | export class SingleComponent {
13 | value = 0;
14 | }
15 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/01-nested-component.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { NestedContainerComponent } from './01-nested-component';
5 |
6 | test('renders the current value and can increment and decrement', async () => {
7 | const user = userEvent.setup();
8 | await render(NestedContainerComponent);
9 |
10 | const incrementControl = screen.getByRole('button', { name: /increment/i });
11 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
12 | const valueControl = screen.getByTestId('value');
13 |
14 | expect(valueControl).toHaveTextContent('0');
15 |
16 | await user.click(incrementControl);
17 | await user.click(incrementControl);
18 | expect(valueControl).toHaveTextContent('2');
19 |
20 | await user.click(decrementControl);
21 | expect(valueControl).toHaveTextContent('1');
22 | });
23 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/01-nested-component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from '@angular/core';
2 |
3 | @Component({
4 | standalone: true,
5 | selector: 'atl-button',
6 | template: ' ',
7 | })
8 | export class NestedButtonComponent {
9 | @Input() name = '';
10 | @Output() raise = new EventEmitter();
11 | }
12 |
13 | @Component({
14 | standalone: true,
15 | selector: 'atl-value',
16 | template: ' {{ value }} ',
17 | })
18 | export class NestedValueComponent {
19 | @Input() value?: number;
20 | }
21 |
22 | @Component({
23 | standalone: true,
24 | selector: 'atl-fixture',
25 | template: `
26 |
27 |
28 |
29 | `,
30 | imports: [NestedButtonComponent, NestedValueComponent],
31 | })
32 | export class NestedContainerComponent {
33 | value = 0;
34 | }
35 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/02-input-output.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { InputOutputComponent } from './02-input-output';
5 |
6 | test('is possible to set input and listen for output', async () => {
7 | const user = userEvent.setup();
8 | const sendValue = jest.fn();
9 |
10 | await render(InputOutputComponent, {
11 | inputs: {
12 | value: 47,
13 | },
14 | on: {
15 | sendValue,
16 | },
17 | });
18 |
19 | const incrementControl = screen.getByRole('button', { name: /increment/i });
20 | const sendControl = screen.getByRole('button', { name: /send/i });
21 | const valueControl = screen.getByTestId('value');
22 |
23 | expect(valueControl).toHaveTextContent('47');
24 |
25 | await user.click(incrementControl);
26 | await user.click(incrementControl);
27 | await user.click(incrementControl);
28 | expect(valueControl).toHaveTextContent('50');
29 |
30 | await user.click(sendControl);
31 | expect(sendValue).toHaveBeenCalledTimes(1);
32 | expect(sendValue).toHaveBeenCalledWith(50);
33 | });
34 |
35 | test.skip('is possible to set input and listen for output with the template syntax', async () => {
36 | const user = userEvent.setup();
37 | const sendSpy = jest.fn();
38 |
39 | await render('', {
40 | imports: [InputOutputComponent],
41 | on: {
42 | sendValue: sendSpy,
43 | },
44 | });
45 |
46 | const incrementControl = screen.getByRole('button', { name: /increment/i });
47 | const sendControl = screen.getByRole('button', { name: /send/i });
48 | const valueControl = screen.getByTestId('value');
49 |
50 | expect(valueControl).toHaveTextContent('47');
51 |
52 | await user.click(incrementControl);
53 | await user.click(incrementControl);
54 | await user.click(incrementControl);
55 | expect(valueControl).toHaveTextContent('50');
56 |
57 | await user.click(sendControl);
58 | expect(sendSpy).toHaveBeenCalledTimes(1);
59 | expect(sendSpy).toHaveBeenCalledWith(50);
60 | });
61 |
62 | test('is possible to set input and listen for output (deprecated)', async () => {
63 | const user = userEvent.setup();
64 | const sendValue = jest.fn();
65 |
66 | await render(InputOutputComponent, {
67 | inputs: {
68 | value: 47,
69 | },
70 | componentOutputs: {
71 | sendValue: {
72 | emit: sendValue,
73 | } as any,
74 | },
75 | });
76 |
77 | const incrementControl = screen.getByRole('button', { name: /increment/i });
78 | const sendControl = screen.getByRole('button', { name: /send/i });
79 | const valueControl = screen.getByTestId('value');
80 |
81 | expect(valueControl).toHaveTextContent('47');
82 |
83 | await user.click(incrementControl);
84 | await user.click(incrementControl);
85 | await user.click(incrementControl);
86 | expect(valueControl).toHaveTextContent('50');
87 |
88 | await user.click(sendControl);
89 | expect(sendValue).toHaveBeenCalledTimes(1);
90 | expect(sendValue).toHaveBeenCalledWith(50);
91 | });
92 |
93 | test('is possible to set input and listen for output with the template syntax (deprecated)', async () => {
94 | const user = userEvent.setup();
95 | const sendSpy = jest.fn();
96 |
97 | await render('', {
98 | imports: [InputOutputComponent],
99 | componentProperties: {
100 | sendValue: sendSpy,
101 | },
102 | });
103 |
104 | const incrementControl = screen.getByRole('button', { name: /increment/i });
105 | const sendControl = screen.getByRole('button', { name: /send/i });
106 | const valueControl = screen.getByTestId('value');
107 |
108 | expect(valueControl).toHaveTextContent('47');
109 |
110 | await user.click(incrementControl);
111 | await user.click(incrementControl);
112 | await user.click(incrementControl);
113 | expect(valueControl).toHaveTextContent('50');
114 |
115 | await user.click(sendControl);
116 | expect(sendSpy).toHaveBeenCalledTimes(1);
117 | expect(sendSpy).toHaveBeenCalledWith(50);
118 | });
119 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/02-input-output.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Input, Output } from '@angular/core';
2 |
3 | @Component({
4 | standalone: true,
5 | selector: 'atl-fixture',
6 | template: `
7 |
8 | {{ value }}
9 |
10 |
11 |
12 | `,
13 | })
14 | export class InputOutputComponent {
15 | @Input() value = 0;
16 | @Output() sendValue = new EventEmitter();
17 | }
18 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/03-forms.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { FormsComponent } from './03-forms';
5 |
6 | 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 () => {
7 | const user = userEvent.setup();
8 | await render(FormsComponent);
9 |
10 | const nameControl = screen.getByRole('textbox', { name: /name/i });
11 | const scoreControl = screen.getByRole('spinbutton', { name: /score/i });
12 | const colorControl = screen.getByRole('combobox', { name: /color/i });
13 | const errors = screen.getByRole('alert');
14 |
15 | expect(errors).toContainElement(screen.queryByText('name is required'));
16 | expect(errors).toContainElement(screen.queryByText('score must be greater than 1'));
17 | expect(errors).toContainElement(screen.queryByText('color is required'));
18 |
19 | expect(nameControl).toBeInvalid();
20 | await user.type(nameControl, 'Tim');
21 | await user.clear(scoreControl);
22 | await user.type(scoreControl, '12');
23 | fireEvent.blur(scoreControl);
24 | await user.selectOptions(colorControl, 'G');
25 |
26 | expect(screen.queryByText('name is required')).not.toBeInTheDocument();
27 | expect(screen.getByText('score must be lesser than 10')).toBeInTheDocument();
28 | expect(screen.queryByText('color is required')).not.toBeInTheDocument();
29 |
30 | expect(scoreControl).toBeInvalid();
31 | await user.clear(scoreControl);
32 | await user.type(scoreControl, '7');
33 | fireEvent.blur(scoreControl);
34 | expect(scoreControl).toBeValid();
35 |
36 | expect(errors).not.toBeInTheDocument();
37 |
38 | expect(nameControl).toHaveValue('Tim');
39 | expect(scoreControl).toHaveValue(7);
40 | expect(colorControl).toHaveValue('G');
41 |
42 | const form = screen.getByRole('form');
43 | expect(form).toHaveFormValues({
44 | name: 'Tim',
45 | score: 7,
46 | color: 'G',
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/03-forms.ts:
--------------------------------------------------------------------------------
1 | import { NgForOf, NgIf } from '@angular/common';
2 | import { Component } from '@angular/core';
3 | import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
4 |
5 | @Component({
6 | standalone: true,
7 | selector: 'atl-fixture',
8 | imports: [ReactiveFormsModule, NgForOf, NgIf],
9 | template: `
10 |
33 | `,
34 | })
35 | export class FormsComponent {
36 | colors = [
37 | { id: 'R', value: 'Red' },
38 | { id: 'B', value: 'Blue' },
39 | { id: 'G', value: 'Green' },
40 | ];
41 |
42 | form = this.formBuilder.group({
43 | name: ['', [Validators.required]],
44 | score: [0, { validators: [Validators.min(1), Validators.max(10)], updateOn: 'blur' }],
45 | color: [null as string | null, Validators.required],
46 | });
47 |
48 | constructor(private formBuilder: FormBuilder) {}
49 |
50 | get formErrors() {
51 | return Object.keys(this.form.controls)
52 | .map((formKey) => {
53 | const controlErrors = this.form.get(formKey)?.errors;
54 | if (controlErrors) {
55 | return Object.keys(controlErrors).map((keyError) => {
56 | const error = controlErrors[keyError];
57 | switch (keyError) {
58 | case 'required':
59 | return `${formKey} is required`;
60 | case 'min':
61 | return `${formKey} must be greater than ${error.min}`;
62 | case 'max':
63 | return `${formKey} must be lesser than ${error.max}`;
64 | default:
65 | return `${formKey} is invalid`;
66 | }
67 | });
68 | }
69 | return [];
70 | })
71 | .reduce((errors, value) => errors.concat(value), [])
72 | .filter(Boolean);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/04-forms-with-material.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { MaterialFormsComponent } from './04-forms-with-material';
5 |
6 | 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 () => {
7 | const user = userEvent.setup();
8 |
9 | const { fixture } = await render(MaterialFormsComponent);
10 |
11 | const nameControl = screen.getByLabelText(/name/i);
12 | const scoreControl = screen.getByRole('spinbutton', { name: /score/i });
13 | const colorControl = screen.getByPlaceholderText(/color/i);
14 | const dateControl = screen.getByRole('textbox', { name: /Choose a date/i });
15 | const checkboxControl = screen.getByRole('checkbox', { name: /agree/i });
16 |
17 | const errors = screen.getByRole('alert');
18 |
19 | expect(errors).toContainElement(screen.queryByText('name is required'));
20 | expect(errors).toContainElement(screen.queryByText('score must be greater than 1'));
21 | expect(errors).toContainElement(screen.queryByText('color is required'));
22 | expect(errors).toContainElement(screen.queryByText('agree is required'));
23 |
24 | await user.type(nameControl, 'Tim');
25 | await user.clear(scoreControl);
26 | await user.type(scoreControl, '12');
27 | await user.click(colorControl);
28 | await user.click(screen.getByText(/green/i));
29 |
30 | expect(checkboxControl).not.toBeChecked();
31 | await user.click(checkboxControl);
32 | expect(checkboxControl).toBeChecked();
33 | expect(checkboxControl).toBeValid();
34 |
35 | expect(screen.queryByText('name is required')).not.toBeInTheDocument();
36 | expect(screen.getByText('score must be lesser than 10')).toBeInTheDocument();
37 | expect(screen.queryByText('color is required')).not.toBeInTheDocument();
38 | expect(screen.queryByText('agree is required')).not.toBeInTheDocument();
39 |
40 | expect(scoreControl).toBeInvalid();
41 | await user.clear(scoreControl);
42 | await user.type(scoreControl, '7');
43 | expect(scoreControl).toBeValid();
44 |
45 | await user.type(dateControl, '08/11/2022');
46 |
47 | expect(errors).not.toBeInTheDocument();
48 |
49 | expect(nameControl).toHaveValue('Tim');
50 | expect(scoreControl).toHaveValue(7);
51 | expect(colorControl).toHaveTextContent('Green');
52 | expect(checkboxControl).toBeChecked();
53 |
54 | const form = screen.getByRole('form');
55 | expect(form).toHaveFormValues({
56 | name: 'Tim',
57 | score: 7,
58 | });
59 |
60 | // material doesn't add these to the form
61 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('agree')?.value).toBe(true);
62 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('color')?.value).toBe('G');
63 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('date')?.value).toEqual(new Date(2022, 7, 11));
64 | });
65 |
66 | test('set and show pre-set form values', async () => {
67 | const user = userEvent.setup();
68 |
69 | const { fixture, detectChanges } = await render(MaterialFormsComponent);
70 |
71 | fixture.componentInstance.form.setValue({
72 | name: 'Max',
73 | score: 4,
74 | color: 'B',
75 | date: new Date(2022, 7, 11),
76 | agree: true,
77 | });
78 | detectChanges();
79 |
80 | const nameControl = screen.getByLabelText(/name/i);
81 | const scoreControl = screen.getByRole('spinbutton', { name: /score/i });
82 | const colorControl = screen.getByPlaceholderText(/color/i);
83 | const checkboxControl = screen.getByRole('checkbox', { name: /agree/i });
84 |
85 | expect(nameControl).toHaveValue('Max');
86 | expect(scoreControl).toHaveValue(4);
87 | expect(colorControl).toHaveTextContent('Blue');
88 | expect(checkboxControl).toBeChecked();
89 | await user.click(checkboxControl);
90 |
91 | const form = screen.getByRole('form');
92 | expect(form).toHaveFormValues({
93 | name: 'Max',
94 | score: 4,
95 | });
96 |
97 | // material doesn't add these to the form
98 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('agree')?.value).toBe(false);
99 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('color')?.value).toBe('B');
100 | expect((fixture.componentInstance as MaterialFormsComponent).form?.get('date')?.value).toEqual(new Date(2022, 7, 11));
101 | });
102 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/04-forms-with-material.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3 | import { NgForOf, NgIf } from '@angular/common';
4 | import { MatCheckboxModule } from '@angular/material/checkbox';
5 | import { MatInputModule } from '@angular/material/input';
6 | import { MatSelectModule } from '@angular/material/select';
7 | import { MatDatepickerModule } from '@angular/material/datepicker';
8 | import { MatNativeDateModule } from '@angular/material/core';
9 | @Component({
10 | standalone: true,
11 | imports: [
12 | MatInputModule,
13 | MatSelectModule,
14 | MatDatepickerModule,
15 | MatNativeDateModule,
16 | MatCheckboxModule,
17 | ReactiveFormsModule,
18 | NgForOf,
19 | NgIf,
20 | ],
21 | selector: 'atl-fixture',
22 | template: `
23 |
68 | `,
69 | styles: [
70 | `
71 | form {
72 | display: flex;
73 | flex-direction: column;
74 | }
75 |
76 | form > * {
77 | width: 100%;
78 | }
79 |
80 | [role='alert'] {
81 | color: red;
82 | }
83 | `,
84 | ],
85 | })
86 | export class MaterialFormsComponent {
87 | colors = [
88 | { id: 'R', value: 'Red' },
89 | { id: 'B', value: 'Blue' },
90 | { id: 'G', value: 'Green' },
91 | ];
92 | form = this.formBuilder.group({
93 | name: ['', [Validators.required]],
94 | score: [0, [Validators.min(1), Validators.max(10)]],
95 | color: [null as string | null, Validators.required],
96 | date: [null as Date | null, Validators.required],
97 | agree: [false, Validators.requiredTrue],
98 | });
99 |
100 | constructor(private formBuilder: FormBuilder) {}
101 |
102 | get colorControlDisplayValue(): string | undefined {
103 | const selectedId = this.form.get('color')?.value;
104 | return this.colors.filter((color) => color.id === selectedId)[0]?.value;
105 | }
106 |
107 | get formErrors() {
108 | return Object.keys(this.form.controls)
109 | .map((formKey) => {
110 | const controlErrors = this.form.get(formKey)?.errors;
111 | if (controlErrors) {
112 | return Object.keys(controlErrors).map((keyError) => {
113 | const error = controlErrors[keyError];
114 | switch (keyError) {
115 | case 'required':
116 | return `${formKey} is required`;
117 | case 'min':
118 | return `${formKey} must be greater than ${error.min}`;
119 | case 'max':
120 | return `${formKey} must be lesser than ${error.max}`;
121 | default:
122 | return `${formKey} is invalid`;
123 | }
124 | });
125 | }
126 | return [];
127 | })
128 | .reduce((errors, value) => errors.concat(value), [])
129 | .filter(Boolean);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/05-component-provider.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { render, screen } from '@testing-library/angular';
3 | import { provideMock, Mock, createMock } from '@testing-library/angular/jest-utils';
4 | import userEvent from '@testing-library/user-event';
5 |
6 | import { ComponentWithProviderComponent, CounterService } from './05-component-provider';
7 |
8 | test('renders the current value and can increment and decrement', async () => {
9 | const user = userEvent.setup();
10 |
11 | await render(ComponentWithProviderComponent, {
12 | componentProviders: [
13 | {
14 | provide: CounterService,
15 | useValue: new CounterService(),
16 | },
17 | ],
18 | });
19 |
20 | const incrementControl = screen.getByRole('button', { name: /increment/i });
21 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
22 | const valueControl = screen.getByTestId('value');
23 |
24 | expect(valueControl).toHaveTextContent('0');
25 |
26 | await user.click(incrementControl);
27 | await user.click(incrementControl);
28 | expect(valueControl).toHaveTextContent('2');
29 |
30 | await user.click(decrementControl);
31 | expect(valueControl).toHaveTextContent('1');
32 | });
33 |
34 | test('renders the current value and can increment and decrement with a mocked jest-utils service', async () => {
35 | const user = userEvent.setup();
36 |
37 | const counter = createMock(CounterService);
38 | let fakeCounterValue = 50;
39 | counter.increment.mockImplementation(() => (fakeCounterValue += 10));
40 | counter.decrement.mockImplementation(() => (fakeCounterValue -= 10));
41 | counter.value.mockImplementation(() => fakeCounterValue);
42 |
43 | await render(ComponentWithProviderComponent, {
44 | componentProviders: [
45 | {
46 | provide: CounterService,
47 | useValue: counter,
48 | },
49 | ],
50 | });
51 |
52 | const incrementControl = screen.getByRole('button', { name: /increment/i });
53 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
54 | const valueControl = screen.getByTestId('value');
55 |
56 | expect(valueControl).toHaveTextContent('50');
57 |
58 | await user.click(incrementControl);
59 | await user.click(incrementControl);
60 | expect(valueControl).toHaveTextContent('70');
61 |
62 | await user.click(decrementControl);
63 | expect(valueControl).toHaveTextContent('60');
64 | });
65 |
66 | test('renders the current value and can increment and decrement with provideMocked from jest-utils', async () => {
67 | const user = userEvent.setup();
68 |
69 | await render(ComponentWithProviderComponent, {
70 | componentProviders: [provideMock(CounterService)],
71 | });
72 |
73 | const incrementControl = screen.getByRole('button', { name: /increment/i });
74 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
75 |
76 | await user.click(incrementControl);
77 | await user.click(incrementControl);
78 | await user.click(decrementControl);
79 |
80 | const counterService = TestBed.inject(CounterService) as Mock;
81 | expect(counterService.increment).toHaveBeenCalledTimes(2);
82 | expect(counterService.decrement).toHaveBeenCalledTimes(1);
83 | });
84 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/05-component-provider.ts:
--------------------------------------------------------------------------------
1 | import { Component, Injectable } from '@angular/core';
2 |
3 | @Injectable({
4 | providedIn: 'root',
5 | })
6 | export class CounterService {
7 | private _value = 0;
8 |
9 | increment() {
10 | this._value += 1;
11 | }
12 |
13 | decrement() {
14 | this._value -= 1;
15 | }
16 |
17 | value() {
18 | return this._value;
19 | }
20 | }
21 |
22 | @Component({
23 | standalone: true,
24 | selector: 'atl-fixture',
25 | template: `
26 |
27 | {{ counter.value() }}
28 |
29 | `,
30 | providers: [CounterService],
31 | })
32 | export class ComponentWithProviderComponent {
33 | constructor(public counter: CounterService) {}
34 | }
35 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/06-with-ngrx-store.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { StoreModule } from '@ngrx/store';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { WithNgRxStoreComponent, reducer } from './06-with-ngrx-store';
6 |
7 | test('works with ngrx store', async () => {
8 | const user = userEvent.setup();
9 |
10 | await render(WithNgRxStoreComponent, {
11 | imports: [
12 | StoreModule.forRoot(
13 | {
14 | value: reducer,
15 | },
16 | {
17 | runtimeChecks: {},
18 | },
19 | ),
20 | ],
21 | });
22 |
23 | const incrementControl = screen.getByRole('button', { name: /increment/i });
24 | const decrementControl = screen.getByRole('button', { name: /decrement/i });
25 | const valueControl = screen.getByTestId('value');
26 |
27 | expect(valueControl).toHaveTextContent('0');
28 |
29 | await user.click(incrementControl);
30 | await user.click(incrementControl);
31 | expect(valueControl).toHaveTextContent('20');
32 |
33 | await user.click(decrementControl);
34 | expect(valueControl).toHaveTextContent('10');
35 | });
36 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/06-with-ngrx-store.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component } from '@angular/core';
3 | import { createSelector, Store, createAction, createReducer, on, select } from '@ngrx/store';
4 |
5 | const increment = createAction('increment');
6 | const decrement = createAction('decrement');
7 | export const reducer = createReducer(
8 | 0,
9 | on(increment, (state) => state + 1),
10 | on(decrement, (state) => state - 1),
11 | );
12 |
13 | const selectValue = createSelector(
14 | (state: any) => state.value,
15 | (value) => value * 10,
16 | );
17 |
18 | @Component({
19 | standalone: true,
20 | imports: [AsyncPipe],
21 | selector: 'atl-fixture',
22 | template: `
23 |
24 | {{ value | async }}
25 |
26 | `,
27 | })
28 | export class WithNgRxStoreComponent {
29 | value = this.store.pipe(select(selectValue));
30 | constructor(private store: Store) {}
31 |
32 | increment() {
33 | this.store.dispatch(increment());
34 | }
35 |
36 | decrement() {
37 | this.store.dispatch(decrement());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/07-with-ngrx-mock-store.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { provideMockStore, MockStore } from '@ngrx/store/testing';
3 | import { render, screen } from '@testing-library/angular';
4 | import userEvent from '@testing-library/user-event';
5 |
6 | import { WithNgRxMockStoreComponent, selectItems } from './07-with-ngrx-mock-store';
7 |
8 | test('works with provideMockStore', async () => {
9 | const user = userEvent.setup();
10 |
11 | await render(WithNgRxMockStoreComponent, {
12 | providers: [
13 | provideMockStore({
14 | selectors: [
15 | {
16 | selector: selectItems,
17 | value: ['Four', 'Seven'],
18 | },
19 | ],
20 | }),
21 | ],
22 | });
23 |
24 | const store = TestBed.inject(MockStore);
25 | store.dispatch = jest.fn();
26 |
27 | await user.click(screen.getByText(/seven/i));
28 |
29 | expect(store.dispatch).toHaveBeenCalledWith({ type: '[Item List] send', item: 'Seven' });
30 | });
31 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe, NgForOf } from '@angular/common';
2 | import { Component } from '@angular/core';
3 | import { createSelector, Store, select } from '@ngrx/store';
4 |
5 | export const selectItems = createSelector(
6 | (state: any) => state.items,
7 | (items) => items,
8 | );
9 |
10 | @Component({
11 | standalone: true,
12 | imports: [AsyncPipe, NgForOf],
13 | selector: 'atl-fixture',
14 | template: `
15 |
16 | -
17 |
18 |
19 |
20 | `,
21 | })
22 | export class WithNgRxMockStoreComponent {
23 | items = this.store.pipe(select(selectItems));
24 | constructor(private store: Store) {}
25 |
26 | send(item: string) {
27 | this.store.dispatch({ type: '[Item List] send', item });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/08-directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, screen } from '@testing-library/angular';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { SpoilerDirective } from './08-directive';
6 |
7 | test('it is possible to test directives with container component', async () => {
8 | @Component({
9 | template: ``,
10 | imports: [SpoilerDirective],
11 | standalone: true,
12 | })
13 | class FixtureComponent {}
14 |
15 | const user = userEvent.setup();
16 | await render(FixtureComponent);
17 |
18 | const directive = screen.getByTestId('dir');
19 |
20 | expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
21 | expect(screen.getByText('SPOILER')).toBeInTheDocument();
22 |
23 | await user.hover(directive);
24 | expect(screen.queryByText('SPOILER')).not.toBeInTheDocument();
25 | expect(screen.getByText('I am visible now...')).toBeInTheDocument();
26 |
27 | await user.unhover(directive);
28 | expect(screen.getByText('SPOILER')).toBeInTheDocument();
29 | expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
30 | });
31 |
32 | test('it is possible to test directives', async () => {
33 | const user = userEvent.setup();
34 |
35 | await render('', {
36 | imports: [SpoilerDirective],
37 | });
38 |
39 | const directive = screen.getByTestId('dir');
40 |
41 | expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
42 | expect(screen.getByText('SPOILER')).toBeInTheDocument();
43 |
44 | await user.hover(directive);
45 | expect(screen.queryByText('SPOILER')).not.toBeInTheDocument();
46 | expect(screen.getByText('I am visible now...')).toBeInTheDocument();
47 |
48 | await user.unhover(directive);
49 | expect(screen.getByText('SPOILER')).toBeInTheDocument();
50 | expect(screen.queryByText('I am visible now...')).not.toBeInTheDocument();
51 | });
52 |
53 | test('it is possible to test directives with props', async () => {
54 | const user = userEvent.setup();
55 | const hidden = 'SPOILER ALERT';
56 | const visible = 'There is nothing to see here ...';
57 |
58 | await render('', {
59 | imports: [SpoilerDirective],
60 | componentProperties: {
61 | hidden,
62 | visible,
63 | },
64 | });
65 |
66 | expect(screen.queryByText(visible)).not.toBeInTheDocument();
67 | expect(screen.getByText(hidden)).toBeInTheDocument();
68 |
69 | await user.hover(screen.getByText(hidden));
70 | expect(screen.queryByText(hidden)).not.toBeInTheDocument();
71 | expect(screen.getByText(visible)).toBeInTheDocument();
72 |
73 | await user.unhover(screen.getByText(visible));
74 | expect(screen.getByText(hidden)).toBeInTheDocument();
75 | expect(screen.queryByText(visible)).not.toBeInTheDocument();
76 | });
77 |
78 | test('it is possible to test directives with props in template', async () => {
79 | const user = userEvent.setup();
80 | const hidden = 'SPOILER ALERT';
81 | const visible = 'There is nothing to see here ...';
82 |
83 | await render(``, {
84 | imports: [SpoilerDirective],
85 | });
86 |
87 | expect(screen.queryByText(visible)).not.toBeInTheDocument();
88 | expect(screen.getByText(hidden)).toBeInTheDocument();
89 |
90 | await user.hover(screen.getByText(hidden));
91 | expect(screen.queryByText(hidden)).not.toBeInTheDocument();
92 | expect(screen.getByText(visible)).toBeInTheDocument();
93 |
94 | await user.unhover(screen.getByText(visible));
95 | expect(screen.getByText(hidden)).toBeInTheDocument();
96 | expect(screen.queryByText(visible)).not.toBeInTheDocument();
97 | });
98 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/08-directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, HostListener, ElementRef, Input, OnInit } from '@angular/core';
2 |
3 | @Directive({
4 | standalone: true,
5 | selector: '[atlSpoiler]',
6 | })
7 | export class SpoilerDirective implements OnInit {
8 | @Input() hidden = 'SPOILER';
9 | @Input() visible = 'I am visible now...';
10 |
11 | constructor(private el: ElementRef) {}
12 |
13 | ngOnInit() {
14 | this.el.nativeElement.textContent = this.hidden;
15 | }
16 |
17 | @HostListener('mouseover')
18 | onMouseOver() {
19 | this.el.nativeElement.textContent = this.visible;
20 | }
21 |
22 | @HostListener('mouseleave')
23 | onMouseLeave() {
24 | this.el.nativeElement.textContent = this.hidden;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/09-router.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { DetailComponent, RootComponent, HiddenDetailComponent } from './09-router';
5 |
6 | test('it can navigate to routes', async () => {
7 | const user = userEvent.setup();
8 | await render(RootComponent, {
9 | routes: [
10 | {
11 | path: '',
12 | children: [
13 | {
14 | path: 'detail/:id',
15 | component: DetailComponent,
16 | },
17 | {
18 | path: 'hidden-detail',
19 | component: HiddenDetailComponent,
20 | },
21 | ],
22 | },
23 | ],
24 | });
25 |
26 | expect(screen.queryByText(/Detail one/i)).not.toBeInTheDocument();
27 |
28 | await user.click(screen.getByRole('link', { name: /load one/i }));
29 | expect(await screen.findByRole('heading', { name: /Detail one/i })).toBeInTheDocument();
30 |
31 | await user.click(screen.getByRole('link', { name: /load three/i }));
32 | expect(screen.queryByRole('heading', { name: /Detail one/i })).not.toBeInTheDocument();
33 | expect(await screen.findByRole('heading', { name: /Detail three/i })).toBeInTheDocument();
34 |
35 | await user.click(screen.getByRole('link', { name: /back to parent/i }));
36 | expect(screen.queryByRole('heading', { name: /Detail three/i })).not.toBeInTheDocument();
37 |
38 | await user.click(screen.getByRole('link', { name: /load two/i }));
39 | expect(await screen.findByRole('heading', { name: /Detail two/i })).toBeInTheDocument();
40 |
41 | await user.click(screen.getByRole('link', { name: /hidden x/i }));
42 | expect(await screen.findByText(/You found the treasure!/i)).toBeInTheDocument();
43 | });
44 |
45 | test('it can navigate to routes - workaround', async () => {
46 | const { navigate } = await render(RootComponent, {
47 | routes: [
48 | {
49 | path: '',
50 | children: [
51 | {
52 | path: 'detail/:id',
53 | component: DetailComponent,
54 | },
55 | {
56 | path: 'hidden-detail',
57 | component: HiddenDetailComponent,
58 | },
59 | ],
60 | },
61 | ],
62 | });
63 |
64 | expect(screen.queryByText(/Detail one/i)).not.toBeInTheDocument();
65 |
66 | await navigate(screen.getByRole('link', { name: /load one/i }));
67 | expect(screen.getByRole('heading', { name: /Detail one/i })).toBeInTheDocument();
68 |
69 | await navigate(screen.getByRole('link', { name: /load three/i }));
70 | expect(screen.queryByRole('heading', { name: /Detail one/i })).not.toBeInTheDocument();
71 | expect(screen.getByRole('heading', { name: /Detail three/i })).toBeInTheDocument();
72 |
73 | await navigate(screen.getByRole('link', { name: /back to parent/i }));
74 | expect(screen.queryByRole('heading', { name: /Detail three/i })).not.toBeInTheDocument();
75 |
76 | await navigate(screen.getByRole('link', { name: /load two/i }));
77 | expect(screen.getByRole('heading', { name: /Detail two/i })).toBeInTheDocument();
78 | await navigate(screen.getByRole('link', { name: /hidden x/i }));
79 | expect(screen.getByText(/You found the treasure!/i)).toBeInTheDocument();
80 | });
81 |
82 | test('it can navigate to routes with a base path', async () => {
83 | const basePath = 'base';
84 | const { navigate } = await render(RootComponent, {
85 | routes: [
86 | {
87 | path: basePath,
88 | children: [
89 | {
90 | path: 'detail/:id',
91 | component: DetailComponent,
92 | },
93 | {
94 | path: 'hidden-detail',
95 | component: HiddenDetailComponent,
96 | },
97 | ],
98 | },
99 | ],
100 | });
101 |
102 | expect(screen.queryByRole('heading', { name: /Detail one/i })).not.toBeInTheDocument();
103 |
104 | await navigate(screen.getByRole('link', { name: /load one/i }), basePath);
105 | expect(screen.getByRole('heading', { name: /Detail one/i })).toBeInTheDocument();
106 |
107 | await navigate(screen.getByRole('link', { name: /load three/i }), basePath);
108 | expect(screen.queryByRole('heading', { name: /Detail one/i })).not.toBeInTheDocument();
109 | expect(screen.getByRole('heading', { name: /Detail three/i })).toBeInTheDocument();
110 |
111 | await navigate(screen.getByRole('link', { name: /back to parent/i }));
112 | expect(screen.queryByRole('heading', { name: /Detail three/i })).not.toBeInTheDocument();
113 |
114 | // It's possible to just use strings
115 | await navigate('base/detail/two?text=Hello&subtext=World');
116 | expect(screen.getByRole('heading', { name: /Detail two/i })).toBeInTheDocument();
117 | expect(screen.getByText(/Hello World/i)).toBeInTheDocument();
118 |
119 | await navigate('/hidden-detail', basePath);
120 | expect(screen.getByText(/You found the treasure!/i)).toBeInTheDocument();
121 | });
122 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/09-router.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component } from '@angular/core';
3 | import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
4 | import { map } from 'rxjs/operators';
5 |
6 | @Component({
7 | standalone: true,
8 | imports: [RouterLink, RouterOutlet],
9 | selector: 'atl-main',
10 | template: `
11 | Load one | Load two |
12 | Load three |
13 |
14 |
15 |
16 |
17 | `,
18 | })
19 | export class RootComponent {}
20 |
21 | @Component({
22 | standalone: true,
23 | imports: [RouterLink, AsyncPipe],
24 | selector: 'atl-detail',
25 | template: `
26 | Detail {{ id | async }}
27 |
28 | {{ text | async }} {{ subtext | async }}
29 |
30 | Back to parent
31 | hidden x
32 | `,
33 | })
34 | export class DetailComponent {
35 | id = this.route.paramMap.pipe(map((params) => params.get('id')));
36 | text = this.route.queryParams.pipe(map((params) => params['text']));
37 | subtext = this.route.queryParams.pipe(map((params) => params['subtext']));
38 | constructor(private route: ActivatedRoute) {}
39 | }
40 |
41 | @Component({
42 | standalone: true,
43 | selector: 'atl-detail-hidden',
44 | template: ' You found the treasure! ',
45 | })
46 | export class HiddenDetailComponent {}
47 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/10-inject-token-dependency.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 |
3 | import { DataInjectedComponent, DATA } from './10-inject-token-dependency';
4 |
5 | test('injects data into the component', async () => {
6 | await render(DataInjectedComponent, {
7 | providers: [
8 | {
9 | provide: DATA,
10 | useValue: { text: 'Hello boys and girls' },
11 | },
12 | ],
13 | });
14 |
15 | expect(screen.getByText(/Hello boys and girls/i)).toBeInTheDocument();
16 | });
17 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/10-inject-token-dependency.ts:
--------------------------------------------------------------------------------
1 | import { Component, InjectionToken, Inject } from '@angular/core';
2 |
3 | export const DATA = new InjectionToken<{ text: string }>('Components Data');
4 |
5 | @Component({
6 | standalone: true,
7 | selector: 'atl-fixture',
8 | template: ' {{ data.text }} ',
9 | })
10 | export class DataInjectedComponent {
11 | constructor(@Inject(DATA) public data: { text: string }) {}
12 | }
13 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/11-ng-content.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 |
3 | import { CellComponent } from './11-ng-content';
4 |
5 | test('it is possible to test ng-content without selector', async () => {
6 | const projection = 'it should be showed into a p element!';
7 |
8 | await render(`${projection}`, {
9 | imports: [CellComponent],
10 | });
11 |
12 | expect(screen.getByText(projection)).toBeInTheDocument();
13 | expect(screen.getByTestId('one-cell-with-ng-content')).toContainHTML(`${projection}
`);
14 | });
15 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/11-ng-content.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 |
3 | @Component({
4 | standalone: true,
5 | selector: 'atl-fixture',
6 | template: `
7 |
8 |
9 |
10 | `,
11 | changeDetection: ChangeDetectionStrategy.OnPush,
12 | })
13 | export class CellComponent {}
14 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/12-service-component.spec.ts:
--------------------------------------------------------------------------------
1 | import { of } from 'rxjs';
2 | import { render, screen } from '@testing-library/angular';
3 | import { createMock } from '@testing-library/angular/jest-utils';
4 |
5 | import { Customer, CustomersComponent, CustomersService } from './12-service-component';
6 |
7 | test('renders the provided customers with manual mock', async () => {
8 | const customers: Customer[] = [
9 | {
10 | id: '1',
11 | name: 'sarah',
12 | },
13 | {
14 | id: '2',
15 | name: 'charlotte',
16 | },
17 | ];
18 | await render(CustomersComponent, {
19 | componentProviders: [
20 | {
21 | provide: CustomersService,
22 | useValue: {
23 | load() {
24 | return of(customers);
25 | },
26 | },
27 | },
28 | ],
29 | });
30 |
31 | const listItems = screen.getAllByRole('listitem');
32 | expect(listItems).toHaveLength(customers.length);
33 |
34 | customers.forEach((customer) => screen.getByText(new RegExp(customer.name, 'i')));
35 | });
36 |
37 | test('renders the provided customers with createMock', async () => {
38 | const customers: Customer[] = [
39 | {
40 | id: '1',
41 | name: 'sarah',
42 | },
43 | {
44 | id: '2',
45 | name: 'charlotte',
46 | },
47 | ];
48 |
49 | const customersService = createMock(CustomersService);
50 | customersService.load = jest.fn(() => of(customers));
51 |
52 | await render(CustomersComponent, {
53 | componentProviders: [
54 | {
55 | provide: CustomersService,
56 | useValue: customersService,
57 | },
58 | ],
59 | });
60 |
61 | const listItems = screen.getAllByRole('listitem');
62 | expect(listItems).toHaveLength(customers.length);
63 |
64 | customers.forEach((customer) => screen.getByText(new RegExp(customer.name, 'i')));
65 | });
66 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/12-service-component.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe, NgForOf } from '@angular/common';
2 | import { Component, Injectable } from '@angular/core';
3 | import { Observable, of } from 'rxjs';
4 |
5 | export class Customer {
6 | id!: string;
7 | name!: string;
8 | }
9 |
10 | @Injectable({
11 | providedIn: 'root',
12 | })
13 | export class CustomersService {
14 | load(): Observable {
15 | return of([]);
16 | }
17 | }
18 |
19 | @Component({
20 | standalone: true,
21 | imports: [AsyncPipe, NgForOf],
22 | selector: 'atl-fixture',
23 | template: `
24 |
25 | -
26 | {{ customer.name }}
27 |
28 |
29 | `,
30 | })
31 | export class CustomersComponent {
32 | customers$ = this.service.load();
33 | constructor(private service: CustomersService) {}
34 | }
35 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/13-scrolling.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen, waitForElementToBeRemoved } from '@testing-library/angular';
2 |
3 | import { CdkVirtualScrollOverviewExampleComponent } from './13-scrolling.component';
4 |
5 | test('should scroll to load more items', async () => {
6 | await render(CdkVirtualScrollOverviewExampleComponent);
7 |
8 | const item0 = await screen.findByText(/Item #0/i);
9 | expect(item0).toBeVisible();
10 |
11 | screen.getByTestId('scroll-viewport').scrollTop = 500;
12 | await waitForElementToBeRemoved(() => screen.queryByText(/Item #0/i));
13 |
14 | const item12 = await screen.findByText(/Item #12/i);
15 | expect(item12).toBeVisible();
16 | });
17 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/13-scrolling.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { ScrollingModule } from '@angular/cdk/scrolling';
3 |
4 | @Component({
5 | standalone: true,
6 | imports: [ScrollingModule],
7 | selector: 'atl-cdk-virtual-scroll-overview-example',
8 | template: `
9 |
10 | {{ item }}
11 |
12 | `,
13 | styles: [
14 | `
15 | .example-viewport {
16 | height: 200px;
17 | width: 200px;
18 | border: 1px solid black;
19 | }
20 |
21 | .example-item {
22 | height: 50px;
23 | }
24 | `,
25 | ],
26 | changeDetection: ChangeDetectionStrategy.OnPush,
27 | })
28 | export class CdkVirtualScrollOverviewExampleComponent {
29 | items = Array.from({ length: 100 }).map((_, i) => `Item #${i}`);
30 | }
31 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/14-async-component.spec.ts:
--------------------------------------------------------------------------------
1 | import { fakeAsync, tick } from '@angular/core/testing';
2 | import { render, screen, fireEvent } from '@testing-library/angular';
3 |
4 | import { AsyncComponent } from './14-async-component';
5 |
6 | test.skip('can use fakeAsync utilities', fakeAsync(async () => {
7 | await render(AsyncComponent);
8 |
9 | const load = await screen.findByRole('button', { name: /load/i });
10 | fireEvent.click(load);
11 |
12 | tick(10_000);
13 |
14 | const hello = await screen.findByText('Hello world');
15 | expect(hello).toBeInTheDocument();
16 | }));
17 |
18 | test('can use fakeTimer utilities', async () => {
19 | jest.useFakeTimers();
20 | await render(AsyncComponent);
21 |
22 | const load = await screen.findByRole('button', { name: /load/i });
23 |
24 | // userEvent not working with fake timers
25 | fireEvent.click(load);
26 |
27 | jest.advanceTimersByTime(10_000);
28 |
29 | const hello = await screen.findByText('Hello world');
30 | expect(hello).toBeInTheDocument();
31 | });
32 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/14-async-component.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe, NgIf } from '@angular/common';
2 | import { Component, OnDestroy } from '@angular/core';
3 | import { Subject } from 'rxjs';
4 | import { delay, filter, mapTo } from 'rxjs/operators';
5 |
6 | @Component({
7 | standalone: true,
8 | imports: [AsyncPipe, NgIf],
9 | selector: 'atl-fixture',
10 | template: `
11 |
12 | {{ data }}
13 | `,
14 | })
15 | export class AsyncComponent implements OnDestroy {
16 | actions = new Subject();
17 | data$ = this.actions.pipe(
18 | filter((x) => x === 'LOAD'),
19 | mapTo('Hello world'),
20 | delay(10_000),
21 | );
22 |
23 | load() {
24 | this.actions.next('LOAD');
25 | }
26 |
27 | ngOnDestroy() {
28 | this.actions.complete();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/15-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { MatDialogRef } from '@angular/material/dialog';
2 | import { render, screen } from '@testing-library/angular';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { DialogComponent, DialogContentComponent } from './15-dialog.component';
6 |
7 | test('dialog closes', async () => {
8 | const user = userEvent.setup();
9 |
10 | const closeFn = jest.fn();
11 | await render(DialogContentComponent, {
12 | providers: [
13 | {
14 | provide: MatDialogRef,
15 | useValue: {
16 | close: closeFn,
17 | },
18 | },
19 | ],
20 | });
21 |
22 | const cancelButton = await screen.findByRole('button', { name: /cancel/i });
23 | await user.click(cancelButton);
24 |
25 | expect(closeFn).toHaveBeenCalledTimes(1);
26 | });
27 |
28 | test('closes the dialog via the backdrop', async () => {
29 | const user = userEvent.setup();
30 |
31 | await render(DialogComponent);
32 |
33 | const openDialogButton = await screen.findByRole('button', { name: /open dialog/i });
34 | await user.click(openDialogButton);
35 |
36 | const dialogControl = await screen.findByRole('dialog');
37 | expect(dialogControl).toBeInTheDocument();
38 | const dialogTitleControl = await screen.findByRole('heading', { name: /dialog title/i });
39 | expect(dialogTitleControl).toBeInTheDocument();
40 |
41 | // eslint-disable-next-line testing-library/no-node-access
42 | await user.click(document.querySelector('.cdk-overlay-backdrop')!);
43 |
44 | expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
45 |
46 | const dialogTitle = screen.queryByRole('heading', { name: /dialog title/i });
47 | expect(dialogTitle).not.toBeInTheDocument();
48 | });
49 |
50 | test('opens and closes the dialog with buttons', async () => {
51 | const user = userEvent.setup();
52 |
53 | await render(DialogComponent);
54 |
55 | const openDialogButton = await screen.findByRole('button', { name: /open dialog/i });
56 | await user.click(openDialogButton);
57 |
58 | const dialogControl = await screen.findByRole('dialog');
59 | expect(dialogControl).toBeInTheDocument();
60 | const dialogTitleControl = await screen.findByRole('heading', { name: /dialog title/i });
61 | expect(dialogTitleControl).toBeInTheDocument();
62 |
63 | const cancelButton = await screen.findByRole('button', { name: /cancel/i });
64 | await user.click(cancelButton);
65 |
66 | expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
67 |
68 | const dialogTitle = screen.queryByRole('heading', { name: /dialog title/i });
69 | expect(dialogTitle).not.toBeInTheDocument();
70 | });
71 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/15-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
3 |
4 | @Component({
5 | standalone: true,
6 | imports: [MatDialogModule],
7 | selector: 'atl-dialog-overview-example',
8 | template: '',
9 | })
10 | export class DialogComponent {
11 | constructor(public dialog: MatDialog) {}
12 |
13 | openDialog(): void {
14 | this.dialog.open(DialogContentComponent);
15 | }
16 | }
17 |
18 | @Component({
19 | standalone: true,
20 | imports: [MatDialogModule],
21 | selector: 'atl-dialog-overview-example-dialog',
22 | template: `
23 | Dialog Title
24 | Dialog content
25 |
26 |
27 |
28 |
29 | `,
30 | })
31 | export class DialogContentComponent {
32 | constructor(public dialogRef: MatDialogRef) {}
33 |
34 | cancel(): void {
35 | this.dialogRef.close();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { InputGetterSetter } from './16-input-getter-setter';
3 |
4 | test('should run logic in the input setter and getter', async () => {
5 | await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
6 | const valueControl = screen.getByTestId('value');
7 | const getterValueControl = screen.getByTestId('value-getter');
8 |
9 | expect(valueControl).toHaveTextContent('I am value from setter Angular');
10 | expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
11 | });
12 |
13 | test('should run logic in the input setter and getter while re-rendering', async () => {
14 | const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
15 |
16 | expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter Angular');
17 | expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter Angular');
18 |
19 | await rerender({ componentProperties: { value: 'React' } });
20 |
21 | // note we have to re-query because the elements are not the same anymore
22 | expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter React');
23 | expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter React');
24 | });
25 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/16-input-getter-setter.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | standalone: true,
5 | selector: 'atl-fixture',
6 | template: `
7 | {{ derivedValue }}
8 | {{ value }}
9 | `,
10 | })
11 | // eslint-disable-next-line @angular-eslint/component-class-suffix
12 | export class InputGetterSetter {
13 | @Input() set value(value: string) {
14 | this.originalValue = value;
15 | this.derivedValue = 'I am value from setter ' + value;
16 | }
17 |
18 | get value() {
19 | return 'I am value from getter ' + this.originalValue;
20 | }
21 |
22 | private originalValue?: string;
23 | derivedValue?: string;
24 | }
25 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/17-component-with-attribute-selector.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { ComponentWithAttributeSelectorComponent } from './17-component-with-attribute-selector';
3 |
4 | // Note: At this stage it is not possible to use the render(ComponentWithAttributeSelectorComponent, {...}) syntax
5 | // for components with attribute selectors!
6 | test('is possible to set input of component with attribute selector through template', async () => {
7 | await render(
8 | ``,
9 | {
10 | imports: [ComponentWithAttributeSelectorComponent],
11 | },
12 | );
13 |
14 | const valueControl = screen.getByTestId('value');
15 |
16 | expect(valueControl).toHaveTextContent('42');
17 | });
18 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/17-component-with-attribute-selector.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | standalone: true,
5 | selector: 'atl-fixture-component-with-attribute-selector[value]',
6 | template: ` {{ value }} `,
7 | })
8 | export class ComponentWithAttributeSelectorComponent {
9 | @Input() value!: number;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/18-html-as-input.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { Pipe, PipeTransform } from '@angular/core';
3 |
4 | @Pipe({
5 | standalone: true,
6 | name: 'stripHTML',
7 | })
8 | class StripHTMLPipe implements PipeTransform {
9 | transform(stringValueWithHTML: string): string {
10 | return stringValueWithHTML.replace(/<[^>]*>?/gm, '');
11 | }
12 | }
13 |
14 | const STRING_WITH_HTML =
15 | 'Some database field with stripped HTML
';
16 |
17 | // https://github.com/testing-library/angular-testing-library/pull/271
18 | test('passes HTML as component properties', async () => {
19 | await render(`{{ stringWithHtml | stripHTML }}
`, {
20 | componentProperties: {
21 | stringWithHtml: STRING_WITH_HTML,
22 | },
23 | imports: [StripHTMLPipe],
24 | });
25 |
26 | expect(screen.getByText('Some database field with stripped HTML')).toBeInTheDocument();
27 | });
28 |
29 | test('throws when passed HTML is passed in directly', async () => {
30 | await expect(() =>
31 | render(` {{ '${STRING_WITH_HTML}' | stripHTML }}
`, {
32 | imports: [StripHTMLPipe],
33 | }),
34 | ).rejects.toThrow();
35 | });
36 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/19-standalone-component.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { StandaloneComponent, StandaloneWithChildComponent } from './19-standalone-component';
3 |
4 | test('can render a standalone component', async () => {
5 | await render(StandaloneComponent);
6 |
7 | const content = screen.getByTestId('standalone');
8 |
9 | expect(content).toHaveTextContent('Standalone Component');
10 | });
11 |
12 | test('can render a standalone component with a child', async () => {
13 | await render(StandaloneWithChildComponent, {
14 | componentProperties: { name: 'Bob' },
15 | });
16 |
17 | const childContent = screen.getByTestId('standalone');
18 | expect(childContent).toHaveTextContent('Standalone Component');
19 |
20 | expect(screen.getByText('Hi Bob')).toBeInTheDocument();
21 | expect(screen.getByText('This has a child')).toBeInTheDocument();
22 | });
23 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/19-standalone-component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'atl-standalone',
5 | template: `Standalone Component
`,
6 | standalone: true,
7 | })
8 | export class StandaloneComponent {}
9 |
10 | @Component({
11 | selector: 'atl-standalone-with-child',
12 | template: `Hi {{ name }}
13 | This has a child
14 | `,
15 | standalone: true,
16 | imports: [StandaloneComponent],
17 | })
18 | export class StandaloneWithChildComponent {
19 | @Input()
20 | name?: string;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/20-test-harness.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
2 | import { MatButtonHarness } from '@angular/material/button/testing';
3 | import { MatSnackBarHarness } from '@angular/material/snack-bar/testing';
4 | import { render, screen } from '@testing-library/angular';
5 | import userEvent from '@testing-library/user-event';
6 |
7 | import { HarnessComponent } from './20-test-harness';
8 |
9 | test.skip('can be used with TestHarness', async () => {
10 | const view = await render(``, {
11 | imports: [HarnessComponent],
12 | });
13 | const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture);
14 |
15 | const buttonHarness = await loader.getHarness(MatButtonHarness);
16 | const button = await buttonHarness.host();
17 | button.click();
18 |
19 | const snackbarHarness = await loader.getHarness(MatSnackBarHarness);
20 | expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i);
21 | });
22 |
23 | test.skip('can be used in combination with TestHarness', async () => {
24 | const user = userEvent.setup();
25 |
26 | const view = await render(HarnessComponent);
27 | const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture);
28 |
29 | await user.click(screen.getByRole('button'));
30 |
31 | const snackbarHarness = await loader.getHarness(MatSnackBarHarness);
32 | expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i);
33 |
34 | expect(screen.getByText(/Pizza Party!!!/i)).toBeInTheDocument();
35 | });
36 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/20-test-harness.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { MatButtonModule } from '@angular/material/button';
3 | import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
4 |
5 | @Component({
6 | selector: 'atl-harness',
7 | standalone: true,
8 | imports: [MatButtonModule, MatSnackBarModule],
9 | template: `
10 |
11 | `,
12 | })
13 | export class HarnessComponent {
14 | constructor(private snackBar: MatSnackBar) {}
15 |
16 | openSnackBar() {
17 | return this.snackBar.open('Pizza Party!!!');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/21-deferable-view.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'atl-deferable-view-child',
5 | template: ` Hello from deferred child component
`,
6 | standalone: true,
7 | })
8 | export class DeferableViewChildComponent {}
9 |
10 | @Component({
11 | template: `
12 | @defer (on timer(2s)) {
13 |
14 | } @placeholder {
15 | Hello from placeholder
16 | } @loading {
17 | Hello from loading
18 | } @error {
19 | Hello from error
20 | }
21 | `,
22 | imports: [DeferableViewChildComponent],
23 | standalone: true,
24 | })
25 | export class DeferableViewComponent {}
26 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/21-deferable-view.spec.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/angular';
2 | import { DeferBlockState } from '@angular/core/testing';
3 | import { DeferableViewComponent } from './21-deferable-view.component';
4 |
5 | test('renders deferred views based on state', async () => {
6 | const { renderDeferBlock } = await render(DeferableViewComponent);
7 |
8 | expect(screen.getByText(/Hello from placeholder/i)).toBeInTheDocument();
9 |
10 | await renderDeferBlock(DeferBlockState.Loading);
11 | expect(screen.getByText(/Hello from loading/i)).toBeInTheDocument();
12 |
13 | await renderDeferBlock(DeferBlockState.Complete);
14 | expect(screen.getByText(/Hello from deferred child component/i)).toBeInTheDocument();
15 | });
16 |
17 | test('initially renders deferred views based on given state', async () => {
18 | await render(DeferableViewComponent, {
19 | deferBlockStates: DeferBlockState.Error,
20 | });
21 |
22 | expect(screen.getByText(/Hello from error/i)).toBeInTheDocument();
23 | });
24 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { aliasedInput, render, screen, within } from '@testing-library/angular';
2 | import { SignalInputComponent } from './22-signal-inputs.component';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | test('works with signal inputs', async () => {
6 | await render(SignalInputComponent, {
7 | inputs: {
8 | ...aliasedInput('greeting', 'Hello'),
9 | name: 'world',
10 | age: '45',
11 | },
12 | });
13 |
14 | const inputValue = within(screen.getByTestId('input-value'));
15 | expect(inputValue.getByText(/hello world of 45 years old/i)).toBeInTheDocument();
16 | });
17 |
18 | test('works with computed', async () => {
19 | await render(SignalInputComponent, {
20 | inputs: {
21 | ...aliasedInput('greeting', 'Hello'),
22 | name: 'world',
23 | age: '45',
24 | },
25 | });
26 |
27 | const computedValue = within(screen.getByTestId('computed-value'));
28 | expect(computedValue.getByText(/hello world of 45 years old/i)).toBeInTheDocument();
29 | });
30 |
31 | test('can update signal inputs', async () => {
32 | const { fixture } = await render(SignalInputComponent, {
33 | inputs: {
34 | ...aliasedInput('greeting', 'Hello'),
35 | name: 'world',
36 | age: '45',
37 | },
38 | });
39 |
40 | const inputValue = within(screen.getByTestId('input-value'));
41 | const computedValue = within(screen.getByTestId('computed-value'));
42 |
43 | expect(inputValue.getByText(/hello world of 45 years old/i)).toBeInTheDocument();
44 |
45 | fixture.componentInstance.name.set('updated');
46 | // set doesn't trigger change detection within the test, findBy is needed to update the template
47 | expect(await inputValue.findByText(/hello updated of 45 years old/i)).toBeInTheDocument();
48 | expect(await computedValue.findByText(/hello updated of 45 years old/i)).toBeInTheDocument();
49 |
50 | // it's not recommended to access the model directly, but it's possible
51 | expect(fixture.componentInstance.name()).toBe('updated');
52 | });
53 |
54 | test('output emits a value', async () => {
55 | const submitFn = jest.fn();
56 | await render(SignalInputComponent, {
57 | inputs: {
58 | ...aliasedInput('greeting', 'Hello'),
59 | name: 'world',
60 | age: '45',
61 | },
62 | on: {
63 | submitValue: submitFn,
64 | },
65 | });
66 |
67 | await userEvent.click(screen.getByRole('button'));
68 |
69 | expect(submitFn).toHaveBeenCalledWith('world');
70 | });
71 |
72 | test('model update also updates the template', async () => {
73 | const { fixture } = await render(SignalInputComponent, {
74 | inputs: {
75 | ...aliasedInput('greeting', 'Hello'),
76 | name: 'initial',
77 | age: '45',
78 | },
79 | });
80 |
81 | const inputValue = within(screen.getByTestId('input-value'));
82 | const computedValue = within(screen.getByTestId('computed-value'));
83 |
84 | expect(inputValue.getByText(/hello initial/i)).toBeInTheDocument();
85 | expect(computedValue.getByText(/hello initial/i)).toBeInTheDocument();
86 |
87 | await userEvent.clear(screen.getByRole('textbox'));
88 | await userEvent.type(screen.getByRole('textbox'), 'updated');
89 |
90 | expect(inputValue.getByText(/hello updated/i)).toBeInTheDocument();
91 | expect(computedValue.getByText(/hello updated/i)).toBeInTheDocument();
92 | expect(fixture.componentInstance.name()).toBe('updated');
93 |
94 | fixture.componentInstance.name.set('new value');
95 | // set doesn't trigger change detection within the test, findBy is needed to update the template
96 | expect(await inputValue.findByText(/hello new value/i)).toBeInTheDocument();
97 | expect(await computedValue.findByText(/hello new value/i)).toBeInTheDocument();
98 |
99 | // it's not recommended to access the model directly, but it's possible
100 | expect(fixture.componentInstance.name()).toBe('new value');
101 | });
102 |
103 | test('works with signal inputs, computed values, and rerenders', async () => {
104 | const view = await render(SignalInputComponent, {
105 | inputs: {
106 | ...aliasedInput('greeting', 'Hello'),
107 | name: 'world',
108 | age: '45',
109 | },
110 | });
111 |
112 | const inputValue = within(screen.getByTestId('input-value'));
113 | const computedValue = within(screen.getByTestId('computed-value'));
114 |
115 | expect(inputValue.getByText(/hello world of 45 years old/i)).toBeInTheDocument();
116 | expect(computedValue.getByText(/hello world of 45 years old/i)).toBeInTheDocument();
117 |
118 | await view.rerender({
119 | inputs: {
120 | ...aliasedInput('greeting', 'bye'),
121 | name: 'test',
122 | age: '0',
123 | },
124 | });
125 |
126 | expect(inputValue.getByText(/bye test of 0 years old/i)).toBeInTheDocument();
127 | expect(computedValue.getByText(/bye test of 0 years old/i)).toBeInTheDocument();
128 | });
129 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/22-signal-inputs.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed, input, model, numberAttribute, output } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 |
4 | @Component({
5 | selector: 'atl-signal-input',
6 | template: `
7 | {{ greetings() }} {{ name() }} of {{ age() }} years old
8 | {{ greetingMessage() }}
9 |
10 |
11 | `,
12 | standalone: true,
13 | imports: [FormsModule],
14 | })
15 | export class SignalInputComponent {
16 | greetings = input('', {
17 | alias: 'greeting',
18 | });
19 | age = input.required({ transform: numberAttribute });
20 | name = model.required();
21 | submitValue = output();
22 |
23 | greetingMessage = computed(() => `${this.greetings()} ${this.name()} of ${this.age()} years old`);
24 |
25 | submitName() {
26 | this.submitValue.emit(this.name());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/23-host-directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { aliasedInput, render, screen } from '@testing-library/angular';
2 | import { HostDirectiveComponent } from './23-host-directive';
3 |
4 | test('can set input properties of host directives using aliasedInput', async () => {
5 | await render(HostDirectiveComponent, {
6 | inputs: {
7 | ...aliasedInput('atlText', 'Hello world'),
8 | },
9 | });
10 |
11 | expect(screen.getByText(/hello world/i)).toBeInTheDocument();
12 | });
13 |
14 | test('can set input properties of host directives using componentInputs', async () => {
15 | await render(HostDirectiveComponent, {
16 | componentInputs: {
17 | atlText: 'Hello world',
18 | },
19 | });
20 |
21 | expect(screen.getByText(/hello world/i)).toBeInTheDocument();
22 | });
23 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/23-host-directive.ts:
--------------------------------------------------------------------------------
1 | import { Component, Directive, ElementRef, input, OnInit } from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[atlText]',
5 | })
6 | export class TextDirective implements OnInit {
7 | atlText = input('');
8 |
9 | constructor(private el: ElementRef) {}
10 |
11 | ngOnInit() {
12 | this.el.nativeElement.textContent = this.atlText();
13 | }
14 | }
15 |
16 | @Component({
17 | selector: 'atl-host-directive',
18 | template: ``,
19 | hostDirectives: [{ directive: TextDirective, inputs: ['atlText'] }],
20 | })
21 | export class HostDirectiveComponent {}
22 |
--------------------------------------------------------------------------------
/apps/example-app/src/app/examples/README.md:
--------------------------------------------------------------------------------
1 | # 🦔 Angular Testing Library Examples
2 |
3 | Follow these three steps to run the example tests:
4 |
5 | - clone or download the repository
6 | - move into the repository and install the needed dependencies with `npm install`
7 | - use the command `npx nx test example-app` from within the root of this repository to run the tests
8 |
9 | The tests in this repository are written with [Jest](https://jestjs.io/), but you can use the test runner of your choice.
10 |
11 | If you're looking for an example that is not in this repository, feel free to create an [issue](https://github.com/testing-library/angular-testing-library/issues/new).
12 |
--------------------------------------------------------------------------------
/apps/example-app/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
2 | import '@testing-library/jest-dom';
3 |
4 | setupZoneTestEnv();
5 |
--------------------------------------------------------------------------------
/apps/example-app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [],
6 | "allowJs": true,
7 | "target": "ES2022",
8 | "useDefineForClassFields": false
9 | },
10 | "files": ["src/main.ts"],
11 | "include": ["src/**/*.d.ts"],
12 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "jest.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/example-app/tsconfig.editor.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "types": ["jest", "node"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/example-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "compilerOptions": {
6 | "target": "es2020"
7 | },
8 | "angularCompilerOptions": {
9 | "strictInjectionParameters": true,
10 | "strictInputAccessModifiers": true,
11 | "strictTemplates": true
12 | },
13 | "references": [
14 | {
15 | "path": "./tsconfig.app.json"
16 | },
17 | {
18 | "path": "./tsconfig.spec.json"
19 | },
20 | {
21 | "path": "./tsconfig.editor.json"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/apps/example-app/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node", "@testing-library/jest-dom"]
7 | },
8 | "files": ["src/test-setup.ts"],
9 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | // TODO - https://github.com/nrwl/nx/issues/22576
4 |
5 | /** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */
6 | const config = (async () => (await import('./eslint.config.mjs')).default)();
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from "@eslint/js";
4 | import tseslint from "typescript-eslint";
5 | import angular from "angular-eslint";
6 | import jestDom from 'eslint-plugin-jest-dom';
7 | import testingLibrary from 'eslint-plugin-testing-library';
8 |
9 | export default tseslint.config(
10 | {
11 | files: ["**/*.ts"],
12 | extends: [
13 | eslint.configs.recommended,
14 | ...tseslint.configs.recommended,
15 | ...tseslint.configs.stylistic,
16 | ...angular.configs.tsRecommended,
17 | ],
18 | processor: angular.processInlineTemplates,
19 | rules: {
20 | "@angular-eslint/directive-selector": [
21 | "error",
22 | {
23 | type: "attribute",
24 | prefix: "atl",
25 | style: "camelCase",
26 | },
27 | ],
28 | "@angular-eslint/component-selector": [
29 | "error",
30 | {
31 | type: "element",
32 | prefix: "atl",
33 | style: "kebab-case",
34 | },
35 | ],
36 | "@typescript-eslint/no-explicit-any": "off",
37 | "@typescript-eslint/no-unused-vars": [
38 | "error",
39 | {
40 | "argsIgnorePattern": "^_",
41 | "varsIgnorePattern": "^_",
42 | "caughtErrorsIgnorePattern": "^_"
43 | }
44 | ],
45 | // These are needed for test cases
46 | "@angular-eslint/prefer-standalone": "off",
47 | "@angular-eslint/no-input-rename": "off",
48 | "@angular-eslint/no-input-rename": "off",
49 | },
50 | },
51 | {
52 | files: ["**/*.spec.ts"],
53 | extends: [
54 | jestDom.configs["flat/recommended"],
55 | testingLibrary.configs["flat/angular"],
56 | ],
57 | },
58 | {
59 | files: ["**/*.html"],
60 | extends: [
61 | ...angular.configs.templateRecommended,
62 | ...angular.configs.templateAccessibility,
63 | ],
64 | rules: {},
65 | }
66 | );
67 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | const { getJestProjectsAsync } = require('@nx/jest');
2 |
3 | export default async () => ({
4 | projects: await getJestProjectsAsync(),
5 | });
6 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nx/jest/preset').default;
2 |
3 | module.exports = {
4 | ...nxPreset,
5 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
6 | transform: {
7 | '^.+\\.(ts|mjs|js|html)$': [
8 | 'jest-preset-angular',
9 | {
10 | tsconfig: '/tsconfig.spec.json',
11 | stringifyContentPathRegex: '\\.(html|svg)$',
12 | },
13 | ],
14 | },
15 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
16 | resolver: '@nx/jest/plugins/resolver',
17 | moduleFileExtensions: ['ts', 'js', 'html'],
18 | globals: {},
19 | snapshotSerializers: [
20 | 'jest-preset-angular/build/serializers/no-ng-attributes',
21 | 'jest-preset-angular/build/serializers/ng-snapshot',
22 | 'jest-preset-angular/build/serializers/html-comment',
23 | ],
24 | /* TODO: Update to latest Jest snapshotFormat
25 | * By default Nx has kept the older style of Jest Snapshot formats
26 | * to prevent breaking of any existing tests with snapshots.
27 | * It's recommend you update to the latest format.
28 | * You can do this by removing snapshotFormat property
29 | * and running tests with --update-snapshot flag.
30 | * Example: "nx affected --targets=test --update-snapshot"
31 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
32 | */
33 | snapshotFormat: { escapeString: true, printBasicPrototype: true },
34 | };
35 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '*.{ts,js}': ['eslint --fix'],
3 | '*.{ts,js,json,md}': ['prettier --write'],
4 | };
5 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "workspaceLayout": {
3 | "appsDir": "apps",
4 | "libsDir": "projects"
5 | },
6 | "cli": {
7 | "analytics": false,
8 | "cache": {
9 | "enabled": true,
10 | "path": "./.cache/angular",
11 | "environment": "all"
12 | }
13 | },
14 | "tasksRunnerOptions": {
15 | "default": {
16 | "options": {
17 | "canTrackAnalytics": false,
18 | "showUsageWarnings": true
19 | }
20 | }
21 | },
22 | "generators": {
23 | "@nrlw/workspace:library": {
24 | "linter": "eslint",
25 | "unitTestRunner": "jest",
26 | "strict": true,
27 | "standaloneConfig": true,
28 | "buildable": true
29 | },
30 | "@nx/angular:application": {
31 | "style": "scss",
32 | "linter": "eslint",
33 | "unitTestRunner": "jest",
34 | "e2eTestRunner": "cypress",
35 | "strict": true,
36 | "standaloneConfig": true,
37 | "tags": ["type:app"]
38 | },
39 | "@nx/angular:library": {
40 | "linter": "eslint",
41 | "unitTestRunner": "jest",
42 | "strict": true,
43 | "standaloneConfig": true,
44 | "publishable": true
45 | },
46 | "@nx/angular:component": {
47 | "style": "scss",
48 | "displayBlock": true,
49 | "changeDetection": "OnPush"
50 | },
51 | "@schematics/angular": {
52 | "component": {
53 | "style": "scss",
54 | "displayBlock": true,
55 | "changeDetection": "OnPush"
56 | }
57 | }
58 | },
59 | "defaultProject": "example-app",
60 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
61 | "targetDefaults": {
62 | "build": {
63 | "dependsOn": ["^build"],
64 | "inputs": ["production", "^production"],
65 | "cache": true
66 | },
67 | "test": {
68 | "inputs": ["default", "^production"],
69 | "cache": true
70 | },
71 | "@nx/jest:jest": {
72 | "inputs": ["default", "^production"],
73 | "cache": true,
74 | "options": {
75 | "passWithNoTests": true
76 | },
77 | "configurations": {
78 | "ci": {
79 | "ci": true,
80 | "codeCoverage": true
81 | }
82 | }
83 | },
84 | "@nx/eslint:lint": {
85 | "inputs": ["default", "{workspaceRoot}/eslint.config.cjs"],
86 | "cache": true
87 | }
88 | },
89 | "namedInputs": {
90 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
91 | "sharedGlobals": [],
92 | "production": [
93 | "default",
94 | "!{projectRoot}/**/*.spec.[jt]s",
95 | "!{projectRoot}/tsconfig.spec.json",
96 | "!{projectRoot}/karma.conf.js",
97 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
98 | "!{projectRoot}/jest.config.[jt]s",
99 | "!{projectRoot}/eslint.config.cjs",
100 | "!{projectRoot}/src/test-setup.[jt]s"
101 | ]
102 | },
103 | "nxCloudAccessToken": "M2Q4YjlkNjMtMzY1NC00ZjkwLTk1ZjgtZjg5Y2VkMzFjM2FifHJlYWQtd3JpdGU=",
104 | "parallel": 3,
105 | "useInferencePlugins": false,
106 | "defaultBase": "main"
107 | }
108 |
--------------------------------------------------------------------------------
/other/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/angular-testing-library/e1e046c75c297fd8ab06237178d036553b1ff215/other/logo.jpg
--------------------------------------------------------------------------------
/other/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/angular-testing-library/e1e046c75c297fd8ab06237178d036553b1ff215/other/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@testing-library/angular-app",
3 | "version": "0.0.0-semantically-released",
4 | "scripts": {
5 | "ng": "nx",
6 | "nx": "nx",
7 | "start": "nx serve",
8 | "prebuild": "rimraf dist",
9 | "build": "nx run-many --target=build --projects=testing-library",
10 | "build:schematics": "tsc -p ./projects/testing-library/tsconfig.schematics.json",
11 | "test": "nx run-many --target=test --all --parallel=1",
12 | "lint": "nx run-many --all --target=lint",
13 | "e2e": "nx e2e",
14 | "affected:apps": "nx affected:apps",
15 | "affected:libs": "nx affected:libs",
16 | "affected:build": "nx affected:build",
17 | "affected:e2e": "nx affected:e2e",
18 | "affected:test": "nx affected:test",
19 | "affected:lint": "nx affected:lint",
20 | "affected:dep-graph": "nx affected:dep-graph",
21 | "affected": "nx affected",
22 | "format": "nx format:write",
23 | "format:write": "nx format:write",
24 | "format:check": "nx format:check",
25 | "pre-commit": "lint-staged",
26 | "semantic-release": "semantic-release",
27 | "prepare": "git config core.hookspath .githooks"
28 | },
29 | "dependencies": {
30 | "@angular/animations": "19.0.1",
31 | "@angular/cdk": "19.0.1",
32 | "@angular/common": "19.0.1",
33 | "@angular/compiler": "19.0.1",
34 | "@angular/core": "19.0.1",
35 | "@angular/material": "19.0.1",
36 | "@angular/platform-browser": "19.0.1",
37 | "@angular/platform-browser-dynamic": "19.0.1",
38 | "@angular/router": "19.0.1",
39 | "@ngrx/store": "19.0.0",
40 | "@nx/angular": "20.3.0",
41 | "@testing-library/dom": "^10.4.0",
42 | "rxjs": "7.8.0",
43 | "tslib": "~2.8.1",
44 | "zone.js": "^0.15.0"
45 | },
46 | "devDependencies": {
47 | "@angular-devkit/build-angular": "19.0.1",
48 | "@angular-devkit/core": "19.0.1",
49 | "@angular-devkit/schematics": "19.0.1",
50 | "@angular-eslint/builder": "19.0.2",
51 | "@angular-eslint/eslint-plugin": "19.0.2",
52 | "@angular-eslint/eslint-plugin-template": "19.0.2",
53 | "@angular-eslint/schematics": "19.0.2",
54 | "@angular-eslint/template-parser": "19.0.2",
55 | "@angular/cli": "~19.0.6",
56 | "@angular/compiler-cli": "19.0.1",
57 | "@angular/forms": "19.0.1",
58 | "@angular/language-service": "19.0.1",
59 | "@eslint/eslintrc": "^2.1.1",
60 | "@nx/eslint": "20.3.0",
61 | "@nx/eslint-plugin": "20.3.0",
62 | "@nx/jest": "20.3.0",
63 | "@nx/node": "20.3.0",
64 | "@nx/plugin": "20.3.0",
65 | "@nx/workspace": "20.3.0",
66 | "@schematics/angular": "18.2.9",
67 | "@testing-library/jasmine-dom": "^1.3.3",
68 | "@testing-library/jest-dom": "^6.6.3",
69 | "@testing-library/user-event": "^14.5.2",
70 | "@types/jasmine": "4.3.1",
71 | "@types/jest": "29.5.14",
72 | "@types/node": "22.10.1",
73 | "@types/testing-library__jasmine-dom": "^1.3.4",
74 | "@typescript-eslint/types": "^8.19.0",
75 | "@typescript-eslint/utils": "^8.19.0",
76 | "angular-eslint": "^19.0.2",
77 | "autoprefixer": "^10.4.20",
78 | "cpy-cli": "^5.0.0",
79 | "eslint": "^9.8.0",
80 | "eslint-plugin-jest-dom": "~5.5.0",
81 | "eslint-plugin-testing-library": "~7.1.1",
82 | "jasmine-core": "4.2.0",
83 | "jasmine-spec-reporter": "7.0.0",
84 | "jest": "29.7.0",
85 | "jest-environment-jsdom": "29.7.0",
86 | "jest-preset-angular": "14.4.2",
87 | "karma": "6.4.0",
88 | "karma-chrome-launcher": "^3.2.0",
89 | "karma-coverage": "^2.2.1",
90 | "karma-jasmine": "5.1.0",
91 | "karma-jasmine-html-reporter": "2.0.0",
92 | "lint-staged": "^15.3.0",
93 | "ng-mocks": "^14.13.1",
94 | "ng-packagr": "19.0.1",
95 | "nx": "20.3.0",
96 | "postcss": "^8.4.49",
97 | "postcss-import": "14.1.0",
98 | "postcss-preset-env": "7.5.0",
99 | "postcss-url": "10.1.3",
100 | "prettier": "2.6.2",
101 | "rimraf": "^5.0.10",
102 | "semantic-release": "^24.2.1",
103 | "ts-jest": "29.1.0",
104 | "ts-node": "10.9.1",
105 | "typescript": "5.6.2",
106 | "typescript-eslint": "^8.19.0"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 2,
4 | useTabs: false,
5 | semi: true,
6 | singleQuote: true,
7 | trailingComma: 'all',
8 | bracketSpacing: true,
9 | };
10 |
--------------------------------------------------------------------------------
/projects/testing-library/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | // TODO - https://github.com/nrwl/nx/issues/22576
4 |
5 | /** @type {import('@typescript-eslint/utils/ts-eslint').FlatConfig.ConfigPromise} */
6 | const config = (async () => (await import('./eslint.config.mjs')).default)();
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/projects/testing-library/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import tseslint from "typescript-eslint";
4 | import rootConfig from "../../eslint.config.mjs";
5 |
6 | export default tseslint.config(
7 | ...rootConfig,
8 | );
9 |
--------------------------------------------------------------------------------
/projects/testing-library/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/public_api';
2 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/public_api';
2 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/ng-package.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/src/lib/create-mock.ts:
--------------------------------------------------------------------------------
1 | import { Type, Provider } from '@angular/core';
2 |
3 | export type Mock = T & { [K in keyof T]: T[K] & jest.Mock };
4 |
5 | export function createMock(type: Type): Mock {
6 | const mock: any = {};
7 |
8 | function mockFunctions(proto: any) {
9 | if (!proto) {
10 | return;
11 | }
12 |
13 | for (const prop of Object.getOwnPropertyNames(proto)) {
14 | if (prop === 'constructor') {
15 | continue;
16 | }
17 |
18 | const descriptor = Object.getOwnPropertyDescriptor(proto, prop);
19 | if (typeof descriptor?.value === 'function') {
20 | mock[prop] = jest.fn();
21 | }
22 | }
23 |
24 | mockFunctions(Object.getPrototypeOf(proto));
25 | }
26 |
27 | mockFunctions(type.prototype);
28 |
29 | return mock;
30 | }
31 |
32 | export function createMockWithValues(type: Type, values: Partial>): Mock {
33 | const mock = createMock(type);
34 |
35 | Object.entries(values).forEach(([field, value]) => {
36 | (mock as any)[field] = value;
37 | });
38 |
39 | return mock;
40 | }
41 |
42 | export function provideMock(type: Type): Provider {
43 | return {
44 | provide: type,
45 | useValue: createMock(type),
46 | };
47 | }
48 |
49 | export function provideMockWithValues(type: Type, values: Partial>): Provider {
50 | return {
51 | provide: type,
52 | useValue: createMockWithValues(type, values),
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-mock';
2 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/src/public_api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of testing-library
3 | */
4 |
5 | export * from './lib';
6 |
--------------------------------------------------------------------------------
/projects/testing-library/jest-utils/tests/create-mock.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { fireEvent, render, screen } from '@testing-library/angular';
4 |
5 | import { createMock, provideMock, provideMockWithValues, Mock } from '../src/public_api';
6 |
7 | class FixtureService {
8 | constructor(private foo: string, public bar: string) {}
9 |
10 | print() {
11 | console.log(this.foo, this.bar);
12 | }
13 |
14 | concat() {
15 | return this.foo + this.bar;
16 | }
17 | }
18 |
19 | @Component({
20 | selector: 'atl-fixture',
21 | template: ` `,
22 | })
23 | class FixtureComponent {
24 | constructor(private service: FixtureService) {}
25 |
26 | print() {
27 | this.service.print();
28 | }
29 | }
30 |
31 | test('mocks all functions', () => {
32 | const mock = createMock(FixtureService);
33 | expect(mock.print.mock).toBeDefined();
34 | });
35 |
36 | test('provides a mock service', async () => {
37 | await render(FixtureComponent, {
38 | providers: [provideMock(FixtureService)],
39 | });
40 | const service = TestBed.inject(FixtureService);
41 |
42 | fireEvent.click(screen.getByText('Print'));
43 | expect(service.print).toHaveBeenCalledTimes(1);
44 | });
45 |
46 | test('provides a mock service with values', async () => {
47 | await render(FixtureComponent, {
48 | providers: [
49 | provideMockWithValues(FixtureService, {
50 | bar: 'value',
51 | concat: jest.fn(() => 'a concatenated value'),
52 | }),
53 | ],
54 | });
55 |
56 | const service = TestBed.inject(FixtureService);
57 |
58 | fireEvent.click(screen.getByText('Print'));
59 |
60 | expect(service.bar).toEqual('value');
61 | expect(service.concat()).toEqual('a concatenated value');
62 | expect(service.print).toHaveBeenCalled();
63 | });
64 |
65 | test('is possible to write a mock implementation', async () => {
66 | await render(FixtureComponent, {
67 | providers: [provideMock(FixtureService)],
68 | });
69 |
70 | const service = TestBed.inject(FixtureService) as Mock;
71 |
72 | fireEvent.click(screen.getByText('Print'));
73 | expect(service.print).toHaveBeenCalled();
74 | });
75 |
--------------------------------------------------------------------------------
/projects/testing-library/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | displayName: {
3 | name: 'ATL',
4 | color: 'magenta',
5 | },
6 | preset: '../../jest.preset.js',
7 | setupFilesAfterEnv: ['/test-setup.ts'],
8 | };
9 |
--------------------------------------------------------------------------------
/projects/testing-library/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/@testing-library/angular",
4 | "deleteDestPath": false,
5 | "assets": ["schematics/**/*.json"],
6 | "lib": {
7 | "entryFile": "index.ts"
8 | },
9 | "allowedNonPeerDependencies": ["@testing-library/dom"]
10 | }
11 |
--------------------------------------------------------------------------------
/projects/testing-library/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@testing-library/angular",
3 | "version": "0.0.0-semantically-released",
4 | "description": "Test your Angular components with the dom-testing-library",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/testing-library/angular-testing-library.git"
8 | },
9 | "keywords": [
10 | "angular",
11 | "ngx",
12 | "ng",
13 | "typescript",
14 | "angular2",
15 | "test",
16 | "dom-testing-library"
17 | ],
18 | "author": "Tim Deschryver",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/testing-library/angular-testing-library/issues"
22 | },
23 | "homepage": "https://github.com/testing-library/angular-testing-library#readme",
24 | "schematics": "./schematics/collection.json",
25 | "ng-add": {
26 | "save": "devDependencies"
27 | },
28 | "ng-update": {
29 | "migrations": "./schematics/migrations/migrations.json"
30 | },
31 | "peerDependencies": {
32 | "@angular/animations": ">= 17.0.0",
33 | "@angular/common": ">= 17.0.0",
34 | "@angular/platform-browser": ">= 17.0.0",
35 | "@angular/router": ">= 17.0.0",
36 | "@angular/core": ">= 17.0.0",
37 | "@testing-library/dom": "^10.0.0"
38 | },
39 | "dependencies": {
40 | "tslib": "^2.3.1"
41 | },
42 | "publishConfig": {
43 | "access": "public"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/projects/testing-library/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testing-library",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "library",
5 | "sourceRoot": "projects/testing-library/src",
6 | "prefix": "lib",
7 | "tags": [],
8 | "targets": {
9 | "build-package": {
10 | "executor": "@nx/angular:package",
11 | "outputs": ["{workspaceRoot}/dist/@testing-library/angular"],
12 | "options": {
13 | "project": "projects/testing-library/ng-package.json"
14 | },
15 | "configurations": {
16 | "production": {
17 | "tsConfig": "projects/testing-library/tsconfig.lib.prod.json"
18 | },
19 | "development": {
20 | "tsConfig": "projects/testing-library/tsconfig.lib.json"
21 | }
22 | },
23 | "defaultConfiguration": "production"
24 | },
25 | "lint": {
26 | "executor": "@nx/eslint:lint"
27 | },
28 | "build": {
29 | "executor": "nx:run-commands",
30 | "options": {
31 | "parallel": false,
32 | "commands": [
33 | {
34 | "command": "nx run testing-library:build-package"
35 | },
36 | {
37 | "command": "npm run build:schematics"
38 | },
39 | {
40 | "command": "cpy ./README.md ./dist/@testing-library/angular"
41 | }
42 | ]
43 | }
44 | },
45 | "test": {
46 | "executor": "@nx/jest:jest",
47 | "options": {
48 | "jestConfig": "projects/testing-library/jest.config.ts",
49 | "passWithNoTests": false
50 | },
51 | "outputs": ["{workspaceRoot}/coverage/projects/testing-library"]
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "schematics": {
3 | "ng-add": {
4 | "aliases": ["init"],
5 | "factory": "./ng-add",
6 | "schema": "./ng-add/schema.json",
7 | "description": "Add @testing-library/angular to your application"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
2 | import * as path from 'path';
3 | import { EmptyTree } from '@angular-devkit/schematics';
4 |
5 | test('adds DTL to devDependencies', async () => {
6 | const tree = await setup({});
7 | const pkg = tree.readContent('package.json');
8 |
9 | expect(pkg).toMatchInlineSnapshot(`
10 | "{
11 | \\"devDependencies\\": {
12 | \\"@testing-library/dom\\": \\"^10.0.0\\"
13 | }
14 | }"
15 | `);
16 | });
17 |
18 | test('ignores if DTL is already listed as a dev dependency', async () => {
19 | const tree = await setup({ devDependencies: { '@testing-library/dom': '^9.0.0' } });
20 | const pkg = tree.readContent('package.json');
21 |
22 | expect(pkg).toMatchInlineSnapshot(`"{\\"devDependencies\\":{\\"@testing-library/dom\\":\\"^9.0.0\\"}}"`);
23 | });
24 |
25 | test('ignores if DTL is already listed as a dependency', async () => {
26 | const tree = await setup({ dependencies: { '@testing-library/dom': '^11.0.0' } });
27 | const pkg = tree.readContent('package.json');
28 |
29 | expect(pkg).toMatchInlineSnapshot(`"{\\"dependencies\\":{\\"@testing-library/dom\\":\\"^11.0.0\\"}}"`);
30 | });
31 |
32 | async function setup(packageJson: object) {
33 | const collectionPath = path.join(__dirname, '../migrations.json');
34 | const schematicRunner = new SchematicTestRunner('schematics', collectionPath);
35 |
36 | const tree = new UnitTestTree(new EmptyTree());
37 | tree.create('package.json', JSON.stringify(packageJson));
38 |
39 | await schematicRunner.runSchematic(`atl-add-dtl-as-dev-dependency`, {}, tree);
40 |
41 | return tree;
42 | }
43 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.ts:
--------------------------------------------------------------------------------
1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2 | import {
3 | addPackageJsonDependency,
4 | getPackageJsonDependency,
5 | NodeDependencyType,
6 | } from '@schematics/angular/utility/dependencies';
7 |
8 | const dtl = '@testing-library/dom';
9 |
10 | export default function (): Rule {
11 | return async (tree: Tree, context: SchematicContext) => {
12 | const dtlDep = getPackageJsonDependency(tree, dtl);
13 | if (dtlDep) {
14 | context.logger.info(`Skipping installation of '@testing-library/dom' because it's already installed.`);
15 | } else {
16 | context.logger.info(`Adding '@testing-library/dom' as a peer dependency.`);
17 | addPackageJsonDependency(tree, { name: dtl, type: NodeDependencyType.Dev, overwrite: false, version: '^10.0.0' });
18 | }
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/migrations/migrations.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3 | "schematics": {
4 | "atl-add-dtl-as-dev-dependency": {
5 | "description": "Add @testing-library/dom as a dev dependency",
6 | "version": "17.0.0-beta.3",
7 | "factory": "./dtl-as-dev-dependency/index"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/ng-add/index.ts:
--------------------------------------------------------------------------------
1 | import { chain, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2 | import {
3 | addPackageJsonDependency,
4 | getPackageJsonDependency,
5 | NodeDependencyType,
6 | } from '@schematics/angular/utility/dependencies';
7 | import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
8 | import { Schema } from './schema';
9 |
10 | export default function ({ installJestDom, installUserEvent }: Schema): Rule {
11 | return () => {
12 | return chain([
13 | addDependency('@testing-library/dom', '^10.0.0', NodeDependencyType.Dev),
14 | installJestDom ? addDependency('@testing-library/jest-dom', '^6.4.8', NodeDependencyType.Dev) : noop(),
15 | installUserEvent ? addDependency('@testing-library/user-event', '^14.5.2', NodeDependencyType.Dev) : noop(),
16 | installDependencies(),
17 | ]);
18 | };
19 | }
20 |
21 | function addDependency(packageName: string, version: string, dependencyType: NodeDependencyType) {
22 | return (tree: Tree, context: SchematicContext) => {
23 | const dtlDep = getPackageJsonDependency(tree, packageName);
24 | if (dtlDep) {
25 | context.logger.info(`Skipping installation of '${packageName}' because it's already installed.`);
26 | } else {
27 | context.logger.info(`Adding '${packageName}' as a dev dependency.`);
28 | addPackageJsonDependency(tree, { name: packageName, type: dependencyType, overwrite: false, version });
29 | }
30 |
31 | return tree;
32 | };
33 | }
34 |
35 | export function installDependencies() {
36 | return (_tree: Tree, context: SchematicContext) => {
37 | context.addTask(new NodePackageInstallTask());
38 |
39 | context.logger.info(
40 | `Correctly installed @testing-library/angular.
41 | See our docs at https://testing-library.com/docs/angular-testing-library/intro/ to get started.`,
42 | );
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/ng-add/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema",
3 | "$id": "SchematicsTestingLibraryAngular",
4 | "title": "testing-library-angular",
5 | "type": "object",
6 | "properties": {
7 | "installJestDom": {
8 | "type": "boolean",
9 | "description": "Install jest-dom as a dependency.",
10 | "$default": {
11 | "$source": "argv",
12 | "index": 0
13 | },
14 | "default": false,
15 | "x-prompt": "Would you like to install jest-dom?"
16 | },
17 | "installUserEvent": {
18 | "type": "boolean",
19 | "description": "Install user-event as a dependency.",
20 | "$default": {
21 | "$source": "argv",
22 | "index": 1
23 | },
24 | "default": false,
25 | "x-prompt": "Would you like to install user-event?"
26 | }
27 | },
28 | "additionalProperties": false,
29 | "required": []
30 | }
31 |
--------------------------------------------------------------------------------
/projects/testing-library/schematics/ng-add/schema.ts:
--------------------------------------------------------------------------------
1 | export interface Schema {
2 | installJestDom: boolean;
3 | installUserEvent: boolean;
4 | }
5 |
--------------------------------------------------------------------------------
/projects/testing-library/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from './models';
2 |
3 | let config: Config = {
4 | dom: {},
5 | defaultImports: [],
6 | };
7 |
8 | export function configure(newConfig: Partial | ((config: Partial) => Partial)) {
9 | if (typeof newConfig === 'function') {
10 | // Pass the existing config out to the provided function
11 | // and accept a delta in return
12 | newConfig = newConfig(config);
13 | }
14 |
15 | // Merge the incoming config delta
16 | config = {
17 | ...config,
18 | ...newConfig,
19 | };
20 | }
21 |
22 | export function getConfig() {
23 | return config;
24 | }
25 |
--------------------------------------------------------------------------------
/projects/testing-library/src/public_api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of testing-library
3 | */
4 |
5 | export * from './lib/models';
6 | export * from './lib/config';
7 | export * from './lib/testing-library';
8 |
--------------------------------------------------------------------------------
/projects/testing-library/test-setup.ts:
--------------------------------------------------------------------------------
1 | import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
2 | import '@testing-library/jest-dom';
3 | import { TextEncoder, TextDecoder } from 'util';
4 |
5 | setupZoneTestEnv();
6 |
7 | Object.assign(global, { TextDecoder, TextEncoder });
8 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/auto-cleanup.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { render } from '../src/public_api';
3 |
4 | @Component({
5 | selector: 'atl-fixture',
6 | template: `Hello {{ name }}!`,
7 | })
8 | class FixtureComponent {
9 | @Input() name = '';
10 | }
11 |
12 | describe('Angular auto clean up - previous components only get cleanup up on init (based on root-id)', () => {
13 | it('first', async () => {
14 | await render(FixtureComponent, {
15 | componentProperties: {
16 | name: 'first',
17 | },
18 | });
19 | });
20 |
21 | it('second', async () => {
22 | await render(FixtureComponent, {
23 | componentProperties: {
24 | name: 'second',
25 | },
26 | });
27 | expect(document.body.innerHTML).not.toContain('first');
28 | });
29 | });
30 |
31 | describe('ATL auto clean up - after each test the containers get removed', () => {
32 | it('first', async () => {
33 | await render(FixtureComponent, {
34 | removeAngularAttributes: true,
35 | });
36 | });
37 |
38 | it('second', () => {
39 | expect(document.body).toBeEmptyDOMElement();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/config.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { render, configure, Config } from '../src/public_api';
4 | import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
5 |
6 | @Component({
7 | selector: 'atl-fixture',
8 | template: `
9 |
15 | `,
16 | standalone: false,
17 | })
18 | class FormsComponent {
19 | form = this.formBuilder.group({
20 | name: [''],
21 | });
22 |
23 | constructor(private formBuilder: FormBuilder) {}
24 | }
25 |
26 | let originalConfig: Config;
27 | beforeEach(() => {
28 | // Grab the existing configuration so we can restore
29 | // it at the end of the test
30 | configure((existingConfig) => {
31 | originalConfig = existingConfig as Config;
32 | // Don't change the existing config
33 | return {};
34 | });
35 | });
36 |
37 | afterEach(() => {
38 | configure(originalConfig);
39 | });
40 |
41 | beforeEach(() => {
42 | configure({
43 | defaultImports: [ReactiveFormsModule],
44 | });
45 | });
46 |
47 | test('adds default imports to the testbed', async () => {
48 | await render(FormsComponent);
49 |
50 | const reactive = TestBed.inject(ReactiveFormsModule);
51 | expect(reactive).not.toBeNull();
52 | });
53 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/debug.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, screen } from '../src/public_api';
3 |
4 | @Component({
5 | selector: 'atl-fixture',
6 | template: `
7 | rawr
8 |
9 | `,
10 | })
11 | class FixtureComponent {}
12 |
13 | test('debug', async () => {
14 | jest.spyOn(console, 'log').mockImplementation();
15 | const { debug } = await render(FixtureComponent);
16 |
17 | // eslint-disable-next-line testing-library/no-debugging-utils
18 | debug();
19 |
20 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('rawr'));
21 | (console.log as any).mockRestore();
22 | });
23 |
24 | test('debug allows to be called with an element', async () => {
25 | jest.spyOn(console, 'log').mockImplementation();
26 | const { debug } = await render(FixtureComponent);
27 | const btn = screen.getByTestId('btn');
28 |
29 | // eslint-disable-next-line testing-library/no-debugging-utils
30 | debug(btn);
31 |
32 | expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('rawr'));
33 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining(`I'm a button`));
34 | (console.log as any).mockRestore();
35 | });
36 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/defer-blocks.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { DeferBlockBehavior, DeferBlockState } from '@angular/core/testing';
3 | import { render, screen, fireEvent } from '../src/public_api';
4 |
5 | test('renders a defer block in different states using the official API', async () => {
6 | const { fixture } = await render(FixtureComponent);
7 |
8 | const deferBlockFixture = (await fixture.getDeferBlocks())[0];
9 |
10 | await deferBlockFixture.render(DeferBlockState.Loading);
11 | expect(screen.getByText(/loading/i)).toBeInTheDocument();
12 | expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
13 |
14 | await deferBlockFixture.render(DeferBlockState.Complete);
15 | expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
16 | expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
17 | });
18 |
19 | test('renders a defer block in different states using ATL', async () => {
20 | const { renderDeferBlock } = await render(FixtureComponent);
21 |
22 | await renderDeferBlock(DeferBlockState.Loading);
23 | expect(screen.getByText(/loading/i)).toBeInTheDocument();
24 | expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
25 |
26 | await renderDeferBlock(DeferBlockState.Complete, 0);
27 | expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
28 | expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
29 | });
30 |
31 | test('renders a defer block in different states using DeferBlockBehavior.Playthrough', async () => {
32 | await render(FixtureComponent, {
33 | deferBlockBehavior: DeferBlockBehavior.Playthrough,
34 | });
35 |
36 | expect(await screen.findByText(/loading/i)).toBeInTheDocument();
37 | expect(await screen.findByText(/Defer block content/i)).toBeInTheDocument();
38 | });
39 |
40 | test('renders a defer block in different states using DeferBlockBehavior.Playthrough event', async () => {
41 | await render(FixtureComponentWithEventsComponent, {
42 | deferBlockBehavior: DeferBlockBehavior.Playthrough,
43 | });
44 |
45 | const button = screen.getByRole('button', { name: /click/i });
46 | fireEvent.click(button);
47 |
48 | expect(screen.getByText(/empty defer block/i)).toBeInTheDocument();
49 | });
50 |
51 | test('renders a defer block initially in the loading state', async () => {
52 | await render(FixtureComponent, {
53 | deferBlockStates: DeferBlockState.Loading,
54 | });
55 |
56 | expect(screen.getByText(/loading/i)).toBeInTheDocument();
57 | expect(screen.queryByText(/Defer block content/i)).not.toBeInTheDocument();
58 | });
59 |
60 | test('renders a defer block initially in the complete state', async () => {
61 | await render(FixtureComponent, {
62 | deferBlockStates: DeferBlockState.Complete,
63 | });
64 |
65 | expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
66 | expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
67 | });
68 |
69 | test('renders a defer block in an initial state using the array syntax', async () => {
70 | await render(FixtureComponent, {
71 | deferBlockStates: [{ deferBlockState: DeferBlockState.Complete, deferBlockIndex: 0 }],
72 | });
73 |
74 | expect(screen.getByText(/Defer block content/i)).toBeInTheDocument();
75 | expect(screen.queryByText(/load/i)).not.toBeInTheDocument();
76 | });
77 |
78 | @Component({
79 | template: `
80 | @defer {
81 | Defer block content
82 | } @loading {
83 | Loading...
84 | }
85 | `,
86 | })
87 | class FixtureComponent {}
88 |
89 | @Component({
90 | template: `
91 |
92 | @defer(on interaction(trigger)) {
93 | empty defer block
94 | }
95 | `,
96 | })
97 | class FixtureComponentWithEventsComponent {}
98 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/detect-changes.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { fakeAsync } from '@angular/core/testing';
3 | import { FormControl, ReactiveFormsModule } from '@angular/forms';
4 | import { delay } from 'rxjs/operators';
5 | import { render, fireEvent, screen } from '../src/public_api';
6 |
7 | @Component({
8 | selector: 'atl-fixture',
9 | template: `
10 |
11 |
12 | `,
13 | standalone: true,
14 | imports: [ReactiveFormsModule],
15 | })
16 | class FixtureComponent implements OnInit {
17 | inputControl = new FormControl();
18 | caption = 'Button';
19 |
20 | ngOnInit() {
21 | this.inputControl.valueChanges.pipe(delay(400)).subscribe(() => (this.caption = 'Button updated after 400ms'));
22 | }
23 | }
24 |
25 | describe('detectChanges', () => {
26 | it('does not recognize change if execution is delayed', async () => {
27 | await render(FixtureComponent);
28 |
29 | fireEvent.input(screen.getByTestId('input'), {
30 | target: {
31 | value: 'What a great day!',
32 | },
33 | });
34 | expect(screen.getByTestId('button').innerHTML).toBe('Button');
35 | });
36 |
37 | it('exposes detectChanges triggering a change detection cycle', fakeAsync(async () => {
38 | const { detectChanges } = await render(FixtureComponent);
39 |
40 | fireEvent.input(screen.getByTestId('input'), {
41 | target: {
42 | value: 'What a great day!',
43 | },
44 | });
45 |
46 | // TODO: The code should be running in the fakeAsync zone to call this function ?
47 | // tick(500);
48 | await new Promise((resolve) => setTimeout(resolve, 500));
49 |
50 | detectChanges();
51 |
52 | expect(screen.getByTestId('button').innerHTML).toBe('Button updated after 400ms');
53 | }));
54 |
55 | it('does not throw on a destroyed fixture', async () => {
56 | const { fixture } = await render(FixtureComponent);
57 |
58 | fixture.destroy();
59 |
60 | fireEvent.input(screen.getByTestId('input'), {
61 | target: {
62 | value: 'What a great day!',
63 | },
64 | });
65 | expect(screen.getByTestId('button').innerHTML).toBe('Button');
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/find-by.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { timer } from 'rxjs';
3 | import { render, screen } from '../src/public_api';
4 | import { mapTo } from 'rxjs/operators';
5 | import { AsyncPipe } from '@angular/common';
6 |
7 | @Component({
8 | selector: 'atl-fixture',
9 | template: ` {{ result | async }}
`,
10 | imports: [AsyncPipe],
11 | })
12 | class FixtureComponent {
13 | result = timer(30).pipe(mapTo('I am visible'));
14 | }
15 |
16 | describe('screen', () => {
17 | it('waits for element to be added to the DOM', async () => {
18 | await render(FixtureComponent);
19 | await expect(screen.findByText('I am visible')).resolves.toBeTruthy();
20 | });
21 |
22 | it('rejects when something cannot be found', async () => {
23 | await render(FixtureComponent);
24 | await expect(screen.findByText('I am invisible', {}, { timeout: 40 })).rejects.toThrow('x');
25 | });
26 | });
27 |
28 | describe('rendered component', () => {
29 | it('waits for element to be added to the DOM', async () => {
30 | const { findByText } = await render(FixtureComponent);
31 | /// We wish to test the utility function from `render` here.
32 | // eslint-disable-next-line testing-library/prefer-screen-queries
33 | await expect(findByText('I am visible')).resolves.toBeTruthy();
34 | });
35 |
36 | it('rejects when something cannot be found', async () => {
37 | const { findByText } = await render(FixtureComponent);
38 | /// We wish to test the utility function from `render` here.
39 | // eslint-disable-next-line testing-library/prefer-screen-queries
40 | await expect(findByText('I am invisible', {}, { timeout: 40 })).rejects.toThrow('x');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/fire-event.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, fireEvent, screen } from '../src/public_api';
3 | import { FormsModule } from '@angular/forms';
4 |
5 | describe('fireEvent', () => {
6 | @Component({
7 | selector: 'atl-fixture',
8 | template: `
9 | Hello {{ name }}
`,
10 | standalone: true,
11 | imports: [FormsModule],
12 | })
13 | class FixtureComponent {
14 | name = '';
15 | }
16 |
17 | it('automatically detect changes when event is fired', async () => {
18 | await render(FixtureComponent);
19 |
20 | fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } });
21 |
22 | expect(screen.getByText('Hello Tim')).toBeInTheDocument();
23 | });
24 |
25 | it('can disable automatic detect changes when event is fired', async () => {
26 | const { detectChanges } = await render(FixtureComponent, {
27 | autoDetectChanges: false,
28 | });
29 |
30 | fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } });
31 |
32 | expect(screen.queryByText('Hello Tim')).not.toBeInTheDocument();
33 |
34 | detectChanges();
35 |
36 | expect(screen.getByText('Hello Tim')).toBeInTheDocument();
37 | });
38 |
39 | it('does not call detect changes when fixture is destroyed', async () => {
40 | const { fixture } = await render(FixtureComponent);
41 |
42 | fixture.destroy();
43 |
44 | // should otherwise throw
45 | fireEvent.input(screen.getByTestId('input'), { target: { value: 'Bonjour' } });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/integration.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Injectable, Input, Output } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { of, BehaviorSubject } from 'rxjs';
4 | import { debounceTime, switchMap, map, startWith } from 'rxjs/operators';
5 | import { render, screen, waitFor, waitForElementToBeRemoved, within } from '../src/lib/testing-library';
6 | import userEvent from '@testing-library/user-event';
7 | import { AsyncPipe, NgForOf } from '@angular/common';
8 |
9 | const DEBOUNCE_TIME = 1_000;
10 |
11 | @Injectable()
12 | class EntitiesService {
13 | fetchAll() {
14 | return of([]);
15 | }
16 | }
17 |
18 | @Injectable()
19 | class ModalService {
20 | open(...args: any[]) {
21 | console.log('open', ...args);
22 | }
23 | }
24 |
25 | @Component({
26 | selector: 'atl-table',
27 | template: `
28 |
29 |
30 | {{ entity.name }} |
31 |
32 |
33 | |
34 |
35 |
36 | `,
37 | imports: [NgForOf],
38 | })
39 | class TableComponent {
40 | @Input() entities: any[] = [];
41 | @Output() edit = new EventEmitter();
42 | }
43 |
44 | @Component({
45 | template: `
46 | Entities Title
47 |
48 |
52 |
53 | `,
54 | imports: [TableComponent, AsyncPipe],
55 | })
56 | class EntitiesComponent {
57 | query = new BehaviorSubject('');
58 | readonly entities = this.query.pipe(
59 | debounceTime(DEBOUNCE_TIME),
60 | switchMap((q) =>
61 | this.entitiesService.fetchAll().pipe(map((ent: any) => ent.filter((e: any) => e.name.includes(q)))),
62 | ),
63 | startWith(entities),
64 | );
65 |
66 | constructor(private entitiesService: EntitiesService, private modalService: ModalService) {}
67 |
68 | newEntityClicked() {
69 | this.modalService.open('new entity');
70 | }
71 |
72 | editEntityClicked(entity: string) {
73 | setTimeout(() => {
74 | this.modalService.open('edit entity', entity);
75 | }, 100);
76 | }
77 | }
78 |
79 | const entities = [
80 | {
81 | id: 1,
82 | name: 'Entity 1',
83 | },
84 | {
85 | id: 2,
86 | name: 'Entity 2',
87 | },
88 | {
89 | id: 3,
90 | name: 'Entity 3',
91 | },
92 | ];
93 |
94 | async function setup() {
95 | jest.useFakeTimers();
96 | const user = userEvent.setup();
97 |
98 | await render(EntitiesComponent, {
99 | providers: [
100 | {
101 | provide: EntitiesService,
102 | useValue: {
103 | fetchAll: jest.fn().mockReturnValue(of(entities)),
104 | },
105 | },
106 | {
107 | provide: ModalService,
108 | useValue: {
109 | open: jest.fn(),
110 | },
111 | },
112 | ],
113 | });
114 |
115 | const modalMock = TestBed.inject(ModalService);
116 |
117 | return {
118 | modalMock,
119 | user,
120 | };
121 | }
122 |
123 | test('renders the heading', async () => {
124 | await setup();
125 |
126 | expect(await screen.findByRole('heading', { name: /Entities Title/i })).toBeInTheDocument();
127 | });
128 |
129 | test('renders the entities', async () => {
130 | await setup();
131 |
132 | expect(await screen.findByRole('cell', { name: /Entity 1/i })).toBeInTheDocument();
133 | expect(await screen.findByRole('cell', { name: /Entity 2/i })).toBeInTheDocument();
134 | expect(await screen.findByRole('cell', { name: /Entity 3/i })).toBeInTheDocument();
135 | });
136 |
137 | test.skip('finds the cell', async () => {
138 | const { user } = await setup();
139 |
140 | await user.type(await screen.findByRole('textbox', { name: /Search entities/i }), 'Entity 2', {});
141 |
142 | jest.advanceTimersByTime(DEBOUNCE_TIME);
143 |
144 | await waitForElementToBeRemoved(() => screen.queryByRole('cell', { name: /Entity 1/i }));
145 | expect(await screen.findByRole('cell', { name: /Entity 2/i })).toBeInTheDocument();
146 | });
147 |
148 | test.skip('opens the modal', async () => {
149 | const { modalMock, user } = await setup();
150 | await user.click(await screen.findByRole('button', { name: /New Entity/i }));
151 | expect(modalMock.open).toHaveBeenCalledWith('new entity');
152 |
153 | const row = await screen.findByRole('row', {
154 | name: /Entity 2/i,
155 | });
156 |
157 | await user.click(
158 | await within(row).findByRole('button', {
159 | name: /edit/i,
160 | }),
161 | );
162 | await waitFor(() => expect(modalMock.open).toHaveBeenCalledWith('edit entity', 'Entity 2'));
163 | });
164 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/integrations/ng-mocks.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
2 | import { By } from '@angular/platform-browser';
3 |
4 | import { MockComponent } from 'ng-mocks';
5 | import { render } from '../../src/public_api';
6 | import { NgIf } from '@angular/common';
7 |
8 | test('sends the correct value to the child input', async () => {
9 | const utils = await render(TargetComponent, {
10 | imports: [MockComponent(ChildComponent)],
11 | inputs: { value: 'foo' },
12 | });
13 |
14 | const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
15 | expect(children).toHaveLength(1);
16 |
17 | const mockComponent = children[0].componentInstance;
18 | expect(mockComponent.someInput).toBe('foo');
19 | });
20 |
21 | test('sends the correct value to the child input 2', async () => {
22 | const utils = await render(TargetComponent, {
23 | imports: [MockComponent(ChildComponent)],
24 | inputs: { value: 'bar' },
25 | });
26 |
27 | const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
28 | expect(children).toHaveLength(1);
29 |
30 | const mockComponent = children[0].componentInstance;
31 | expect(mockComponent.someInput).toBe('bar');
32 | });
33 |
34 | @Component({
35 | selector: 'atl-child',
36 | template: 'child',
37 | standalone: true,
38 | imports: [NgIf],
39 | })
40 | class ChildComponent {
41 | @ContentChild('something')
42 | public injectedSomething: TemplateRef | undefined;
43 |
44 | @Input()
45 | public someInput = '';
46 |
47 | @Output()
48 | public someOutput = new EventEmitter();
49 |
50 | public childMockComponent() {
51 | /* noop */
52 | }
53 | }
54 |
55 | @Component({
56 | selector: 'atl-target-mock-component',
57 | template: ` `,
58 | standalone: true,
59 | imports: [ChildComponent],
60 | })
61 | class TargetComponent {
62 | @Input() value = '';
63 | public trigger = (obj: any) => obj;
64 | }
65 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-188.spec.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/testing-library/angular-testing-library/issues/188
2 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
3 | import { render, screen } from '../../src/public_api';
4 |
5 | @Component({
6 | template: `Hello {{ formattedName }}
`,
7 | })
8 | class BugOnChangeComponent implements OnChanges {
9 | @Input() name?: string;
10 |
11 | formattedName?: string;
12 |
13 | ngOnChanges(changes: SimpleChanges) {
14 | if (changes.name) {
15 | this.formattedName = changes.name.currentValue.toUpperCase();
16 | }
17 | }
18 | }
19 |
20 | test('should output formatted name after rendering', async () => {
21 | await render(BugOnChangeComponent, { componentProperties: { name: 'name' } });
22 |
23 | expect(screen.getByText('Hello NAME')).toBeInTheDocument();
24 | });
25 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-230.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, waitFor, screen } from '../../src/public_api';
3 | import { NgClass } from '@angular/common';
4 |
5 | @Component({
6 | template: ` `,
7 | imports: [NgClass],
8 | })
9 | class LoopComponent {
10 | get classes() {
11 | return {
12 | someClass: true,
13 | };
14 | }
15 | }
16 |
17 | test('wait does not end up in a loop', async () => {
18 | await render(LoopComponent);
19 |
20 | await expect(
21 | waitFor(() => {
22 | expect(true).toBe(false);
23 | }),
24 | ).rejects.toThrow();
25 | });
26 |
27 | test('find does not end up in a loop', async () => {
28 | await render(LoopComponent);
29 |
30 | await expect(screen.findByText('foo')).rejects.toThrow();
31 | });
32 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-280.spec.ts:
--------------------------------------------------------------------------------
1 | import { Location } from '@angular/common';
2 | import { Component, NgModule } from '@angular/core';
3 | import { RouterLink, RouterModule, RouterOutlet, Routes } from '@angular/router';
4 | import { RouterTestingModule } from '@angular/router/testing';
5 | import userEvent from '@testing-library/user-event';
6 | import { render, screen } from '../../src/public_api';
7 |
8 | @Component({
9 | template: ` Navigate
10 | `,
11 | imports: [RouterOutlet],
12 | })
13 | class MainComponent {}
14 |
15 | @Component({
16 | template: ` first page
17 | go to second`,
18 | imports: [RouterLink],
19 | })
20 | class FirstComponent {}
21 |
22 | @Component({
23 | template: `second page
24 | `,
25 | })
26 | class SecondComponent {
27 | constructor(private location: Location) {}
28 | goBack() {
29 | this.location.back();
30 | }
31 | }
32 |
33 | const routes: Routes = [
34 | { path: '', redirectTo: '/first', pathMatch: 'full' },
35 | { path: 'first', component: FirstComponent },
36 | { path: 'second', component: SecondComponent },
37 | ];
38 |
39 | @NgModule({
40 | imports: [RouterModule.forRoot(routes)],
41 | exports: [RouterModule],
42 | })
43 | class AppRoutingModule {}
44 |
45 | test('navigate to second page and back', async () => {
46 | await render(MainComponent, { imports: [AppRoutingModule, RouterTestingModule] });
47 |
48 | expect(await screen.findByText('Navigate')).toBeInTheDocument();
49 | expect(await screen.findByText('first page')).toBeInTheDocument();
50 |
51 | await userEvent.click(await screen.findByText('go to second'));
52 |
53 | expect(await screen.findByText('second page')).toBeInTheDocument();
54 | expect(await screen.findByText('navigate back')).toBeInTheDocument();
55 |
56 | await userEvent.click(await screen.findByText('navigate back'));
57 |
58 | expect(await screen.findByText('first page')).toBeInTheDocument();
59 | });
60 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-318.spec.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnDestroy, OnInit} from '@angular/core';
2 | import {Router} from '@angular/router';
3 | import {RouterTestingModule} from '@angular/router/testing';
4 | import {Subject, takeUntil} from 'rxjs';
5 | import {render} from "@testing-library/angular";
6 |
7 | @Component({
8 | selector: 'atl-app-fixture',
9 | template: '',
10 | })
11 | class FixtureComponent implements OnInit, OnDestroy {
12 | unsubscribe$ = new Subject();
13 |
14 | constructor(private router: Router) {}
15 |
16 | ngOnInit(): void {
17 | this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => {
18 | this.eventReceived(evt)
19 | });
20 | }
21 |
22 | ngOnDestroy(): void {
23 | this.unsubscribe$.next();
24 | this.unsubscribe$.complete();
25 | }
26 |
27 | eventReceived(evt: any) {
28 | console.log(evt);
29 | }
30 | }
31 |
32 |
33 | test('it does not invoke router events on init', async () => {
34 | const eventReceived = jest.fn();
35 | await render(FixtureComponent, {
36 | imports: [RouterTestingModule],
37 | componentProperties: {
38 | eventReceived
39 | }
40 | });
41 | expect(eventReceived).not.toHaveBeenCalled();
42 | });
43 |
44 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-346.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render } from '../../src/public_api';
3 |
4 | test('issue 364 detectChangesOnRender', async () => {
5 | @Component({
6 | selector: 'atl-fixture',
7 | template: `{{ myObj.myProp }}`,
8 | })
9 | class MyComponent {
10 | myObj: any = null;
11 | }
12 |
13 | // autoDetectChanges invokes change detection, which makes the test fail
14 | await render(MyComponent, {
15 | detectChangesOnRender: false,
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-386.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { throwError } from 'rxjs';
3 | import { render, screen, fireEvent } from '../../src/public_api';
4 |
5 | @Component({
6 | selector: 'atl-fixture',
7 | template: ``,
8 | styles: [],
9 | })
10 | class TestComponent {
11 | onTest() {
12 | throwError(() => new Error('myerror')).subscribe();
13 | }
14 | }
15 |
16 | describe('TestComponent', () => {
17 | beforeEach(() => {
18 | jest.useFakeTimers();
19 | });
20 |
21 | afterEach(() => {
22 | jest.runAllTicks();
23 | jest.useRealTimers();
24 | });
25 |
26 | it('does not fail', async () => {
27 | await render(TestComponent);
28 | fireEvent.click(screen.getByText('Test'));
29 | });
30 |
31 | it('fails because of the previous one', async () => {
32 | await render(TestComponent);
33 | fireEvent.click(screen.getByText('Test'));
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-389.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { render, screen } from '../../src/public_api';
3 |
4 | @Component({
5 | selector: 'atl-fixture',
6 | template: `Hello {{ name }}`,
7 | })
8 | class TestComponent {
9 | @Input('aliasName') name = '';
10 | }
11 |
12 | test('allows you to set componentInputs using the name alias', async () => {
13 | await render(TestComponent, { componentInputs: { aliasName: 'test' } });
14 | expect(screen.getByText('Hello test')).toBeInTheDocument();
15 | });
16 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-396-standalone-stub-child.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, screen } from '../../src/public_api';
3 |
4 | test('stub', async () => {
5 | await render(FixtureComponent, {
6 | componentImports: [StubComponent],
7 | });
8 |
9 | expect(screen.getByText('Hello from stub')).toBeInTheDocument();
10 | });
11 |
12 | test('configure', async () => {
13 | await render(FixtureComponent, {
14 | configureTestBed: (testBed) => {
15 | testBed.overrideComponent(FixtureComponent, {
16 | add: {
17 | imports: [StubComponent],
18 | },
19 | remove: {
20 | imports: [ChildComponent],
21 | },
22 | });
23 | },
24 | });
25 |
26 | expect(screen.getByText('Hello from stub')).toBeInTheDocument();
27 | });
28 |
29 | test('child', async () => {
30 | await render(FixtureComponent);
31 | expect(screen.getByText('Hello from child')).toBeInTheDocument();
32 | });
33 |
34 | @Component({
35 | selector: 'atl-child',
36 | template: `Hello from child`,
37 | standalone: true,
38 | })
39 | class ChildComponent {}
40 |
41 | @Component({
42 | selector: 'atl-child',
43 | template: `Hello from stub`,
44 | standalone: true,
45 | host: { 'collision-id': StubComponent.name },
46 | })
47 | class StubComponent {}
48 |
49 | @Component({
50 | selector: 'atl-fixture',
51 | template: ``,
52 | standalone: true,
53 | imports: [ChildComponent],
54 | })
55 | class FixtureComponent {}
56 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Directive, Input, OnInit } from '@angular/core';
2 | import { render, screen } from '../../src/public_api';
3 |
4 | test('the value set in the directive constructor is overriden by the input binding', async () => {
5 | await render(``, {
6 | imports: [FixtureComponent, InputOverrideViaConstructorDirective],
7 | });
8 |
9 | expect(screen.getByText('set by test')).toBeInTheDocument();
10 | });
11 |
12 | test('the value set in the directive onInit is used instead of the input binding', async () => {
13 | await render(``, {
14 | imports: [FixtureComponent, InputOverrideViaOnInitDirective],
15 | });
16 |
17 | expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument();
18 | });
19 |
20 | test('the value set in the directive constructor is used instead of the input value', async () => {
21 | await render(``, {
22 | imports: [FixtureComponent, InputOverrideViaConstructorDirective],
23 | });
24 |
25 | expect(screen.getByText('set by directive constructor')).toBeInTheDocument();
26 | });
27 |
28 | test('the value set in the directive ngOnInit is used instead of the input value and the directive constructor', async () => {
29 | await render(``, {
30 | imports: [FixtureComponent, InputOverrideViaConstructorDirective, InputOverrideViaOnInitDirective],
31 | });
32 |
33 | expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument();
34 | });
35 |
36 | @Component({
37 | standalone: true,
38 | selector: 'atl-fixture',
39 | template: `{{ input }}`,
40 | })
41 | class FixtureComponent {
42 | @Input() public input = 'default value';
43 | }
44 |
45 | @Directive({
46 | // eslint-disable-next-line @angular-eslint/directive-selector
47 | selector: 'atl-fixture',
48 | standalone: true,
49 | })
50 | class InputOverrideViaConstructorDirective {
51 | constructor(private fixture: FixtureComponent) {
52 | this.fixture.input = 'set by directive constructor';
53 | }
54 | }
55 |
56 | @Directive({
57 | // eslint-disable-next-line @angular-eslint/directive-selector
58 | selector: 'atl-fixture',
59 | standalone: true,
60 | })
61 | class InputOverrideViaOnInitDirective implements OnInit {
62 | constructor(private fixture: FixtureComponent) {}
63 |
64 | ngOnInit(): void {
65 | this.fixture.input = 'set by directive ngOnInit';
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-398-component-without-host-id.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { render, screen } from '../../src/public_api';
3 |
4 | test('should create the app', async () => {
5 | await render(FixtureComponent);
6 | expect(screen.getByRole('heading')).toBeInTheDocument();
7 | });
8 |
9 | test('should re-create the app', async () => {
10 | await render(FixtureComponent);
11 | expect(screen.getByRole('heading')).toBeInTheDocument();
12 | });
13 |
14 | @Component({
15 | selector: 'atl-fixture',
16 | standalone: true,
17 | template: 'My title
',
18 | host: {
19 | '[attr.id]': 'null', // this breaks the cleaning up of tests
20 | },
21 | })
22 | class FixtureComponent {}
23 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef } from '@angular/core';
2 | import { NgIf } from '@angular/common';
3 | import { render } from '../../src/public_api';
4 |
5 | test('declaration specific dependencies should be available for components', async () => {
6 | @Component({
7 | selector: 'atl-test',
8 | standalone: true,
9 | template: `Test
`,
10 | })
11 | class TestComponent {
12 | // eslint-disable-next-line @typescript-eslint/no-empty-function
13 | constructor(_elementRef: ElementRef) {}
14 | }
15 |
16 | await expect(async () => await render(TestComponent)).not.toThrow();
17 | });
18 |
19 | test('standalone directives imported in standalone components', async () => {
20 | @Component({
21 | selector: 'atl-test',
22 | standalone: true,
23 | imports: [NgIf],
24 | template: `Test
`,
25 | })
26 | class TestComponent {}
27 |
28 | await render(TestComponent);
29 | });
30 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-435.spec.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { BehaviorSubject } from 'rxjs';
3 | import { Component, Inject, Injectable } from '@angular/core';
4 | import { screen, render } from '../../src/public_api';
5 |
6 | // Service
7 | @Injectable()
8 | class DemoService {
9 | buttonTitle = new BehaviorSubject('Click me');
10 | }
11 |
12 | // Component
13 | @Component({
14 | selector: 'atl-issue-435',
15 | standalone: true,
16 | imports: [CommonModule],
17 | providers: [DemoService],
18 | template: `
19 |
23 | `,
24 | })
25 | class DemoComponent {
26 | constructor(@Inject(DemoService) public demoService: DemoService) {}
27 | }
28 |
29 | test('issue #435', async () => {
30 | await render(DemoComponent);
31 |
32 | const button = screen.getByRole('button', {
33 | name: /Click me/,
34 | });
35 |
36 | expect(button).toBeVisible();
37 | });
38 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-437.spec.ts:
--------------------------------------------------------------------------------
1 | import userEvent from '@testing-library/user-event';
2 | import { screen, render } from '../../src/public_api';
3 | import { MatSidenavModule } from '@angular/material/sidenav';
4 |
5 | afterEach(() => {
6 | jest.useRealTimers();
7 | });
8 |
9 | test('issue #437', async () => {
10 | const user = userEvent.setup();
11 | await render(
12 | `
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | `,
24 | { imports: [MatSidenavModule] },
25 | );
26 |
27 | await screen.findByTestId('test-button');
28 |
29 | await user.click(screen.getByTestId('test-button'));
30 | });
31 |
32 | test('issue #437 with fakeTimers', async () => {
33 | jest.useFakeTimers();
34 | const user = userEvent.setup({
35 | advanceTimers: jest.advanceTimersByTime,
36 | });
37 | await render(
38 | `
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | `,
50 | { imports: [MatSidenavModule] },
51 | );
52 |
53 | await screen.findByTestId('test-button');
54 |
55 | await user.click(screen.getByTestId('test-button'));
56 | });
57 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-492.spec.ts:
--------------------------------------------------------------------------------
1 | import { AsyncPipe } from '@angular/common';
2 | import { Component, inject, Injectable } from '@angular/core';
3 | import { render, screen } from '../../src/public_api';
4 | import { Observable, BehaviorSubject, map } from 'rxjs';
5 |
6 | test('displays username', async () => {
7 | // stubbed user service using a Subject
8 | const user = new BehaviorSubject({ name: 'username 1' });
9 | const userServiceStub: Partial = {
10 | getName: () => user.asObservable().pipe(map((u) => u.name)),
11 | };
12 |
13 | // render the component with injection of the stubbed service
14 | await render(UserComponent, {
15 | componentProviders: [
16 | {
17 | provide: UserService,
18 | useValue: userServiceStub,
19 | },
20 | ],
21 | });
22 |
23 | // assert first username emitted is rendered
24 | expect(await screen.findByRole('heading', { name: 'username 1' })).toBeInTheDocument();
25 |
26 | // emitting a second username
27 | user.next({ name: 'username 2' });
28 |
29 | // assert the second username is rendered
30 | expect(await screen.findByRole('heading', { name: 'username 2' })).toBeInTheDocument();
31 | });
32 |
33 | @Component({
34 | selector: 'atl-user',
35 | standalone: true,
36 | template: `{{ username$ | async }}
`,
37 | imports: [AsyncPipe],
38 | })
39 | class UserComponent {
40 | readonly username$: Observable = inject(UserService).getName();
41 | }
42 |
43 | @Injectable()
44 | class UserService {
45 | getName(): Observable {
46 | throw new Error('Not implemented');
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-493.spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient, provideHttpClient } from '@angular/common/http';
2 | import { provideHttpClientTesting } from '@angular/common/http/testing';
3 | import { Component, input } from '@angular/core';
4 | import { render, screen } from '../../src/public_api';
5 |
6 | test('succeeds', async () => {
7 | await render(DummyComponent, {
8 | inputs: {
9 | value: 'test',
10 | },
11 | providers: [provideHttpClientTesting(), provideHttpClient()],
12 | });
13 |
14 | expect(screen.getByText('test')).toBeVisible();
15 | });
16 |
17 | @Component({
18 | selector: 'atl-dummy',
19 | standalone: true,
20 | imports: [],
21 | template: '{{ value() }}
',
22 | })
23 | class DummyComponent {
24 | value = input.required();
25 | // @ts-expect-error http is unused but needed for the test
26 | constructor(private http: HttpClient) {}
27 | }
28 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/issues/issue-67.spec.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/testing-library/angular-testing-library/issues/67
2 | import { Component } from '@angular/core';
3 | import { render, screen } from '../../src/public_api';
4 |
5 | @Component({
6 | template: `
7 |
8 |
9 |
13 |
14 | `,
15 | })
16 | class BugGetByLabelTextComponent {}
17 |
18 | test('first step to reproduce the bug: skip this test to avoid the error or remove the for attribute of label', async () => {
19 | expect(await render(BugGetByLabelTextComponent)).toBeDefined();
20 | });
21 |
22 | test('second step: bug happens :`(', async () => {
23 | await render(BugGetByLabelTextComponent);
24 |
25 | const checkboxByTestId = screen.getByTestId('checkbox');
26 | const checkboxByLabelTest = screen.getByLabelText('TEST');
27 |
28 | expect(checkboxByTestId).toBe(checkboxByLabelTest);
29 | });
30 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/navigate.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TestBed } from '@angular/core/testing';
3 | import { Router } from '@angular/router';
4 | import { render } from '../src/public_api';
5 |
6 | @Component({
7 | selector: 'atl-fixture',
8 | template: ``,
9 | })
10 | class FixtureComponent {}
11 |
12 | test('should navigate correctly', async () => {
13 | const { navigate } = await render(FixtureComponent, {
14 | routes: [{ path: 'details', component: FixtureComponent }],
15 | });
16 |
17 | const router = TestBed.inject(Router);
18 | const navSpy = jest.spyOn(router, 'navigate');
19 |
20 | navigate('details');
21 |
22 | expect(navSpy).toHaveBeenCalledWith(['details']);
23 | });
24 |
25 | test('should pass queryParams if provided', async () => {
26 | const { navigate } = await render(FixtureComponent, {
27 | routes: [{ path: 'details', component: FixtureComponent }],
28 | });
29 |
30 | const router = TestBed.inject(Router);
31 | const navSpy = jest.spyOn(router, 'navigate');
32 |
33 | navigate('details?sortBy=name&sortOrder=asc');
34 |
35 | expect(navSpy).toHaveBeenCalledWith(['details'], {
36 | queryParams: {
37 | sortBy: 'name',
38 | sortOrder: 'asc',
39 | },
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/providers/component-provider.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Provider } from '@angular/core';
2 | import { Component } from '@angular/core';
3 | import { render, screen } from '../../src/public_api';
4 |
5 | test('shows the service value', async () => {
6 | await render(FixtureComponent);
7 |
8 | expect(screen.getByText('foo')).toBeInTheDocument();
9 | });
10 |
11 | test('shows the provided service value', async () => {
12 | await render(FixtureComponent, {
13 | componentProviders: [
14 | {
15 | provide: Service,
16 | useValue: {
17 | foo() {
18 | return 'bar';
19 | },
20 | },
21 | },
22 | ],
23 | });
24 |
25 | expect(screen.getByText('bar')).toBeInTheDocument();
26 | });
27 |
28 | test('shows the provided service value with template syntax', async () => {
29 | await render(FixtureComponent, {
30 | componentProviders: [
31 | {
32 | provide: Service,
33 | useValue: {
34 | foo() {
35 | return 'bar';
36 | },
37 | },
38 | },
39 | ],
40 | });
41 |
42 | expect(screen.getByText('bar')).toBeInTheDocument();
43 | });
44 |
45 | test('flatten the nested array of component providers', async () => {
46 | const provideService = (): Provider => [
47 | {
48 | provide: Service,
49 | useValue: {
50 | foo() {
51 | return 'bar';
52 | },
53 | },
54 | },
55 | ];
56 | await render(FixtureComponent, {
57 | componentProviders: [provideService()],
58 | });
59 |
60 | expect(screen.getByText('bar')).toBeInTheDocument();
61 | });
62 |
63 | @Injectable()
64 | class Service {
65 | foo() {
66 | return 'foo';
67 | }
68 | }
69 |
70 | @Component({
71 | selector: 'atl-fixture',
72 | template: '{{service.foo()}}',
73 | providers: [Service],
74 | })
75 | class FixtureComponent {
76 | constructor(public service: Service) {}
77 | }
78 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/providers/module-provider.spec.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Component } from '@angular/core';
3 | import { render, screen } from '../../src/public_api';
4 |
5 | test('shows the service value', async () => {
6 | await render(FixtureComponent, {
7 | providers: [Service],
8 | });
9 |
10 | expect(screen.getByText('foo')).toBeInTheDocument();
11 | });
12 |
13 | test('shows the service value with template syntax', async () => {
14 | await render(FixtureComponent, {
15 | providers: [Service],
16 | });
17 |
18 | expect(screen.getByText('foo')).toBeInTheDocument();
19 | });
20 |
21 | test('shows the provided service value', async () => {
22 | await render(FixtureComponent, {
23 | providers: [
24 | {
25 | provide: Service,
26 | useValue: {
27 | foo() {
28 | return 'bar';
29 | },
30 | },
31 | },
32 | ],
33 | });
34 |
35 | expect(screen.getByText('bar')).toBeInTheDocument();
36 | });
37 |
38 | test('shows the provided service value with template syntax', async () => {
39 | await render(FixtureComponent, {
40 | providers: [
41 | {
42 | provide: Service,
43 | useValue: {
44 | foo() {
45 | return 'bar';
46 | },
47 | },
48 | },
49 | ],
50 | });
51 |
52 | expect(screen.getByText('bar')).toBeInTheDocument();
53 | });
54 |
55 | @Injectable()
56 | class Service {
57 | foo() {
58 | return 'foo';
59 | }
60 | }
61 |
62 | @Component({
63 | selector: 'atl-fixture',
64 | template: '{{service.foo()}}',
65 | })
66 | class FixtureComponent {
67 | constructor(public service: Service) {}
68 | }
69 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/render-template.spec.ts:
--------------------------------------------------------------------------------
1 | import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component } from '@angular/core';
2 |
3 | import { render, fireEvent, screen } from '../src/public_api';
4 |
5 | @Directive({
6 | // eslint-disable-next-line @angular-eslint/directive-selector
7 | selector: '[onOff]',
8 | })
9 | class OnOffDirective {
10 | @Input() on = 'on';
11 | @Input() off = 'off';
12 | @Output() clicked = new EventEmitter();
13 |
14 | constructor(private el: ElementRef) {
15 | this.el.nativeElement.textContent = 'init';
16 | }
17 |
18 | @HostListener('click') onClick() {
19 | this.el.nativeElement.textContent = this.el.nativeElement.textContent === this.on ? this.off : this.on;
20 | this.clicked.emit(this.el.nativeElement.textContent);
21 | }
22 | }
23 |
24 | @Directive({
25 | // eslint-disable-next-line @angular-eslint/directive-selector
26 | selector: '[update]',
27 | })
28 | class UpdateInputDirective {
29 | @Input()
30 | set update(value: any) {
31 | this.el.nativeElement.textContent = value;
32 | }
33 |
34 | constructor(private el: ElementRef) {}
35 | }
36 |
37 | @Component({
38 | // eslint-disable-next-line @angular-eslint/component-selector
39 | selector: 'greeting',
40 | template: 'Hello {{ name }}!',
41 | })
42 | class GreetingComponent {
43 | @Input() name = 'World';
44 | }
45 |
46 | test('the directive renders', async () => {
47 | const view = await render('', {
48 | imports: [OnOffDirective],
49 | });
50 |
51 | // eslint-disable-next-line testing-library/no-container
52 | expect(view.container.querySelector('[onoff]')).toBeInTheDocument();
53 | });
54 |
55 | test('the component renders', async () => {
56 | const view = await render('', {
57 | imports: [GreetingComponent],
58 | });
59 |
60 | // eslint-disable-next-line testing-library/no-container
61 | expect(view.container.querySelector('greeting')).toBeInTheDocument();
62 | expect(screen.getByText('Hello Angular!')).toBeInTheDocument();
63 | });
64 |
65 | test('uses the default props', async () => {
66 | await render('', {
67 | imports: [OnOffDirective],
68 | });
69 |
70 | fireEvent.click(screen.getByText('init'));
71 | fireEvent.click(screen.getByText('on'));
72 | fireEvent.click(screen.getByText('off'));
73 | });
74 |
75 | test('overrides input properties', async () => {
76 | await render('', {
77 | imports: [OnOffDirective],
78 | });
79 |
80 | fireEvent.click(screen.getByText('init'));
81 | fireEvent.click(screen.getByText('hello'));
82 | fireEvent.click(screen.getByText('off'));
83 | });
84 |
85 | test('overrides input properties via a wrapper', async () => {
86 | // `bar` will be set as a property on the wrapper component, the property will be used to pass to the directive
87 | await render('', {
88 | imports: [OnOffDirective],
89 | componentProperties: {
90 | bar: 'hello',
91 | },
92 | });
93 |
94 | fireEvent.click(screen.getByText('init'));
95 | fireEvent.click(screen.getByText('hello'));
96 | fireEvent.click(screen.getByText('off'));
97 | });
98 |
99 | test('overrides output properties', async () => {
100 | const clicked = jest.fn();
101 |
102 | await render('', {
103 | imports: [OnOffDirective],
104 | componentProperties: {
105 | clicked,
106 | },
107 | });
108 |
109 | fireEvent.click(screen.getByText('init'));
110 | expect(clicked).toHaveBeenCalledWith('on');
111 |
112 | fireEvent.click(screen.getByText('on'));
113 | expect(clicked).toHaveBeenCalledWith('off');
114 | });
115 |
116 | describe('removeAngularAttributes', () => {
117 | it('should remove angular attributes', async () => {
118 | await render('', {
119 | imports: [OnOffDirective],
120 | removeAngularAttributes: true,
121 | });
122 |
123 | expect(document.querySelector('[ng-version]')).toBeNull();
124 | expect(document.querySelector('[id]')).toBeNull();
125 | });
126 |
127 | it('is disabled by default', async () => {
128 | await render('', {
129 | imports: [OnOffDirective],
130 | });
131 |
132 | expect(document.querySelector('[ng-version]')).not.toBeNull();
133 | expect(document.querySelector('[id]')).not.toBeNull();
134 | });
135 | });
136 |
137 | test('updates properties and invokes change detection', async () => {
138 | const view = await render<{ value: string }>('', {
139 | imports: [UpdateInputDirective],
140 | componentProperties: {
141 | value: 'value1',
142 | },
143 | });
144 |
145 | expect(screen.getByText('value1')).toBeInTheDocument();
146 | view.fixture.componentInstance.value = 'updated value';
147 | expect(screen.getByText('updated value')).toBeInTheDocument();
148 | });
149 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/rerender.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2 | import { render, screen } from '../src/public_api';
3 |
4 | let ngOnChangesSpy: jest.Mock;
5 | @Component({
6 | selector: 'atl-fixture',
7 | template: ` {{ firstName }} {{ lastName }} `,
8 | })
9 | class FixtureComponent implements OnChanges {
10 | @Input() firstName = 'Sarah';
11 | @Input() lastName?: string;
12 | ngOnChanges(changes: SimpleChanges): void {
13 | ngOnChangesSpy(changes);
14 | }
15 | }
16 |
17 | beforeEach(() => {
18 | ngOnChangesSpy = jest.fn();
19 | });
20 |
21 | test('rerenders the component with updated props', async () => {
22 | const { rerender } = await render(FixtureComponent);
23 | expect(screen.getByText('Sarah')).toBeInTheDocument();
24 |
25 | const firstName = 'Mark';
26 | await rerender({ componentProperties: { firstName } });
27 |
28 | expect(screen.getByText(firstName)).toBeInTheDocument();
29 | });
30 |
31 | test('rerenders without props', async () => {
32 | const { rerender } = await render(FixtureComponent);
33 | expect(screen.getByText('Sarah')).toBeInTheDocument();
34 |
35 | await rerender();
36 |
37 | expect(screen.getByText('Sarah')).toBeInTheDocument();
38 | expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); // one time initially and one time for rerender
39 | });
40 |
41 | test('rerenders the component with updated inputs', async () => {
42 | const { rerender } = await render(FixtureComponent);
43 | expect(screen.getByText('Sarah')).toBeInTheDocument();
44 |
45 | const firstName = 'Mark';
46 | await rerender({ inputs: { firstName } });
47 |
48 | expect(screen.getByText(firstName)).toBeInTheDocument();
49 | });
50 |
51 | test('rerenders the component with updated inputs and resets other props', async () => {
52 | const firstName = 'Mark';
53 | const lastName = 'Peeters';
54 | const { rerender } = await render(FixtureComponent, {
55 | inputs: {
56 | firstName,
57 | lastName,
58 | },
59 | });
60 |
61 | expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
62 |
63 | const firstName2 = 'Chris';
64 | await rerender({ inputs: { firstName: firstName2 } });
65 |
66 | expect(screen.getByText(firstName2)).toBeInTheDocument();
67 | expect(screen.queryByText(firstName)).not.toBeInTheDocument();
68 | expect(screen.queryByText(lastName)).not.toBeInTheDocument();
69 |
70 | expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
71 | const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
72 | expect(rerenderedChanges).toEqual({
73 | lastName: {
74 | previousValue: 'Peeters',
75 | currentValue: undefined,
76 | firstChange: false,
77 | },
78 | firstName: {
79 | previousValue: 'Mark',
80 | currentValue: 'Chris',
81 | firstChange: false,
82 | },
83 | });
84 | });
85 |
86 | test('rerenders the component with updated inputs and keeps other props when partial is true', async () => {
87 | const firstName = 'Mark';
88 | const lastName = 'Peeters';
89 | const { rerender } = await render(FixtureComponent, {
90 | inputs: {
91 | firstName,
92 | lastName,
93 | },
94 | });
95 |
96 | expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
97 |
98 | const firstName2 = 'Chris';
99 | await rerender({ inputs: { firstName: firstName2 }, partialUpdate: true });
100 |
101 | expect(screen.queryByText(firstName)).not.toBeInTheDocument();
102 | expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
103 |
104 | expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
105 | const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
106 | expect(rerenderedChanges).toEqual({
107 | firstName: {
108 | previousValue: 'Mark',
109 | currentValue: 'Chris',
110 | firstChange: false,
111 | },
112 | });
113 | });
114 |
115 | test('rerenders the component with updated props and resets other props with componentProperties', async () => {
116 | const firstName = 'Mark';
117 | const lastName = 'Peeters';
118 | const { rerender } = await render(FixtureComponent, {
119 | componentProperties: {
120 | firstName,
121 | lastName,
122 | },
123 | });
124 |
125 | expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
126 |
127 | const firstName2 = 'Chris';
128 | await rerender({ componentProperties: { firstName: firstName2 } });
129 |
130 | expect(screen.getByText(firstName2)).toBeInTheDocument();
131 | expect(screen.queryByText(firstName)).not.toBeInTheDocument();
132 | expect(screen.queryByText(lastName)).not.toBeInTheDocument();
133 |
134 | expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
135 | const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
136 | expect(rerenderedChanges).toEqual({
137 | lastName: {
138 | previousValue: 'Peeters',
139 | currentValue: undefined,
140 | firstChange: false,
141 | },
142 | firstName: {
143 | previousValue: 'Mark',
144 | currentValue: 'Chris',
145 | firstChange: false,
146 | },
147 | });
148 | });
149 |
150 | test('rerenders the component with updated props keeps other props when partial is true', async () => {
151 | const firstName = 'Mark';
152 | const lastName = 'Peeters';
153 | const { rerender } = await render(FixtureComponent, {
154 | componentProperties: {
155 | firstName,
156 | lastName,
157 | },
158 | });
159 |
160 | expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
161 |
162 | const firstName2 = 'Chris';
163 | await rerender({ componentProperties: { firstName: firstName2 }, partialUpdate: true });
164 |
165 | expect(screen.queryByText(firstName)).not.toBeInTheDocument();
166 | expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
167 |
168 | expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
169 | const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
170 | expect(rerenderedChanges).toEqual({
171 | firstName: {
172 | previousValue: 'Mark',
173 | currentValue: 'Chris',
174 | firstChange: false,
175 | },
176 | });
177 | });
178 |
179 | test('change detection gets not called if `detectChangesOnRender` is set to false', async () => {
180 | const { rerender } = await render(FixtureComponent);
181 | expect(screen.getByText('Sarah')).toBeInTheDocument();
182 |
183 | const firstName = 'Mark';
184 | await rerender({ inputs: { firstName }, detectChangesOnRender: false });
185 |
186 | expect(screen.getByText('Sarah')).toBeInTheDocument();
187 | expect(screen.queryByText(firstName)).not.toBeInTheDocument();
188 | });
189 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { render, screen, waitForElementToBeRemoved } from '../src/public_api';
3 | import { timer } from 'rxjs';
4 | import { NgIf } from '@angular/common';
5 |
6 | @Component({
7 | selector: 'atl-fixture',
8 | template: ` 👋
`,
9 | imports: [NgIf],
10 | })
11 | class FixtureComponent implements OnInit {
12 | visible = true;
13 | ngOnInit() {
14 | timer(500).subscribe(() => (this.visible = false));
15 | }
16 | }
17 |
18 | test('waits for element to be removed (callback)', async () => {
19 | await render(FixtureComponent);
20 |
21 | await waitForElementToBeRemoved(() => screen.queryByTestId('im-here'));
22 |
23 | expect(screen.queryByTestId('im-here')).not.toBeInTheDocument();
24 | });
25 |
26 | test('waits for element to be removed (element)', async () => {
27 | await render(FixtureComponent);
28 |
29 | await waitForElementToBeRemoved(screen.queryByTestId('im-here'));
30 |
31 | expect(screen.queryByTestId('im-here')).not.toBeInTheDocument();
32 | });
33 |
34 | test('allows to override options', async () => {
35 | await render(FixtureComponent);
36 |
37 | await expect(waitForElementToBeRemoved(() => screen.queryByTestId('im-here'), { timeout: 200 })).rejects.toThrow(
38 | /Timed out in waitForElementToBeRemoved/i,
39 | );
40 | });
41 |
--------------------------------------------------------------------------------
/projects/testing-library/tests/wait-for.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { timer } from 'rxjs';
3 | import { render, screen, waitFor, fireEvent } from '../src/public_api';
4 |
5 | @Component({
6 | selector: 'atl-fixture',
7 | template: `
8 |
9 | {{ result }}
10 | `,
11 | })
12 | class FixtureComponent {
13 | result = '';
14 |
15 | load() {
16 | timer(500).subscribe(() => (this.result = 'Success'));
17 | }
18 | }
19 |
20 | test('waits for assertion to become true', async () => {
21 | await render(FixtureComponent);
22 |
23 | expect(screen.queryByText('Success')).not.toBeInTheDocument();
24 |
25 | fireEvent.click(screen.getByTestId('button'));
26 |
27 | expect(await screen.findByText('Success')).toBeInTheDocument();
28 | });
29 |
30 | test('allows to override options', async () => {
31 | await render(FixtureComponent);
32 |
33 | fireEvent.click(screen.getByTestId('button'));
34 |
35 | await expect(waitFor(() => screen.getByText('Success'), { timeout: 200 })).rejects.toThrow(
36 | /Unable to find an element with the text: Success/i,
37 | );
38 | });
39 |
--------------------------------------------------------------------------------
/projects/testing-library/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.lib.json"
8 | },
9 | {
10 | "path": "./tsconfig.lib.prod.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ],
16 | "compilerOptions": {
17 | "target": "es2020"
18 | },
19 | "angularCompilerOptions": {
20 | "strictInjectionParameters": true,
21 | "strictInputAccessModifiers": true,
22 | "strictTemplates": true,
23 | "flatModuleId": "AUTOGENERATED",
24 | "flatModuleOutFile": "AUTOGENERATED"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/projects/testing-library/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "declaration": true,
6 | "declarationMap": true,
7 | "inlineSources": true,
8 | "types": ["node", "jest"],
9 | "target": "ES2022",
10 | "useDefineForClassFields": false
11 | },
12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
13 | "include": ["**/*.ts"]
14 | }
15 |
--------------------------------------------------------------------------------
/projects/testing-library/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false,
5 | "target": "ES2022",
6 | "useDefineForClassFields": false
7 | },
8 | "angularCompilerOptions": {
9 | "compilationMode": "partial"
10 | },
11 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/projects/testing-library/tsconfig.schematics.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "target": "ES2020",
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "resolveJsonModule": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "../../dist/@testing-library/angular/schematics",
12 | "removeComments": true,
13 | "skipLibCheck": true,
14 | "sourceMap": false
15 | },
16 | "include": ["schematics/**/*.ts"],
17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/projects/testing-library/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "types": ["node", "jest", "@testing-library/jest-dom"]
6 | },
7 | "files": ["test-setup.ts"],
8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behavior to automatically normalize line endings.
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.vsix
3 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that launches the extension inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | .gitignore
4 | vsc-extension-quickstart.md
5 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "vscode-testing-library-render" extension will be documented in this file.
4 |
5 | ## 0.0.3
6 |
7 | - docs: add logo
8 |
9 | ## 0.0.2
10 |
11 | - fix: highlight on next line
12 |
13 | ## 0.0.1
14 |
15 | - feat: initial release
16 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/README.md:
--------------------------------------------------------------------------------
1 | # vscode-atl-render
2 |
3 | This extension adds HTML highlighting to the render method of the Angular Testing Library.
4 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/language-configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "comments": {
3 | "blockComment": [""]
4 | },
5 | "brackets": [
6 | [""],
7 | ["<", ">"],
8 | ["{", "}"],
9 | ["(", ")"],
10 | ["[", "]"]
11 | ],
12 | "autoClosingPairs": [
13 | { "open": "{", "close": "}" },
14 | { "open": "[", "close": "]" },
15 | { "open": "(", "close": ")" },
16 | { "open": "'", "close": "'" },
17 | { "open": "\"", "close": "\"" },
18 | { "open": "", "notIn": ["comment", "string"] },
19 | { "open": "/**", "close": "*/", "notIn": ["string"] }
20 | ],
21 | "surroundingPairs": [
22 | { "open": "'", "close": "'" },
23 | { "open": "\"", "close": "\"" },
24 | { "open": "`", "close": "`" },
25 | { "open": "{", "close": "}" },
26 | { "open": "[", "close": "]" },
27 | { "open": "(", "close": ")" },
28 | { "open": "<", "close": ">" }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/other/hedgehog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/angular-testing-library/e1e046c75c297fd8ab06237178d036553b1ff215/projects/vscode-atl-render/other/hedgehog.png
--------------------------------------------------------------------------------
/projects/vscode-atl-render/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-atl-render",
3 | "displayName": "Angular Testing Library Render Highlighting",
4 | "description": "HTML highlighting in ATL the render method",
5 | "version": "0.0.3",
6 | "icon": "other/logo.png",
7 | "publisher": "timdeschryver",
8 | "license": "MIT",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/testing-library/angular-testing-library.git"
12 | },
13 | "homepage": "https://github.com/testing-library/angular-testing-library/blob/main/README.md",
14 | "engines": {
15 | "vscode": "^1.57.0"
16 | },
17 | "categories": [
18 | "Programming Languages"
19 | ],
20 | "contributes": {
21 | "configuration": [
22 | {
23 | "id": "atl-render",
24 | "title": "Angular Testing Library Render",
25 | "properties": {
26 | "atl-render.format.enabled": {
27 | "type": "boolean",
28 | "description": "Enable/disable formatting of render template strings.",
29 | "default": true
30 | }
31 | }
32 | }
33 | ],
34 | "grammars": [
35 | {
36 | "scopeName": "atl.render",
37 | "path": "./syntaxes/atl-render.json",
38 | "injectTo": [
39 | "source.ts"
40 | ],
41 | "embeddedLanguages": {
42 | "text.html": "html"
43 | }
44 | }
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/projects/vscode-atl-render/syntaxes/atl-render.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3 | "scopeName": "atl.render",
4 | "injectionSelector": "L:source.ts -comment",
5 | "name": "atl.render",
6 | "patterns": [
7 | {
8 | "include": "#renderMethod"
9 | }
10 | ],
11 | "repository": {
12 | "renderMethod": {
13 | "name": "renderMethod",
14 | "begin": "(?x)(\\b(?:\\w+\\.)*(?:render)\\s*)(\\()",
15 | "beginCaptures": {
16 | "1": {
17 | "name": "entity.name.function.ts"
18 | },
19 | "2": {
20 | "name": "meta.brace.round.ts"
21 | }
22 | },
23 | "end": "(\\))",
24 | "endCaptures": {
25 | "0": {
26 | "name": "meta.brace.round.ts"
27 | }
28 | },
29 | "patterns": [
30 | {
31 | "include": "#renderTemplate"
32 | },
33 | {
34 | "include": "source.ts"
35 | }
36 | ]
37 | },
38 | "renderTemplate": {
39 | "contentName": "text.html",
40 | "begin": "[`|'|\"]",
41 | "beginCaptures": {
42 | "0": {
43 | "name": "string"
44 | }
45 | },
46 | "end": "\\0",
47 | "endCaptures": {
48 | "0": {
49 | "name": "string"
50 | }
51 | },
52 | "patterns": [
53 | {
54 | "include": "text.html.derivative"
55 | },
56 | {
57 | "include": "template.ng"
58 | }
59 | ]
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pkgRoot: 'dist/@testing-library/angular',
3 | branches: ['main', { name: 'beta', prerelease: true }],
4 | };
5 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "declaration": false,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "importHelpers": true,
9 | "lib": ["es2018", "dom"],
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "outDir": "./dist/out-tsc",
13 | "sourceMap": true,
14 | "target": "ES2020",
15 | "typeRoots": ["node_modules/@types"],
16 | "strict": true,
17 | "exactOptionalPropertyTypes": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "noImplicitOverride": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noImplicitReturns": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "paths": {
25 | "@testing-library/angular": ["projects/testing-library"],
26 | "@testing-library/angular/jest-utils": ["projects/testing-library/jest-utils"]
27 | }
28 | },
29 | "exclude": ["node_modules", "tmp"]
30 | }
31 |
--------------------------------------------------------------------------------