Repository: testing-library/svelte-testing-library
Pull request #339 (2024-03-17 05:49:21)

fix: ensure fireEvent is exported
## Overview

#328 moved exports from `pure.js` to `index.js` and switched everything to wildcard exports:

https://github.com/testing-library/svelte-testing-library/blob/0593819ca0e083eadf536d20d9d61944d594fb91/src/index.js#L15-L16

If two wildcard exports emit the same export, [neither will be included](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export). 

> There is also `export * from "mod"`, although there's no `import * from "mod"`. This re-exports all named exports from `mod` as the named exports of the current module, but the default export of `mod` is not re-exported. If there are two wildcard exports statements that implicitly re-export the same name, **neither one is re-exported**.

**This means the current release on `next` is not exporting `fireEvent`**. This PR moves the STL exports back to named exports to resolve the bug

## Observing the bug

```shell
pnpm add -D svelte @testing-library/svelte@next
node
> const stl = await import('@testing-library/svelte')
> stl.fireEvent
undefined
```

## Why didn't tests catch this?

Vite/Vitest appears to resolve exports differently from local files compared to `node_modules`. Since the test suite imports directly from `src/index.js`, the behavior is slightly different:

- The conflicting wildcard export of `fireEvent` is available
- It is set to whatever the _last_ export was

In our case, since `export * from '@testing-library/dom'` is listed second, that means the tests are running with the vanilla `fireEvent`, and coincidentally passing because a no-op `await` in the test was enough of a wait for Svelte to flush pending changes.

Without a more sophisticated test suite that packs and installs the library into example projects (which is a good long term idea!) this particular problem is hard to guard against with unit tests.

## Change log

- Update exports, with comments so we don't forget that the explicit named exports are important
- Update the tests to ensure `fireEvent` is returning a Promise
- Fix import/export sorting lint errors in files I was touching and looking at

mcous: @yanick did you see this one? Relatively bad regression on the `next` tag

