kit
kit copied to clipboard
Shim SvelteKit runtime import aliases / Importing `$app/*` fails
Describe the bug
As far as I can tell there's no way to use sveltekit runtime imports (eg: $app/navigation) outside of sveltekit dev/build. Which makes testing virtually impossible. If there is a way to shim these imports outside of the main sveltekit context I haven't found it, so perhaps documentation is needed.
My particular use-case is with Storybook, where UI components that rely on any sveltekit modules break the whole setup. I tried aliasing them with webpack (pointing to .svelte-kit/dev/...) but that didn't work either.
Another use-case is publishing components for sveltekit that would need to rely on those imports.
To Reproduce
- Setup storybook with Sveltekit
- Create a component that imports a runtime module (eg:
$app/env) - Run storybook and see if fail (
cannot resolve module $app/env)
Severity Not blocking, but makes building a component library with Storybook or other development/testing frameworks impossible. So, severe annoyance?
This sounds loosely related to allowing SvelteKit to build components as suggested in https://github.com/sveltejs/kit/issues/518
Yep that would solve the second use case (distributing components/actions that rely on sveltekit), but I don’t think it would address the first? Testing etc components of a sveltekit app outside the main sveltekit dev/build runtime? Storybook being a very common use case
TBH I am hitting this issue trying to just do basic testing with uvu and typescript. I have ts files that import $app/env and uvu fails to resolve this using ts-node but I know you guys have set testing as a post 1.0. But any hacks or workarounds would be greatly appreciated. https://github.com/sveltejs/kit/issues/19
Are we able to use something like https://github.com/eirslett/storybook-builder-vite to help with this - or is this a problem outside of Vite?
@madeleineostoja for Storybook, in the .storybook/main.js/cjs, can you try using preprocess.replace like this for each of your aliases (https://github.com/sveltejs/svelte-preprocess#replace-values):
const path = require('path');
const sveltePreprocess = require('svelte-preprocess');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx|svelte)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-svelte-csf'],
svelteOptions: {
preprocess: [
sveltePreprocess({
replace: [['~', path.resolve('./src')]],
}),
],
},
};
Ah, I see the challenge - the modules like $app/env are the problem, not just aliases. My workaround won't help there.
Yeah this isn't to do with your own custom aliases, but the runtime modules of sveltekit. There needs to be a way to consume or shim them in non-sveltekit contexts like testing. Updated issue title to be clearer on the issue
I'd like to voice my support for a feature that would allow libraries to neatly get access to the $app/navigation, $app/stores, and $app/env modules.
As the creator and maintainer of SvelteKitAuth we're trying to provide a way for users to augment the SvelteKit session with authentication data using the getSession() hook, and upon changes in the session it would be nice to reset it internally instead of expecting users to do something like signOut().then(session.set), and a similar story for routing. Since signIn() either generates a redirect URL or sends a direct fetch() request depending on the payload provided, currently we're returning the URL and expect users to route themselves with goto() as such:
signIn().then(goto);
Letting libraries handle these things internally means less boilerplate for our users, so getting access to the SvelteKit router in a global module, instead of a scoped one would be useful. This is how other frameworks and libraries such as the Vue Router and React Router handle this, as they make use of the global React instance SvelteKit might have to work around the fact that Svelte doesn't provide such a thing, but create its own global context or a singleton.
I've fixed this in storybook by using their new vite-builder and adding manual aliases to sveltekit's $app runtime module
async viteFinal(config) {
config.resolve.alias = {
$app: path.resolve('./.svelte-kit/dev/runtime/app')
}
return config;
}
With the caveat being that you have to have run sveltekit dev first to generate those runtime modules. I think this is worth documenting (the path to the alias if nothing else) for others that need to shim these modules until Svelte comes up with an official workaround
Here's an example of someone having to mock this stuff out for testing: https://github.com/rossyman/svelte-add-jest/issues/14#issuecomment-891387235
Author of comment of the said person who had to mock this stuff. I don't mind mocking - fine as a workaround for unit testing anyway. It wasn't exactly simple to figure out the correct mocks though.
It looks like import.meta.env also might need to be mocked: https://github.com/sveltejs/kit/pull/2210#discussion_r691627049
Here's the test mock we've been using to handle $app/stores. It doesn't handle things like import.meta.env.
<script>
import {setContext} from 'svelte';
import {writable} from 'svelte/store';
export let Component;
export let stores = {
page: writable('/'),
navigating: writable(null),
session: writable(null),
};
setContext('__svelte__', stores);
</script>
<svelte:component this={Component} {...$$restProps} />
So you just pass it the actual component to be tested and (optionally) the stores you want to use for $app/stores.
EDIT: Added spreading of other props onto the component to be tested.
I think there's a couple ways to do test setup thus far:
- There's the
TestHarness.svelteoption mentioned above (https://github.com/sveltejs/kit/issues/1485#issuecomment-902965385) along withbabel-plugin-transform-vite-meta-env - Or do mocking. For ESM, Jest has a brand new, experimental, and undocumented
unstable_mockModule
I'm not sure what the tradeoffs are and which is the better approach.
Also, if there's anything SvelteKit can do to make testing easier I'd be happy to support changes there.
Related, there's a request to mock fetch (https://github.com/sveltejs/kit/issues/19#issuecomment-914178415), which I haven't seen anyone do yet.
As the one who made that request to mock fetch, I thought I'd give an update on what I've found so far:
- When testing components with
svelte-jester(via https://github.com/rossyman/svelte-add-jest), I'm outside the Svelte-Kit environment, soload()functions are never run, and I can just pass props to the component to provide it with test data. This reduces the need to provide a mockfetchin unit testing scenarios. - When doing E2E tests with Playwright, I can use Playwright's
page.routefeature to intercept browser requests to specific URLs and return my test data instead of what that URL would have returned (and the URL is never hit). This works only in the browser that Playwright is driving, and does not work server-side. But I can addkit: { ssr: !(process.env.NODE_ENV === 'test') }to my svelte.config.js to turn off server-side rendering, so thatload()is only ever run on the client where Playwright can intercept it. - If I want to run E2E tests in an environment as close to production as possible, though, I'd want to run them with server-side rendering turned on. I could, in theory, use
externalFetchto rewrite URLs from loaded pages to return test data instead. But that doesn't help me with internal URLs like/api/blogpost/3, whereexternalFetchis not called.
So without the ability to mock fetch, I can run almost all the test scenarios I need. I can unit-test my components, and I can run E2E tests with SSR turned off and intercept the fetch requests in the E2E browser. The only thing I can't do is run E2E tests with server-side rendering turned on. For that scenario, I believe I would need to be able to mock the fetch function passed into load().
@rmunn
so load() functions are never run
In at least pre-esm jest@26 and svelte-jester@1, you can mock $app/env.js to set browser to true or false, which will cause the svelte component to server side render or client side render - the load function is called when ssr'd.
Theoretically this should now also be possible in ESM jest@27 and svelte-jester@2, via the new unstable_mockModule mocking method, but I have not tried that yet.
jest.mock('$app/env.js', () => ({
amp: false,
browser: false,
dev: true,
mode: 'test'
}))
In my tests directory, I find it simplest to have two test files index-client.js and index-server.js, with the browser set to true and false, respectively.

Here's an image of a coverage report showing lines of the load function being hit:

Maybe I'm missing something about mocking fetch, but you should be able to just mock fetch in the same way. jest.mock('fetch', ...)?
In the same code this is pulled from, my load function is loading graphql over HTTP, however I am using a graphql library for that, not fetch directly, and thus, just mock the graphql library to return the responses I would get in good, bad, and other important cases. Similarly, if you were using axios instead of fetch, you could mock the response of the .get or whatever. You don't want to rely on networks in unit tests as that is outside of the scope of the unit. Things like that are best left for integration tests.
Other sveltekit mocking tips:
import.meta.env
- Put all import.meta.env usage in one file, I have been naming that file
lib/env.js, so I can reference it via$lib/env.js. This way in the majority of situations you can just mock that one file to set the envs you want to use for the test context:
lib/env.js
export const VITE_HASURA_GRAPHQL_URL = import.meta.env.VITE_HASURA_GRAPHQL_URL
export const VITE_HASURA_GRAPHQL_WS_URL = import.meta.env.VITE_HASURA_GRAPHQL_WS_URL
test.js
// ENV mocks
jest.mock('$lib/env', () => ({
VITE_HASURA_GRAPHQL_URL:
'http://fakeendpoint.example.com/v1/graphql',
VITE_HASURA_GRAPHQL_WS_URL:
'ws://fakeendpoint.example.com/v1/graphql'
}))
$app/navigation.js
jest.mock('$app/navigation.js', () => ({
goto: jest.fn()
}))
@patrickleet wrote:
Here's an image of a coverage report showing lines of the load function being hit:
What's the setup you're using for those tests? I've looked at https://github.com/CloudNativeEntrepreneur/sveltekit-eventsourced-funnel, but there you're using render() from @testing-library/svelte, which is the same thing that I'm using that doesn't call load(). (That is, it doesn't call the load() function from the module context the way Svelte-Kit does; it's just using Svelte to compile the component). I haven't yet found an example of running Jest tests in a Svelte-Kit context. Your coverage report shows that that's what you're doing, so I'd be interested to see how you've set that up. Would you be able to share your Jest config and/or a couple of your unit tests that are running Svelte-Kit's load() function, so I have an example of how to do that?
@rmunn
This is from a different repo for a client, so it's private, unfortunately - it's still on jest@26 / svelte-jester@1, and the "funnel" example was more of an example of event sourcing with svelte than a unit test demonstration - just happened to be public and relevant to some of the recent testing changes.
That said - let me see about pulling out relevant pieces...
Ok - looked ... Looks like I just exported/imported load in the server test.
companies/index-server.js
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/svelte'
import companiesIndex, { load } from '$routes/companies/index.svelte'
import debug from 'debug'
const log = debug('tests')
log('starting suite routes/companies/index.svelte')
// Sveltekit Mocks
jest.mock('$app/env.js', () => ({
amp: false,
browser: false,
dev: true,
mode: 'test'
}))
jest.mock('$app/navigation.js', () => ({
goto: jest.fn()
}))
// In more recent tests I've started using the "TestHarness" instead of this `svelte` mock with a fake getContext
jest.mock('svelte', () => {
const { writable } = require('svelte/store')
const actualSvelte = jest.requireActual('svelte')
const fakeGetContext = jest.fn((name) => {
if (name === '__svelte__') {
return fakeSvelteKitContext
}
})
const fakeSvelteKitContext = {
page: writable({
path: '/',
query: new URLSearchParams({
offset: 0,
limit: 5
})
}),
navigating: writable(false)
}
const mockedSvelteKit = {
...actualSvelte,
getContext: fakeGetContext
}
return mockedSvelteKit
})
// End Sveltekit mocks
// ENV mocks
jest.mock('$lib/env', () => ({
VITE_PRESIDIO_HASURA_GRAPHQL_URL:
'http://fakeendpoint.example.com/v1/graphql',
VITE_PRESIDIO_HASURA_GRAPHQL_INTERNAL_URL:
'http://fakeendpoint.example.com/v1/graphql',
VITE_PRESIDIO_HASURA_GRAPHQL_WS_URL:
'ws://fakeendpoint.example.com/v1/graphql'
}))
// Network mocks
jest.mock('$lib/data/urql', () => ({
client: {
query: jest.fn(() => {
const result = {
data: {
companies: [
{
id: 'test-1',
name: 'Test 1',
logo_url:
'https://res.cloudinary.com/crunchbase-production/image/upload/v1418896144/nzn3gfio6p8lupehf6nv.jpg',
__typename: 'companies'
},
{
id: 'test-2',
name: 'Test 2',
logo_url:
'https://res.cloudinary.com/crunchbase-production/image/upload/v1397199104/34852c1debc24e028c4082caa0efb427.jpg',
__typename: 'companies'
},
{
id: 'test-3',
name: 'Test 3',
logo_url:
'https://res.cloudinary.com/crunchbase-production/image/upload/rswshbdwsa7bg39kjtjk',
__typename: 'companies'
}
],
companies_aggregate: {
aggregate: {
count: 3
}
}
}
}
return {
toPromise: jest.fn(() => Promise.resolve(result))
}
})
}
}))
// mock store that uses URL query string
jest.mock('$lib/stores/queryStore')
const ctx = {
page: {
query: {
get: jest.fn((key) => {
const params = {
limit: 50,
isClientFilter: true,
order: 'asc',
offset: 0
}
return params[key]
})
}
}
}
describe('routes/companiesIndex.svelte - server', () => {
// browser false is default mock
describe('server side rendering', () => {
it('should server render empty', async () => {
const { getByText } = render(companiesIndex)
expect(getByText('Companies')).toBeInTheDocument()
expect(getByText('No companies')).toBeInTheDocument()
})
it('should server render empty with data but empty companies result', async () => {
const props = await load(ctx)
props.companies = []
const { getByText } = render(companiesIndex, { props })
expect(getByText('Companies')).toBeInTheDocument()
expect(getByText('No companies')).toBeInTheDocument()
})
it('should server render with data', async () => {
const { getByText } = render(companiesIndex, await load(ctx))
expect(getByText('Companies')).toBeInTheDocument()
expect(getByText('Test 1')).toBeInTheDocument()
})
})
describe('#load', () => {
it('should query graphql endpoint and return found companies', async () => {
const { client } = require('$lib/data/urql')
let result = await load(ctx)
expect(client.query).toBeCalled()
expect(result.props.companies.length).toBe(3)
})
})
})
And the load function from that component:
export async function load({ page }) {
const variables = {
limit: parseInt(page.query.get('limit'), 10) || defaults.limit,
isClientFilter: page.query.get('isClientFilter') !== 'false',
nameFilter: `%${page.query.get('nameFilter') || ''}%`,
order: page.query.get('order') || 'asc',
offset: parseInt(page.query.get('offset'), 10) || defaults.offset
}
const result = await client.query(QUERY, variables).toPromise()
const { data } = result
const { companies } = data
return {
props: {
companies,
count: data.companies_aggregate.aggregate.count
}
}
}
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
jest.json
{
"roots": ["<rootDir>/src", "<rootDir>/__tests__/unit"],
"testEnvironment": "node",
"modulePaths": ["<rootDir>/src"],
"moduleDirectories": ["node_modules"],
"transform": {
"^.+\\.svelte$": "svelte-jester",
"^.+\\.(ts|tsx|js|jsx)$": ["esbuild-jest"]
},
"moduleFileExtensions": ["js", "svelte"],
"moduleNameMapper": {
"^\\$app(.*)$": "<rootDir>/.svelte-kit/build/runtime/app$1",
"^\\$lib(.*)$": "<rootDir>/src/lib$1",
"^\\$routes(.*)$": "<rootDir>/src/routes$1"
},
"setupFilesAfterEnv": ["@testing-library/jest-dom/extend-expect"],
"coverageThreshold": {
"global": {
"branches": 0,
"functions": 0,
"lines": 0,
"statements": 0
}
},
"collectCoverageFrom": ["src/**/*.{js,svelte}"],
"testTimeout": 30000
}
If you don't mind using experimental features of the latest Node.js, along with esmodules/import/export, you can use an esmodule loader hook to mock SvelteKit's $app/navigation-style import aliases.
Rough beginner example link follows. One could mock the module into a no-op, or rewrite it to .svelte-kit/dev/runtime/app for real functionality (after svelte-kit dev has run):
- https://www.npmjs.com/package/create-esm-loader#2-create-directory-aliases
I've built a loader for .svelte files, which can also do pre-processing, and no-op imports of .css, pre-processed assets, and now SvelteKit's import { goto } from '$app/navigation':
- https://github.com/brev/esm-loader-svelte
This allows me to simply test my .svelte components in node/es6/esm with uvu, like @alexkornitzer but without typescript.
- Context: https://github.com/lukeed/uvu/issues/122
These loader hooks do not chain well yet, so it's far from a perfect solution, in general.
EDIT: Big update in my next comment below!
As an update to my previous post, I've made a lot of new progress, details are here:
https://github.com/sveltejs/kit/issues/19#issuecomment-1041134457
Now supporting:
- Handling
import.meta.env, etc. - Handling aliases like
$liband$app/*. - Mocking in general (
fetch, etc.) Also, mocking of Stores before component rendering to provide context to components tested in isolation.
Copy-pastable repro here: https://github.com/sveltejs/kit/issues/4432
https://github.com/michaelwooley/storybook-experimental-vite demonstrates setting up Storybook with the new vite.config.js. We still need to figure out how to handle $navigation and $stores
There's a great example of Vitest mocks here: https://github.com/sveltejs/kit/issues/5525#issuecomment-1186390654
I'm facing similar issue in a sveltekit app. The error message is Uncaught TypeError: Cannot read properties of undefined (reading 'disable_scroll_handling') at navigation-15d5f540.js: which causes 500 status error. I've read through issue https://github.com/sveltejs/kit/issues/4432 as well and it doesn't seem applicable. The issue happens randomly but frequently in production mode and in low speed networks.
Any idea?
Got this issue as well when I use afterNavigate in a +page while using the adapter static and chunking strategy of vite (splitVendorChunkPlugin).
Sample repo btw. https://github.com/peterpeterparker/my-app-yolo
@peterpeterparker your issue sounds different than what this one is talking about. Can you file a new issue for it?
Sure. I just commented here because #4432 was closed and linked with the issue.
So here you go 👉 https://github.com/sveltejs/kit/issues/7415
The introduction of $env has made the surface of this issue widen from just $app/*. Without the ability to shim these things unit/component testing becomes increasingly difficult (ref: https://github.com/microsoft/playwright/issues/18465) and, in practice, leads to awkward workarounds in order to properly support testing environments such as Playwright component testing.