Skip to content

feat(svelte5): incorporate Svelte 5 support into main entry point #375

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

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
},
rules: {
'no-undef-init': 'off',
'prefer-const': 'off',
},
},
{
Expand All @@ -49,5 +50,6 @@ module.exports = {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: { $state: 'readonly', $props: 'readonly' },
ignorePatterns: ['!/.*'],
}
20 changes: 2 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ primary guiding principle is:
This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's `devDependencies`:

```
```shell
npm install --save-dev @testing-library/svelte
```

This library has `peerDependencies` listings for `svelte >= 3`.
This library supports `svelte` versions `3`, `4`, and `5`.

You may also be interested in installing `@testing-library/jest-dom` so you can use
[the custom jest matchers](https://github.com/testing-library/jest-dom).
Expand All @@ -102,22 +102,6 @@ See the [setup docs][] for more detailed setup instructions, including for other
[vitest]: https://vitest.dev/
[setup docs]: https://testing-library.com/docs/svelte-testing-library/setup

### Svelte 5 support

If you are riding the bleeding edge of Svelte 5, you'll need to either
import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or add an alias to your `vite.config.js`:

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

## Docs

See the [**docs**](https://testing-library.com/docs/svelte-testing-library/intro) over at the Testing Library website.
Expand Down
8 changes: 3 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'

const IS_SVELTE_5 = SVELTE_VERSION >= '5'
const SVELTE_TRANSFORM_PATTERN =
SVELTE_VERSION >= '5' ? '^.+\\.svelte(?:\\.js)?$' : '^.+\\.svelte$'

export default {
testMatch: ['<rootDir>/src/__tests__/**/*.test.js'],
transform: {
'^.+\\.svelte$': 'svelte-jester',
[SVELTE_TRANSFORM_PATTERN]: 'svelte-jester',
},
moduleFileExtensions: ['js', 'svelte'],
extensionsToTreatAsEsm: ['.svelte'],
Expand All @@ -14,9 +15,6 @@ export default {
injectGlobals: false,
moduleNameMapper: {
'^vitest$': '<rootDir>/src/__tests__/_jest-vitest-alias.js',
'^@testing-library/svelte$': IS_SVELTE_5
? '<rootDir>/src/svelte5-index.js'
: '<rootDir>/src/index.js',
},
resetMocks: true,
restoreMocks: true,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"./svelte5": {
"types": "./types/index.d.ts",
"default": "./src/svelte5-index.js"
"default": "./src/index.js"
},
"./vitest": {
"default": "./src/vitest.js"
Expand Down Expand Up @@ -120,7 +120,7 @@
"prettier-plugin-svelte": "3.2.3",
"svelte": "^3 || ^4 || ^5",
"svelte-check": "^3.6.3",
"svelte-jester": "^3.0.0",
"svelte-jester": "^5.0.0",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vitest": "^1.5.2"
Expand Down
9 changes: 2 additions & 7 deletions src/__tests__/auto-cleanup.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

import { IS_SVELTE_5 } from './utils.js'

const importSvelteTestingLibrary = async () =>
IS_SVELTE_5 ? import('../svelte5-index.js') : import('../index.js')

const globalAfterEach = vi.fn()

describe('auto-cleanup', () => {
Expand All @@ -19,7 +14,7 @@ describe('auto-cleanup', () => {
})

test('calls afterEach with cleanup if globally defined', async () => {
const { render } = await importSvelteTestingLibrary()
const { render } = await import('../index.js')

expect(globalAfterEach).toHaveBeenCalledTimes(1)
expect(globalAfterEach).toHaveBeenLastCalledWith(expect.any(Function))
Expand All @@ -35,7 +30,7 @@ describe('auto-cleanup', () => {
test('does not call afterEach if process STL_SKIP_AUTO_CLEANUP is set', async () => {
process.env.STL_SKIP_AUTO_CLEANUP = 'true'

await importSvelteTestingLibrary()
await import('../index.js')

expect(globalAfterEach).toHaveBeenCalledTimes(0)
})
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/fixtures/Comp.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- svelte-ignore options_deprecated_accessors -->
<svelte:options accessors />

<script>
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/fixtures/CompRunes.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
let { name = 'World' } = $props()

let buttonText = $state('Button')

function handleClick() {
buttonText = 'Button Clicked'
}
</script>

<h1 data-testid="test">Hello {name}!</h1>

<button onclick={handleClick}>{buttonText}</button>
2 changes: 1 addition & 1 deletion src/__tests__/fixtures/Mounter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
})
</script>

<button />
<button></button>
14 changes: 9 additions & 5 deletions src/__tests__/render.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { render } from '@testing-library/svelte'
import { describe, expect, test } from 'vitest'
import { beforeAll, describe, expect, test } from 'vitest'

import Comp from './fixtures/Comp.svelte'
import { IS_SVELTE_5 } from './utils.js'
import { COMPONENT_FIXTURES } from './utils.js'

describe('render', () => {
describe.each(COMPONENT_FIXTURES)('render ($mode)', ({ component }) => {
const props = { name: 'World' }
let Comp

beforeAll(async () => {
Comp = await import(component)
})

test('renders component into the document', () => {
const { getByText } = render(Comp, { props })
Expand Down Expand Up @@ -65,7 +69,7 @@ describe('render', () => {
expect(baseElement.firstChild).toBe(container)
})

test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => {
test('should accept anchor option', () => {
const baseElement = document.body
const target = document.createElement('section')
const anchor = document.createElement('div')
Expand Down
26 changes: 15 additions & 11 deletions src/__tests__/rerender.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { act, render, screen } from '@testing-library/svelte'
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
import { describe, expect, test, vi } from 'vitest'
import { beforeAll, describe, expect, test, vi } from 'vitest'

import Comp from './fixtures/Comp.svelte'
import { COMPONENT_FIXTURES, IS_SVELTE_5, MODE_RUNES } from './utils.js'

describe.each(COMPONENT_FIXTURES)('rerender ($mode)', ({ mode, component }) => {
let Comp

beforeAll(async () => {
Comp = await import(component)
})

describe('rerender', () => {
test('updates props', async () => {
const { rerender } = render(Comp, { name: 'World' })
const element = screen.getByText('Hello World!')
Expand All @@ -29,13 +34,12 @@ describe('rerender', () => {
)
})

test('change props with accessors', async () => {
const { component, getByText } = render(
Comp,
SVELTE_VERSION < '5'
? { accessors: true, props: { name: 'World' } }
: { name: 'World' }
)
test.skipIf(mode === MODE_RUNES)('change props with accessors', async () => {
const componentOptions = IS_SVELTE_5
? { name: 'World' }
: { accessors: true, props: { name: 'World' } }

const { component, getByText } = render(Comp, componentOptions)
const element = getByText('Hello World!')

expect(element).toBeInTheDocument()
Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,20 @@ export const IS_JSDOM = window.navigator.userAgent.includes('jsdom')
export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js

export const IS_SVELTE_5 = SVELTE_VERSION >= '5'

export const MODE_LEGACY = 'legacy'

export const MODE_RUNES = 'runes'

export const COMPONENT_FIXTURES = [
{
mode: MODE_LEGACY,
component: './fixtures/Comp.svelte',
isEnabled: true,
},
{
mode: MODE_RUNES,
component: './fixtures/CompRunes.svelte',
isEnabled: IS_SVELTE_5,
},
].filter(({ isEnabled }) => isEnabled)
27 changes: 27 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Rendering core for svelte-testing-library.
*
* Defines how components are added to and removed from the DOM.
* Will switch to legacy, class-based mounting logic
* if it looks like we're in a Svelte <= 4 environment.
*/
import * as LegacyCore from './legacy.js'
import * as ModernCore from './modern.svelte.js'
import {
createValidateOptions,
UnknownSvelteOptionsError,
} from './validate-options.js'

const { mount, unmount, updateProps, allowedOptions } =
ModernCore.IS_MODERN_SVELTE ? ModernCore : LegacyCore

/** Validate component options. */
const validateOptions = createValidateOptions(allowedOptions)

export {
mount,
UnknownSvelteOptionsError,
unmount,
updateProps,
validateOptions,
}
46 changes: 46 additions & 0 deletions src/core/legacy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Legacy rendering core for svelte-testing-library.
*
* Supports Svelte <= 4.
*/

/** Allowed options for the component constructor. */
const allowedOptions = [
'target',
'accessors',
'anchor',
'props',
'hydrate',
'intro',
'context',
]

/**
* Mount the component into the DOM.
*
* The `onDestroy` callback is included for strict backwards compatibility
* with previous versions of this library. It's mostly unnecessary logic.
*/
const mount = (Component, options, onDestroy) => {
const component = new Component(options)

if (typeof onDestroy === 'function') {
component.$$.on_destroy.push(() => {
onDestroy(component)
})
}

return component
}

/** Remove the component from the DOM. */
const unmount = (component) => {
component.$destroy()
}

/** Update the component's props. */
const updateProps = (component, nextProps) => {
component.$set(nextProps)
}

export { allowedOptions, mount, unmount, updateProps }
50 changes: 50 additions & 0 deletions src/core/modern.svelte.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Modern rendering core for svelte-testing-library.
*
* Supports Svelte >= 5.
*/
import * as Svelte from 'svelte'

/** Props signals for each rendered component. */
const propsByComponent = new Map()

/** Whether we're using Svelte >= 5. */
const IS_MODERN_SVELTE = typeof Svelte.mount === 'function'

/** Allowed options to the `mount` call. */
const allowedOptions = [
'target',
'anchor',
'props',
'events',
'context',
'intro',
]

/** Mount the component into the DOM. */
const mount = (Component, options) => {
const props = $state(options.props ?? {})
const component = Svelte.mount(Component, { ...options, props })

propsByComponent.set(component, props)

return component
}

/** Remove the component from the DOM. */
const unmount = (component) => {
propsByComponent.delete(component)
Svelte.unmount(component)
}

/**
* Update the component's props.
*
* Relies on the `$state` signal added in `mount`.
*/
const updateProps = (component, nextProps) => {
const prevProps = propsByComponent.get(component)
Object.assign(prevProps, nextProps)
}

export { allowedOptions, IS_MODERN_SVELTE, mount, unmount, updateProps }
Loading
Loading