Patch:
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 6021776..778d507 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -23,6 +23,9 @@ module.exports = {
       parserOptions: {
         parser: '@typescript-eslint/parser',
       },
+      rules: {
+        'no-undef-init': 'off',
+      },
     },
     {
       files: ['*.ts'],
diff --git a/.prettierignore b/.prettierignore
index 113f29b..5e50c20 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,4 @@
 scripts/*
 .eslintignore
 .prettierignore
+.all-contributorsrc
diff --git a/.prettierrc.yaml b/.prettierrc.yaml
index e1bf864..0a2ace3 100644
--- a/.prettierrc.yaml
+++ b/.prettierrc.yaml
@@ -4,6 +4,6 @@ trailingComma: es5
 plugins:
   - prettier-plugin-svelte
 overrides:
-  - files: "*.svelte"
+  - files: '*.svelte'
     options:
       parser: svelte
diff --git a/src/index.js b/src/index.js
index 3e58608..46fd662 100644
--- a/src/index.js
+++ b/src/index.js
@@ -13,5 +13,9 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
   })
 }
 
-export * from './pure.js'
+// export all base queries, screen, etc.
 export * from '@testing-library/dom'
+
+// export svelte-specific functions and custom `fireEvent`
+// `fireEvent` must be a named export to take priority over wildcard export above
+export { act, cleanup, fireEvent, render } from './pure.js'
diff --git a/src/svelte5-index.js b/src/svelte5-index.js
index 8dc11e8..86ff742 100644
--- a/src/svelte5-index.js
+++ b/src/svelte5-index.js
@@ -13,6 +13,10 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
   })
 }
 
-export { act, fireEvent } from './pure.js'
-export * from './svelte5.js'
+// export all base queries, screen, etc.
 export * from '@testing-library/dom'
+
+// export svelte-specific functions and custom `fireEvent`
+// `fireEvent` must be a named export to take priority over wildcard export above
+export { act, fireEvent } from './pure.js'
+export { cleanup, render } from './svelte5.js'
diff --git a/svelte.config.js b/svelte.config.js
index 61eb947..b0683fd 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -1,7 +1,7 @@
 import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
 
 export default {
-    // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
-    // for more information about preprocessors
-    preprocess: vitePreprocess(),
+  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
+  // for more information about preprocessors
+  preprocess: vitePreprocess(),
 }
diff --git a/vite.config.js b/vite.config.js
index 1ef160e..d9085ef 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,5 +1,6 @@
+import path from 'node:path'
+
 import { svelte } from '@sveltejs/vite-plugin-svelte'
-import path from 'path'
 import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
 import { defineConfig } from 'vite'
 


Test patch:
diff --git a/src/__tests__/act.test.js b/src/__tests__/act.test.js
index 57fa5b9..75c9ded 100644
--- a/src/__tests__/act.test.js
+++ b/src/__tests__/act.test.js
@@ -1,25 +1,13 @@
-import { beforeEach, describe, expect, test } from 'vitest'
+import { setTimeout } from 'node:timers/promises'
+
+import { act, render } from '@testing-library/svelte'
+import { describe, expect, test } from 'vitest'
 
-import { act, fireEvent, render as stlRender } from '@testing-library/svelte'
 import Comp from './fixtures/Comp.svelte'
 
 describe('act', () => {
-  let props
-
-  const render = () => {
-    return stlRender(Comp, {
-      props
-    })
-  }
-
-  beforeEach(() => {
-    props = {
-      name: 'World'
-    }
-  })
-
   test('state updates are flushed', async () => {
-    const { getByText } = render()
+    const { getByText } = render(Comp)
     const button = getByText('Button')
 
     expect(button).toHaveTextContent('Button')
@@ -31,24 +19,13 @@ describe('act', () => {
     expect(button).toHaveTextContent('Button Clicked')
   })
 
-  test('findByTestId returns the element', async () => {
-    const { findByTestId } = render()
-
-    expect(await findByTestId('test')).toHaveTextContent(`Hello ${props.name}!`)
-  })
-
   test('accepts async functions', async () => {
-    const sleep = (ms) =>
-      new Promise((resolve) => {
-        setTimeout(() => resolve(), ms)
-      })
-
-    const { getByText } = render()
+    const { getByText } = render(Comp)
     const button = getByText('Button')
 
     await act(async () => {
-      await sleep(100)
-      await fireEvent.click(button)
+      await setTimeout(100)
+      button.click()
     })
 
     expect(button).toHaveTextContent('Button Clicked')
diff --git a/src/__tests__/auto-cleanup.test.js b/src/__tests__/auto-cleanup.test.js
index 4d38d35..206d101 100644
--- a/src/__tests__/auto-cleanup.test.js
+++ b/src/__tests__/auto-cleanup.test.js
@@ -1,6 +1,6 @@
+import { render } from '@testing-library/svelte'
 import { describe, expect, test } from 'vitest'
 
-import { render } from '@testing-library/svelte'
 import Comp from './fixtures/Comp.svelte'
 
 describe('auto-cleanup', () => {
diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js
index 3e49548..7131624 100644
--- a/src/__tests__/cleanup.test.js
+++ b/src/__tests__/cleanup.test.js
@@ -1,7 +1,6 @@
+import { cleanup, render } from '@testing-library/svelte'
 import { describe, expect, test, vi } from 'vitest'
-import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
 
-import { act, cleanup, render } from '@testing-library/svelte'
 import Mounter from './fixtures/Mounter.svelte'
 
 const onExecuted = vi.fn()
@@ -16,8 +15,8 @@ describe('cleanup', () => {
     expect(document.body).toBeEmptyDOMElement()
   })
 
-  test.runIf(SVELTE_VERSION < '5')('cleanup unmounts component', async () => {
-    await act(renderSubject)
+  test('cleanup unmounts component', () => {
+    renderSubject()
     cleanup()
 
     expect(onDestroyed).toHaveBeenCalledOnce()
diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js
index e7f2fd5..0fd672d 100644
--- a/src/__tests__/context.test.js
+++ b/src/__tests__/context.test.js
@@ -1,6 +1,6 @@
+import { render } from '@testing-library/svelte'
 import { expect, test } from 'vitest'
 
-import { render } from '@testing-library/svelte'
 import Comp from './fixtures/Context.svelte'
 import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
 
diff --git a/src/__tests__/debug.test.js b/src/__tests__/debug.test.js
index 769c0c7..e2a9eda 100644
--- a/src/__tests__/debug.test.js
+++ b/src/__tests__/debug.test.js
@@ -1,19 +1,13 @@
 import { prettyDOM } from '@testing-library/dom'
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
-
 import { render } from '@testing-library/svelte'
+import { describe, expect, test, vi } from 'vitest'
+
 import Comp from './fixtures/Comp.svelte'
 
 describe('debug', () => {
-  beforeEach(() => {
-    vi.spyOn(console, 'log').mockImplementation(() => {})
-  })
-
-  afterEach(() => {
-    console.log.mockRestore()
-  })
-
   test('pretty prints the base element', () => {
+    vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
+
     const { baseElement, debug } = render(Comp, { props: { name: 'world' } })
 
     debug()
diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js
index 16f688c..e0aba4d 100644
--- a/src/__tests__/events.test.js
+++ b/src/__tests__/events.test.js
@@ -1,6 +1,6 @@
+import { fireEvent, render } from '@testing-library/svelte'
 import { describe, expect, test } from 'vitest'
 
-import { fireEvent, render } from '@testing-library/svelte'
 import Comp from './fixtures/Comp.svelte'
 
 describe('events', () => {
@@ -8,8 +8,9 @@ describe('events', () => {
     const { getByText } = render(Comp, { props: { name: 'World' } })
     const button = getByText('Button')
 
-    await fireEvent.click(button)
+    const result = fireEvent.click(button)
 
+    await expect(result).resolves.toBe(true)
     expect(button).toHaveTextContent('Button Clicked')
   })
 
@@ -17,14 +18,15 @@ describe('events', () => {
     const { getByText } = render(Comp, { props: { name: 'World' } })
     const button = getByText('Button')
 
-    await fireEvent(
+    const result = fireEvent(
       button,
       new MouseEvent('click', {
         bubbles: true,
-        cancelable: true
+        cancelable: true,
       })
     )
 
+    await expect(result).resolves.toBe(true)
     expect(button).toHaveTextContent('Button Clicked')
   })
 })
diff --git a/src/__tests__/fixtures/Comp.svelte b/src/__tests__/fixtures/Comp.svelte
index c739725..ba23d88 100644
--- a/src/__tests__/fixtures/Comp.svelte
+++ b/src/__tests__/fixtures/Comp.svelte
@@ -1,7 +1,7 @@
 <svelte:options accessors />
 
 <script>
-  export let name
+  export let name = 'World'
 
   let buttonText = 'Button'
 
diff --git a/src/__tests__/fixtures/Context.svelte b/src/__tests__/fixtures/Context.svelte
index be8253b..d6515d5 100644
--- a/src/__tests__/fixtures/Context.svelte
+++ b/src/__tests__/fixtures/Context.svelte
@@ -1,7 +1,7 @@
 <script>
-  import { getContext } from 'svelte';
+  import { getContext } from 'svelte'
 
-  const ctx = getContext('foo');
+  const ctx = getContext('foo')
 </script>
 
 <div>{ctx.message}</div>
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js
index df0792b..d650368 100644
--- a/src/__tests__/mount.test.js
+++ b/src/__tests__/mount.test.js
@@ -1,6 +1,6 @@
+import { act, render, screen } from '@testing-library/svelte'
 import { describe, expect, test, vi } from 'vitest'
 
-import { act, render, screen } from '@testing-library/svelte'
 import Mounter from './fixtures/Mounter.svelte'
 import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
 
diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js
index 9221012..ea445d5 100644
--- a/src/__tests__/render.test.js
+++ b/src/__tests__/render.test.js
@@ -1,8 +1,8 @@
 import { render } from '@testing-library/svelte'
-import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
 import { describe, expect, test } from 'vitest'
 
 import Comp from './fixtures/Comp.svelte'
+import { IS_SVELTE_5 } from './utils.js'
 
 describe('render', () => {
   const props = { name: 'World' }
@@ -65,24 +65,21 @@ describe('render', () => {
     expect(baseElement.firstChild).toBe(container)
   })
 
-  test.runIf(SVELTE_VERSION < '5')(
-    'should accept anchor option in Svelte v4',
-    () => {
-      const baseElement = document.body
-      const target = document.createElement('section')
-      const anchor = document.createElement('div')
-      baseElement.appendChild(target)
-      target.appendChild(anchor)
-
-      const { getByTestId } = render(
-        Comp,
-        { props, target, anchor },
-        { baseElement }
-      )
-      const firstElement = getByTestId('test')
-
-      expect(target.firstChild).toBe(firstElement)
-      expect(target.lastChild).toBe(anchor)
-    }
-  )
+  test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => {
+    const baseElement = document.body
+    const target = document.createElement('section')
+    const anchor = document.createElement('div')
+    baseElement.appendChild(target)
+    target.appendChild(anchor)
+
+    const { getByTestId } = render(
+      Comp,
+      { props, target, anchor },
+      { baseElement }
+    )
+    const firstElement = getByTestId('test')
+
+    expect(target.firstChild).toBe(firstElement)
+    expect(target.lastChild).toBe(anchor)
+  })
 })
diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js
index 6efda86..ca4b8e8 100644
--- a/src/__tests__/rerender.test.js
+++ b/src/__tests__/rerender.test.js
@@ -15,7 +15,7 @@ describe('rerender', () => {
   })
 
   test('warns if incorrect arguments shape used', async () => {
-    vi.stubGlobal('console', { warn: vi.fn() })
+    vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
 
     const { rerender } = render(Comp, { name: 'World' })
     const element = screen.getByText('Hello World!')
diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js
index 7e7dd3d..a9c8e02 100644
--- a/src/__tests__/transition.test.js
+++ b/src/__tests__/transition.test.js
@@ -1,12 +1,11 @@
+import { render, screen, waitFor } from '@testing-library/svelte'
 import { userEvent } from '@testing-library/user-event'
 import { beforeEach, describe, expect, test, vi } from 'vitest'
 
-import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
-
-import { render, screen, waitFor } from '@testing-library/svelte'
 import Transitioner from './fixtures/Transitioner.svelte'
+import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
 
-describe.runIf(!IS_SVELTE_5)('transitions', () => {
+describe.skipIf(IS_SVELTE_5)('transitions', () => {
   beforeEach(() => {
     if (!IS_JSDOM) return
 
diff --git a/src/vitest.js b/src/vitest.js
index 2f9930c..71977e6 100644
--- a/src/vitest.js
+++ b/src/vitest.js
@@ -1,6 +1,5 @@
-import { afterEach } from 'vitest'
-
 import { act, cleanup } from '@testing-library/svelte'
+import { afterEach } from 'vitest'
 
 afterEach(async () => {
   await act()