Skip to content

Latest commit

 

History

History
236 lines (178 loc) · 6.03 KB

readme.md

File metadata and controls

236 lines (178 loc) · 6.03 KB

Playwright React

This library makes it possible to unit test react components in a real browser with Playwright.

Inspired by @cypress/react.

This is a proof of concept and is probably quite instable. I do not recommend anyone to use it at this stage.

Install

npm install -D @playwright/test tjoskar/playwright-react

Usage

This is a proof of concept and in a exploratory stage.

There is currently four ways of using this lib.

Let's say you want to test this component:

// MyComponent.tsx
interface Props {
  name: string;
}

export function MyComponent(props: Props) {
  return <h1>Hello {props.name}</h1>;
}

Option 1: Using React.mount inside the test

// MyComponent.spec.ts
import { expect, test } from '@playwright/test';
import { createElement } from 'react';
import { setup } from '@tjoskar/playwright-react';

// Set up the components you want to test. You can add as many as you want.
const mount = setup({
  MyComponent: () => import('./MyComponent').then(c => c.MyComponent),
});

test('Test MyComponent', async ({ page }) => {
  await mount(page, ({ MyComponent }) => {
    // You will get typescript intellisense here, but it can be better
    return createElement(MyComponent, { name: "John" });
  });

  await expect(page.locator('text=Hello John')).toBeVisible();
});

👍

  • All setup in the same file
  • Can use playwright's api

👎

  • Can not use jsx/tsx. See playwright#7121
  • The code will be compiled by both Playwright (for runing in node) and esbuild (for runing in the browser)
  • Closure will not work inside mount, ex. you can not access variables outside mount. Even if it looks like it.
  • react.createElement has bad ts types (pops is always optional).

Option 2 (recomended right now): Import an external component (component under test)

// __tests__/ComponentUnderTest.tsx
import React from "react";
import { MyComponent } from "../MyComponent";

export const Stannis = () => <MyComponent name="Stannis" />;
// __tests__/MyComponent.spec.ts
import { expect } from "@playwright/test";
import { componentTest } from '@tjoskar/playwright-react';

componentTest("Test MyComponent", async ({ page, mount }) => {
  await mount(() => import('./ComponentUnderTest').then(c => c.Stannis));

  await expect(page.locator('text=Hello! My name is Stannis')).toBeVisible();
});

👍

  • Easy to understad
  • Can use playwright's api

👎

  • Need seperate file for complex props (we do not need a seperate file in the example above)

Option 3: Execute the test in the browser

// __tests__/MyComponent.comp-spec.tsx
import React from "react";
import { render, screen } from '@testing-library/react'
import { MyComponent } from "../MyComponent";

export const test = async () => {
  render(<MyComponent name="Stannis" />);

  screen.getByText(/Stannis/i);
}
// __tests__/MyComponent.spec.ts
import { expect } from "@playwright/test";
import { componentTest } from '@tjoskar/playwright-react';

componentTest("Test MyComponent", async ({ execute }) => {
  await execute(() => import('./MyComponent.comp-spec').then(c => c.test));
});

👍

  • Can use (react) testing library

👎

  • Can not use playwright's api
  • Need a assert lib (this can be solved)
  • Need seperate file (this can be solved)

Option 4: Pre compile the test

See tjoskar#1 for more information

Assert click events

// __tests__/ComponentUnderTest.tsx
import React from "react";
import { MyComponent } from "../MyComponent";

export const attachClickListener = ({ spy }: TestArgs) => {
  const onClick = spy('click');

  return () => <MyComponent name="Dexter" onClick={onClick} />;
};
// __tests__/MyComponent.spec.ts
import { expect } from "@playwright/test";
import { componentTest } from '@tjoskar/playwright-react';

componentTest("Test MyComponent with a spy function", async ({ page, mount }) => {
  const { events } = await mount((utils) =>
    import("./ComponentUnderTest").then((c) => c.attachClickListener(utils))
  );

  expect(events.callCount('click')).toBe(0);
  await page.locator("text=Hello! My name is Dexter").click();
  expect(events.callCount('click')).toBe(1);
  expect(events.args('click')[0][0]).toBe('Dexter');
});

Image snapshot

Create a component you want to take a snapshot of:

// src/components/__tests__/MyText.snap.tsx
import React from 'react';
import { MyText } from '../MyText';

export const tests = [{
  name: 'My text: Stannis',
  render(): JSX.Element {
    return <MyText text="Stannis" />
  }
}]

Create a test that will find all snapshots test and render them, one by one:

// src/snapshots.spec.tsx
import { takeSnapshot } from "@kivra/playwright-react";

takeSnapshot();

Update your playwright.config.ts:

// playwright.config.ts
import { PlaywrightTestConfig } from '@kivra/playwright-react';

const config: PlaywrightTestConfig = {
  testDir: 'src',
  testMatch: '**/*.spec.ts',
  use: {
    viewport: { height: 1080, width: 1920 },
  },
  react: {
    snapshotFileGlob: './src/**/*.snap.tsx',
  }
};
export default config;

Create a Wrapper component that will wrap every snapshot:

// .playwright/Wrapper.tsx
import React from "react";
import { mountAndTakeSnapshot } from '@kivra/playwright-react/client';
import { Wrapper } from './Wrapper';

function Wrapper({ children }: { children: JSX.Element }) {
  return (
    <div>
      <Theme color="dark">{children}</Theme>
      <Theme color="light">{children}</Theme>
    </div>
  );
}

mountAndTakeSnapshot(Test => (
  <Wrapper>
    <Test />
  </Wrapper>
));

TODO: Document the server setup

Playwright will now take a snapshot of all your components in your ./src/**/*.snap.tsx files

Development

To test this in example, fisrt pack this lib with npm run build && npm pack and then run npm install inside example. It does not work to install it by npm install .. due to linking isuues.