This library brings Angular support to Playwright's experimental Component Testing.
This will allow us to test our Angular components with Playwright without building the whole app and with more control.
@jscutlery/playwright-ct-angular
currently supports:
- ✅ Testing Versatile Angular Components
- 🎛 Passing type-safe inputs to the tested components
- 🎭 Spying on component outputs in a type-safe fashion
https://user-images.githubusercontent.com/2674658/206226065-ba856329-dda7-43b1-9c28-4416b190f4d4.mp4
First, we will have to set up Playwright Component Testing as mentioned below.
Then, we can write our first test in .../src/greetings.component.pw.ts
:
import { expect, test } from '@jscutlery/playwright-ct-angular';
import { GreetingsComponent } from './greetings.component';
test('<jc-greetings> should be polite', async ({ mount }) => {
const locator = await mount(GreetingsComponent);
expect(locator).toHaveText('👋 Hello!');
});
import { expect, test } from '@jscutlery/playwright-ct-angular';
import { GreetingsComponent } from './greetings.component';
test('<jc-greetings> should be polite', async ({ mount }) => {
const locator = await mount(GreetingsComponent, {inputs: {name: 'Edouard'}});
expect(locator).toHaveText('👋 Hello Edouard!');
});
mount()
will automatically create a jest-mock
's spy, subscribe to the outputs given through the spyOutputs
option and return them in the spies
property, in a type-safe way showing only the outputs that are spied on.
Note that the spyOutputs
is type-safe and will only allow properties that exist on the component.
import { expect, test } from '@jscutlery/playwright-ct-angular';
import { NameEditorComponent } from './name-editor.component';
test('<jc-name-editor> should be polite', async ({ mount }) => {
const locator = await mount(NameEditorComponent, {spyOutputs: ['nameChange']});
await locator.getByLabel('Name').type('Edouard');
expect(locator.spies.nameChange).lastCalledWith('Edouard');
});
We can also pass custom output callback functions for some extreme cases or if we want to use a custom spy implementation for example or just debug.
await mount(NameEditorComponent, {
outputs: {
nameChange(name) {
console.log(name);
}
}
});
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, {
inputs: {
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 {
private _repo = inject(RecipeRepositoryFake);
@Input() set recipes(recipes: Recipe[]) {
this._repo.recipes = 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));
}
}
In order to import styles that are shared between our tests, we can do so by importing them in playwright/index.ts
.
We can also customize the shared playwright/index.html
nearby.
If we want to load some specific styles for a single test, we 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 {}
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']
}
}
}
};
Cf. /tests/playwright-ct-angular-wide/src
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 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.
The magical workaround behind the scenes is that at build time:
- Playwright analyses all the calls to
mount()
, - it grabs the first parameter (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 with 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
// 🛑 this won't work
const cmp = MyComponent;
await mount(cmp);
// 🛑 this won't work
test(MyComponent.name, async ({ mount }) => {});
// 🛑 this won't work
@Component({...})
class GreetingsComponent {}
test('<jc-greetings>', async ({ mount }) => {
await mount(GreetingsComponent);
});
This makes the following impossible:
- passing providers to the
mount()
function - passing modules to the
mount()
function (this is what currently makes the usage of mounting templates impossible) - use non-standalone components
...use non-Versatile Angular Style
We'll need a vite plugin to support traditional DI, external templates and stylesheets etc...
If you really can't make it to Versatile Angular Style, we can think of a couple of workarounds like using a vite plugin like Brandon Roberts' vite-plugin-angular
We could also implement some custom transform that registers the providers and import modules, like Playwright does for the component class.
# You can run this command in an existing workspace.
yarn create playwright --ct # or npm init playwright@latest -- --ct
# Choose React
# ? Which framework do you use? (experimental) …
# ❯ react
# vue
# svelte
# solid
yarn add -D @jscutlery/playwright-ct-angular @playwright/test # or npm install -D @jscutlery/playwright-ct-angular @playwright/test
Update playwright-ct-config.ts
and replace:
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
with
import type { PlaywrightTestConfig } from '@jscutlery/playwright-ct-angular';
import { devices } from '@jscutlery/playwright-ct-angular';
In order to avoid collisions with other tests (e.g. Jest / Vitest),
We can replace the default matching extension .spec.ts
with .pw.ts
:
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /pw\.ts$/,
...
}