vitest: Tests are failing after updating to Vite 4.1.0

Describe the bug

I’m using SvelteKit with Vitest. I updated SvelteKit and Vitest to their latest versions - 1.5.0 and 0.28.4 respectfully. However, updating Vite from 4.0.4 to 4.1.0 caused 15% of my tests to fail (60/400).

Most of the errors seem unreasonable. For example, removing a DOM element throws a Failed to execute removeChild on Node error. Or Found multiple elements with the text: ... where there’s just a single element containing that exact copy; Error: Unable to fire a "click" event - please provide a DOM element., Error: unable to find element with text, etc.

Reproduction

Here’s a simple StackBlitz example.

  1. Open the link
  2. Wait for the installation to complete
  3. Stop the execution in the terminal (Cmd + C)
  4. Run npm run test
  5. The test fails
image

Then do the following:

  1. Open package.json
  2. Change the version of vite from 4.1.0 to 4.0.4
  3. Update the dependencies
  4. Open the terminal again and run npm run test
  5. The test succeeds
image

The example explained

It’s a simple component with an if statement:

<script lang="ts">
  import { onMount } from "svelte"

  let showMessage = false

  onMount(() => {
    showMessage = true
  })
</script>

{#if showMessage}
  <div>Hello</div>
{/if}

Test file:

import { render } from "@testing-library/svelte"
import Hello from "../Hello.svelte"

describe("Hello component", () => {
  it("should show message", async () => {
    const { getByText } = render(Hello)
    expect(getByText("Hello")).toBeInTheDocument()
  })
})

Result with Vite 4.1.0:

TestingLibraryElementError: Unable to find an element with the text: Hello. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

This is just a single example but similar issues occur where there’s some sort of an asynchronous code used (or conditionals), like promises, setTimeout, useFakeTimers() and advanceTimersByTime(), TestingLIbrary’s waitFor() function, etc.

More dependencies & config

See StackBlitz example for more info

package.json

"@sveltejs/kit": "^1.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
"jsdom": "^21.1.0",

vite.config.ts

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    globals: true,
    environment: "jsdom",
    setupFiles: ["src/setupTest.js"],
  }
});

setupTest.js

import "@testing-library/jest-dom"

System Info

System:
    OS: macOS 13.1
    CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 1.84 GB / 16.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 19.2.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 8.13.1 
  Browsers:
    Brave Browser: 109.1.47.186
    Chrome: 109.0.5414.119
    Firefox: 105.0.1
    Safari: 16.2
  npmPackages:
    vite: 4.1.0 => 4.1.0 
    vitest: ^0.28.4 => 0.28.4

Used Package Manager

npm

Validations

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 19
  • Comments: 30 (15 by maintainers)

Commits related to this issue

Most upvoted comments

I guess it needs to be:

export default defineConfig({
  test: {
    alias: [
      { find: /^svelte$/, replacement: "svelte/internal" }
    ]
  }
})

to be more accurate.

Test passes with following:

<script lang="ts">
-	import { onMount } from "svelte"
+	import { onMount } from "svelte/internal"
 RERUN  src/lib/Hello.svelte x1

 ✓ src/lib/__test__/Hello.test.ts (1)

 Test Files  1 passed (1)

Maybe related to https://github.com/sveltejs/svelte/issues/5534? I’m not sure why this passes when vite version is lowered though.

Would you mind expanding a little bit on what should be done to implement said workaround?

Add “browser” to your resolve.conditions property in the config file.

Just wanna leave a hack here that worked for us, without having to change all the imports.

Create or add this to your test setup file

import * as svelteinternal from 'svelte/internal';
import { vi } from 'vitest';

beforeAll(() => {
    vi.mock('svelte', () => svelteinternal);
});

Started to rollback local vite commit by commit. Test works after reverting https://github.com/vitejs/vite/pull/11595 (👋 @bluwy).

It seems that entrypoint for Svelte is resolved to svelte/ssr.mjs instead of svelte/index.mjs. In vite@4.0.4 the decision was made based on package.json’s module field but in vite@4.1.0 it’s done based on exports.

vite@4.0.4:
/Users/x/y/vitest-example-project/node_modules/.pnpm/svelte@3.55.1/node_modules/svelte/index.mjs

vite@4.1.0:
/Users/x/y/vitest-example-project/node_modules/.pnpm/svelte@3.55.1/node_modules/svelte/ssr.mjs

Parameters used here below: https://github.com/vitejs/vite/blob/d953536aae448e2bea0f3a7cb3c0062b16d45597/packages/vite/src/node/plugins/resolve.ts#L1108-L1112

{
  key: '.',
  browser: false,
  require: false,
  conditions: [ 'development', 'module', 'svelte', 'node' ]
}
-> Result = './ssr.mjs'

Svelte’s entrypoints: https://github.com/sveltejs/svelte/blob/82d2982845df188631993db6b18c2842e3613acf/package.json#L23-L87

It does make sense that ssr.mjs is imported in Node. I guess the onMount intentionally does not run on SSR? Not sure what should be done here. Maybe vite-plugin-svelte should detect test environment and flip the import? Or if Svelte intentionally does not run onMount on SSR, maybe it should do it when it’s run in tests. 😕

Another work-around for projects for now so that test code does not need changes:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    alias: [
      {
        find: /svelte\/ssr.mjs/,
        replacement: "svelte/index.mjs",
      },
    ],

Just chipping in that this issue is still present as of vitest 0.34.6.

For now I recommend using a workaround with conditions.

This is much deeper problem than Svelte issue. Vitest processes svelte/vue/… files with web mode, and processes it with SSR transforms afterwards. But typescript and javascript files are processed with SSR mode enabled (plugins receive ssr flag).

This creates an issue, if code is imported from the same package in two different modes, - especially if code depends on a singleton. Currently to bypass this Vitest hardcodes node condition, but this is not reliable of course (as we can see here Svelte expects Svelte to always run in SSR mode, if it’s running inside node, which is not true for Vitest - it’s always running in Node, not in browser).

One of the reason why Vitest doesn’t process everything with web mode is performance - Vite is (at least, was) really slow to transform a single ts/js file. The other issue is that library authors also expect browser condition to be run in a browser, so they don’t follow node ESM spec (file in browser condition doesn’t have correct extension).

@sheremet-va it still has an issue. experimentalOptimizer doesn’t help

This should be fixed in the latest version. If you still have problems, considered enabling deps.experimentalOptimizer.

If I set resolve.conditions to browser in vite.config.js

resolve: {
  conditions: [
    ...process.env.VITEST ? ['browser'] : []
  ]
}

I get another error multiple times, when running the unit tests

Error: require() of ES Module C:\...\node_modules\jose\dist\browser\index.js from C:\...\node_modules\openid-clien
t\lib\client.js not supported.
Instead change the require of index.js in C:\...\node_modules\openid-client\lib\client.js to a dynamic import() which is available in all CommonJS module
s.

@sheremet-va, nice catch! How did you find that?

I can confirm that this was the cause of my bug since after changing all onMount and onDestroy imports in the relevant files, all 60 tests that were failing succeed now.