Skip to content

Commit

Permalink
feat(svelte5): add Svelte 5 support into main entry point
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous committed Jun 9, 2024
1 parent ee1c966 commit 70f6d81
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 234 deletions.
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
11 changes: 7 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'

// TODO(mcous, 2024-06-09): Svelte 5 support blocked by
// https://github.com/svelteness/svelte-jester/pull/283
const IS_SVELTE_5 = SVELTE_VERSION >= '5'
const SVELTE_LEGACY_TRANSFORM_PATTERN = '^.+\\.svelte$'
const SVELTE_MODERN_TRANSFORM_PATTERN = '^.+\\.svelte(?:\\.js)?$'

export default {
testMatch: ['<rootDir>/src/__tests__/**/*.test.js'],
transform: {
'^.+\\.svelte$': 'svelte-jester',
[IS_SVELTE_5
? SVELTE_MODERN_TRANSFORM_PATTERN
: SVELTE_LEGACY_TRANSFORM_PATTERN]: 'svelte-jester',
},
moduleFileExtensions: ['js', 'svelte'],
extensionsToTreatAsEsm: ['.svelte'],
Expand All @@ -14,9 +20,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
2 changes: 1 addition & 1 deletion 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
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)
46 changes: 46 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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'

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

/** Validate component options. */
const validateOptions = (options) => {
const isProps = !Object.keys(options).some((option) =>
allowedOptions.includes(option)
)

if (isProps) {
return { props: options }
}

// Check if any props and Svelte options were accidentally mixed.
const unknownOptions = Object.keys(options).filter(
(option) => !allowedOptions.includes(option)
)

if (unknownOptions.length > 0) {
throw TypeError(`Unknown options.
Unknown: [ ${unknownOptions.join(', ')} ]
Allowed: [ ${allowedOptions.join(', ')} ]
To pass both Svelte options and props to a component,
or to use props that share a name with a Svelte option,
you must place all your props under the \`props\` key:
render(Component, { props: { /** props here **/ } })
`)
}

return options
}

export { mount, 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

0 comments on commit 70f6d81

Please sign in to comment.