Skip to content

Commit d9ad666

Browse files
committed
docs(ui): add stories for Org page
1 parent 189a568 commit d9ad666

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

app/pages/org/[org].stories.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import Org from './[org].vue'
2+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
3+
import { pageDecorator } from '../../../.storybook/decorators'
4+
import {
5+
mockOrgPackagesSuccess,
6+
mockOrgPackagesSingle,
7+
mockOrgPackagesEmpty,
8+
mockOrgPackagesNotFound,
9+
mockOrgPackagesLoading,
10+
} from '../../storybook/mocks/handlers/registry-org'
11+
12+
const meta: Meta = {
13+
component: Org,
14+
parameters: {
15+
layout: 'fullscreen',
16+
},
17+
decorators: [pageDecorator],
18+
}
19+
20+
export default meta
21+
type Story = StoryObj
22+
23+
/**
24+
* Default org page showing the @npmx organization with multiple packages.
25+
* Displays package list with filtering, sorting, and view mode controls.
26+
* The MSW handler mocks both the org packages endpoint and Algolia search.
27+
*/
28+
export const Default: Story = {
29+
parameters: {
30+
msw: { handlers: mockOrgPackagesSuccess },
31+
},
32+
render: () => ({
33+
components: { Org },
34+
setup() {
35+
useRouter().replace('/org/npmx')
36+
},
37+
template: '<Org />',
38+
}),
39+
}
40+
41+
/**
42+
* Organization with only a single package.
43+
* Shows the org page layout with minimal content.
44+
*/
45+
export const SinglePackage: Story = {
46+
parameters: {
47+
msw: { handlers: mockOrgPackagesSingle },
48+
},
49+
render: () => ({
50+
components: { Org },
51+
setup() {
52+
useRouter().replace('/org/single-org')
53+
},
54+
template: '<Org />',
55+
}),
56+
}
57+
58+
/**
59+
* Empty organization with zero packages.
60+
* Shows the "This organization has no packages" message.
61+
*/
62+
export const EmptyOrg: Story = {
63+
parameters: {
64+
msw: { handlers: mockOrgPackagesEmpty },
65+
},
66+
render: () => ({
67+
components: { Org },
68+
setup() {
69+
useRouter().replace('/org/empty-org')
70+
},
71+
template: '<Org />',
72+
}),
73+
}
74+
75+
/**
76+
* Organization not found (404 error).
77+
* The org endpoint returns a 404 error and the page displays an error state.
78+
*/
79+
export const NotFound: Story = {
80+
parameters: {
81+
msw: { handlers: mockOrgPackagesNotFound },
82+
},
83+
render: () => ({
84+
components: { Org },
85+
setup() {
86+
useRouter().replace('/org/nonexistent-org')
87+
},
88+
template: '<Org />',
89+
}),
90+
}
91+
92+
/**
93+
* Loading state when the API request is pending.
94+
* MSW handlers delay responses indefinitely to show the loading spinner.
95+
*/
96+
export const Loading: Story = {
97+
parameters: {
98+
msw: { handlers: mockOrgPackagesLoading },
99+
},
100+
render: () => ({
101+
components: { Org },
102+
setup() {
103+
useRouter().replace('/org/npmx')
104+
},
105+
template: '<Org />',
106+
}),
107+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { http, HttpResponse } from 'msw'
2+
3+
/**
4+
* Helper to create mock AlgoliaHit objects (mimics Algolia API response format)
5+
*/
6+
function createMockAlgoliaHit(
7+
name: string,
8+
overrides: {
9+
description?: string
10+
version?: string
11+
downloadsLast30Days?: number
12+
keywords?: string[]
13+
modified?: number
14+
license?: string
15+
} = {},
16+
) {
17+
return {
18+
objectID: name,
19+
name,
20+
version: overrides.version || '1.2.3',
21+
description: overrides.description || `Mock package ${name}`,
22+
modified: overrides.modified || new Date('2026-01-22T10:07:07.000Z').getTime(),
23+
homepage: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
24+
repository: {
25+
url: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
26+
type: 'git',
27+
},
28+
owners: [
29+
{
30+
name: 'Patak Dog',
31+
email: 'patak@patak.dog',
32+
},
33+
],
34+
downloadsLast30Days: overrides.downloadsLast30Days || 100000,
35+
downloadsRatio: 1,
36+
popular: (overrides.downloadsLast30Days || 100000) > 50000,
37+
keywords: overrides.keywords || [],
38+
deprecated: false,
39+
isDeprecated: false,
40+
license: overrides.license || 'MIT',
41+
isSecurityHeld: false,
42+
}
43+
}
44+
45+
/**
46+
* Mock handler: Org with multiple packages (default success scenario)
47+
*/
48+
export const mockOrgPackagesSuccess = [
49+
// Return the org package list
50+
http.get('/api/registry/org/:org/packages', ({ params }) => {
51+
const org = params.org as string
52+
const packages = [
53+
`@${org}/xmpn`,
54+
`@${org}/schema`,
55+
`@${org}/i18n`,
56+
`@${org}/noodle`,
57+
`@${org}/tester`,
58+
`${org}`,
59+
]
60+
61+
return HttpResponse.json({
62+
packages,
63+
count: packages.length,
64+
})
65+
}),
66+
67+
// Mock Algolia getObjects endpoint for package metadata
68+
http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
69+
const body = (await request.json()) as any
70+
const requests = body?.requests || []
71+
72+
// Return AlgoliaHit objects for each requested package
73+
const results = requests.map((req: any) => {
74+
const packageName = req.objectID
75+
const orgMatch = packageName.match(/@([\w-]+)\//) || [packageName, packageName]
76+
const org = orgMatch[1]
77+
const packageShortName = packageName.replace(`@${org}/`, '').replace(org, '')
78+
79+
return createMockAlgoliaHit(packageName, {
80+
description: `${org.charAt(0).toUpperCase() + org.slice(1)} ${packageShortName} - mocked package`,
81+
downloadsLast30Days: 88477,
82+
keywords: [org, packageShortName],
83+
modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
84+
})
85+
})
86+
87+
return HttpResponse.json({ results })
88+
}),
89+
]
90+
91+
/**
92+
* Mock handler: Org with single package
93+
*/
94+
export const mockOrgPackagesSingle = [
95+
http.get('/api/registry/org/:org/packages', ({ params }) => {
96+
const org = params.org as string
97+
const packageName = `@${org}/only-package`
98+
99+
return HttpResponse.json({
100+
packages: [packageName],
101+
count: 1,
102+
})
103+
}),
104+
105+
http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
106+
const body = (await request.json()) as any
107+
const requests = body?.requests || []
108+
109+
// Return AlgoliaHit objects for each requested package
110+
const results = requests.map((req: any) => {
111+
const packageName = req.objectID
112+
113+
return createMockAlgoliaHit(packageName, {
114+
description: 'The only package in this organization',
115+
downloadsLast30Days: 5308, // 1234 weekly
116+
keywords: ['single', 'lonely'],
117+
modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
118+
})
119+
})
120+
121+
return HttpResponse.json({ results })
122+
}),
123+
]
124+
125+
/**
126+
* Mock handler: Empty org (no packages)
127+
*/
128+
export const mockOrgPackagesEmpty = [
129+
http.get('/api/registry/org/:org/packages', () => {
130+
return HttpResponse.json({
131+
packages: [],
132+
count: 0,
133+
})
134+
}),
135+
]
136+
137+
/**
138+
* Mock handler: Org not found (404 error)
139+
*/
140+
export const mockOrgPackagesNotFound = [
141+
http.get('/api/registry/org/:org/packages', () => {
142+
return HttpResponse.json(
143+
{
144+
error: 'Not Found',
145+
message: 'Organization not found',
146+
},
147+
{ status: 404 },
148+
)
149+
}),
150+
]
151+
152+
/**
153+
* Mock handler: Loading state (requests never resolve)
154+
*/
155+
export const mockOrgPackagesLoading = [
156+
http.get('/api/registry/org/:org/packages', async () => {
157+
// Delay indefinitely to show loading state
158+
await new Promise(() => {})
159+
return HttpResponse.json({ packages: [], count: 0 })
160+
}),
161+
http.post('https://*.algolia.net/1/indexes/*/objects', async () => {
162+
// Delay indefinitely to show loading state
163+
await new Promise(() => {})
164+
return HttpResponse.json({ results: [] })
165+
}),
166+
]

0 commit comments

Comments
 (0)