Skip to content

Commit 6b9ff97

Browse files
authored
Merge pull request #31369 from xeho91/feat/support-mocking-sveltekit-app-state
SvelteKit: Add support for mocking `$app/state`
2 parents 68140f7 + e873a2f commit 6b9ff97

File tree

12 files changed

+455
-17
lines changed

12 files changed

+455
-17
lines changed

code/frameworks/sveltekit/build-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const config: BuildEntries = {
4646
],
4747
},
4848
extraOutputs: {
49+
'./internal/mocks/app/state.svelte.js': './static/app-state-mock.svelte.js',
4950
'./internal/MockProvider.svelte': './static/MockProvider.svelte',
5051
},
5152
};

code/frameworks/sveltekit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"./internal/MockProvider.svelte": "./static/MockProvider.svelte",
3636
"./internal/mocks/app/forms": "./dist/mocks/app/forms.js",
3737
"./internal/mocks/app/navigation": "./dist/mocks/app/navigation.js",
38+
"./internal/mocks/app/state.svelte.js": "./static/app-state-mock.svelte.js",
3839
"./internal/mocks/app/stores": "./dist/mocks/app/stores.js",
3940
"./node": {
4041
"types": "./dist/node/index.d.ts",

code/frameworks/sveltekit/src/mocks/app/stores.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ function createMockedStore(contextName: string) {
1515
] as const;
1616
}
1717

18-
export const [page, setPage] = createMockedStore('page-ctx');
19-
export const [navigating, setNavigating] = createMockedStore('navigating-ctx');
20-
const [updated, setUpdated] = createMockedStore('updated-ctx');
18+
export const [page, setAppStoresPage] = createMockedStore('page-ctx');
19+
export const [navigating, setAppStoresNavigating] = createMockedStore('navigating-ctx');
20+
const [updated, setAppStoresUpdated] = createMockedStore('updated-ctx');
2121

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

24-
export { updated, setUpdated };
24+
export { updated, setAppStoresUpdated };
2525

