diff --git a/code/e2e-tests/framework-svelte.spec.ts b/code/e2e-tests/framework-svelte.spec.ts index 2b6e447dd232..ceffacc525d7 100644 --- a/code/e2e-tests/framework-svelte.spec.ts +++ b/code/e2e-tests/framework-svelte.spec.ts @@ -1,12 +1,16 @@ /* 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 @@ -14,11 +18,6 @@ test.describe('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); @@ -55,10 +54,41 @@ test.describe('Svelte', () => { const sbPage = new SbPage(page); const lines: string[] = []; page.on('console', (msg) => { - lines.push(msg.text()); + 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(); + }); +}); diff --git a/code/frameworks/sveltekit/README.md b/code/frameworks/sveltekit/README.md index fd103c8764b1..d9242efcdbf1 100644 --- a/code/frameworks/sveltekit/README.md +++ b/code/frameworks/sveltekit/README.md @@ -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) @@ -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. | @@ -100,6 +102,77 @@ yarn remove storybook-builder-vite yarn remove @storybook/builder-vite ``` +## How to mock + +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 `` 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 `` 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 @@ -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. diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index eb9d0a5c035d..a6550677ab25 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -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" @@ -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:*" @@ -72,6 +76,7 @@ "bundler": { "entries": [ "./src/index.ts", + "./src/preview.ts", "./src/preset.ts" ], "platform": "node" diff --git a/code/frameworks/sveltekit/src/mocks/app/forms.ts b/code/frameworks/sveltekit/src/mocks/app/forms.ts new file mode 100644 index 000000000000..d1b26867c7d6 --- /dev/null +++ b/code/frameworks/sveltekit/src/mocks/app/forms.ts @@ -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() {} diff --git a/code/frameworks/sveltekit/src/mocks/app/navigation.ts b/code/frameworks/sveltekit/src/mocks/app/navigation.ts new file mode 100644 index 000000000000..8d23ddbea46a --- /dev/null +++ b/code/frameworks/sveltekit/src/mocks/app/navigation.ts @@ -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() {} diff --git a/code/frameworks/sveltekit/src/mocks/app/stores.ts b/code/frameworks/sveltekit/src/mocks/app/stores.ts new file mode 100644 index 000000000000..5f47acb1df6c --- /dev/null +++ b/code/frameworks/sveltekit/src/mocks/app/stores.ts @@ -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, + }; +} diff --git a/code/frameworks/sveltekit/src/plugins/config-overrides.ts b/code/frameworks/sveltekit/src/plugins/config-overrides.ts index db5294a13242..d132764d6e5c 100644 --- a/code/frameworks/sveltekit/src/plugins/config-overrides.ts +++ b/code/frameworks/sveltekit/src/plugins/config-overrides.ts @@ -1,4 +1,4 @@ -import type { Plugin } from 'vite'; +import { type Plugin } from 'vite'; export function configOverrides() { return { diff --git a/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts new file mode 100644 index 000000000000..873ce8bf3517 --- /dev/null +++ b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts @@ -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; +} diff --git a/code/frameworks/sveltekit/src/preset.ts b/code/frameworks/sveltekit/src/preset.ts index e92e45079f63..45cfe7d0a6dd 100644 --- a/code/frameworks/sveltekit/src/preset.ts +++ b/code/frameworks/sveltekit/src/preset.ts @@ -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 = (input: I): I => @@ -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 = async (config, options) => { const baseConfig = await svelteViteFinal(config, options); @@ -25,7 +30,9 @@ export const viteFinal: NonNullable = async (confi 'vite-plugin-sveltekit-compile', 'vite-plugin-sveltekit-guard', ]) - ).concat(configOverrides()); + ) + .concat(configOverrides()) + .concat(mockSveltekitStores()); return { ...baseConfig, plugins }; }; diff --git a/code/frameworks/sveltekit/src/preview.ts b/code/frameworks/sveltekit/src/preview.ts new file mode 100644 index 000000000000..a43431b5103d --- /dev/null +++ b/code/frameworks/sveltekit/src/preview.ts @@ -0,0 +1,124 @@ +import type { Decorator } from '@storybook/svelte'; +import { action } from '@storybook/addon-actions'; +import { onMount } from 'svelte'; +import { setAfterNavigateArgument } from './mocks/app/navigation'; +import { setNavigating, setPage, setUpdated } from './mocks/app/stores'; +import type { HrefConfig, NormalizedHrefConfig, SvelteKitParameters } from './types'; + +const normalizeHrefConfig = (hrefConfig: HrefConfig): NormalizedHrefConfig => { + if (typeof hrefConfig === 'function') { + return { callback: hrefConfig, asRegex: false }; + } + return hrefConfig; +}; + +export const decorators: Decorator[] = [ + (Story, ctx) => { + const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {}; + setPage(svelteKitParameters?.stores?.page); + setNavigating(svelteKitParameters?.stores?.navigating); + setUpdated(svelteKitParameters?.stores?.updated); + setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate); + + onMount(() => { + const globalClickListener = (e: MouseEvent) => { + // we add a global click event listener and we check if there's a link in the composedPath + const path = e.composedPath(); + const element = path.findLast((el) => el instanceof HTMLElement && el.tagName === 'A'); + if (element && element instanceof HTMLAnchorElement) { + // if the element is an a-tag we get the href of the element + // and compare it to the hrefs-parameter set by the user + const to = element.getAttribute('href'); + if (!to) { + return; + } + e.preventDefault(); + const defaultActionCallback = () => action('navigate')(to, e); + if (!svelteKitParameters.hrefs) { + defaultActionCallback(); + return; + } + + let callDefaultCallback = true; + // we loop over every href set by the user and check if the href matches + // if it does we call the callback provided by the user and disable the default callback + Object.entries(svelteKitParameters.hrefs).forEach(([href, hrefConfig]) => { + const { callback, asRegex } = normalizeHrefConfig(hrefConfig); + const isMatch = asRegex ? new RegExp(href).test(to) : to === href; + if (isMatch) { + callDefaultCallback = false; + callback?.(to, e); + } + }); + if (callDefaultCallback) { + defaultActionCallback(); + } + } + }; + + /** + * Function that create and add listeners for the event that are emitted by + * the mocked functions. The event name is based on the function name + * + * eg. storybook:goto, storybook:invalidateAll + * @param baseModule the base module where the function lives (navigation|forms) + * @param functions the list of functions in that module that emit events + * @returns a function to remove all the listener added + */ + function createListeners(baseModule: keyof SvelteKitParameters, functions: string[]) { + // the array of every added listener, we can use this in the return function + // to clean them + const toRemove: Array<{ + eventType: string; + listener: (event: { detail: any[] }) => void; + }> = []; + functions.forEach((func) => { + // we loop over every function and check if the user actually passed + // a function in sveltekit_experimental[baseModule][func] eg. sveltekit_experimental.navigation.goto + if ( + (svelteKitParameters as any)[baseModule]?.[func] && + (svelteKitParameters as any)[baseModule][func] instanceof Function + ) { + // we create the listener that will just get the detail array from the custom element + // and call the user provided function spreading this args in...this will basically call + // the function that the user provide with the same arguments the function is invoked to + + // eg. if it calls goto("/my-route") inside the component the function sveltekit_experimental.navigation.goto + // it provided to storybook will be called with "/my-route" + const listener = ({ detail = [] as any[] }) => { + const args = Array.isArray(detail) ? detail : []; + (svelteKitParameters as any)[baseModule][func](...args); + }; + const eventType = `storybook:${func}`; + toRemove.push({ eventType, listener }); + // add the listener to window + (window.addEventListener as any)(eventType, listener); + } + }); + return () => { + // loop over every listener added and remove them + toRemove.forEach(({ eventType, listener }) => { + // @ts-expect-error apparently you can't remove a custom listener to the window with TS + window.removeEventListener(eventType, listener); + }); + }; + } + + const removeNavigationListeners = createListeners('navigation', [ + 'goto', + 'invalidate', + 'invalidateAll', + ]); + const removeFormsListeners = createListeners('forms', ['enhance']); + window.addEventListener('click', globalClickListener); + + return () => { + window.removeEventListener('click', globalClickListener); + removeNavigationListeners(); + removeFormsListeners(); + }; + }); + + return Story(); + }, +]; diff --git a/code/frameworks/sveltekit/src/types.ts b/code/frameworks/sveltekit/src/types.ts index 647ea6fb5653..c3f04a22bc82 100644 --- a/code/frameworks/sveltekit/src/types.ts +++ b/code/frameworks/sveltekit/src/types.ts @@ -1,5 +1,7 @@ +import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; import type { StorybookConfig as StorybookConfigBase } from '@storybook/types'; -import type { StorybookConfigVite, BuilderOptions } from '@storybook/builder-vite'; +import type { enhance } from './mocks/app/forms'; +import type { goto, invalidate, invalidateAll } from './mocks/app/navigation'; type FrameworkName = '@storybook/sveltekit'; type BuilderName = '@storybook/builder-vite'; @@ -25,12 +27,34 @@ type StorybookConfigFramework = { }; }; -/** - * The interface for Storybook configuration in `main.ts` files. - */ export type StorybookConfig = Omit< StorybookConfigBase, keyof StorybookConfigVite | keyof StorybookConfigFramework > & StorybookConfigVite & StorybookConfigFramework; + +export type NormalizedHrefConfig = { + callback: (to: string, event: Event) => void; + asRegex?: boolean; +}; + +export type HrefConfig = NormalizedHrefConfig | NormalizedHrefConfig['callback']; + +export type SvelteKitParameters = Partial<{ + hrefs: Record; + stores: { + page: Record; + navigating: Record; + updated: boolean; + }; + navigation: { + goto: typeof goto; + invalidate: typeof invalidate; + invalidateAll: typeof invalidateAll; + afterNavigate: Record; + }; + forms: { + enhance: typeof enhance; + }; +}>; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Forms.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Forms.svelte new file mode 100644 index 000000000000..371a17656bea --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Forms.svelte @@ -0,0 +1,7 @@ + + +
+ +
\ No newline at end of file diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Hrefs.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Hrefs.svelte new file mode 100644 index 000000000000..4e7d69e0e051 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Hrefs.svelte @@ -0,0 +1,8 @@ +
diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Navigation.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Navigation.svelte new file mode 100644 index 000000000000..f857ae36a843 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Navigation.svelte @@ -0,0 +1,26 @@ + + + + + + + diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Stores.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Stores.svelte new file mode 100644 index 000000000000..164b00f7fa8b --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/Stores.svelte @@ -0,0 +1,17 @@ + + +

Directly importing

+
{JSON.stringify($page, null, 2)}
+
{JSON.stringify($navigating, null, 2)}
+
{JSON.stringify($updated, null, 2)}
+ +

With getStores

+
{JSON.stringify($pageStore, null, 2)}
+
{JSON.stringify($navigatingStore, null, 2)}
+
{JSON.stringify($updatedStore, null, 2)}
diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/forms.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/forms.stories.js new file mode 100644 index 000000000000..72b584baef76 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/forms.stories.js @@ -0,0 +1,26 @@ +import { expect, fn, within } from '@storybook/test'; +import Forms from './Forms.svelte'; + +export default { + title: 'stories/sveltekit/modules/forms', + component: Forms, + tags: ['autodocs'], +}; + +const enhance = fn(); + +export const Enhance = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('enhance'); + button.click(); + expect(enhance).toHaveBeenCalled(); + }, + parameters: { + sveltekit_experimental: { + forms: { + enhance, + }, + }, + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/hrefs.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/hrefs.stories.js new file mode 100644 index 000000000000..f1cbf4973534 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/hrefs.stories.js @@ -0,0 +1,53 @@ +import { expect, fn, within } from '@storybook/test'; +import Hrefs from './Hrefs.svelte'; + +export default { + title: 'stories/sveltekit/modules/hrefs', + component: Hrefs, + tags: ['autodocs'], +}; + +export const DefaultActions = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + // eslint-disable-next-line no-undef + const initialUrl = window.location.toString(); + + const basicHref = canvas.getByText('/basic-href'); + basicHref.click(); + + const complexHref = canvas.getByText( + '/deep/nested/link?with=true&multiple-params=200#and-an-id' + ); + complexHref.click(); + + // eslint-disable-next-line no-undef + const finalUrl = window.location.toString(); + expect(finalUrl).toBe(initialUrl); + }, +}; + +const basicStringMatch = fn(); +const noMatch = fn(); +const exactStringMatch = fn(); +const regexMatch = fn(); + +export const Callbacks = { + parameters: { + sveltekit_experimental: { + hrefs: { + '/basic-href': basicStringMatch, + '/basic': noMatch, + '/deep/nested/link?with=true&multiple-params=200#and-an-id': exactStringMatch, + 'nested/link\\?with': { callback: regexMatch, asRegex: true }, + }, + }, + }, + play: async (ctx) => { + await DefaultActions.play(ctx); + expect(basicStringMatch).toHaveBeenCalledTimes(1); + expect(noMatch).not.toHaveBeenCalled(); + expect(exactStringMatch).toHaveBeenCalledTimes(1); + expect(regexMatch).toHaveBeenCalledTimes(1); + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/navigation.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/navigation.stories.js new file mode 100644 index 000000000000..529997126f7c --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/navigation.stories.js @@ -0,0 +1,82 @@ +import { expect, fn, within } from '@storybook/test'; +import Navigation from './Navigation.svelte'; + +export default { + title: 'stories/sveltekit/modules/navigation', + component: Navigation, + tags: ['autodocs'], +}; + +const goto = fn(); + +export const Goto = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('goto'); + button.click(); + expect(goto).toHaveBeenCalledWith('/storybook'); + }, + parameters: { + sveltekit_experimental: { + navigation: { + goto, + }, + }, + }, +}; + +const invalidate = fn(); + +export const Invalidate = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('invalidate', { exact: true }); + button.click(); + expect(invalidate).toHaveBeenCalledWith('/storybook'); + }, + parameters: { + sveltekit_experimental: { + navigation: { + invalidate, + }, + }, + }, +}; + +const invalidateAll = fn(); + +export const InvalidateAll = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('invalidateAll'); + button.click(); + expect(invalidateAll).toHaveBeenCalledWith(); + }, + parameters: { + sveltekit_experimental: { + navigation: { + invalidateAll, + }, + }, + }, +}; + +const afterNavigateFn = fn(); + +export const AfterNavigate = { + async play() { + expect(afterNavigateFn).toHaveBeenCalledWith({ test: 'passed' }); + }, + args: { + afterNavigateFn, + }, + parameters: { + sveltekit_experimental: { + navigation: { + afterNavigate: { + test: 'passed', + }, + }, + }, + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/stores.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/stores.stories.js new file mode 100644 index 000000000000..7f7401cf8bee --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-js/stores.stories.js @@ -0,0 +1,116 @@ +import Stores from './Stores.svelte'; + +export default { + title: 'stories/sveltekit/modules/stores', + component: Stores, + tags: ['autodocs'], +}; + +export const AllUndefined = {}; + +export const PageStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + }, + }, + }, +}; + +export const NavigatingStore = { + parameters: { + sveltekit_experimental: { + stores: { + navigating: { + route: { + id: '/storybook', + }, + }, + }, + }, + }, +}; + +export const UpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + updated: true, + }, + }, + }, +}; + +export const PageAndNavigatingStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + navigating: { + route: { + id: '/storybook', + }, + }, + }, + }, + }, +}; + +export const PageAndUpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + updated: true, + }, + }, + }, +}; + +export const NavigatingAndUpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + navigating: { + route: { + id: '/storybook', + }, + }, + updated: true, + }, + }, + }, +}; + +export const AllThreeStores = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + navigating: { + route: { + id: '/storybook', + }, + }, + updated: true, + }, + }, + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte new file mode 100644 index 000000000000..371a17656bea --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Forms.svelte @@ -0,0 +1,7 @@ + + +
+ +
\ No newline at end of file diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Hrefs.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Hrefs.svelte new file mode 100644 index 000000000000..4e7d69e0e051 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Hrefs.svelte @@ -0,0 +1,8 @@ + diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Navigation.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Navigation.svelte new file mode 100644 index 000000000000..d97b6fe8a2df --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Navigation.svelte @@ -0,0 +1,25 @@ + + + + + + + diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Stores.svelte b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Stores.svelte new file mode 100644 index 000000000000..164b00f7fa8b --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/Stores.svelte @@ -0,0 +1,17 @@ + + +

Directly importing

+
{JSON.stringify($page, null, 2)}
+
{JSON.stringify($navigating, null, 2)}
+
{JSON.stringify($updated, null, 2)}
+ +

With getStores

+
{JSON.stringify($pageStore, null, 2)}
+
{JSON.stringify($navigatingStore, null, 2)}
+
{JSON.stringify($updatedStore, null, 2)}
diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/forms.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/forms.stories.js new file mode 100644 index 000000000000..72b584baef76 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/forms.stories.js @@ -0,0 +1,26 @@ +import { expect, fn, within } from '@storybook/test'; +import Forms from './Forms.svelte'; + +export default { + title: 'stories/sveltekit/modules/forms', + component: Forms, + tags: ['autodocs'], +}; + +const enhance = fn(); + +export const Enhance = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('enhance'); + button.click(); + expect(enhance).toHaveBeenCalled(); + }, + parameters: { + sveltekit_experimental: { + forms: { + enhance, + }, + }, + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/hrefs.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/hrefs.stories.js new file mode 100644 index 000000000000..f1cbf4973534 --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/hrefs.stories.js @@ -0,0 +1,53 @@ +import { expect, fn, within } from '@storybook/test'; +import Hrefs from './Hrefs.svelte'; + +export default { + title: 'stories/sveltekit/modules/hrefs', + component: Hrefs, + tags: ['autodocs'], +}; + +export const DefaultActions = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + // eslint-disable-next-line no-undef + const initialUrl = window.location.toString(); + + const basicHref = canvas.getByText('/basic-href'); + basicHref.click(); + + const complexHref = canvas.getByText( + '/deep/nested/link?with=true&multiple-params=200#and-an-id' + ); + complexHref.click(); + + // eslint-disable-next-line no-undef + const finalUrl = window.location.toString(); + expect(finalUrl).toBe(initialUrl); + }, +}; + +const basicStringMatch = fn(); +const noMatch = fn(); +const exactStringMatch = fn(); +const regexMatch = fn(); + +export const Callbacks = { + parameters: { + sveltekit_experimental: { + hrefs: { + '/basic-href': basicStringMatch, + '/basic': noMatch, + '/deep/nested/link?with=true&multiple-params=200#and-an-id': exactStringMatch, + 'nested/link\\?with': { callback: regexMatch, asRegex: true }, + }, + }, + }, + play: async (ctx) => { + await DefaultActions.play(ctx); + expect(basicStringMatch).toHaveBeenCalledTimes(1); + expect(noMatch).not.toHaveBeenCalled(); + expect(exactStringMatch).toHaveBeenCalledTimes(1); + expect(regexMatch).toHaveBeenCalledTimes(1); + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/navigation.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/navigation.stories.js new file mode 100644 index 000000000000..529997126f7c --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/navigation.stories.js @@ -0,0 +1,82 @@ +import { expect, fn, within } from '@storybook/test'; +import Navigation from './Navigation.svelte'; + +export default { + title: 'stories/sveltekit/modules/navigation', + component: Navigation, + tags: ['autodocs'], +}; + +const goto = fn(); + +export const Goto = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('goto'); + button.click(); + expect(goto).toHaveBeenCalledWith('/storybook'); + }, + parameters: { + sveltekit_experimental: { + navigation: { + goto, + }, + }, + }, +}; + +const invalidate = fn(); + +export const Invalidate = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('invalidate', { exact: true }); + button.click(); + expect(invalidate).toHaveBeenCalledWith('/storybook'); + }, + parameters: { + sveltekit_experimental: { + navigation: { + invalidate, + }, + }, + }, +}; + +const invalidateAll = fn(); + +export const InvalidateAll = { + async play({ canvasElement }) { + const canvas = within(canvasElement); + const button = canvas.getByText('invalidateAll'); + button.click(); + expect(invalidateAll).toHaveBeenCalledWith(); + }, + parameters: { + sveltekit_experimental: { + navigation: { + invalidateAll, + }, + }, + }, +}; + +const afterNavigateFn = fn(); + +export const AfterNavigate = { + async play() { + expect(afterNavigateFn).toHaveBeenCalledWith({ test: 'passed' }); + }, + args: { + afterNavigateFn, + }, + parameters: { + sveltekit_experimental: { + navigation: { + afterNavigate: { + test: 'passed', + }, + }, + }, + }, +}; diff --git a/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/stores.stories.js b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/stores.stories.js new file mode 100644 index 000000000000..7f7401cf8bee --- /dev/null +++ b/code/frameworks/sveltekit/template/stories_svelte-kit-skeleton-ts/stores.stories.js @@ -0,0 +1,116 @@ +import Stores from './Stores.svelte'; + +export default { + title: 'stories/sveltekit/modules/stores', + component: Stores, + tags: ['autodocs'], +}; + +export const AllUndefined = {}; + +export const PageStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + }, + }, + }, +}; + +export const NavigatingStore = { + parameters: { + sveltekit_experimental: { + stores: { + navigating: { + route: { + id: '/storybook', + }, + }, + }, + }, + }, +}; + +export const UpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + updated: true, + }, + }, + }, +}; + +export const PageAndNavigatingStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + navigating: { + route: { + id: '/storybook', + }, + }, + }, + }, + }, +}; + +export const PageAndUpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + updated: true, + }, + }, + }, +}; + +export const NavigatingAndUpdatedStore = { + parameters: { + sveltekit_experimental: { + stores: { + navigating: { + route: { + id: '/storybook', + }, + }, + updated: true, + }, + }, + }, +}; + +export const AllThreeStores = { + parameters: { + sveltekit_experimental: { + stores: { + page: { + data: { + test: 'passed', + }, + }, + navigating: { + route: { + id: '/storybook', + }, + }, + updated: true, + }, + }, + }, +}; diff --git a/code/renderers/svelte/template/stories/decorators-runs-once.stories.js b/code/renderers/svelte/template/stories/decorators-runs-once.stories.js index 649d9f8a18bb..5186ccb14d88 100644 --- a/code/renderers/svelte/template/stories/decorators-runs-once.stories.js +++ b/code/renderers/svelte/template/stories/decorators-runs-once.stories.js @@ -7,7 +7,7 @@ export default { }, decorators: [ (Story) => { - console.log('decorator'); + console.log('decorator called'); return Story(); }, ], diff --git a/code/yarn.lock b/code/yarn.lock index d2928cb17c0e..760db61c9a4e 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -7659,6 +7659,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/sveltekit@workspace:frameworks/sveltekit" dependencies: + "@storybook/addon-actions": "workspace:*" "@storybook/builder-vite": "workspace:*" "@storybook/svelte": "workspace:*" "@storybook/svelte-vite": "workspace:*"