testing-library/svelte-testing-library

Svelte extend not triggered when using render

Closed this issue · 3 comments

I'm having an issue when trying to test custom elements (using jsdom) in Svelte.
When I use const result = render(MyComponent, { props: { options: '["v1","v2","v3"]', value: '', multiple: false } }); the component get rendered and all functions work as it should.

However the extend in this part of the code is never executed:

<svelte:options
  customElement={{
    tag: 'atp-select',
    shadow: 'none',
    extend: customElementConstructor => {
      return class extends customElementConstructor {
        constructor() {
          console.log('customElementConstructor called');
          super();
          this.self = this;
        }
      };
    },
  }}
/>

How can I make sure the code int the extend is executed when testing?

I don't work with compiling Svelte to custom elements, but I wouldn't expect any custom element code (like extends) to run if you pass the component the Svelte's mount function, which is what render does. You'll likely want to avoid using @testing-library/svelte at all.

Instead, you probably want to compile your components and use the DOM directly:

import { test } from 'vitest'
import { getByRole } from '@testing-library/dom'

import 'my-custom-component-library'

test('my custom component', () => {
  const subject = document.createElement('atp-select')
  
  // ...
})

Let me know if you get this working, since it might be useful to throw into the docs somewhere

Yes, you are correct, loading the element with document.createElement does call the extend and makes the element available to test.
I had to add the element to the document.body and add an delay in order to trigger the render of the element, so my final code looks like this:

import { expect, test, vi } from 'vitest';
import './Select.svelte'; // my custom element, this needs to only load

// Svelte throws an animate error when running in vitest, this catches this error
Element.prototype.animate = vi
    .fn()
    .mockImplementation(() => ({ cancel: vi.fn(), finished: Promise.resolve() }));

// create a delay so the component can render
const waitForRender = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

test('Click on option, multiple false', async () => {
  const subject = document.createElement('atp-select');
  subject.setAttribute('options', JSON.stringify(["v1", "v2", "v3"]));
  document.body.appendChild(subject);
  await waitForRender(0);

  const select = subject.querySelector('.select') as HTMLDivElement;
  await select.click();

  const options = subject.querySelectorAll('.options button') as unknown as HTMLButtonElement[];
  await options[1].click();

  await expect(options[1].outerHTML).include('checkmark');
});

It would be great is this could be added in the documentation somewhere, since this is probally not the most obvious to use when testing components.

@EricVanEldik great to hear you got it working! I'm going to close this issue since it doesn't involve the @testing-library/svelte library. If you need additional help, the Svelte Discord and/or the DOM Testing Library Discord may be good resources. Otherwise I'm happy to continue helping in this thread.

A couple quick recommendations:

  • Remove anything you add to document.body in a beforeEach or afterEach to make sure your tests don't conflict with each other
  • Use @testing-library/dom and its auto-waiting findBy... queries so you don't have to add a delay yourself
    import { screen } from '@testing-library/dom';
    
    // ...
    const subject = document.createElement('atp-select');
    // ...
    document.body.appendChild(subject);
    const select = await screen.findByRole('select');
  • Avoid mocking Element.prototype.animate, which JSDOM does not provide. Instead:
  • Use @testing-library/user-event rather than calling element.click() for a more accurate simulation of a user interacting with your app
    • Alternatively, use Vitest's browser mode which actually fires up a browser and clicks things