Upgrade to Angular 21
Hi Joe, hope you are well.
Picking up a new Angular project and noticed Angular 21 just came out, decided to try updating to 21.
Obviously no pressure to land this - I wanted to check out the new features.
- Updated Angular and dependencies to 21
- Updated npm dependencies
- Fix stylelint breaking change (added
rules: {}to config)
- Fix stylelint breaking change (added
- Added npm package-lock.json
peer:truechange ...shakes fist at dependabot - Zoneless change detection:
- Migrated to Angular 21's zoneless change detection (provideZonelessChangeDetection())
- Removed zone.js dependency - smaller bundle size (-40kB)
- Karma to Vitest migration:
- Angular 21 defaults to Vitest, so migrated from Karma
- Removed Karma/Jasmine dependencies and config files
- Tests now run via ng test which uses Vitest under the hood
- VS Code Vitest extension workaround:
- Added vitest.config.ts and vitest.setup.ts to enable IDE Test Explorer integration
- This is a temporary hack - Angular's builder doesn't yet expose Vitest directly to IDEs
- The config proxies source files to Angular's pre-compiled output in .angular/cache
- Will be obsolete when resolved: https://github.com/angular/angular-cli/issues/31734
Commits build on each other, may want to avoid Karma-Vitest migration for now given the hack I had to write to make IDE integration work. I like how fast the unit tests run though, and without a Chrome dependency. Spent a lot of time this year with Playwright, but felt like too much for this so I didn't add that though I probably will in my project.
Cheers,
Phil
@replete this looks amazing. Let me get caught up a bit and review.
As for playwright, I did add an e2e testing section to the README.md that goes over enabling playwright and the eslint config for it. Can you think of project-agnostic configuration above and beyond this to include in either the repo or the README.md for playwright?
During my new Angular project I will evaluate how difficult it would be to implement a similar jsdom browserless angular component unit testing project into Playwright, removing Vitest completely.
I suspect this would escape the current scope of this repository and double the README size, but I'll let you know how it goes. One testing framework to rule them all .. maybe.
One thing that comes to mind is eslint and testing. I have been using airbnb rules, and found myself disabling many rules for the testing project. Rules for client-side code felt hampering and pointless in testing land, especially with a data-driven test framework.
Naming conventions became increasingly important at scale, e.g. foo.<playwright-project>.test.js (e.g. foo.smoke.test.js, bar.int.test.js). Sharing constants from playwright.config.js. I have pages of things to add...
playwright single-framework tradeoffs
I have done some investigations and discovered significant tradeoffs with my 'one testing framework' idea:
Pros:
- Single framework for unit/API/integration/E2E/a11y/perf tests
- Fewer dependencies, simpler developer onboarding
- Unified config, assertions, reporting, CI/CD
- Still fast for unit tests (browser binaries only load when actually needed)
Cons:
- Against Angular conventions - harder team onboarding?
- No Angular schematic migrations when framework updates
- Custom TestBed apparatus needed ... which we will have to maintain
- Vitest has better watch mode/HMR for TDD workflow (watches test and source files)
I think the Angular team would have considered this for sure, though they've not published it. So I'm writing off this idea, though it is still appealing and technically feasible.
Others seem to be working on this too, suggesting this idea is not without merits:
- https://github.com/sand4rt/playwright-ct-angular
Hopefully the Vitest IDE integration gets fixed with a vitest plugin or something to unify the testbed with ng-test.
ng e2e thoughts
A mature project with proper testing is likely to require Playwright with multiple projects (e2e, integration, smoke, security, a11y, api, perf) - its quite a journey and probably only relevant for some using this project. Chunky deps (inc browsers) - how many people are thinking about e2es when cloning a starter template, and when there are problems do you want to maintain it...
ng e2e community schematic did a fair job of getting a starting point together though, although for my multi-suite projects I'd not have root folders for test projects - if we did something slightly different would that work against those schematic migrations in future, also being community-driven perhaps quality not as high (not sure)...
So the playwright schematic did work after I made some changes:
... I'll share a branch in a bit with this for you to look at
EDIT: Here's a branch that adds playwright/e2e atop this PR
@replete
I was testing out the playwright branch and ran into an issue with npm run e2e:all. It works fine the first time, but after that it errors out:
Error: Project(s) "e2e-report", "e2e-test-results" not found. Available projects: "e2e-chromium", "e2e-firefox", "e2e-webkit"
Looks like the glob pattern e2e-* in angular.json is picking up the output directories from playwright.config.ts (e2e-report and e2e-test-results) after the first test run and treating them as projects on subsequent runs.
Curious what you think the best approach is here to avoid the collision?
Also might be worth adding a note in the README's e2e section about running npx playwright install first if folks don't have the browsers installed yet. Could save some confusion for new users.
Let me know what you think!
@joematthews Somehow I completely missed that.
I pushed a commit to the playwright branch that places the artifact folders into ./.playwright. A bit tidier.
Interesting tradeoffs with a starter template, keep it simple with sensible defaults, vs inevitable conventions needed when the project scales. Some of my decisions might be unnecessary for the scope of the project to be honest, e.g. the .e2e.test.ts - that is mainly for purposes of fuzzing in a project with many types of test in the same area - npm run test "login.e2e" (or "login.int", "login.sec" etc..). Easily out of scope really. If you want to land that it might be simpler to keep *.test.ts (because they are actual tests) for playwright e2es, and *.spec.ts for the unit tests (specifications). The line can become blurry... you can see why you might wish to keep it optional.
I just cloned your template, and found that your vitest workaround doesn't work perfect.
Setup:
C:\REDACTED>git clone --depth=1 --origin=upstream https://github.com/replete/extreme-angular.git -b upgrade-to-angular-21 xa
Cloning into 'xa'...
remote: Enumerating objects: 48, done.
remote: Counting objects: 100% (48/48), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 48 (delta 1), reused 26 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (48/48), 150.24 KiB | 6.53 MiB/s, done.
Resolving deltas: 100% (1/1), done.
C:\REDACTED>cd xa
C:\REDACTED\xa>npm i
...
added 945 packages, and audited 946 packages in 13s
...
Running test using ng test works:
C:\REDACTED\xa>npx ng test --watch=false
Initial chunk files | Names | Raw size
spec-app.js | spec-app | 47.97 kB |
chunk-XSGC6WRG.js | - | 4.05 kB |
init-testbed.js | init-testbed | 1.20 kB |
styles.css | styles | 57 bytes |
| Initial total | 53.28 kB
Application bundle generation complete. [4.120 seconds] - 2025-12-04T11:45:04.515Z
RUN v4.0.14 C:/REDACTED/xa
✓ extreme-angular src/app/app.spec.ts (2 tests) 101ms
✓ App (2)
✓ should create the app 74ms
✓ should render title 25ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 12:45:06
Duration 11.71s (transform 109ms, setup 659ms, import 261ms, tests 101ms, environment 9.99s)
Running tests using vitest fails:
C:\REDACTED\xa>npx vitest --watch=false
[vitest] Rebuilding Angular tests...
Initial chunk files | Names | Raw size
spec-app.js | spec-app | 47.97 kB |
chunk-XSGC6WRG.js | - | 4.05 kB |
init-testbed.js | init-testbed | 1.20 kB |
styles.css | styles | 57 bytes |
| Initial total | 53.28 kB
Application bundle generation complete. [1.315 seconds] - 2025-12-04T11:46:14.365Z
Build output files successfully dumped to 'C:\REDACTED\xa\.angular\cache\21.0.1\extreme-angular\unit-test\output-files'.
RUN v4.0.14 C:/REDACTED/xa
✓ extreme-angular src/app/app.spec.ts (2 tests) 84ms
✓ App (2)
✓ should create the app 60ms
✓ should render title 23ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 12:46:14
Duration 1.21s (transform 119ms, setup 282ms, import 156ms, tests 84ms, environment 543ms)
RUN v4.0.14 C:/REDACTED/xa
❯ src/app/app.spec.ts (2 tests | 2 failed) 6ms
❯ App (2)
× should create the app 4ms
× should render title 1ms
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL src/app/app.spec.ts > App > should create the app
FAIL src/app/app.spec.ts > App > should render title
Error: Component 'App' is not resolved:
- templateUrl: ./app.html
- styleUrl: ./app.scss
Did you run and wait for 'resolveComponentResources()'?
❯ Function.get REDACTED/xa/node_modules/@angular/core/fesm2022/_debug_node-chunk.mjs:17706:17
❯ getComponentDef REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:1145:16
❯ isStandaloneComponent REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:1141:15
❯ queueTypesFromModulesArrayRecur REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:925:20
❯ TestBedCompiler.queueTypesFromModulesArray REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:943:5
❯ TestBedCompiler.configureTestingModule REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:567:12
❯ TestBedImpl.configureTestingModule REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:1393:19
❯ Function.configureTestingModule .REDACTED/xa/node_modules/@angular/core/fesm2022/testing.mjs:1264:33
❯ src/app/app.spec.ts:7:19
5| describe('App', () => {
6| beforeEach(async () => {
7| await TestBed.configureTestingModule({
| ^
8| imports: [App],
9| providers: [provideZonelessChangeDetection()],
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
Test Files 1 failed (1)
Tests 2 failed (2)
Start at 12:46:15
Duration 1.15s (transform 59ms, setup 363ms, import 87ms, tests 6ms, environment 540ms)
Obviously, the vitest failure propagates into VS Code with the Vitest extenstion.
https://medium.com/@italo.masserano/how-to-use-angular-20-experimental-vitest-support-outside-of-ng-test-72447c69a6b7 , which seems to address a similar topic, somehow does call resolveComponentResources in a beforeAll test hook, which your template does not do.
Versions:
Angular CLI : 21.0.1
Angular : 21.0.1
Node.js : 22.21.1
Package Manager : npm 10.9.4
Operating System : win32 x64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.0.1 │ ^21.0.1 │
│ @angular/cli │ 21.0.1 │ ^21.0.1 │
│ @angular/common │ 21.0.1 │ ^21.0.1 │
│ @angular/compiler │ 21.0.1 │ ^21.0.1 │
│ @angular/compiler-cli │ 21.0.1 │ ^21.0.1 │
│ @angular/core │ 21.0.1 │ ^21.0.1 │
│ @angular/forms │ 21.0.1 │ ^21.0.1 │
│ @angular/platform-browser │ 21.0.1 │ ^21.0.1 │
│ @angular/router │ 21.0.1 │ ^21.0.1 │
│ rxjs │ 7.8.2 │ ~7.8.2 │
│ typescript │ 5.9.3 │ ~5.9.3 │
│ vitest │ 4.0.14 │ ^4.0.14 │
└───────────────────────────┴───────────────────┴───────────────────┘
The failure seems to be related to Windows as host OS. Your template works fine in node:22.21.1 from dockerhub, which is based on Debian bookworm.
Indeed. In Windows, I have unit-test\output-files\spec-app.js, while in Linux, I have unit-test/output-files/spec-app-app.js. This merge request requires the latter name.
In Windows, the output-files filename seems to be not that easily predictable. I created src/app/foo/app.spec.ts, which resultet in unit-test/output-files/spec-app.js and unit-test/output-files/spec-app-2.js. Possibly this behaviour can be disabled in Angular (risking overly long filenames that may break on Windows).
@karcherm Thanks for the info. I read the angular cli source and can see the issue, it's an angular bug. I have added a workaround, but I do not have Windows therefore I can not test it. If you have Windows, and a devcontainer it would be easiest for you to test this, and fix it if it isn't quite right. Hope that helps.