Browser mode runs test twice in case of a self-importing svelte component
Describe the bug
So, i know this is a bit of a weird case but please bear with me.
To test components with snippets in svelte 5 i can create a snippet programmatically with createRawSnippet but that's an intentionally low level api which makes things a bit annoying to test. But today i realized that i could do something like this which is very clean and elegant.
Button.test.svelte
<script module lang="ts">
import { expect, it } from 'vitest';
import { render as r } from 'vitest-browser-svelte';
import Test from './Button.test.svelte';
it('should render a snippet in the button', async () => {
const instance = r(Test, {
children: rendering_this
});
const button = instance.getByRole('button');
expect(button.getByText('Pretty cool eh?').query()).toBeDefined();
});
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
import Button from './Button.svelte';
let { children }: { children: Snippet } = $props();
</script>
{#snippet rendering_this()}
<b>Pretty cool eh?</b>
{/snippet}
<Button>
{@render children()}
</Button>
so basically i can create a svelte file, name it Something.test.svelte include it in the test file and then write a test bed in the test component itself, self import it in the module and write my tests there. This works absolutely fine with @testing-library/svelte.
But i start to use vitest browser testing i have the inconvenient side effect that my tests run twice. I suspect this is a bug with vitest and it would be cool if it was fixed because i really like this pattern and i want to spread it.
Reproduction
- Go to this stackblitz
- run
pnpm test(i think vitest browser mode is a bit glitchy on stackblitz but sometimes it works and you'll see two tests being run)
System Info
System:
OS: Linux 5.0 undefined
CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
Memory: 0 Bytes / 0 Bytes
Shell: 1.0 - /bin/jsh
Binaries:
Node: 18.20.3 - /usr/local/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 10.2.3 - /usr/local/bin/npm
pnpm: 8.15.6 - /usr/local/bin/pnpm
npmPackages:
@vitest/browser: ^2.1.8 => 2.1.8
vite: ^5.4.11 => 5.4.11
vitest: ^2.0.4 => 2.1.8
Used Package Manager
pnpm
Validations
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [X] Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord Chat Server.
- [X] The provided reproduction is a minimal reproducible example of the bug.
As browser network devtool shows, Button.test.svelte module is loaded twice. One is /full-path-to/src/lib/Button.test.svelte?import&browserv=xxx and the other is /src/lib/Button.test.svelte. I don't know about the purpose of ?browserv, but it's coming from here. @sheremet-va Do you know why we need this?
https://github.com/vitest-dev/vitest/blob/f9a628438a5462436b59dd9bdeffddada19a9e81/packages/browser/src/client/tester/runner.ts#L138-L150
(But eliminating ?browserv=xxx query is not enough since there are also ?import and /full-path-to/... parts, which is inconsistent with how Vite normalizes normal import.)
I came up with a rough hack to avoid the issue, which is to transform/load /full-path-to/src/lib/Button.test.svelte?import&browserv=xxx in a following way:
// response of /full-path-to/src/lib/Button.test.svelte?import&browserv=xxx
import "/src/lib/Button.test.svelte"
// normally `Button.test.svelte` content is returned in this module,
// but instead, it's now replaced with `/src/lib/Button.test.svelte` import.
// in this way, browser only evaluates `Button.test.svelte` content only once.
https://stackblitz.com/edit/stackblitz-starters-pwcfznwc?file=vite.config.ts
{
name: 'fix-duplicate-module',
resolveId: {
order: 'pre',
handler(source) {
if (source.includes('/Button.test.svelte?browserv=')) {
return '\0virtual:fix-duplicate-module' + source;
}
},
},
load(id) {
if (id.startsWith('\0virtual:fix-duplicate-module')) {
id = id.slice('\0virtual:fix-duplicate-module'.length);
id = id.split('?')[0];
return `import ${JSON.stringify(id)}`;
}
},
}
Btw, the same can happen in plain js tests too, for example:
// repro.test.js
import { it } from 'vitest';
import './repro.test.js';
it("repro", () => {})
Thanks for the exploration, for the hack and for the even more minimal reproduction. I wonder if the self import tricks vite into not rewriting the inner module but that's just a random guess.
Anyway i can be more helpful?
There is an easier workaround to do await import(import.meta.url)
There is an easier workaround to do
await import(import.meta.url)
In the hack plugin? Because it doesn't seem to work in the file itself
There is an easier workaround to do
await import(import.meta.url)In the hack plugin? Because it doesn't seem to work in the file itself
In the test file. You need to use it inside the test or a describe callback though.
In the test file. You need to use it inside the test or a describe callback though.
Doesn't seems to work because when you import it it also run the test function and it's within the test function leading to an error.
Yeah, I tried this too and it should technically work, but Vitest's dynamic import transform drops /* @vite-ignore*/, so Vite import analysis logs a warning
it('should render a snippet in the button', async () => {
const { default: Test } = await import(/* @vite-ignore*/ import.meta.url);
const instance = r(Test, {
children: rendering_this
});
10:44:07 AM [vite] warning:
/home/hiroshi/Downloads/stackblitz-starters-om7pgwvq/src/lib/Button.test.svelte
17 | // import Test from './Button.test.svelte';
18 | it('should render a snippet in the button', async () => {
19 | const { default: Test } = await globalThis["__vitest_browser_runner__"].wrapDynamicImport(() => import(import.meta.url));
| ^^^^^^^^^^^^^^^
20 | const instance = r(Test, { children: rendering_this });
21 | const button = instance.getByRole('button');
The above dynamic import cannot be analyzed by Vite.
See https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations for supported dynamic import formats. If this is intended to be left as-is, you can use the /* @vite-ignore */ comment inside the import() call to suppress this warning.
In my opinion, I'm not sure if we can support your self-import test pattern robustly even if we deal with ?browserv hash.
How does this pattern work in production? Doesn't it mean you also bundle Vitest?
How does this pattern work in production? Doesn't it mean you also bundle Vitest?
The .svelte is only used to render the actual component easily, it's never imported in application code so it will be treeshaken
In my opinion, I'm not sure if we can support your self-import test pattern robustly even if we deal with
?browservhash.
Can you explain a bit why is that the case?
In my opinion, I'm not sure if we can support your self-import test pattern robustly even if we deal with
?browservhash.Can you explain a bit why is that the case?
The point is that, other than ?browserv, we need to copy how Vite's import analysis normalizes import specifier to not duplicate the module. Okay, it looks like it's now contained in this function and maybe fine. https://github.com/vitejs/vite/blob/3aa10b7d618b178aec0f027b1f5fcd3353d2b166/packages/vite/src/node/plugins/importAnalysis.ts#L103-L140
Also if we assume test files are under root, what's need is basically testFullePath.slice(config.root.length).
In case this helps: I've also found Playwright Locators to "act twice" in a way that non-browser testing doesn't. In my case, a click handler added child elements, and the Locator.click on the parent also triggered a click on the child, but only with Playwright. I worked around that with using DOM click method directly, asHTMLElement(button.element()).click();
I don't think we need browserv anymore since we create separate iframes for tests and during watch rerun it will be recreated.
So this might be solved? 👀 Should I try?
So this might be solved? 👀 Should I try?
I still think that import(import.meta.url) should work for your case because that is what Storybook does for their integration and it works.
But I also think that we don't need the browserv query anymore, and using a static import provides better ergonomics. You can try to investigate it, some good points to start are:
- https://github.com/vitest-dev/vitest/blob/075ab35207ce7e6ad6bd3abb02ba6d14d5108c7b/packages/browser/src/client/tester/runner.ts#L272
- https://github.com/vitest-dev/vitest/blob/075ab35207ce7e6ad6bd3abb02ba6d14d5108c7b/packages/browser/src/node/plugin.ts#L340