Skip to content

Commit 879772a

Browse files
authored
feat(msw): Add msw package (#7551)
1 parent a7a38ab commit 879772a

27 files changed

+5490
-28
lines changed

.changeset/petite-clubs-grab.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/clerk-js/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"dequal": "2.0.3"
7979
},
8080
"devDependencies": {
81+
"@clerk/msw": "workspace:^",
8182
"@clerk/testing": "workspace:^",
8283
"@rsdoctor/rspack-plugin": "^0.4.13",
8384
"@rspack/cli": "^1.6.0",

packages/clerk-js/rspack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ const devConfig = ({ mode, env }) => {
501501
...(isSandbox
502502
? {
503503
historyApiFallback: true,
504+
static: ['sandbox/public'],
504505
}
505506
: {}),
506507
},
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# `clerk-js` Sandbox
2+
3+
This folder contains a sandbox environment for iterating on the Clerk UI components. Each main top-level component gets its own page.
4+
5+
## Running the sandbox
6+
7+
You can start the sandbox by running `pnpm dev:sandbox` **in the root of the `javascript` repo**. This will start the server on <a href="http://localhost:4000"><code>http://localhost:4000</code></a>. It will also run the development server for `@clerk/ui`.
8+
9+
## Setting component props
10+
11+
You can pass specific props to a given component by running the following in the console:
12+
13+
```
14+
components.<componentName>.setProps({ ... });
15+
```
16+
17+
For example, to set props for the `SignIn` component:
18+
19+
```js
20+
components.signIn.setProps({
21+
/* ... */
22+
});
23+
```
24+
25+
Doing so will change the URL of the page you're on to include the configured props as a URL query parameter. This allows you to share a link to the specific configuration of the props you've set.
26+
27+
## Activating API mocking scenarios
28+
29+
You can also activate specific API mocking scenarios to avoid making calls to the Clerk API. Activate a scenario with the following:
30+
31+
```js
32+
scenario.setScenario('ScenarioName');
33+
```
34+
35+
You can also use `scenario.availableScenarios` to see a list of valid scenarios. You can also pass this to `setScenario`:
36+
37+
```js
38+
scenario.setScenario(scenario.UserButtonLoggedIn);
39+
```
40+
41+
Like `setProps`, this command will persist the active scenario to the URL.

packages/clerk-js/sandbox/app.ts

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1+
import { PageMocking, type MockScenario } from '@clerk/msw';
12
import * as l from '../../localizations';
23
import type { Clerk as ClerkType } from '../';
3-
4-
const AVAILABLE_LOCALES = Object.keys(l) as (keyof typeof l)[];
5-
6-
function fillLocalizationSelect() {
7-
const select = document.getElementById('localizationSelect') as HTMLSelectElement;
8-
9-
for (const locale of AVAILABLE_LOCALES) {
10-
if (locale === 'enUS') {
11-
select.add(new Option(locale, locale, true, true));
12-
continue;
13-
}
14-
15-
select.add(new Option(locale, locale));
16-
}
17-
}
4+
import * as scenarios from './scenarios';
185

196
interface ComponentPropsControl {
207
setProps: (props: unknown) => void;
218
getProps: () => any | null;
229
}
2310

11+
interface ScenarioControls {
12+
setScenario: (scenario: AvailableScenario | null) => void;
13+
availableScenarios: typeof AVAILABLE_SCENARIOS;
14+
}
15+
16+
const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
17+
18+
const AVAILABLE_LOCALES = Object.keys(l) as (keyof typeof l)[];
19+
2420
const AVAILABLE_COMPONENTS = [
2521
'clerk', // While not a component, we want to support passing options to the Clerk class.
2622
'signIn',
@@ -39,17 +35,57 @@ const AVAILABLE_COMPONENTS = [
3935
'taskChooseOrganization',
4036
'taskResetPassword',
4137
] as const;
38+
type AvailableComponent = (typeof AVAILABLE_COMPONENTS)[number];
4239

43-
const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
40+
const AVAILABLE_SCENARIOS = Object.keys(scenarios) as (keyof typeof scenarios)[];
41+
type AvailableScenario = (typeof AVAILABLE_SCENARIOS)[number];
4442

45-
const urlParams = new URL(window.location.href).searchParams;
46-
for (const [component, encodedProps] of urlParams.entries()) {
47-
if (AVAILABLE_COMPONENTS.includes(component as (typeof AVAILABLE_COMPONENTS)[number])) {
48-
localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`, encodedProps);
43+
function fillLocalizationSelect() {
44+
const select = document.getElementById('localizationSelect') as HTMLSelectElement;
45+
46+
for (const locale of AVAILABLE_LOCALES) {
47+
if (locale === 'enUS') {
48+
select.add(new Option(locale, locale, true, true));
49+
continue;
50+
}
51+
52+
select.add(new Option(locale, locale));
53+
}
54+
}
55+
56+
function getScenario(): (() => MockScenario) | null {
57+
const scenarioName = localStorage.getItem(`${COMPONENT_PROPS_NAMESPACE}-scenario`);
58+
if (scenarioName && AVAILABLE_SCENARIOS.includes(scenarioName as AvailableScenario)) {
59+
return scenarios[scenarioName as AvailableScenario];
60+
}
61+
return null;
62+
}
63+
64+
function setScenario(scenario: AvailableScenario | null) {
65+
if (!scenario) {
66+
localStorage.removeItem(`${COMPONENT_PROPS_NAMESPACE}-scenario`);
67+
const url = new URL(window.location.href);
68+
url.searchParams.delete('scenario');
69+
window.location.href = url.toString();
70+
return;
71+
}
72+
73+
if (!AVAILABLE_SCENARIOS.includes(scenario)) {
74+
throw new Error(`Invalid scenario: "${scenario}". Available scenarios: ${AVAILABLE_SCENARIOS.join(', ')}`);
4975
}
76+
localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-scenario`, scenario);
77+
78+
const url = new URL(window.location.href);
79+
url.searchParams.set('scenario', scenario);
80+
window.location.href = url.toString();
5081
}
5182

52-
function setComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number], props: unknown) {
83+
const scenarioControls: ScenarioControls = {
84+
setScenario,
85+
availableScenarios: AVAILABLE_SCENARIOS,
86+
};
87+
88+
function setComponentProps(component: AvailableComponent, props: unknown) {
5389
const encodedProps = JSON.stringify(props);
5490

5591
const url = new URL(window.location.href);
@@ -58,7 +94,7 @@ function setComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number], pro
5894
window.location.href = url.toString();
5995
}
6096

