playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Feature] Web Component (Lit) support for test-ct

Open alexkrolick opened this issue 2 years ago • 11 comments

Feature request - support for web components in "component test" mode.

Looking into the Svelte/React/Vue implementation, I think it would be even simpler to do for native web components since there is no framework-specific Vite plugin needed, even if using a library like Lit. You might even consider web components the base case for component tests, since all you need is the environment setup/teardown; this would open up experimentation with lesser-known frameworks that aren't going to get an official plugin since they can do some work in userspace for "mount" style commands.

alexkrolick avatar May 18 '22 00:05 alexkrolick

If I understand the current architecture, the test function is extended with a mount fixture on import. So each plugin makes sure that this signature:

const locator = await mount(comp: T, opts?: U);

uses comp and opts to render something to the #root element and return a locator to it. Wouldn't a more general "base case" be a (primitive | function | HTMLElement | Array) -> HTML?

// Primitive value -> root.innerHTML = (comp ?? '').toString():
mount(undefined) -> <div id="root"></div>
mount(0) -> <div id="root">0</div>
mount('hello') -> <div id="root">hello</div>
mount('<button>Click me</button>') -> <div id="root"><button>Click me</button></div>

// Object instanceof HTMLElement -> root.appendChild(comp):
mount(document.createElement('div')) -> <div id="root"><div></div></div>
// That should also work for Custom Elements (Web Components) if you've registered it:
mount(new MyComponent()) -> <div id="root"><my-component></my-component></div>
// A base-plugin could also auto-register custom elements if they're not already:
//     if(!customElements.get(compName)) customElements.define(compName, comp);

// Functions could just mount the awaited return value:
// Note: if return value is also a function, throw Error to prevent recursion
mount(() => 'hello') -> <div id="root">hello</div>
mount(async () => 'hello') -> <div id="root">hello</div>

// Array could just be iterated/joined:
mount([1, 1]) -> <div id="root">11</div>

// If an Object isn't supported, it can just fail the test.
mount({}) -> Error('Unknown component type, did you forget to import test from a plugin?')
mount(new Date()) -> Error('Unknown component type, did you forget to import test from a plugin?')

Since React, Vue and Svelte plugins all share some code in common and eventually libraries/frameworks requiring a plugin/build-step/other tooling would need to do some similar pattern and return their mount function, but for the raw patterns/libraries out there I think it would actually be nice to be able to component test out of the box when no work is needed like ReactDOM.render, Vue.createApp().mount, etc.

bjarkihall avatar May 20 '22 00:05 bjarkihall

I'm not 100% clear on why the test runner needs to know about mount at all. For example, in Jest with JSDOM, the test provides a clean DOM environment per test file (although arguably it should be per test()), and userspace code can execute in that context, like render() from React Testing Library or mount() in Enzyme.

There is a concept of a component "registry" in each of the existing plugins but it's not clear what that is for. Maybe it's important to the existing reporter infrastructure? https://github.com/microsoft/playwright/blob/main/packages/playwright-ct-svelte/registerSource.mjs#L21-L24

The plugins also load in the framework-specific Vite plugins which could potentially be externalized (#14295), or decoupled into a build-agnostic config.

alexkrolick avatar May 20 '22 16:05 alexkrolick

The only reason current mount implementation is in the test runner is code reuse. There is nothing suggesting that it needs to be there, same functionality can be implemented outside, by the third party, w/o visible effects to the user.

pavelfeldman avatar May 25 '22 20:05 pavelfeldman

Looking at the docs page for Playwright component testing, I was confused that there's no "no framework" option. Eg I'd like to test vanilla web components or plain html + js. Lit (and Angular :) ) would be great, but given a good foundation those could be added by the community.

Are there any examples for "no framework" component testing?

johncrim avatar Dec 30 '22 19:12 johncrim

Just published a NPM package for lit and native web components until there is official support: https://github.com/sand4rt/playwright-ct-web.

sand4rt avatar Dec 30 '22 23:12 sand4rt

Sample code using evaluate:

test("display in a small container", async ({ page }) => {
    await page.goto("/debug-page")
    await page.evaluate(() => {
        const body = document.querySelector("body")
        if (body) {
            body.innerHTML = `<div style="height:200px; background-color: red;">
            <my-web-component></my-web-component>
            </div>`
        }
    })
    await page.screenshot() // to see the result
})

The only requirement is to have a "debug-page" somewhere where you can put your components + load relevant JS code of your app.

Edit: as a nicer reusable function:

export async function mount(page: Page, html: string) {
    await page.goto("/components") // we need a page that loads the relevant JS (it's possible to list all your registered web components in this page, if you are patient enough)
    await page.evaluate((html) => {
        const body = document.querySelector("body") // you can craft yourself a nicer page with a specific "#component" div
        if (body) {
            body.innerHTML = html
        }
    }, html) // it's not a normal closure, you have to pass the argument again here
}

Note that rendering plain HTML is however slightly different from rendering Lit elements using html. You cannot set properties this way.

eric-burel avatar Mar 29 '23 15:03 eric-burel

@eric-burel ~~how would you invoke registration at run-time?~~

oh, manually

yinonov avatar Apr 20 '23 20:04 yinonov

Thank you @sand4rt and @eric-burel for work arounds.

I'd love to have "in-library" support for testing lit web components in Playwright. We're exploring adding Playwright testing to Microsoft Graph Toolkit and would like to have component level tests rather than only page level tests hitting our Storybook site.

gavinbarron avatar Aug 29 '23 16:08 gavinbarron

@eric-burel Thanks to your example we were able to use Playwright to test Lit components in the docmaps project. Much appreciated!

Just a note for everyone, that example is not using Playwright component testing at all. It is a way to render Lit components dynamically with @playwright/test alone. So your test imports will look like this:

import { expect, Locator, test, Page, Request, Route } from '@playwright/test';

Another note: when he says you can't set properties, he is referring to a specific Lit feature called property expressions. His approach does support setting ordinary properties on Lit components via html attributes, like so:

async function renderWidget(page: Page, id: string) {
  await page.goto('/');
  await page.evaluate(
    ({ serverUrl, id }) => {
      const root = document.querySelector('#root');
      if (root) {
        console.log('body found');
        root.innerHTML = `<docmaps-widget serverurl='${serverUrl}' doi='${id}'></docmaps-widget>`;
      }
    },
    { serverUrl: STAGING_SERVER_URL, id }, // This is not a regular closure, so we need to pass in the variables we want to use
  );
  await page.waitForSelector('docmaps-widget');

  return page.locator('docmaps-widget');
}

Here's a link to the part of our project that is tested with this strategy for anyone who wants to see it applied in the real world.

andrewedstrom avatar Nov 21 '23 20:11 andrewedstrom

Just published a NPM package for lit and native web components until there is official support: https://github.com/sand4rt/playwright-ct-web. It has an identical API to Playwright's API.

No chance you would want to send a PR to this repo to add it, with the experience you have about this? :) Would be amazing having native support.

thernstig avatar Nov 27 '23 14:11 thernstig

Just published a NPM package for lit and native web components until there is official support: https://github.com/sand4rt/playwright-ct-web. It has an identical API to Playwright's API.

No chance you would want to send a PR to this repo to add it, with the experience you have about this? :) Would be amazing having native support.

Hopefully in the future ;) Angular is first: https://github.com/microsoft/playwright/pull/27783

sand4rt avatar Nov 27 '23 15:11 sand4rt