Skip to content
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

SvelteKit: Add experimental page and navigation mocking #24795

Merged
merged 33 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dca97a1
feat: tighter integration with sveltekit
paoloricciuti Nov 9, 2023
0a103b1
fix callbacks for goto, enhance, invalidate, invalidateAll
paoloricciuti Nov 10, 2023
a19c1e4
add tests
paoloricciuti Nov 10, 2023
8bcd99a
Merge branch 'next' into next
paoloricciuti Nov 10, 2023
a42fc78
add check field to update store and write README
paoloricciuti Nov 10, 2023
e184135
add components to files
paoloricciuti Nov 10, 2023
adc9704
add mocks files to files
paoloricciuti Nov 10, 2023
dddc1c9
remove logs + fix afternavigate tests
paoloricciuti Nov 10, 2023
1693051
Merge remote-tracking branch 'upstream/next' into next
paoloricciuti Nov 13, 2023
9cb6088
Merge branch 'next' into next
paoloricciuti Nov 13, 2023
ca7cc86
Merge branch 'next' into next
paoloricciuti Nov 18, 2023
445b8d4
move logic to preview.ts from SvelteDecorator
paoloricciuti Nov 20, 2023
a1ddf3c
Merge branch 'next' into next
paoloricciuti Nov 20, 2023
c686a26
fix CJS to ESM
paoloricciuti Nov 20, 2023
e48e387
Merge branch 'next' into next
paoloricciuti Nov 21, 2023
2db2a5c
Merge branch 'next' into next
JReinhold Nov 21, 2023
2d21b39
Update code/frameworks/sveltekit/README.md
paoloricciuti Nov 21, 2023
63b1b0e
Update code/frameworks/sveltekit/README.md
paoloricciuti Nov 21, 2023
230089a
fix PR comments
paoloricciuti Nov 21, 2023
b542502
Update code/frameworks/sveltekit/src/mocks/app/stores.ts
paoloricciuti Nov 21, 2023
9de928d
better regex handling of links + add experiemental
paoloricciuti Nov 21, 2023
f57a6b8
add actions as default behavior, cleanup decorator
JReinhold Nov 21, 2023
da77256
e2e test for default sveltekit actions
JReinhold Nov 21, 2023
e3def1d
Merge branch 'fix-svelte-renderer-firing-decorator-twice' of github.c…
JReinhold Nov 21, 2023
4784208
fix decorator e2e test in dev
JReinhold Nov 21, 2023
3b7ee70
Merge branch 'next' into next
paoloricciuti Nov 22, 2023
b274849
Merge branch 'next' into next
JReinhold Nov 22, 2023
967f685
make parameter types public
JReinhold Nov 22, 2023
9593add
fix types for SveltekitParameters
paoloricciuti Nov 22, 2023
9e1bad0
remove afterNavigate import
paoloricciuti Nov 22, 2023
70ceaaf
Merge branch 'next' into next
paoloricciuti Nov 22, 2023
d86a2a4
Merge branch 'next' into next
paoloricciuti Nov 22, 2023
62e7fbb
Merge branch 'next' of github.com:storybookjs/storybook into paoloric…
JReinhold Nov 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 47 additions & 6 deletions code/e2e-tests/framework-svelte.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
/* eslint-disable jest/no-disabled-tests */
import { test, expect } from '@playwright/test';
import process from 'process';
import dedent from 'ts-dedent';
import { SbPage } from './util';

const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006';
const templateName = process.env.STORYBOOK_TEMPLATE_NAME;

test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);
await new SbPage(page).waitUntilLoaded();
});

test.describe('Svelte', () => {
test.skip(
// eslint-disable-next-line jest/valid-title
!templateName?.includes('svelte'),
'Only run this test on Svelte'
);

test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);
await new SbPage(page).waitUntilLoaded();
});

test('JS story has auto-generated args table', async ({ page }) => {
const sbPage = new SbPage(page);

Expand Down Expand Up @@ -50,4 +49,46 @@ test.describe('Svelte', () => {
const expectedSource = '<ButtonJavaScript primary/>';
await expect(sourceCode.textContent()).resolves.toContain(expectedSource);
});

test('Decorators runs only once', async ({ page }) => {
const sbPage = new SbPage(page);
const lines: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (text === 'decorator called') {
lines.push(text);
}
});

await sbPage.navigateToStory('stories/renderers/svelte/decorators-runs-once', 'default');
expect(lines).toHaveLength(1);
});
});

