├── .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 |
39 | 40 |
Email is invalid
41 | 42 |
Password is invalid
43 | 44 |
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 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | 27 |
28 | 29 |
30 |

{{ error }}

31 |
32 |
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 |
24 | 25 | Name 26 | 27 | 28 | 29 | I Agree 30 | 31 | 32 | Score 33 | 43 | 44 | 45 | 46 | Color 47 | 48 | 49 | {{ colorControlDisplayValue }} 50 | 51 | --- 52 | {{ color.value }} 53 | 54 | 55 | 56 | 57 | Choose a date 58 | 59 | MM/DD/YYYY 60 | 61 | 62 | 63 | 64 |
65 |

{{ error }}

66 |
67 |
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 |
10 |
11 | 12 | 13 |
14 |
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 | 31 | 34 | 35 |
{{ entity.name }} 32 | 33 |
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 | --------------------------------------------------------------------------------