61-
function getComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number]): unknown | null {
97+
function getComponentProps(component: AvailableComponent): unknown | null {
6298
const url = new URL(window.location.href);
6399
const encodedProps = url.searchParams.get(component);
64100
if (encodedProps) {
@@ -73,7 +109,7 @@ function getComponentProps(component: (typeof AVAILABLE_COMPONENTS)[number]): un
73109
return null;
74110
}
75111

76-
function buildComponentControls(component: (typeof AVAILABLE_COMPONENTS)[number]): ComponentPropsControl {
112+
function buildComponentControls(component: AvailableComponent): ComponentPropsControl {
77113
return {
78114
setProps(props) {
79115
setComponentProps(component, props);
@@ -84,7 +120,7 @@ function buildComponentControls(component: (typeof AVAILABLE_COMPONENTS)[number]
84120
};
85121
}
86122

87-
const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], ComponentPropsControl> = {
123+
const componentControls: Record<AvailableComponent, ComponentPropsControl> = {
88124
clerk: buildComponentControls('clerk'),
89125
signIn: buildComponentControls('signIn'),
90126
signUp: buildComponentControls('signUp'),
@@ -105,11 +141,21 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
105141

106142
declare global {
107143
interface Window {
108-
components: Record<(typeof AVAILABLE_COMPONENTS)[number], ComponentPropsControl>;
144+
components: Record<AvailableComponent, ComponentPropsControl>;
145+
scenario: typeof scenarioControls;
146+
AVAILABLE_SCENARIOS: Record<AvailableScenario, AvailableScenario>;
109147
}
110148
}
111149

112150
window.components = componentControls;
151+
window.scenario = scenarioControls;
152+
window.AVAILABLE_SCENARIOS = AVAILABLE_SCENARIOS.reduce(
153+
(acc, scenario) => {
154+
acc[scenario] = scenario;
155+
return acc;
156+
},
157+
{} as Record<AvailableScenario, AvailableScenario>,
158+
);
113159

114160
const Clerk = window.Clerk;
115161
function assertClerkIsLoaded(c: ClerkType | undefined): asserts c is ClerkType {
@@ -118,8 +164,6 @@ function assertClerkIsLoaded(c: ClerkType | undefined): asserts c is ClerkType {
118164
}
119165
}
120166

121-
const app = document.getElementById('app') as HTMLDivElement;
122-
123167
function mountIndex(element: HTMLDivElement) {
124168
assertClerkIsLoaded(Clerk);
125169
const user = Clerk.user;
@@ -267,6 +311,17 @@ function otherOptions() {
267311
return { updateOtherOptions };
268312
}
269313

314+
const urlParams = new URL(window.location.href).searchParams;
315+
for (const [component, encodedProps] of urlParams.entries()) {
316+
if (AVAILABLE_COMPONENTS.includes(component as AvailableComponent)) {
317+
localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-${component}`, encodedProps);
318+
}
319+
320+
if (component === 'scenario' && AVAILABLE_SCENARIOS.includes(encodedProps as AvailableScenario)) {
321+
localStorage.setItem(`${COMPONENT_PROPS_NAMESPACE}-scenario`, encodedProps);
322+
}
323+
}
324+
270325
void (async () => {
271326
assertClerkIsLoaded(Clerk);
272327
fillLocalizationSelect();
@@ -280,6 +335,8 @@ void (async () => {
280335
}
281336
});
282337

338+
const app = document.getElementById('app') as HTMLDivElement;
339+
283340
const routes = {
284341
'/': () => {
285342
mountIndex(app);
@@ -373,6 +430,17 @@ void (async () => {
373430
if (route in routes) {
374431
const renderCurrentRoute = routes[route];
375432
addCurrentRouteIndicator(route);
433+
434+
const scenario = getScenario();
435+
if (scenario) {
436+
const mocking = new PageMocking({
437+
onStateChange: state => {
438+
console.log('Mocking state changed:', state);
439+
},
440+
});
441+
await mocking.initialize(route, { scenario });
442+
}
443+
376444
await Clerk.load({
377445
...(componentControls.clerk.getProps() ?? {}),
378446
signInUrl: '/sign-in',

0 commit comments

Comments
 (0)