test.describe('SvelteKit', () => {
test.skip(
// eslint-disable-next-line jest/valid-title
!templateName?.includes('svelte-kit'),
'Only run this test on SvelteKit'
);

test('Links are logged in Actions panel', async ({ page }) => {
const sbPage = new SbPage(page);

await sbPage.navigateToStory('stories/sveltekit/modules/hrefs', 'default-actions');
const root = sbPage.previewRoot();
const link = root.locator('a', { hasText: 'Link to /basic-href' });
await link.click();

await sbPage.viewAddonPanel('Actions');
const basicLogItem = await page.locator('#storybook-panel-root #panel-tab-content', {
hasText: `/basic-href`,
});

await expect(basicLogItem).toBeVisible();
const complexLogItem = await page.locator('#storybook-panel-root #panel-tab-content', {
hasText: `/deep/nested`,
});
await expect(complexLogItem).toBeVisible();
});
});
80 changes: 77 additions & 3 deletions code/frameworks/sveltekit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Check out our [Frameworks API](https://storybook.js.org/blog/framework-api/) ann
- [In a project with Storybook](#in-a-project-with-storybook)
- [Automatic migration](#automatic-migration)
- [Manual migration](#manual-migration)
- [How to mock](#how-to-mock)
- [Mocking links](#mocking-links)
- [Troubleshooting](#troubleshooting)
- [Error: `ERR! SyntaxError: Identifier '__esbuild_register_import_meta_url__' has already been declared` when starting Storybook](#error-err-syntaxerror-identifier-__esbuild_register_import_meta_url__-has-already-been-declared-when-starting-storybook)
- [Error: `Cannot read properties of undefined (reading 'disable_scroll_handling')` in preview](#error-cannot-read-properties-of-undefined-reading-disable_scroll_handling-in-preview)
Expand All @@ -26,10 +28,10 @@ However SvelteKit has some [Kit-specific modules](https://kit.svelte.dev/docs/mo
| **Module** | **Status** | **Note** |
| ---------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [`$app/environment`](https://kit.svelte.dev/docs/modules#$app-environment) | ✅ Supported | `version` is always empty in Storybook. |
| [`$app/forms`](https://kit.svelte.dev/docs/modules#$app-forms) | ⏳ Future | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) |
| [`$app/navigation`](https://kit.svelte.dev/docs/modules#$app-navigation) | ⏳ Future | Will use mocks. Tracked in [#20999](https://github.com/storybookjs/storybook/issues/20999) |
| [`$app/forms`](https://kit.svelte.dev/docs/modules#$app-forms) | ✅ Supported | See [How to mock](#how-to-mock) |
| [`$app/navigation`](https://kit.svelte.dev/docs/modules#$app-navigation) | ✅ Supported | See [How to mock](#how-to-mock) |
| [`$app/paths`](https://kit.svelte.dev/docs/modules#$app-paths) | ✅ Supported | Requires SvelteKit 1.4.0 or newer |
| [`$app/stores`](https://kit.svelte.dev/docs/modules#$app-stores) | ✅ Supported | Mocks planned, so you can set different store values per story. |
| [`$app/stores`](https://kit.svelte.dev/docs/modules#$app-stores) | ✅ Supported | See [How to mock](#how-to-mock) |
| [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private) | ⛔ Not supported | They are meant to only be available server-side, and Storybook renders all components on the client. |
| [`$env/dynamic/public`](https://kit.svelte.dev/docs/modules#$env-dynamic-public) | 🚧 Partially supported | Only supported in development mode. Storybook is built as a static app with no server-side API so cannot dynamically serve content. |
| [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private) | ⛔ Not supported | They are meant to only be available server-side, and Storybook renders all components on the client. |
Expand Down Expand Up @@ -100,6 +102,77 @@ yarn remove storybook-builder-vite
yarn remove @storybook/builder-vite
```

## How to mock
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved

To mock a SvelteKit import you can set it on `parameters.sveltekit_experimental`:

```ts
export const MyStory = {
parameters: {
sveltekit_experimental: {
stores: {
page: {
data: {
test: 'passed',
},
},
navigating: {
route: {
id: '/storybook',
},
},
updated: true,
},
},
},
};
```

You can add the name of the module you want to mock to `parameters.sveltekit_experimental` (in the example above we are mocking the `stores` module which correspond to `$app/stores`) and then pass the following kind of objects:

| Module | Path in parameters | Kind of objects |
| ------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `import { page } from "$app/stores"` | `parameters.sveltekit_experimental.stores.page` | A Partial of the page store |
| `import { navigating } from "$app/stores"` | `parameters.sveltekit_experimental.stores.navigating` | A Partial of the navigating store |
| `import { updated } from "$app/stores"` | `parameters.sveltekit_experimental.stores.updated` | A boolean representing the value of updated (you can also access `check()` which will be a noop) |
| `import { goto } from "$app/navigation"` | `parameters.sveltekit_experimental.navigation.goto` | A callback that will be called whenever goto is called |
| `import { invalidate } from "$app/navigation"` | `parameters.sveltekit_experimental.navigation.invalidate` | A callback that will be called whenever invalidate is called |
| `import { invalidateAll } from "$app/navigation"` | `parameters.sveltekit_experimental.navigation.invalidateAll` | A callback that will be called whenever invalidateAll is called |
| `import { afterNavigate } from "$app/navigation"` | `parameters.sveltekit_experimental.navigation.afterNavigate` | An object that will be passed to the afterNavigate function (which will be invoked onMount) called |
| `import { enhance } from "$app/forms"` | `parameters.sveltekit_experimental.forms.enhance` | A callback that will called when a form with `use:enhance` is submitted |

All the other functions are still exported as `noop` from the mocked modules so that your application will still work.

### Mocking links

The default link-handling behavior (ie. clicking an `<a />` tag with an `href` attribute) is to log an action to the Actions panel.

You can override this by setting an object on `parameter.sveltekit_experimental.hrefs`, where the keys are strings representing an href and the values are objects typed as `{ callback: (href, event) => void, asRegex?: boolean }`.

If you have an `<a />` tag inside your code with the `href` attribute that matches one or more of the links defined (treated as regex based on the `asRegex` property) the corresponding `callback` will be called.

Example:

```ts
export const MyStory = {
parameters: {
sveltekit_experimental: {
hrefs: {
'/basic-href': (to, event) => {
console.log(to, event);
},
'/root.*': {
callback: (to, event) => {
console.log(to, event);
},
asRegex: true,
},
},
},
},
};
```

## Troubleshooting

### Error: `ERR! SyntaxError: Identifier '__esbuild_register_import_meta_url__' has already been declared` when starting Storybook
Expand All @@ -125,3 +198,4 @@ You'll experience this if anything in your story is importing from `$app/forms`
## Acknowledgements

Integrating with SvelteKit would not have been possible if it weren't for the fantastic efforts by the Svelte core team - especially [Ben McCann](https://twitter.com/benjaminmccann) - to make integrations with the wider ecosystem possible.
A big thank you also goes out to [Paolo Ricciuti](https://twitter.com/PaoloRicciuti) for improving the mocking capabilities.
7 changes: 6 additions & 1 deletion code/frameworks/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./dist/preview.mjs": {
"import": "./dist/preview.mjs"
},
"./preset": {
"types": "./dist/preset.d.ts",
"require": "./dist/preset.js"
Expand All @@ -43,13 +46,14 @@
"README.md",
"*.js",
"*.d.ts",
"!src/**/*"
"src/mocks/**/*"
],
"scripts": {
"check": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/check.ts",
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@storybook/addon-actions": "workspace:*",
"@storybook/builder-vite": "workspace:*",
"@storybook/svelte": "workspace:*",
"@storybook/svelte-vite": "workspace:*"
Expand All @@ -72,6 +76,7 @@
"bundler": {
"entries": [
"./src/index.ts",
"./src/preview.ts",
"./src/preset.ts"
],
"platform": "node"
Expand Down
17 changes: 17 additions & 0 deletions code/frameworks/sveltekit/src/mocks/app/forms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function enhance(form: HTMLFormElement) {
const listener = (e: Event) => {
e.preventDefault();
const event = new CustomEvent('storybook:enhance');
window.dispatchEvent(event);
};
form.addEventListener('submit', listener);
return {
destroy() {
form.removeEventListener('submit', listener);
},
};
}

export function applyAction() {}

export function deserialize() {}
43 changes: 43 additions & 0 deletions code/frameworks/sveltekit/src/mocks/app/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getContext, onMount, setContext } from 'svelte';

export async function goto(...args: any[]) {
const event = new CustomEvent('storybook:goto', {
detail: args,
});
window.dispatchEvent(event);
}

export function setAfterNavigateArgument(afterNavigateArgs: any) {
setContext('after-navigate-args', afterNavigateArgs);
}

export function afterNavigate(cb: any) {
const argument = getContext('after-navigate-args');
onMount(() => {
if (cb && cb instanceof Function) {
cb(argument);
}
});
}

export function onNavigate() {}

export function beforeNavigate() {}

export function disableScrollHandling() {}

export async function invalidate(...args: any[]) {
const event = new CustomEvent('storybook:invalidate', {
detail: args,
});
window.dispatchEvent(event);
}

export async function invalidateAll() {
const event = new CustomEvent('storybook:invalidateAll');
window.dispatchEvent(event);
}

export function preloadCode() {}

export function preloadData() {}
32 changes: 32 additions & 0 deletions code/frameworks/sveltekit/src/mocks/app/stores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getContext, setContext } from 'svelte';

function createMockedStore(contextName: string) {
return [
{
subscribe(runner: any) {
const page = getContext(contextName);
runner(page);
return () => {};
},
},
(value: unknown) => {
setContext(contextName, value);
},
] as const;
}

export const [page, setPage] = createMockedStore('page-ctx');
export const [navigating, setNavigating] = createMockedStore('navigating-ctx');
const [updated, setUpdated] = createMockedStore('updated-ctx');

(updated as any).check = () => {};

export { updated, setUpdated };

export function getStores() {
return {
page,
navigating,
updated,
};
}
2 changes: 1 addition & 1 deletion code/frameworks/sveltekit/src/plugins/config-overrides.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Plugin } from 'vite';
import { type Plugin } from 'vite';

export function configOverrides() {
return {
Expand Down
17 changes: 17 additions & 0 deletions code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { resolve } from 'node:path';
import { mergeConfig, type Plugin } from 'vite';

export function mockSveltekitStores() {
return {
name: 'storybook:sveltekit-mock-stores',
enforce: 'post',
config: (config) =>
mergeConfig(config, {
resolve: {
alias: {
$app: resolve(__dirname, '../src/mocks/app/'),
},
},
}),
} satisfies Plugin;
}
9 changes: 8 additions & 1 deletion code/frameworks/sveltekit/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PresetProperty } from '@storybook/types';
import { withoutVitePlugins } from '@storybook/builder-vite';
import { dirname, join } from 'path';
import { configOverrides } from './plugins/config-overrides';
import { mockSveltekitStores } from './plugins/mock-sveltekit-stores';
import { type StorybookConfig } from './types';

const getAbsolutePath = <I extends string>(input: I): I =>
Expand All @@ -13,6 +14,10 @@ export const core: PresetProperty<'core', StorybookConfig> = {
builder: getAbsolutePath('@storybook/builder-vite'),
renderer: getAbsolutePath('@storybook/svelte'),
};
export const previewAnnotations: StorybookConfig['previewAnnotations'] = (entry = []) => [
...entry,
join(dirname(require.resolve('@storybook/sveltekit/package.json')), 'dist/preview.mjs'),
];

export const viteFinal: NonNullable<StorybookConfig['viteFinal']> = async (config, options) => {
const baseConfig = await svelteViteFinal(config, options);
Expand All @@ -25,7 +30,9 @@ export const viteFinal: NonNullable<StorybookConfig['viteFinal']> = async (confi
'vite-plugin-sveltekit-compile',
'vite-plugin-sveltekit-guard',
])
).concat(configOverrides());
)
.concat(configOverrides())
.concat(mockSveltekitStores());

return { ...baseConfig, plugins };
};