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

Svelte 5 feedback and support #284

Open
stefanhoelzl opened this issue Dec 17, 2023 · 75 comments
Open

Svelte 5 feedback and support #284

stefanhoelzl opened this issue Dec 17, 2023 · 75 comments

Comments

@stefanhoelzl
Copy link

stefanhoelzl commented Dec 17, 2023

Hello, @mcous has hijacked this post! We've launched experimental Svelte 5 support in @testing-library/svelte. See the Svelte 5 section in the README for setup instructions. No special setup is required to use Svelte 5 if you are using Vitest.

If you find bugs or have other Svelte 5 testing feedback, please add it to this thread! Svelte 5 is still changing rapidly at the time of writing, so feedback is appreciated!

Original post below


Hi! 👋

Firstly, thanks for your work on this project! 🙂

Today I used patch-package to patch @testing-library/[email protected] for the project I'm working on to use svelte 5.

This patches some breaking changes but there might be more work to do for a proper PR (are tests still working? support svelte 4 and 5, maybe more)

But for everyone who likes to try out svelte 5 this patch should be a good starting point.

Here is the diff that solved my problem:

diff --git a/node_modules/@testing-library/svelte/src/pure.js b/node_modules/@testing-library/svelte/src/pure.js
index 04d3cb0..2f041e0 100644
--- a/node_modules/@testing-library/svelte/src/pure.js
+++ b/node_modules/@testing-library/svelte/src/pure.js
@@ -3,7 +3,7 @@ import {
   getQueriesForElement,
   prettyDOM
 } from '@testing-library/dom'
-import { tick } from 'svelte'
+import { tick, createRoot } from 'svelte'
 
 const containerCache = new Set()
 const componentCache = new Set()
@@ -54,18 +54,15 @@ const render = (
     return { props: options }
   }
 
