Repository: testing-library/svelte-testing-library
Pull request #297 (2024-01-27 20:57:52)

ci: run vitest on jsdom and happy-dom
## Overview

This is the third PR in a series to add multiple Svelte versions and test runners to our CI test matrix.

1. #294 
2. #295
3. #297 **<-- you are here**
4. TODO add Jest to CI matrix

This PR supersedes or unblocks #286, depending on what we want our default DOM environment to be for local development

## Change log

- Add scripts to explicitly test using Vitest + jsdom and Vitest + happy-dom
- Hook those scripts up to the CI matrix

## Review requests

How does a CI matrix feel vs running runners on one VM? The matrix keeps the Vitest config simpler, because we don't need to do stuff like isolate the coverage directories, but it uses more "computers" to do so



Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3c799e3..0761056 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,12 +14,14 @@ jobs:
   main:
     # ignore all-contributors PRs
     if: ${{ !contains(github.head_ref, 'all-contributors') }}
-    name: Node ${{ matrix.node }}, Svelte ${{ matrix.svelte }}
+    name: Node ${{ matrix.node }}, Svelte ${{ matrix.svelte }}, ${{ matrix.test-runner }}
+    runs-on: ubuntu-latest
     strategy:
       matrix:
         node: ['16', '18', '20']
         svelte: ['3', '4']
-    runs-on: ubuntu-latest
+        test-runner: ['vitest:jsdom', 'vitest:happy-dom']
+
     steps:
       - name: ⬇️ Checkout repo
         uses: actions/checkout@v4
@@ -35,7 +37,7 @@ jobs:
           npm install --no-save svelte@${{ matrix.svelte }}
 
       - name: ▶️ Run validate script
-        run: npm run validate
+        run: npm run test:${{ matrix.test-runner }}
 
       - name: ⬆️ Upload coverage report
         uses: codecov/codecov-action@v3
diff --git a/package.json b/package.json
index b1197ca..1f3afc5 100644
--- a/package.json
+++ b/package.json
@@ -55,8 +55,10 @@
     "test": "vitest run --coverage",
     "test:watch": "vitest",
     "test:update": "vitest run --update",
+    "test:vitest:jsdom": "vitest run --coverage --environment jsdom",
+    "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom",
     "setup": "npm install && npm run validate",
-    "validate": "npm-run-all test",
+    "validate": "npm-run-all test:vitest:*",
     "contributors:add": "all-contributors add",
     "contributors:generate": "all-contributors generate"
   },
@@ -84,6 +86,7 @@
     "eslint-plugin-simple-import-sort": "10.0.0",
     "eslint-plugin-svelte": "2.35.1",
     "eslint-plugin-vitest-globals": "1.4.0",
+    "happy-dom": "^13.3.1",
     "jsdom": "^22.1.0",
     "npm-run-all": "^4.1.5",
     "prettier": "3.2.4",
diff --git a/vite.config.js b/vite.config.js
index f261029..4baf76f 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -2,14 +2,23 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
 import { defineConfig } from 'vite'
 
 // https://vitejs.dev/config/
-export default defineConfig({
+export default defineConfig(({ mode }) => ({
   plugins: [svelte()],
+  resolve: {
+    // Ensure `browser` exports are used in tests
+    // Vitest prefers modules' `node` export by default
+    // Svelte's `node` export is its SSR bundle, which does not have onMount
+    // https://github.com/testing-library/svelte-testing-library/issues/222#issuecomment-1909993331
+    conditions: mode === 'test' ? ['browser'] : [],
+  },
   test: {
     environment: 'jsdom',
     setupFiles: ['./src/__tests__/_vitest-setup.js'],
+    mockReset: true,
+    unstubGlobals: true,
     coverage: {
       provider: 'v8',
       include: ['src'],
     },
   },
-})
+}))