2626
export function getStores() {
2727
return {

code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function mockSveltekitStores() {
88
alias: {
99
'$app/forms': '@storybook/sveltekit/internal/mocks/app/forms',
1010
'$app/navigation': '@storybook/sveltekit/internal/mocks/app/navigation',
11+
'$app/state': '@storybook/sveltekit/internal/mocks/app/state.svelte.js',
1112
'$app/stores': '@storybook/sveltekit/internal/mocks/app/stores',
1213
},
1314
},

code/frameworks/sveltekit/src/preset.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export const optimizeViteDeps = [
3939
'@storybook/sveltekit/internal/mocks/app/forms',
4040
'@storybook/sveltekit/internal/mocks/app/navigation',
4141
'@storybook/sveltekit/internal/mocks/app/stores',
42+
'@storybook/sveltekit/internal/mocks/app/state.svelte.js',
4243
];

code/frameworks/sveltekit/src/preview.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import type { Decorator } from '@storybook/svelte';
1+
import type { Decorator, Preview } from '@storybook/svelte';
22
import MockProvider from '@storybook/sveltekit/internal/MockProvider.svelte';
3+
import {
4+
setAppStateNavigating,
5+
setAppStatePage,
6+
setAppStateUpdated, // @ts-expect-error no declaration file for this JS module
7+
} from '@storybook/sveltekit/internal/mocks/app/state.svelte.js';
38

49
import type { SvelteKitParameters } from './types';
510

@@ -15,3 +20,11 @@ const svelteKitMocksDecorator: Decorator = (Story, ctx) => {
1520
};
1621

1722
export const decorators: Decorator[] = [svelteKitMocksDecorator];
23+
24+
export const beforeEach: Preview['beforeEach'] = async (ctx) => {
25+
const svelteKitParameters: SvelteKitParameters = ctx.parameters?.sveltekit_experimental ?? {};
26+
27+
setAppStatePage(svelteKitParameters?.state?.page);
28+
setAppStateNavigating(svelteKitParameters?.state?.navigating);
29+
setAppStateUpdated(svelteKitParameters?.state?.updated);
30+
};

code/frameworks/sveltekit/src/types.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,68 @@ export type NormalizedHrefConfig = {
5454

5555
export type HrefConfig = NormalizedHrefConfig | NormalizedHrefConfig['callback'];
5656

57+
/**
58+
* Copied from:
59+
* {@link https://github.com/sveltejs/kit/blob/7bb41aa4263b057a8912f4cdd35db03755d37342/packages/kit/types/index.d.ts#L1102-L1143}
60+
*/
61+
interface Page<
62+
Params extends Record<string, string> = Record<string, string>,
63+
RouteId extends string | null = string | null,
64+
> {
65+
url: URL;
66+
params: Params;
67+
route: {
68+
id: RouteId;
69+
};
70+
status: number;
71+
error: Error | null;
72+
data: Record<string, any>;
73+
state: Record<string, any>;
74+
form: any;
75+
}
76+
77+
/**
78+
* Copied from:
79+
* {@link https://github.com/sveltejs/kit/blob/7bb41aa4263b057a8912f4cdd35db03755d37342/packages/kit/types/index.d.ts#L988}
80+
*/
81+
interface NavigationTarget {
82+
params: Record<string, string> | null;
83+
route: {
84+
id: string | null;
85+
};
86+
url: URL;
87+
}
88+
89+
/**
90+
* Copied from:
91+
* {@link https://github.com/sveltejs/kit/blob/7bb41aa4263b057a8912f4cdd35db03755d37342/packages/kit/types/index.d.ts#L1017C9-L1017C89}
92+
*/
93+
type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate';
94+
95+
/**
96+
* Copied from:
97+
* {@link https://github.com/sveltejs/kit/blob/7bb41aa4263b057a8912f4cdd35db03755d37342/packages/kit/types/index.d.ts#L1017C9-L1017C89}
98+
*/
99+
interface Navigation {
100+
from: NavigationTarget | null;
101+
to: NavigationTarget | null;
102+
type: Exclude<NavigationType, 'enter'>;
103+
willUnload: boolean;
104+
delta?: number;
105+
complete: Promise<void>;
106+
}
107+
57108
export type SvelteKitParameters = Partial<{
58109
hrefs: Record<string, HrefConfig>;
110+
state: {
111+
page: Partial<Page>;
112+
navigating: Partial<Navigation>;
113+
updated: { current: boolean };
114+
};
115+
/**
116+
* @deprecated
117+
* @see {@link https://svelte.dev/docs/kit/$app-stores}
118+
*/
59119
stores: {
60120
page: Record<string, any>;
61121
navigating: Record<string, any>;

code/frameworks/sveltekit/static/MockProvider.svelte

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
import { action } from 'storybook/actions';
44
55
import { setAfterNavigateArgument } from '@storybook/sveltekit/internal/mocks/app/navigation';
6-
import { setNavigating, setPage, setUpdated } from '@storybook/sveltekit/internal/mocks/app/stores';
6+
import { setAppStoresNavigating, setAppStoresPage, setAppStoresUpdated } from '@storybook/sveltekit/internal/mocks/app/stores';
77
8-
9-
const{ svelteKitParameters = {}, children } = $props();
10-
8+
const { svelteKitParameters = {}, children } = $props();
119
1210
// Set context during component initialization - this happens before any child components
13-
setPage(svelteKitParameters?.stores?.page);
14-
setNavigating(svelteKitParameters?.stores?.navigating);
15-
setUpdated(svelteKitParameters?.stores?.updated);
11+
setAppStoresPage(svelteKitParameters?.stores?.page);
12+
setAppStoresNavigating(svelteKitParameters?.stores?.navigating);
13+
setAppStoresUpdated(svelteKitParameters?.stores?.updated);
1614
setAfterNavigateArgument(svelteKitParameters?.navigation?.afterNavigate);
1715
1816
const normalizeHrefConfig = (hrefConfig) => {
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* Inspired by the the code:
3+
* {@link https://github.com/sveltejs/kit/blob/main/packages/kit/src/runtime/client/state.svelte.js}
4+
*
5+
* The differences:
6+
*
7+
* - Legacy Svelte support is not included
8+
* - Not using classes (internal coding style preference)
9+
*
10+
* @module
11+
*/
12+
import { fn } from 'storybook/test';
13+
14+
/**
15+
* @typedef {Object} App
16+
* @property {Object} Error
17+
* @property {string} Error.message
18+
* @property {Object} Locals
19+
* @property {Object} PageData
20+
* @property {Object} PageState
21+
* @property {Object} Platform
22+
*/
23+
24+
/**
25+
* @typedef {Object} Page
26+
* @property {URL} url
27+
* @property {Record<string, string>} params
28+
* @property {Object} route
29+
* @property {string | null} route.id
30+
* @property {number} status
31+
* @property {App.Error | null} error
32+
* @property {App.PageData & Record<string, any>} data
33+
* @property {App.PageState} state
34+
* @property {any} form
35+
*/
36+
37+
const defaultStatePageValues = {
38+
data: {},
39+
form: null,
40+
error: null,
41+
params: {},
42+
route: { id: null },
43+
state: {},
44+
status: -1,
45+
url: new URL(location.origin),
46+
};
47+
48+
/** @type {Page['data']} */
49+
let pageData = $state.raw(defaultStatePageValues.data);
50+
/** @type {Page['form']} */
51+
let pageForm = $state.raw(defaultStatePageValues.form);
52+
/** @type {Page['error']} */
53+
let pageError = $state.raw(defaultStatePageValues.error);
54+
/** @type {Page['params']} */
55+
let pageParams = $state.raw(defaultStatePageValues.params);
56+
/** @type {Page['route']} */
57+
let pageRoute = $state.raw(defaultStatePageValues.route);
58+
/** @type {Page['state']} */
59+
let pageState = $state.raw(defaultStatePageValues.state);
60+
/** @type {Page['status']} */
61+
let pageStatus = $state.raw(defaultStatePageValues.status);
62+
/** @type {Page['url']} */
63+
let pageUrl = $state.raw(defaultStatePageValues.url);
64+
65+
/** @type {Page} */
66+
export let page = {
67+
get data() {
68+
return pageData;
69+
},
70+
set data(newPageData) {
71+
pageData = newPageData;
72+
},
73+
get form() {
74+
return pageForm;
75+
},
76+
set form(newPageForm) {
77+
pageForm = newPageForm;
78+
},
79+
get error() {
80+
return pageError;
81+
},
82+
set error(newPageError) {
83+
pageError = newPageError;
84+
},
85+
get params() {
86+
return pageParams;
87+
},
88+
set params(newPageParams) {
89+
pageParams = newPageParams;
90+
},
91+
get route() {
92+
return pageRoute;
93+
},
94+
set route(newPageRoute) {
95+
pageRoute = newPageRoute;
96+
},
97+
get state() {
98+
return pageState;
99+
},
100+
set state(newPageState) {
101+
pageState = newPageState;
102+
},
103+
get status() {
104+
return pageStatus;
105+
},
106+
set status(newPageStatus) {
107+
pageStatus = newPageStatus;
108+
},
109+
get url() {
110+
return pageUrl;
111+
},
112+
set url(newPageUrl) {
113+
pageUrl = newPageUrl;
114+
},
115+
};
116+
117+
export function setAppStatePage(params = {}) {
118+
page.data = params.data ?? defaultStatePageValues.data;
119+
page.form = params.form ?? defaultStatePageValues.form;
120+
page.error = params.error ?? defaultStatePageValues.error;
121+
page.params = params.params ?? defaultStatePageValues.params;
122+
page.route = params.route ?? defaultStatePageValues.route;
123+
page.state = params.state ?? defaultStatePageValues.state;
124+
page.status = params.status ?? defaultStatePageValues.status;
125+
page.url = params.url ?? defaultStatePageValues.url;
126+
}
127+
128+
/**
129+
* @typedef {Object} NavigationTarget
130+
* @property {Record<string, string> | null} params
131+
* @property {Object} route
132+
* @property {string | null} route.id
133+
* @property {URL} url
134+
*/
135+
136+
/** @typedef {'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'} NavigationType */
137+
138+
/**
139+
* @typedef {Object} Navigation
140+
* @property {NavigationTarget | null} from
141+
* @property {NavigationTarget | null} to
142+
* @property {Exclude<NavigationType, 'enter'>} type
143+
* @property {boolean} willUnload
144+
* @property {number} [delta]
145+
* @property {Promise<void>} complete
146+
*/
147+
148+
const defaultStateNavigatingValues = {
149+
from: null,
150+
to: null,
151+
type: null,
152+
willUnload: null,
153+
delta: null,
154+
complete: null,
155+
};
156+
157+
/** @type {Navigation['from'] | null} */
158+
let navigatingFrom = $state.raw(defaultStateNavigatingValues.from);
159+
/** @type {Navigation['to'] | null} */
160+
let navigatingTo = $state.raw(defaultStateNavigatingValues.to);
161+
/** @type {Navigation['type'] | null} */
162+
let navigatingType = $state.raw(defaultStateNavigatingValues.type);
163+
/** @type {Navigation['willUnload'] | null} */
164+
let navigatingWillUnload = $state.raw(defaultStateNavigatingValues.willUnload);
165+
/** @type {Navigation['delta'] | null} */
166+
let navigatingDelta = $state.raw(defaultStateNavigatingValues.delta);
167+
/** @type {Navigation['complete'] | null} */
168+
let navigatingComplete = $state.raw(defaultStateNavigatingValues.complete);
169+
170+
/** @type {Navigation} */
171+
export let navigating = {
172+
get from() {
173+
return navigatingFrom;
174+
},
175+
set from(newNavigatingFrom) {
176+
navigatingFrom = newNavigatingFrom;
177+
},
178+
get to() {
179+
return navigatingTo;
180+
},
181+
set to(newNavigatingTo) {
182+
navigatingTo = newNavigatingTo;
183+
},
184+
get type() {
185+
return navigatingType;
186+
},
187+
set type(newNavigatingType) {
188+
navigatingType = newNavigatingType;
189+
},
190+
get willUnload() {
191+
return navigatingWillUnload;
192+
},
193+
set willUnload(newNavigatingWillUnload) {
194+
navigatingWillUnload = newNavigatingWillUnload;
195+
},
196+
get delta() {
197+
return navigatingDelta;
198+
},
199+
set delta(newNavigatingDelta) {
200+
navigatingDelta = newNavigatingDelta;
201+
},
202+
get complete() {
203+
return navigatingComplete;
204+
},
205+
set complete(newNavigatingComplete) {
206+
navigatingComplete = newNavigatingComplete;
207+
},
208+
};
209+
210+
export function setAppStateNavigating(params = {}) {
211+
navigating.from = params.from ?? defaultStateNavigatingValues.from;
212+
navigating.to = params.to ?? defaultStateNavigatingValues.to;
213+
navigating.type = params.type ?? defaultStateNavigatingValues.type;
214+
navigating.willUnload = params.willUnload ?? defaultStateNavigatingValues.willUnload;
215+
navigating.delta = params.delta ?? defaultStateNavigatingValues.delta;
216+
navigating.complete = params.complete ?? defaultStateNavigatingValues.complete;
217+
}
218+
219+
/** @type {boolean} */
220+
let updatedCurrent = $state.raw(false);
221+
222+
export let updated = {
223+
get current() {
224+
return updatedCurrent;
225+
},
226+
set current(newCurrent) {
227+
updatedCurrent = newCurrent;
228+
},
229+
check: fn(() => Promise.resolve(updatedCurrent)),
230+
};
231+
232+
export function setAppStateUpdated(params = {}) {
233+
updated.current = params.current ?? false;
234+
}

0 commit comments

Comments
 (0)