-  let component = new ComponentConstructor({
+  let component = createRoot(ComponentConstructor, {
     target,
-    ...checkProps(options)
+    ...checkProps(options),
+    ondestroy: () => componentCache.delete(component)
   })
 
   containerCache.add({ container, target, component })
   componentCache.add(component)
 
-  component.$$.on_destroy.push(() => {
-    componentCache.delete(component)
-  })
-
   return {
     container,
     component,
@@ -73,18 +70,14 @@ const render = (
     rerender: (options) => {
       if (componentCache.has(component)) component.$destroy()
 
-      // eslint-disable-next-line no-new
-      component = new ComponentConstructor({
+      component = createRoot(ComponentConstructor, {
         target,
-        ...checkProps(options)
+        ...checkProps(options),
+        ondestroy: () => componentCache.delete(component)
       })
 
       containerCache.add({ container, target, component })
       componentCache.add(component)
-
-      component.$$.on_destroy.push(() => {
-        componentCache.delete(component)
-      })
     },
     unmount: () => {
       if (componentCache.has(component)) component.$destroy()

This issue body was partially generated by patch-package.

@jsorb84
Copy link

jsorb84 commented Dec 20, 2023

Thank you for this, this works flawlessly, you should def. make a pull request , cheers my friend.

@bottrall
Copy link

Thanks @stefanhoelzl, this is working a treat! Any ideas on how to test snippets? So far I haven't managed to figure out how to provide a snippet to the props API. So it's back to the old slots hack of making a wrapping component for tests.

@sureshjoshi
Copy link

Anyone have any luck with snippets? The wrapper components are boilerplate-laden - was hoping to not have to do that.

If anyone has any pointers, I'm happy to give it a shot myself.

@sureshjoshi
Copy link

sureshjoshi commented Feb 6, 2024

EDIT: Ignore this part - after a LOT of debugging, this seems to be a problem when using happy-dom instead of jsdom - which is insane, since I use happy-dom in all of my other projects... But that's a problem for another day :)

Also, @yanick - for this patch, does screen.debug() work anymore? I can't seem to even get the most basic Svelte component to work due to:

PrettyFormatPluginError: Cannot read properties of null (reading 'toLowerCase')

And that's even with a component which is just <div></div>

@mcous
Copy link
Collaborator

mcous commented Feb 12, 2024

I've been messing around with Svelte 5 in our existing test suite, and have encountered the following issues so far. I didn't see any of these explicitly called out in the change log, but I'm assuming at the moment that they're all intentional.

  • onMount and onDestroy are attached / called asynchronously, outside of the component creation / DOM mount
    • Judicious addition of act() can get the tests passing; if you render then unmount immediately, neither onMount nor onDestroy will be called
    • If we want to guarantee that render will call onMount, we'll need to make render async
  • anchor option has been removed from createRoot
  • Element.animate seems to be required to run animations at all, which will require faking something in jsdom/happy-dom to test components with animations
  • Weird happy-dom issue mentioned in comments above

@sysmat
Copy link

sysmat commented Feb 16, 2024

  • in which version of this library is this?
  • @testing-library/svelte: 4.1.0, "svelte": "5.0.0-next.55", tests not working
Instantiating a component with `new` is no longer valid in Svelte 5

@mcous
Copy link
Collaborator

mcous commented Feb 16, 2024

Rudimentary support for Svelte 5 has been released on the next tag

npm install —-save-dev @testing-library/svelte@next

Since Svelte 5 itself is not ready, expect issues and breaking changes if you try it out!

@sysmat
Copy link

sysmat commented Feb 16, 2024

@mcous thx, so @testing-library/svelte@next is "@testing-library/svelte": "^4.2.0",

with this version it works

@mcous
Copy link
Collaborator

mcous commented Feb 19, 2024

FYI for those following along: as of the latest svelte prerelease, our own Svelte 5 support will no longer work due to sveltejs/svelte#10516, which removed the API we were using to mount components. Will do our best to adjust accordingly

@yanick
Copy link
Collaborator

yanick commented Feb 20, 2024

as of the latest svelte prerelease, our own Svelte 5 support will no longer work due to sveltejs/svelte#10516

:darth vader big NOOO:

@sureshjoshi
Copy link

@mcous If you have any suggestions for directed ways to help, I'm all ears :) Though, less familiar with the internals of the lib.

Is the issue that they removed the API call we use? And that it needs to be migrated to the new one? Or is the new mount API a no-go?

@pboling
Copy link

pboling commented Mar 31, 2024

Anyone know of any updates?

@mcous
Copy link
Collaborator

mcous commented Apr 2, 2024

Experimental Svelte v5 support is available on the next dist tag

npm install --save-dev @testing-library/svelte@next

In your test files:

import { render } from '@testing-library/svelte/svelte5'

@yanick how would you feel about bumping the next branch to 5 (to account for breaking changes semantic-release missed) and merging into main? Are there any more breaks - e.g. dropping svelte@3 - that you'd like to collect on next first?

The svelte@5 support is appropriately marked as experimental and it seems like something we can promote in a minor release whenever svelte@5 goes into production

@pboling
Copy link

pboling commented Apr 2, 2024

Your previous update noted that Svelte 5 support did not work at all. It isn't clear from this update if it has been fixed.

@yanick
Copy link
Collaborator

yanick commented Apr 2, 2024

Fwiw, I'm kinda busy today, but I'll try to turn my attention to deployment tomorrow.

@mcous
Copy link
Collaborator

mcous commented Apr 2, 2024

Your previous update noted that Svelte 5 support did not work at all. It isn't clear from this update if it has been fixed.

It has been fixed for now, it may be broken tomorrow. With the very-in-progress nature of Svelte v5, the best way to find out if it's currently working will be to try it yourself

@sureshjoshi
Copy link

sureshjoshi commented Apr 2, 2024

When I tried testing-lib out a week or so ago, it was working - but also note that there are still bugs depending on whether you're using JSDom vs Happy-Dom, or from svelte itself, or any related libraries.

I've been working on a Svelte 5 headless UI port, and Svelte5 + interop is a bit messy at the moment (to say the least) :)

@mcous
Copy link
Collaborator

mcous commented Apr 18, 2024

Experimental Svelte 5 support has landed in main! We'll do our best to stay on top of things, but here there be dragons 🐉

npm install --save-dev @testing-library/svelte@latest now includes Svelte v5 support

@mcous mcous changed the title Patch for svelte 5 support Svelte 5 feedback and support Apr 24, 2024
@mcous mcous pinned this issue Apr 24, 2024
@MatthewTFarley
Copy link

MatthewTFarley commented Apr 28, 2024

When using svelteTesting Vitest setup function with Svelte 5, the autoCleanup option does not work because the function imports the Svelte 4 cleanup function by way of vitest.js.

I have also run into the Element.animate issue @mcous identified. Here's an example repository demonstrating it. It also has some comments mentioning the cleanup issue in the vite.config.ts file. Using vitest globals: true is a workaround since it will cause the correct cleanup to run here.

@mcous
Copy link
Collaborator

mcous commented Apr 29, 2024

When using svelteTesting Vitest setup function with Svelte 5, the `autoCleanup option does not work because the function imports the Svelte 4 cleanup function by way of vitest.js.

...Using vitest globals: true is a workaround...

@MatthewTFarley auto-cleanup via plugin works if you use the suggested Svelte 5 alias setup from the README! Otherwise, globals mode is probably your best bet. This awkwardness will go away once Svelte 5 is out of prerelease and the svelte5 import is no longer needed.

export default defineConfig({
  plugins: [svelte(), svelteTesting()],
  test: {
    alias: {
      '@testing-library/svelte': '@testing-library/svelte/svelte5',
    },
  },
})

I have also run into the Element.animate issue

I wonder what's going to happen here! The animations work in the browser, but neither jsdom nor happy-dom support Element.animate. I don't know if Svelte's intention is to require Element.animate or include some sort of fallback.

We may have to start thinking about shipping our own Element.animate stub, but faking out browser APIs in this library gets into some iffy-mocking-practices territory

@pboling
Copy link

pboling commented Apr 30, 2024

@mcous I don't know enough to know if what I have should be working, or is expected to be broken. I am on latest versions as of this second of all packages, and latest "next" versions where possible (e.g. Svelte 5 next 118). I have some basic tests working, following the patterns of the internal tests of this actual library (testing-library/svelte-testing-library). I'm trying to ensure my config remains working as Svelte5 nears release, and as I add more complex tests to my test suite, so I am following this issue.

When I use the alias mentioned above and in the readme, my test suite doesn't run:

 FAIL  src/lib/components/forms/input.test.ts [ src/lib/components/forms/input.test.ts ]
Error: Missing "./svelte5/vitest" specifier in "@testing-library/svelte" package
 ❯ e node_modules/.pnpm/[email protected]_@[email protected]/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:47596:25

My config (after adding the alias):

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import { paraglide } from '@inlang/paraglide-sveltekit/vite';
import { svelteTesting } from '@testing-library/svelte/vite';
export default defineConfig(({ mode }) => ({
	plugins: [
		paraglide({
			// recommended
			project: './project.inlang',
			outdir: './src/lib/paraglide',
		}),
		sveltekit(),
		svelteTesting(),
	],
	resolve: {
		conditions: mode === 'test' ? ['browser'] : [],
	},
	test: {
		include: ['src/**/*.{test,spec}.{js,ts}'],
		environment: 'jsdom',
		setupFiles: ['./vitest-setup.ts'],
		alias: {
			'@testing-library/svelte': '@testing-library/svelte/svelte5',
		},
	},
}));

When I remove the alias property from test I am able to run my test suite. Hopefully this is helpful feedback!

@mcous
Copy link
Collaborator

mcous commented Apr 30, 2024

@pboling Looks like maybe our alias needs to be more specific; it's replacing imports it shouldn't. I'll experiment with this a little and update the README shortly. Upon further inspection, I think the problem might lie with a couple updates you should make to your config now that you're using the new svelteTesting plugin.

  • Remove import '@testing-library/svelte/vitest' from your ./vitest-setup.ts file
  • Remove the manual configuration of resolve.conditions

Both of these jobs the plugin now handles for you. I find that this configuration - as listed in the README - behaves well with Svelte 5:

// vite.config.ts
// ...
export default defineConfig({
  plugins: [sveltekit(), svelteTesting()],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    environment: "jsdom",
    alias: {
      "@testing-library/svelte": "@testing-library/svelte/svelte5",
    },
    setupFiles: ["./vitest-setup.ts"],
  },
});
// vitest-setup.ts
// Only needed to configure `@testing-library/jest-dom`
import '@testing-library/jest-dom/vitest`

If this doesn't work, you could try using a more specific alias. In my own projects, this is necessary only if I'm not using the svelteTesting plugin.

{
  // ...
  test: {
    // ...
    alias: [
      {
        find: /^@testing-library\/svelte$/,
        replacement: '@testing-library/svelte/svelte5',
      },
    ],
  // ...
  },
}

(All this being said, something feels off to me here. I think having a separate Svelte 5 import path is probably causing more trouble than it's worth. I don't think the situation will stick around too much longer, though)

@pboling
Copy link

pboling commented May 1, 2024

@mcous The top suggestions you gave are working! I did not need the more specific alternative alias.

@bradyisom
Copy link

It says at the top of this issue to post problems or issues with Svelte 5 testing and feedback. So, here goes:

I am trying to write a test for my Svelte 5 component, but am struggling to figure out how to test a $bindable() prop on my component.

Here is a simple component:

<script lang="ts">
  import type { Snippet } from 'svelte';
  let {
    children,
    checked = $bindable(),
    ...restProps
  }: { children: Snippet; checked?: boolean; disabled?: boolean } = $props();
</script>

<label
  data-testid="checkbox"
  class="flex h-6 items-center text-sm leading-6 text-900"
>
  <input
    data-testid="checkbox-input"
    {...restProps}
    type="checkbox"
    class="h-4 w-4 rounded border border-300 checked:bg-secondary-600 checked:text-on-secondary-600 focus:ring-secondary-600 focus:outline-none focus:ring"
    bind:checked
  />
  <div data-testid="checkbox-label" class="ml-3">
    {@render children()}
  </div>
</label>

Here is a test that tries to test the bindable nature of the checked prop:

import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import userEvent from '@testing-library/user-event';
import { flush } from '$root/test-helpers';

import Checkbox from './Checkbox.svelte';
import { createRawSnippet } from 'svelte';

function getTestSnippet() {
  return createRawSnippet(() => {
    return {
      render: () => `My checkbox`,
    };
  });
}

describe('Checkbox', () => {
  it('should bind to checked', async () => {
    const user = userEvent.setup();
    const checked = $state(false);

    render(Checkbox, {
      props: {
        checked,
        children: getTestSnippet(),
      },
    });
    await flush();

    const input = screen.getByTestId('checkbox-input');
    await user.click(input);
    await flush();

    expect(input).toBeChecked();
    expect(checked).toBe(true);
  });
});

I am trying to simulate the bind:checked syntax in a parent component. I've tried setting the entire props to a $state(). I've also debugged down into the native Svelte mount() code, and it looks like it is expecting an object with a set() function on it to implement binding.

Any help or direction, or verification that there is a bug or issue here, would be appreciated.

@mcous
Copy link
Collaborator

mcous commented Oct 10, 2024

@bradyisom in Svelte 4, binds were not directly testable, and as far as I know, this hasn't changed in Svelte 5. You will likely need a wrapper component - see the Svelte 4 example for testing bind: for inspiration. See earlier comments in this thread for reference: #284 (comment), #284 (comment)

Opinionated advice that you didn't ask for: don't use bind:. It trades short term convenience for long-term scalability issues and bugginess. Use a callback prop to signal changes instead, which will be easier to write tests for and way less susceptible to weird race conditions as your application grows and evolves

@bradyisom
Copy link

@mcous, Thank you for your quick response. I did look at that Svelte 4 example several times, but somehow missed that it was creating a testing/wrapper component. I also got thrown off, because it was using a writable() store instead of a $state().

I will take your unsolicited advice under consideration.

@bradyisom
Copy link

I did refactor my code to use events, instead of binding values. However, I ran into a similar problem when working through dynamically mounting components. I was curious if there was a real solution, if you still want to use $bindable props.

I did a deeper dive into understanding universal reactivity and state. I found that I could actually "bind" the props in my test and test $bindable props by doing something like this:

let checked = $state(false);
render(Checkbox, {
      props: {
        get checked() {
          return checked;
        },
        set checked(value: boolean) {
          checked = value;
        },
        children: getTestSnippet(),
      },
    });

The key is adding the get and set properties on the props object. This explicitly makes the prop bindable. FWIW, in case someone else runs into this issue. It seems like, at the very least, we could add something to the docs to show this pattern for testing $bindable props.

@KieranP
Copy link

KieranP commented Oct 24, 2024

Just started looking at upgrading our app to Svelte5 now that it is released, and getting these errors in vitest using testing-library. App seems to run fine in the browser, so this has something to do with simulated dom/browser testing?

ReferenceError: ResizeObserver is not defined
 ❯ ResizeObserverSingleton.#getObserver node_modules/svelte/src/internal/client/dom/elements/bindings/size.js:54:26
     52|    (this.#observer = new ResizeObserver(
     53|     /** @param {any} entries */ (entries) => {
     54|      for (var entry of entries) {
       |                          ^
     55|       ResizeObserverSingleton.entries.set(entry.target, entry);
     56|       for (var listener of this.#listeners.get(entry.target) || []) {
 ❯ ResizeObserverSingleton.observe node_modules/svelte/src/internal/client/dom/elements/bindings/size.js:38:20
 ❯ Module.bind_element_size node_modules/svelte/src/internal/client/dom/elements/bindings/size.js:104:41
TypeError: element.animate is not a function
 ❯ animate node_modules/svelte/src/internal/client/dom/elements/transitions.js:377:26
    375|   // for bidirectional transitions, we start from the current position,
    376|   // rather than doing a full intro/outro
    377|   var t1 = counterpart?.t() ?? 1 - t2;
       |                          ^
    378|   counterpart?.abort();
    379|
 ❯ Object.in node_modules/svelte/src/internal/client/dom/elements/transitions.js:243:12
 ❯ node_modules/svelte/src/internal/client/dom/elements/transitions.js:298:54
 ❯ Module.untrack node_modules/svelte/src/internal/client/runtime.js:864:10
 ❯ node_modules/svelte/src/internal/client/dom/elements/transitions.js:298:27
 ❯ update_reaction node_modules/svelte/src/internal/client/runtime.js:327:56
 ❯ update_effect node_modules/svelte/src/internal/client/runtime.js:455:18
 ❯ flush_queued_effects node_modules/svelte/src/internal/client/runtime.js:545:4
 ❯ flush_queued_root_effects node_modules/svelte/src/internal/client/runtime.js:526:4
 ❯ process_deferred node_modules/svelte/src/internal/client/runtime.js:572:2
Svelte error: effect_update_depth_exceeded
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops

I'm able to reduce the ResizeObserver errors using this, but not sure if this is the right approach:

vi.stubGlobal(
  'ResizeObserver',
  vi.fn(() => ({
    observe: vi.fn(),
    unobserve: vi.fn(),
    disconnect: vi.fn()
  }))
)

@webJose
Copy link

webJose commented Oct 24, 2024

@KieranP, Svelte v5 now uses ResizeObserver whenever you use bind:clientwidth or bind:clientheight. JsDom doesn't carry a mock for ResizeObserver.

@mcous
Copy link
Collaborator

mcous commented Oct 24, 2024

@KieranP same story as ResizeObserver with Element.animate: Svelte 5 now requires Element.animate for transitions, and neither jsdom nor happy-dom provide it. You can try to knock it out with a mock, but comes with the usual downsides of faking out APIs you don't control at a layer you don't control. You can also consider:

  • Split your components up so you can avoid running component tests in Node for the component doing transitions and/or binding to client size
  • Use Vitest's browser mode for these tests
Svelte error: effect_update_depth_exceeded
Maximum update depth exceeded...

Hard to say what would cause without seeing more of your code. If you can create a minimal reproduction of this particular error, feel free to throw a link in here or in discord if you need help debugging

@KieranP
Copy link

KieranP commented Oct 26, 2024

@mcous Thanks for the response. I'll see what I can do about mocking the APIs. Using browser mode for the basic tests I'm doing seems like overkill in our use case, so I want to try avoid it if possible.

As for effect_update_depth_exceeded, it seems that this is a possible undocumented breaking change; some state change that used to work in my Svelte 4 app but not causes a loop and throwing of effect_update_depth_exceeded error in Svelte 5. I've opened a ticket with the Svelte team.

@bleucitron
Copy link

bleucitron commented Nov 4, 2024

Hi there !
I'm struggling with TypeError: animation.cancel is not a function, i suppose this is because Animation is not provided by jsdom either.

I haven't been able to mock it correctly so far...

@bleucitron
Copy link

Ok, i solved my issue.

I first badly mocked Element.prototype.animate as such:

 Element.prototype.animate = vi
    .fn()
    .mockImplementation(() => ({ finished: Promise.resolve() }));

without realising this actually then builds a malformed Animation when invoked, which causes the animation.cancel is not a function error.

Mocking this way solves this :

  Element.prototype.animate = vi
    .fn()
    .mockImplementation(() => ({ cancel: vi.fn(), finished: Promise.resolve() }));

@cezaryszymanski
Copy link

Hello

Not sure if that was mentioned before, but I cannot really see any way events can be tested with spy after upgrading.
Before I used it like:

  const { container, component } = render(Search, { categories: [TestCategoryWithValues] });
  const onChange = vi.fn();
  component.$on("change", onChange);

But in svelte 5 the $on function is no longer present on the component. Is there any alternative way to do this now?

@bleucitron
Copy link

Not sure if that was mentioned before, but I cannot really see any way events can be tested with spy after upgrading.

I think the only way is to migrate your events to props.

@mcous
Copy link
Collaborator

mcous commented Nov 5, 2024

But in svelte 5 the $on function is no longer present on the component. Is there any alternative way to do this now?

@cezaryszymanski I believe you can pass an events key to render. The Svelte docs are a little sparse on this option, but we pass it along to mount:

render(Component, {
  props: { /* ... */ },
  events: { /* ... */ },
})

Migrating to function props might be easier

@cezaryszymanski
Copy link

cezaryszymanski commented Nov 18, 2024

After the update my test where I do JSON file upload has 2 errors

TypeError: Cannot set property files of [object HTMLInputElement] which has only a getter

and

TypeError: Failed to set the 'files' property on 'HTMLInputElement': The provided value is not of type 'FileList'

I upload files like in the docs example https://testing-library.com/docs/user-event/utility/#upload

The test still ends with success but with this errors

@mcous
Copy link
Collaborator

mcous commented Nov 18, 2024

@cezaryszymanski I'd be happy to help diagnose this issue if you could provide a minimal reproduction in a repository or Stackblitz. It sounds like an issue with @testing-library/user-event and/or JSDOM (or whichever library you're using) rather than @testing-library/svelte, which is only calling Svelte and doesn't affect the behaviors of the DOM elements that get rendered.

Did you update more than just Svelte and STL for your project?

@cezaryszymanski
Copy link

@mcous After some more debugging I thing that the second error I mentioned might be specific to my code, but I managed to reproduce the first one https://stackblitz.com/edit/vitejs-vite-ra2ipx?file=src%2Flib%2FFileInput.test.js

It happens when the input has binded $state property (see FileInput.svelte)

@mcous
Copy link
Collaborator

mcous commented Nov 19, 2024

@cezaryszymanski I see! Yup, there seems to be a difference between how real browsers handle setting fileInput.files = undefined vs jsdom / happy-dom. A real browser will silently no-op, while jsdom will throw an error. Since this code is in Svelte internals, and doesn't appear to be a bug on Svelte's part, there's not much we can do here.

Since this issue is independent of Svelte and @testing-library/svelte, and is instead a difference between how real browsers behave vs jsdom / happy-dom, if you were so inclined, you could try raising issues with jsdom and happy-dom with minimal reproductions of setting fileInput.files to undefined or null. No idea if you'll get any traction there!

In the meantime, for your tests, I would recommend:

  • (*Gets up on soapbox*) Don't use bind
    • (I have about a half-dozen comments in this thread with the same recommendation. IMO bind should be avoided in almost all cases, because it introduces confusion and ambiguity around "why did this thing change?" that makes codebases hard to scale.)
  • Run this test in Vitest browser mode instead of jsdom / happy-dom

@amosjyng
Copy link

amosjyng commented Nov 22, 2024

I have a local variable in my component that keeps track of whether a typewriter transition effect is playing or not. When testing, the onintrostart function gets called, then the transition tick function gets called with t = 0, then the animation somehow gets cancelled and neither the onintroend nor the tick functions get called one last time, leaving the tests to fail because the typewriter effect clears out the HTML textContent without ever getting to restore it. This does not happen in the browser. @bleucitron 's mock of element.animate works for tests on other components, but not this one with this specific effect.

Any ideas on how this might best be handled? Tests were passing in Svelte 4 but not now in Svelte 5.

@mcous
Copy link
Collaborator

mcous commented Nov 22, 2024

Hey @amosjyng! You'd likely need a more complicated fake to simulate transitions - you could try reading through the Svelte internal transition source code to get an idea of what it uses from Element.animate.

That being said, I don't think trying to test animation logic in a fake DOM environment with a mocked out animate method is going to be a very fruitful path. It'll couple your component tests to Svelte internals and only be as good as your ability to simulate a browser in a mock object, assuming the mock doesn't mess up other things.

Instead, I recommend moving these tests to a real browser (via Vitest or Playright) and/or restructuring your code so that the interesting logic is wrapped up in some structure you can test easily. Then, add that unit to a component that simply does wire-up and doesn't have interesting logic nor direct tests of its own

@amosjyng
Copy link

Ah, good point, thanks @mcous! I will give browser tests a try.

@amosjyng
Copy link

Another issue I've noticed is that Vitest stack traces no longer have accurate line numbers for Svelte files.

For example, if you look at KieranP's stack trace above, the actual error is not from the output line for (var entry of entries), but probably from line 36 instead:

ReferenceError: ResizeObserver is not defined
 ❯ ResizeObserverSingleton.#getObserver node_modules/svelte/src/internal/client/dom/elements/bindings/size.js:54:26
     52|    (this.#observer = new ResizeObserver(
     53|     /** @param {any} entries */ (entries) => {
     54|      for (var entry of entries) {
       |                          ^
     55|       ResizeObserverSingleton.entries.set(entry.target, entry);
     56|       for (var listener of this.#listeners.get(entry.target) || []) {

Another example I have:

TypeError: Cannot read properties of undefined (reading 'hooks')
 ❯ handle_error ../node_modules/@sveltejs/kit/src/runtime/client/client.js:1623:7
    1621| function handle_error(error, event) {
    1622|  if (error instanceof HttpError) {
    1623|   return error.body;
       |       ^
    1624|  }
    1625|

or

Error: Textarea input not found
 ❯ src/lib/controls/SendInputForm.svelte:69:12
     68|   onsubmit={submitInput}
     69| >
     70|   <label for="message" class="accessibility-only">{acce…
       |          ^
     71|   <textarea
     72|     id="message"

@peterpeterparker
Copy link

peterpeterparker commented Dec 9, 2024

I'm facing a similar issue to the one someone reported on SO today. The unit test works fine until I add a <style></style> to the component. Does anyone know how to solve this?

FAIL src/tests/lib/components/Back.spec.ts [ src/tests/lib/components/Back.spec.ts ]
TypeError: Error while preprocessing src/lib/components/Back.svelte - Cannot create proxy with a non-object as target or handler
❯ new PartialEnvironment node_modules/vite/dist/node/chunks/dep-yUJfKD1i.js:16766:19
❯ preprocessCSS node_modules/vite/dist/node/chunks/dep-yUJfKD1i.js:48447:23
❯ node_modules/@sveltejs/vite-plugin-svelte/src/preprocess.js:114:10
❯ style node_modules/@sveltejs/vite-plugin-svelte/src/preprocess.js:77:37
❯ process_single_tag node_modules/svelte/src/compiler/preprocess/index.js:283:21
❯ replace_in_code node_modules/svelte/src/compiler/preprocess/replace_in_code.js:70:23
❯ process_tag node_modules/svelte/src/compiler/preprocess/index.js:300:26
❯ Module.preprocess node_modules/svelte/src/compiler/preprocess/index.js:363:25
❯ compileSvelte node_modules/@sveltejs/vite-plugin-svelte/src/utils/compile.js:85:20

I can reproduce the issue with a Svelte sample lib repo: https://github.com/peterpeterparker/svelte-vite-testing-library-css

@mcous
Copy link
Collaborator

mcous commented Dec 9, 2024

@peterpeterparker i don't see the svelteTesting plugin in your reproduction repo - was that intentional?

I also don't see any testing library code in the stacktrace - it seems to be failing at the compile step. Is this issue reproducible without testing-library, just using the basic testing setup from the Svelte docs? If so, this may be an issue to file with the Svelte itself or their Vite plugin.


I took a closer look at your reproduction, and realized it does not include testing-library nor does it follow the instructions in the Svelte doc. You may want to consider updating it to see if you get the same behavior

@peterpeterparker
Copy link

peterpeterparker commented Dec 9, 2024

You're absolutely right, @mcous — there's no svelteTesting in the setup. I had it in my original branch (PR) and naively assumed it was installed by default when I created the sample repo from scratch with the Svelte CLI to reproduce the issue. A bit of a silly mistake on my part 🙈! That said, it does help us rule out Svelte Testing as a root cause of the isse. Sorry for the noise. I'll go ahead and open an issue in the Svelte repo (Updated: I’ve reported it here which happens to be a duplicate of this issue).

P.S. Thanks for the basic testing setup suggestion as well. I tried it earlier this morning, but unfortunately, it didn’t resolve the issue either.

@drewrygh
Copy link

drewrygh commented Dec 18, 2024

For those running into this animation error:

TypeError: element.animate is not a function
 ❯ animate node_modules/svelte/src/internal/client/dom/elements/transitions.js:377:26

A few folks have suggested splitting out the business logic and animation code, so that it's easier to test things independently. I've managed with this approach, with the addition of a conditional check that controls whether the animation should run. It has the added benefit of making animation related code more reusable. If you are working with other developers, you can keep animations in one central place, as opposed to having hardcoded transitions scattered across various components.

// Animations/FlyInOutAnimation.svelte
<script lang="ts">
    import {fly} from "svelte/transition";

    const {className = "", ...rest} = $props();

    const isTestEnv = process?.env?.NODE_ENV === "test";
    const inFlyOptions = {x: 1000, duration: 250};
    const outFlyOptions = { x: 1000, duration: 750};

   // This conditionally applies the transition, so that it still works in the browser, but won't run in a test environment
    function transition(node, options) {
        if (!isTestEnv) {
            return fly(node, options);
        }
    }

</script>

<div class={className} in:transition={inFlyOptions} out:transition={outFlyOptions} {...rest}>
    <slot></slot>
</div>

With this, instead of doing something like:

<div in:fly={{ x: 1000, duration: 250 }} out:fly={{ x: 1000, duration: 500 }}> ... </div>

You could use the animation component

<FlyInOutAnimation> ... </FlyInOutAnimation>

Obviously this won't work in all scenarios, and it may make more sense to use service functions to abstract this away depending on how your code is organized ... but you get the idea. Hopefully this helps.

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

No branches or pull requests