Test patch:
diff --git a/src/__tests__/fixtures/Mounter.svelte b/src/__tests__/fixtures/Mounter.svelte
new file mode 100644
index 0000000..477bb34
--- /dev/null
+++ b/src/__tests__/fixtures/Mounter.svelte
@@ -0,0 +1,11 @@
+<script>
+  import { onDestroy,onMount } from 'svelte'
+
+  export let onMounted
+  export let onDestroyed
+
+  onMount(() => onMounted())
+  onDestroy(() => onDestroyed())
+</script>
+
+<button />
diff --git a/src/__tests__/fixtures/Stopwatch.svelte b/src/__tests__/fixtures/Stopwatch.svelte
deleted file mode 100644
index 2c275e5..0000000
--- a/src/__tests__/fixtures/Stopwatch.svelte
+++ /dev/null
@@ -1,40 +0,0 @@
-<script>
-  import { onDestroy } from 'svelte'
-
-  let timer
-  let lapse = 0
-  let running = false
-
-  function handleRunClick () {
-    if (running) {
-      clearInterval(timer)
-    } else {
-      const startTime = Date.now() - lapse
-
-      timer = setInterval(() => {
-        lapse = Date.now() - startTime
-      }, 1)
-    }
-    running = true
-  }
-
-  function handleClearClick () {
-    clearInterval(timer)
-    lapse = 0
-    running = false
-  }
-
-  onDestroy(() => {
-    clearInterval(timer)
-  })
-</script>
-
-<style></style>
-
-<span>{lapse}ms</span>
-
-<button on:click={handleRunClick}>
-  {running ? 'Stop' : 'Start'}
-</button>
-
-<button on:click={handleClearClick}>Clear</button>
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js
new file mode 100644
index 0000000..830b513
--- /dev/null
+++ b/src/__tests__/mount.test.js
@@ -0,0 +1,33 @@
+import { describe, expect, test, vi } from 'vitest'
+
+import { act, render, screen } from '..'
+import Mounter from './fixtures/Mounter.svelte'
+
+describe('mount and destroy', () => {
+  const handleMount = vi.fn()
+  const handleDestroy = vi.fn()
+
+  test('component is mounted', async () => {
+    await act(() => {
+      render(Mounter, { onMounted: handleMount, onDestroyed: handleDestroy })
+    })
+
+    const content = screen.getByRole('button')
+
+    expect(handleMount).toHaveBeenCalledOnce()
+    expect(content).toBeInTheDocument()
+  })
+
+  test('component is destroyed', async () => {
+    const { unmount } = render(Mounter, {
+      onMounted: handleMount,
+      onDestroyed: handleDestroy,
+    })
+
+    await act(() => unmount())
+    const content = screen.queryByRole('button')
+
+    expect(handleDestroy).toHaveBeenCalledOnce()
+    expect(content).not.toBeInTheDocument()
+  })
+})
diff --git a/src/__tests__/unmount.test.js b/src/__tests__/unmount.test.js
deleted file mode 100644
index 86a29dd..0000000
--- a/src/__tests__/unmount.test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { describe, expect, test, vi } from 'vitest'
-
-import { act, fireEvent, render } from '..'
-import Stopwatch from './fixtures/Stopwatch.svelte'
-
-describe('unmount', () => {
-  test('unmounts component successfully', async () => {
-    console.warn = vi.fn()
-
-    const { unmount, getByText, container } = render(Stopwatch)
-
-    await fireEvent.click(getByText('Start'))
-
-    unmount()
-
-    // Hey there reader! You don't need to have an assertion like this one
-    // this is just me making sure that the unmount function works.
-    // You don't need to do this in your apps. Just rely on the fact that this works.
-    expect(container.innerHTML).toBe('<div></div>')
-
-    await act()
-    expect(console.warn).not.toHaveBeenCalled()
-  })
-
-  test('destroying component directly and calling unmount does not log warning', async () => {
-    console.warn = vi.fn()
-
-    const { unmount, component } = render(Stopwatch)
-
-    component.$destroy()
-    unmount()
-
-    expect(console.warn).not.toHaveBeenCalled()
-  })
-})