testing-library / svelte-testing-library

:chipmunk: Simple and complete Svelte DOM testing utilities that encourage good testing practices
https://testing-library.com/docs/svelte-testing-library/intro
MIT License
620 stars 33 forks source link

Properties set with Svelte action not rendered #398

Closed mledl closed 3 months ago

mledl commented 3 months ago

Hey everyone,

I am having a specific question when it comes to rendering Svelte components for testing. I am working on a project where I need to use an existing webcomponent library. To apply default values to these webcomponents, I am required perform some trick that sets the values during hydration. There are two options, either a wrapper that conditionally renders the webcomponent after the wrapper's onMount function has been run or a Svelte action that sets the properties on the node. Both options work fine when running the Svelte app and we decided to go with the action that sets the properties as follows:

export const setProperties: Action<HTMLElement, Record<string, unknown>> = <T extends object>(
    node: Node,
    properties: T
) => {
    Object.assign(node, { ...properties })

    return {
        update(updatedProperties: T) {
            Object.assign(node, { ...updatedProperties })
        }
    }
}

It is used like that:

<foo-bar-web-component use:setProperties={{ foo, bar }}></foo-bar-web-component>

However we are experiencing issues when it comes to component testing using Vitest and the @testing-library/svelte. The action is executed, but the container property after rendering does not reflect the properties set. Also calling debug does not show them. Is there a good solution to workaround this shortcoming or are we missing something?

We are using Svelte 4 and @testing-library/svelte v4.1.0.

Thank you very much in advance :)

mcous commented 3 months ago

I'm afraid that's not really enough information to diagnose your issue; do you have a reproduction repository / StackBlitz / etc. available? Does the problem happen if you use a vanilla HTML element instead of your custom element?

Also, always worth confirming: is your project set up with the recommended Vitest config and plugin? (See README or the docs.) A very common problem with Svelte 4 and Vitest is that your tests may be using Svelte's SSR code instead of its browser code, which our plugin accounts for and resolves.

mledl commented 3 months ago

I am sorry that I provided to little information. Here is a minimal reproducible example: https://github.com/mledl/svelte-action-testing-library-issue

The web components library was created with stenciljs and there might be some margin of error on their side as well. If you run the example, you will see a native HTML anchor element and web component button element with just properties set and a version with an action setting the properties.

Running the test with the notes in comments in mind will showcase the behaviour. In case of the action, only the id property gets set and the others are left out.

mcous commented 3 months ago

Thanks, that repo is easier to inspect. Off the bat, I noticed that your tests aren't using the recommended testing setup for liquid components. Could this be causing problems?

The second thing that jumped out at me was a discrepancy between your action code and your tests: you're setting properties on the node and then testing via toHaveAttribute - is there a property vs attribute misunderstanding happening here?

After reading through your reproduction repo, I'm fairly confident that your issue is with your test setup, assertions, and/or how you're interacting with this component library rather than anything STL is doing. I'm on vacation until next week, but I'm happy to help dig in further if you're still having issues by then!

mcous commented 3 months ago

@mledl I've dug into this some more, this is "property vs attribute" problem, and has to do with how Svelte treats custom elements and how these custom elements chose to function. Since the testing library and Svelte appear to be working as intended, I'm going to close this issue.

TL;DR: these custom components use properties, and do not reflect their properties to attributes, so you need to test on properties, not attributes, if you want to check their values:

  const ldButtonElements = container.querySelectorAll('ld-button');
  expect(ldButtonElements).toHaveLength(2);
- expect(ldButtonElements[0].getAttribute('mode')).toBe('secondary');
- expect(ldButtonElements[1].getAttribute('mode')).toBe('secondary');
+ expect(ldButtonElements[0].mode).toBe('secondary');
+ expect(ldButtonElements[1].mode).toBe('secondary');

The lack of attribute reflection neatly explains why your test of the component using use:setProperties (ldButtonElements[1]) was failing. However, the failure reason of the test of your component that appears to use attributes in the template (ldButtonElements[0]) is a little more confusing, and has to do with how Svelte deals with custom elements. In your template, you write:

<ld-button id="1" size="lg" mode="secondary">{label}</ld-button>

When Svelte sees a custom element, is uses a simple heuristic to determine if a given template attribute should be an attribute (node.setAttribute(key, value)) or a property (node[key] = value). The heuristic is: "if key in node, set a property, else set an attribute".

These particular custom elements use properties. You can check this out in your browser's JS console pretty easily:

const elem = document.createElement('ld-button')
'mode' in elem // true

Some custom elements reflect properties back to attributes, but it appears that this is not the case for this component library. So continuing in your browser's console:

elem.mode = 'secondary'
elem.getAttribute('mode') // null

The debug logger will print attributes of nodes, not properties, which is why mode et. al. do not show up when you use debug() (and you've registered the custom elements with defineCustomElements).

mledl commented 3 months ago

Thanks for the detailed explanation and the time you took to look at this misunderstanding. Much appreciated!