@jscutlery/playwright-ct-angular
v0.9.6
Published
This library brings **Angular support** to [Playwright's **experimental ** Component Testing](https://playwright.dev/docs/test-components).
Downloads
359
Readme
Playwright Component Testing for Angular (experimental)
This library brings Angular support to Playwright's **experimental ** Component Testing.
This will allow you to test your Angular components with Playwright without building the whole app and with more control.
@jscutlery/playwright-ct-angular
currently supports:
- ✅ Testing Angular components/directives/pipes
- 🎛 Controlling inputs/outputs in a type-safe fashion
- 🥸 Overriding providers
- 🍳 Testing with templates
https://user-images.githubusercontent.com/2674658/206226065-ba856329-dda7-43b1-9c28-4416b190f4d4.mp4
Table of Contents
- Playwright Component Testing for Angular (experimental)
- Table of Contents
- 🚀 Writing your first test
- ⚠️ Known Limitations
- 📦 Setup
🚀 Writing your first test
First, you will have to set up Playwright Component Testing as mentioned below.
⚠️ Make sure to check the known limitations before writing more tests.
✅ Basic Test
Then, you can write your first test in .../src/greetings.component.pw.ts
:
import { expect, test } from '@jscutlery/playwright-ct-angular';
import { GreetingsComponent } from './greetings.component';
test(`GreetingsComponent should be polite`, async ({ mount }) => {
const locator = await mount(GreetingsComponent);
expect(locator).toHaveText('👋 Hello!');
});
🎛 Testing Inputs
import { expect, test } from '@jscutlery/playwright-ct-angular';
import { GreetingsComponent } from './greetings.component';
test(`GreetingsComponent should be polite`, async ({ mount }) => {
const locator = await mount(GreetingsComponent, { props: { name: 'Edouard' } });
expect(locator).toHaveText('👋 Hello Edouard!');
});
Passing output callbacks
You can also pass custom output callback functions for some extreme cases or if you want to use a custom spy implementation for example or just debug.
await mount(NameEditorComponent, {
on: {
nameChange(name) {
console.log(name);
}
}
});
🥸 Providing Test Doubles & Importing Additional Modules
Due to the limitations described below, the recommended approach for providing test doubles or importing additional modules is to create a test container component in another file.
// recipe-search.component.pw.ts
import { defer } from 'rxjs';
import { RecipeSearchTestContainer } from './recipe-search.test-container';
test('...', async ({ mount }) => {
await mount(RecipeSearchTestContainer, {
props: {
recipes: [
beer,
burger
]
}
})
})
// recipe-search.test-container.ts
@Component({
standalone: true,
imports: [RecipeSearchComponent],
template: '<jc-recipe-search></jc-recipe-search>',
providers: [
RecipeRepositoryFake,
{
provide: RecipeRepository,
useExisting: RecipeRepositoryFake,
},
],
})
export class RecipeSearchTestContainer {
recipes = input<Recipe[]>([]);
#repo = inject(RecipeRepositoryFake);
#syncRecipesWithRepo = effect(() => {
this.#repo.setRecipes(this.recipes());
});
}
/* Cf. https://github.com/jscutlery/devkit/tree/main/tests/playwright-ct-angular-wide/src/testing/recipe-repository.fake.ts
* for a better example. */
class RecipeRepositoryFake implements RecipeRepositoryDef {
#recipes: Recipe[] = [];
searchRecipes() {
return defer(() => of(this.#recipes));
}
setRecipes(recipes: Recipe[]) {
this.#recipes = recipes;
}
}
🎨 Using Styles
Shared Styles
In order to import styles that are shared between your tests, you can do so by importing them in playwright/index.ts
.
You can also customize the shared playwright/index.html
nearby.
Specific Styles
If you want to load some specific styles for a single test, you might prefer using a test container component:
import styles from './some-styles.css';
@Component({
template: '<jc-greetings></jc-greetings>',
encapsulation: ViewEncapsulation.None,
styles: [styles]
})
class GreetingsTestContainer {
}
Angular Material & Angular Libraries with styles
As mentioned in Versatile Angular Style Blog Post, Angular Material and other Angular libraries might use a Conditional "style" Export that allows us to import prebuilt styles ( Cf. Angular Package Format managing assets in a library).
In that case, we can add the following configuration to our playwright-ct.config.ts
:
const config: PlaywrightTestConfig = {
// ...
use: {
// ...
ctViteConfig: {
resolve: {
/* @angular/material is using "style" as a Custom Conditional export to expose prebuilt styles etc... */
conditions: ['style']
}
}
}
};
More examples
Cf. /tests/playwright-ct-angular-demo/src
⚠️ Known Limitations
The way Playwright Component Testing works is different from the way things work with Karma, Jest, Vitest, Cypress etc... Playwright Component Testing tests run in a Node.js environment and control the browser through Chrome DevTools Protocol, while the component is rendered in a browser.
This causes a couple of limitations as we can't directly access the TestBed
's or the component's internals,
and we can only exchange serializable data with the component.
It is not possible to hold the component type in a variable
// 🛑 this won't work
const cmp = MyComponent;
await mount(cmp);
It is not possible to use the component type elsewhere in the file.
// 🛑 this won't work
test(MyComponent.name, async ({ mount }) => {
});
It is not possible to declare components in the same file.
// 🛑 this won't work
@Component({ ... })
class GreetingsComponent {
}
test('should work', async ({ mount }) => {
await mount(GreetingsComponent);
});
It is not possible to use anything in providers which is not serializable or "importable".
import { provideAnimations } from '@angular/platform-browser/animations';
import { MY_PROVIDERS } from './my-providers';
import { MyFake } from './my-fake';
@Injectable()
class MyLocalFake {
// ...
}
// 🛑 this won't work because the result of `provideAnimations()` is not serializable
mount(GreetingsComponent, { providers: [provideAnimations()] })
// 🛑 this won't work because `MyLocalFake` is not "importable"
mount(GreetingsComponent, { providers: [{ provide: MyService, useClass: MyLocalFake }] })
// ✅ this works
mount(GreetingsComponent, { providers: MY_PROVIDERS });
// ✅ this works
mount(GreetingsComponent, { providers: [{ provide: MY_VALUE, useValue: 'my-value' }] });
// ✅ this works
mount(GreetingsComponent, { providers: [{ provide: MyService, useClass: MyFake }] });
🪄 The Magic Behind the Scenes
The magical workaround behind the scenes is that at build time:
- Playwright analyses all the calls to
mount()
, - it grabs the arguments (e.g. the component class),
- replaces the component class with a unique string (constructed from the component class name and es-module),
- adds the component's ES module to Vite entrypoints,
- and finally creates a map matching each unique string to the right ES module.
This way, when calling mount()
, Playwright will communicate the unique string to the browser who will know which
ES module to load.
Cf. https://youtu.be/y3YxX4sFJbM
Cf. https://github.com/microsoft/playwright/blob/cac67fb94f2c8a0ee82878054c39790e660f17ca/packages/playwright-test/src/tsxTransform.ts#L153
📦 Setup
1. Install
# You can run this command in an existing workspace.
npm create playwright -- --ct
# Choose React
# ? Which framework do you use? (experimental) …
# ❯ react
# vue
# svelte
# solid
npm add -D @jscutlery/playwright-ct-angular @jscutlery/swc-angular unplugin-swc
npm uninstall -D @playwright/experimental-ct-react
2. Configure
- Update
playwright-ct.config.ts
and replace:
import { defineConfig, devices } from '@playwright/experimental-ct-react';
with
import { defineConfig, devices } from '@jscutlery/playwright-ct-angular';
import { swcAngularUnpluginOptions } from '@jscutlery/swc-angular'
import swc from 'unplugin-swc';
- Configure vite plugin:
export default defineConfig({
use: {
// ...
ctViteConfig: {
// ...
plugins: [
swc.vite(swcAngularUnpluginOptions())
]
}
}
});
3. Change tests extension
In order to avoid collisions with other tests (e.g. Jest / Vitest),
You can replace the default matching extension .spec.ts
with .pw.ts
:
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /pw\.ts$/,
...
}
4. Choose between zoneful or zoneless testing
Zoneful Testing
If you want to use zoneful testing, you have to import zone.js
in your playwright/index.ts
:
// playwright/index.ts
import 'zone.js';
Zoneless Testing
For zoneless testing, you have to provide provideExperimentalZonelessChangeDetection()
in your playwright/index.ts
:
// playwright/index.ts
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { beforeMount } from '@jscutlery/playwright-ct-angular/hooks';
beforeMount(async ({ TestBed }) => {
TestBed.configureTestingModule({
providers: [
provideExperimentalZonelessChangeDetection(),
],
});
});