From 202a15e631421e990ddbf230ab22fbc60a66d55c Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Sun, 12 Apr 2026 18:28:10 -0500 Subject: [PATCH 1/2] docs(ui): add stories for Recharging page --- .storybook/.public/mockServiceWorker.js | 335 ++++++++++++++++++++++++ .storybook/handlers.ts | 59 +++++ .storybook/preview.ts | 4 + app/pages/about.stories.ts | 14 + app/pages/recharging.stories.ts | 30 +++ knip.ts | 3 + package.json | 2 + pnpm-lock.yaml | 248 ++++++++++++++++++ pnpm-workspace.yaml | 4 + 9 files changed, 699 insertions(+) create mode 100644 .storybook/.public/mockServiceWorker.js create mode 100644 .storybook/handlers.ts create mode 100644 app/pages/recharging.stories.ts diff --git a/.storybook/.public/mockServiceWorker.js b/.storybook/.public/mockServiceWorker.js new file mode 100644 index 0000000000..af42f4351e --- /dev/null +++ b/.storybook/.public/mockServiceWorker.js @@ -0,0 +1,335 @@ +/* eslint-disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.13.2' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter(client => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId, requestInterceptedAt) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter(client => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find(client => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map(value => value.trim()) + const filteredValues = values.filter(value => value !== 'msw/passthrough') + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = event => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/.storybook/handlers.ts b/.storybook/handlers.ts new file mode 100644 index 0000000000..42531f6f1d --- /dev/null +++ b/.storybook/handlers.ts @@ -0,0 +1,59 @@ +import { http, HttpResponse } from 'msw' + +export const repoStatsHandler = http.get('/api/repo-stats', () => { + return HttpResponse.json({ + contributors: 123, + commits: 1234, + pullRequests: 1234, + }) +}) + +export const contributorsHandler = http.get('/api/contributors', () => { + return HttpResponse.json([ + { + login: 'mock-steward-a', + id: 1001, + avatar_url: 'https://api.dicebear.com/9.x/initials/svg?seed=steward-a', + html_url: 'https://github.com/mock-steward-a', + contributions: 2800, + role: 'steward', + sponsors_url: 'https://github.com/sponsors/', + }, + { + login: 'mock-steward-b', + id: 1002, + avatar_url: 'https://api.dicebear.com/9.x/initials/svg?seed=steward-b', + html_url: 'https://github.com/mock-steward-b', + contributions: 420, + role: 'steward', + sponsors_url: null, + }, + { + login: 'mock-maintainer-a', + id: 1003, + avatar_url: 'https://api.dicebear.com/9.x/initials/svg?seed=maintainer-a', + html_url: 'https://github.com/mock-maintainer-a', + contributions: 210, + role: 'maintainer', + sponsors_url: null, + }, + { + login: 'mock-contributor-a', + id: 1004, + avatar_url: 'https://api.dicebear.com/9.x/initials/svg?seed=contributor-a', + html_url: 'https://github.com/mock-contributor-a', + contributions: 95, + role: 'contributor', + sponsors_url: 'https://github.com/sponsors/', + }, + { + login: 'mock-contributor-b', + id: 1005, + avatar_url: 'https://api.dicebear.com/9.x/initials/svg?seed=contributor-b', + html_url: 'https://github.com/mock-contributor-b', + contributions: 47, + role: 'contributor', + sponsors_url: null, + }, + ]) +}) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index a31da335cd..30d8f6cfcb 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -4,9 +4,12 @@ import { addons } from 'storybook/preview-api' import { currentLocales } from '../config/i18n' import { fn } from 'storybook/test' import { ACCENT_COLORS } from '../shared/utils/constants' +import { initialize, mswLoader } from 'msw-storybook-addon' import npmxDark from './theme' +initialize() + // related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26 // Stub Nuxt specific globals // @ts-expect-error - dynamic global name @@ -102,6 +105,7 @@ const preview: Preview = { } }, ], + loaders: [mswLoader], } export default preview diff --git a/app/pages/about.stories.ts b/app/pages/about.stories.ts index 7d97312345..24fc23144b 100644 --- a/app/pages/about.stories.ts +++ b/app/pages/about.stories.ts @@ -1,6 +1,7 @@ import About from './about.vue' import type { Meta, StoryObj } from '@storybook-vue/nuxt' import { pageDecorator } from '../../.storybook/decorators' +import { contributorsHandler } from '../../.storybook/handlers' const meta = { component: About, @@ -13,4 +14,17 @@ const meta = { export default meta type Story = StoryObj +/** Contributors section is hidden when there is no API response but the rest of the page renders. */ export const Default: Story = {} + +/** + * WithContributors — the `/api/contributors` endpoint is intercepted by MSW + * so the governance members and community contributors sections are populated. + */ +export const WithContributors: Story = { + parameters: { + msw: { + handlers: [contributorsHandler], + }, + }, +} diff --git a/app/pages/recharging.stories.ts b/app/pages/recharging.stories.ts new file mode 100644 index 0000000000..e1ab4ec15c --- /dev/null +++ b/app/pages/recharging.stories.ts @@ -0,0 +1,30 @@ +import Recharging from './recharging.vue' +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import { pageDecorator } from '../../.storybook/decorators' +import { repoStatsHandler } from '../../.storybook/handlers' + +const meta = { + component: Recharging, + parameters: { + layout: 'fullscreen', + }, + decorators: [pageDecorator], +} satisfies Meta + +export default meta +type Story = StoryObj + +/** The stats grid is hidden when there is no API response but the rest of the page renders. */ +export const Default: Story = {} + +/** + * WithStats — the `/api/repo-stats` endpoint is intercepted by MSW so the + * three-column stats grid (contributors, commits, pull requests) becomes visible. + */ +export const WithStats: Story = { + parameters: { + msw: { + handlers: [repoStatsHandler], + }, + }, +} diff --git a/knip.ts b/knip.ts index b40fc70b0d..32fe773032 100644 --- a/knip.ts +++ b/knip.ts @@ -22,6 +22,9 @@ const config: KnipConfig = { '!cli/src/**', '!lexicons/**', ], + msw: { + entry: ['.storybook/.public/mockServiceWorker.js'], + }, ignoreDependencies: [ '@iconify-json/*', 'puppeteer', diff --git a/package.json b/package.json index d88eb51dfe..69372a08c2 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,8 @@ "h3-next": "npm:h3@2.0.1-rc.16", "knip": "6.0.5", "markdown-it-anchor": "9.2.0", + "msw": "catalog:msw", + "msw-storybook-addon": "catalog:storybook", "schema-dts": "2.0.0", "storybook": "catalog:storybook", "storybook-i18n": "catalog:storybook", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de42bd50f4..727a416d07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,10 @@ settings: excludeLinksFromLockfile: false catalogs: + msw: + msw: + specifier: ^2.13.2 + version: 2.13.2 storybook: '@storybook-vue/nuxt': specifier: 9.0.1 @@ -19,6 +23,9 @@ catalogs: '@storybook/addon-themes': specifier: ^10.3.1 version: 10.3.4 + msw-storybook-addon: + specifier: ^2.0.7 + version: 2.0.7 storybook-i18n: specifier: ^10.1.1 version: 10.1.1 @@ -308,6 +315,12 @@ importers: markdown-it-anchor: specifier: 9.2.0 version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.1) + msw: + specifier: catalog:msw + version: 2.13.2(@types/node@24.12.0)(typescript@6.0.2) + msw-storybook-addon: + specifier: catalog:storybook + version: 2.0.7(msw@2.13.2) schema-dts: specifier: 2.0.0 version: 2.0.0(typescript@6.0.2) @@ -1812,6 +1825,41 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@internationalized/date@3.12.0': resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} @@ -2057,6 +2105,10 @@ packages: '@cfworker/json-schema': optional: true + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + '@napi-rs/canvas-android-arm64@0.1.97': resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==} engines: {node: '>= 10'} @@ -2427,6 +2479,15 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -5040,6 +5101,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -6118,6 +6182,10 @@ packages: clean-git-ref@2.0.1: resolution: {integrity: sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -6232,6 +6300,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} @@ -7187,6 +7259,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -7322,6 +7398,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hex-rgb@4.3.0: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} @@ -7590,6 +7669,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -8344,12 +8426,31 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw-storybook-addon@2.0.7: + resolution: {integrity: sha512-TGmlxXy2TsaB6QcClVKRxqvay5f93xoLguHOihRFQ+gIEIyiyvcoQjkEeuOe7Y9qvddzGB1LyFomzPo9/EpnuQ==} + peerDependencies: + msw: ^2.0.0 + + msw@2.13.2: + resolution: {integrity: sha512-go2H1TIERKkC48pXiwec5l6sbNqYuvqOk3/vHGo1Zd+pq/H63oFawDQerH+WQdUw/flJFHDG7F+QdWMwhntA/A==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8560,6 +8661,9 @@ packages: orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -9346,6 +9450,9 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -9755,6 +9862,9 @@ packages: streamx@2.25.0: resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -9993,6 +10103,13 @@ packages: resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} hasBin: true + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + to-buffer@1.2.2: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} @@ -10019,6 +10136,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -10417,6 +10538,9 @@ packages: uploadthing: optional: true + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + untun@0.1.3: resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} hasBin: true @@ -10861,6 +10985,10 @@ packages: workbox-window@7.4.0: resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -10967,6 +11095,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -12558,6 +12690,34 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@24.12.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.0) + '@inquirer/type': 3.0.10(@types/node@24.12.0) + optionalDependencies: + '@types/node': 24.12.0 + + '@inquirer/core@10.3.2(@types/node@24.12.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@24.12.0)': + optionalDependencies: + '@types/node': 24.12.0 + '@internationalized/date@3.12.0': dependencies: '@swc/helpers': 0.5.21 @@ -12857,6 +13017,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/canvas-android-arm64@0.1.97': optional: true @@ -13901,6 +14070,15 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@opentelemetry/api@1.9.0': {} '@oxc-minify/binding-android-arm-eabi@0.112.0': @@ -15725,6 +15903,8 @@ snapshots: '@types/semver@7.7.1': {} + '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': {} '@types/unist@2.0.11': {} @@ -16993,6 +17173,8 @@ snapshots: clean-git-ref@2.0.1: {} + cli-width@4.1.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -17081,6 +17263,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + core-js-compat@3.49.0: dependencies: browserslist: 4.28.2 @@ -18292,6 +18476,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.13.2: {} + gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -18520,6 +18706,8 @@ snapshots: he@1.2.0: {} + headers-polyfill@4.0.3: {} + hex-rgb@4.3.0: {} hey-listen@1.0.8: {} @@ -18799,6 +18987,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -19707,10 +19897,42 @@ snapshots: ms@2.1.3: {} + msw-storybook-addon@2.0.7(msw@2.13.2): + dependencies: + is-node-process: 1.2.0 + msw: 2.13.2(@types/node@24.12.0)(typescript@6.0.2) + + msw@2.13.2(@types/node@24.12.0)(typescript@6.0.2): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@24.12.0) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.5.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 6.0.2 + transitivePeerDependencies: + - '@types/node' + muggle-string@0.4.1: {} multiformats@9.9.0: {} + mute-stream@2.0.0: {} + nanoid@3.3.11: {} nanotar@0.2.1: {} @@ -20188,6 +20410,8 @@ snapshots: orderedmap@2.1.1: {} + outvariant@1.4.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -21337,6 +21561,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rettime@0.10.1: {} + reusify@1.1.0: {} rolldown-plugin-dts@0.22.5(oxc-resolver@11.19.1)(rolldown@1.0.0-rc.9)(typescript@6.0.2)(vue-tsc@3.2.6): @@ -21929,6 +22155,8 @@ snapshots: - bare-abort-controller - react-native-b4a + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -22185,6 +22413,12 @@ snapshots: tlds@1.261.0: {} + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + to-buffer@1.2.2: dependencies: isarray: 2.0.5 @@ -22205,6 +22439,10 @@ snapshots: totalist@3.0.1: {} + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + tr46@0.0.3: {} tr46@1.0.1: @@ -22651,6 +22889,8 @@ snapshots: db0: 0.3.4(better-sqlite3@12.8.0) ioredis: 5.10.1 + until-async@3.0.2: {} + untun@0.1.3: dependencies: citty: 0.1.6 @@ -23242,6 +23482,12 @@ snapshots: '@types/trusted-types': 2.0.7 workbox-core: 7.4.0 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -23330,6 +23576,8 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} yoga-layout@3.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18b761e851..941c7a0434 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -41,10 +41,14 @@ savePrefix: '' shellEmulator: true catalogs: + msw: + 'msw': '^2.13.2' + storybook: '@storybook-vue/nuxt': '9.0.1' '@storybook/addon-a11y': '^10.3.1' '@storybook/addon-docs': '^10.3.1' '@storybook/addon-themes': '^10.3.1' + 'msw-storybook-addon': '^2.0.7' 'storybook': '^10.3.1' 'storybook-i18n': '^10.1.1' From c9c0b1ea393bc536eb5ac030794430dab9ba5dd9 Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Sun, 12 Apr 2026 18:51:36 -0500 Subject: [PATCH 2/2] fix: show API mocks as default --- app/pages/about.stories.ts | 14 +++++++------- app/pages/recharging.stories.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/pages/about.stories.ts b/app/pages/about.stories.ts index 24fc23144b..aac2597425 100644 --- a/app/pages/about.stories.ts +++ b/app/pages/about.stories.ts @@ -7,6 +7,9 @@ const meta = { component: About, parameters: { layout: 'fullscreen', + msw: { + handlers: [contributorsHandler], + }, }, decorators: [pageDecorator], } satisfies Meta @@ -14,17 +17,14 @@ const meta = { export default meta type Story = StoryObj -/** Contributors section is hidden when there is no API response but the rest of the page renders. */ +/** `/api/contributors` is intercepted by MSW so both governance members and community contributors sections are populated. */ export const Default: Story = {} -/** - * WithContributors — the `/api/contributors` endpoint is intercepted by MSW - * so the governance members and community contributors sections are populated. - */ -export const WithContributors: Story = { +/** Contributors section is hidden with no API response. */ +export const WithoutContributors: Story = { parameters: { msw: { - handlers: [contributorsHandler], + handlers: [], }, }, } diff --git a/app/pages/recharging.stories.ts b/app/pages/recharging.stories.ts index e1ab4ec15c..163e055b76 100644 --- a/app/pages/recharging.stories.ts +++ b/app/pages/recharging.stories.ts @@ -7,6 +7,9 @@ const meta = { component: Recharging, parameters: { layout: 'fullscreen', + msw: { + handlers: [repoStatsHandler], + }, }, decorators: [pageDecorator], } satisfies Meta @@ -14,17 +17,14 @@ const meta = { export default meta type Story = StoryObj -/** The stats grid is hidden when there is no API response but the rest of the page renders. */ +/** `/api/repo-stats` is intercepted by MSW so the three-column stats grid (contributors, commits, pull requests) is visible. */ export const Default: Story = {} -/** - * WithStats — the `/api/repo-stats` endpoint is intercepted by MSW so the - * three-column stats grid (contributors, commits, pull requests) becomes visible. - */ -export const WithStats: Story = { +/** Stats grid is hidden with no API response; the rest of the page renders normally. */ +export const WithoutStats: Story = { parameters: { msw: { - handlers: [repoStatsHandler], + handlers: [], }, }, }