Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
alexkrolick opened this issue May 18, 2022 · 11 comments
Open

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

alexkrolick opened this issue May 18, 2022 · 11 comments

Comments

@alexkrolick
Copy link

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.

@bjarkihall
Copy link

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.

@alexkrolick
Copy link
Author

alexkrolick commented May 20, 2022

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.

@pavelfeldman
Copy link
Member

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.

@johncrim
Copy link

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?

@sand4rt
Copy link
Collaborator

sand4rt commented Dec 30, 2022

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

@eric-burel
Copy link

eric-burel commented Mar 29, 2023

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.

@yinonov
Copy link

yinonov commented Apr 20, 2023

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

oh, manually

@gavinbarron
Copy link

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.

@andrewedstrom
Copy link

@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.

@thernstig
Copy link
Contributor

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.

@sand4rt
Copy link
Collaborator

sand4rt commented Nov 27, 2023

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: #27783

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests