npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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

🚀 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:

  1. Playwright analyses all the calls to mount(),
  2. it grabs the arguments (e.g. the component class),
  3. replaces the component class with a unique string (constructed from the component class name and es-module),
  4. adds the component's ES module to Vite entrypoints,
  5. 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(),
    ],
  });
});

Cf. Zoneless Example's playwright/index.ts