Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions app/pages/org/[org].stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import Org from './[org].vue'
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
import { pageDecorator } from '../../../.storybook/decorators'
import {
mockOrgPackagesSuccess,
mockOrgPackagesSingle,
mockOrgPackagesEmpty,
mockOrgPackagesNotFound,
mockOrgPackagesLoading,
} from '../../storybook/mocks/handlers/registry-org'

const meta: Meta = {
component: Org,
parameters: {
layout: 'fullscreen',
},
decorators: [pageDecorator],
}

export default meta
type Story = StoryObj

/**
* Default org page showing the @npmx organization with multiple packages.
* Displays package list with filtering, sorting, and view mode controls.
* The MSW handler mocks both the org packages endpoint and Algolia search.
*/
export const Default: Story = {
parameters: {
msw: { handlers: mockOrgPackagesSuccess },
},
render: () => ({
components: { Org },
setup() {
const isReady = ref(false)
useRouter()
.replace('/org/npmx')
.then(() => {
isReady.value = true
})
return { isReady }
},
template: '<Org v-if="isReady" />',
}),
}

/**
* Organization with only a single package.
* Shows the org page layout with minimal content.
*/
export const SinglePackage: Story = {
parameters: {
msw: { handlers: mockOrgPackagesSingle },
},
render: () => ({
components: { Org },
setup() {
const isReady = ref(false)
useRouter()
.replace('/org/single-org')
.then(() => {
isReady.value = true
})
return { isReady }
},
template: '<Org v-if="isReady" />',
}),
}

/**
* Empty organization with zero packages.
* Shows the "This organization has no packages" message.
*/
export const EmptyOrg: Story = {
parameters: {
msw: { handlers: mockOrgPackagesEmpty },
},
render: () => ({
components: { Org },
setup() {
const isReady = ref(false)
useRouter()
.replace('/org/empty-org')
.then(() => {
isReady.value = true
})
return { isReady }
},
template: '<Org v-if="isReady" />',
}),
}

/**
* Organization not found (404 error).
* The org endpoint returns a 404 error and the page displays an error state.
*/
export const NotFound: Story = {
parameters: {
msw: { handlers: mockOrgPackagesNotFound },
},
render: () => ({
components: { Org },
setup() {
const isReady = ref(false)
useRouter()
.replace('/org/nonexistent-org')
.then(() => {
isReady.value = true
})
return { isReady }
},
template: '<Org v-if="isReady" />',
}),
}

/**
* Loading state when the API request is pending.
* MSW handlers delay responses indefinitely to show the loading spinner.
*/
export const Loading: Story = {
parameters: {
msw: { handlers: mockOrgPackagesLoading },
},
render: () => ({
components: { Org },
setup() {
const isReady = ref(false)
useRouter()
.replace('/org/npmx')
.then(() => {
isReady.value = true
})
return { isReady }
},
template: '<Org v-if="isReady" />',
}),
}
166 changes: 166 additions & 0 deletions app/storybook/mocks/handlers/registry-org.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { http, HttpResponse } from 'msw'

/**
* Helper to create mock AlgoliaHit objects (mimics Algolia API response format)
*/
function createMockAlgoliaHit(
name: string,
overrides: {
description?: string
version?: string
downloadsLast30Days?: number
keywords?: string[]
modified?: number
license?: string
} = {},
) {
return {
objectID: name,
name,
version: overrides.version || '1.2.3',
description: overrides.description || `Mock package ${name}`,
modified: overrides.modified || new Date('2026-01-22T10:07:07.000Z').getTime(),
homepage: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
repository: {
url: `https://github.com/org/${name.replace('@', '').replace('/', '-')}`,
type: 'git',
},
owners: [
{
name: 'Patak Dog',
email: 'patak@patak.dog',
},
],
downloadsLast30Days: overrides.downloadsLast30Days || 100000,
downloadsRatio: 1,
popular: (overrides.downloadsLast30Days || 100000) > 50000,
keywords: overrides.keywords || [],
deprecated: false,
isDeprecated: false,
license: overrides.license || 'MIT',
isSecurityHeld: false,
}
}

/**
* Mock handler: Org with multiple packages (default success scenario)
*/
export const mockOrgPackagesSuccess = [
// Return the org package list
http.get('/api/registry/org/:org/packages', ({ params }) => {
const org = params.org as string
const packages = [
`@${org}/xmpn`,
`@${org}/schema`,
`@${org}/i18n`,
`@${org}/noodle`,
`@${org}/tester`,
`${org}`,
]

return HttpResponse.json({
packages,
count: packages.length,
})
}),

// Mock Algolia getObjects endpoint for package metadata
http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
const body = (await request.json()) as any
const requests = body?.requests || []

// Return AlgoliaHit objects for each requested package
const results = requests.map((req: any) => {
const packageName = req.objectID
const orgMatch = packageName.match(/@([\w-]+)\//) || [packageName, packageName]
const org = orgMatch[1]
const packageShortName = packageName.replace(`@${org}/`, '').replace(org, '')

return createMockAlgoliaHit(packageName, {
description: `${org.charAt(0).toUpperCase() + org.slice(1)} ${packageShortName} - mocked package`,
downloadsLast30Days: 88477,
keywords: [org, packageShortName],
modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
})
})

return HttpResponse.json({ results })
}),
]

/**
* Mock handler: Org with single package
*/
export const mockOrgPackagesSingle = [
http.get('/api/registry/org/:org/packages', ({ params }) => {
const org = params.org as string
const packageName = `@${org}/only-package`

return HttpResponse.json({
packages: [packageName],
count: 1,
})
}),

http.post('https://*.algolia.net/1/indexes/*/objects', async ({ request }) => {
const body = (await request.json()) as any
const requests = body?.requests || []

// Return AlgoliaHit objects for each requested package
const results = requests.map((req: any) => {
const packageName = req.objectID

return createMockAlgoliaHit(packageName, {
description: 'The only package in this organization',
downloadsLast30Days: 5308, // 1234 weekly
keywords: ['single', 'lonely'],
modified: new Date('2026-01-22T10:07:07.000Z').getTime(),
})
})

return HttpResponse.json({ results })
}),
]

/**
* Mock handler: Empty org (no packages)
*/
export const mockOrgPackagesEmpty = [
http.get('/api/registry/org/:org/packages', () => {
return HttpResponse.json({
packages: [],
count: 0,
})
}),
]

/**
* Mock handler: Org not found (404 error)
*/
export const mockOrgPackagesNotFound = [
http.get('/api/registry/org/:org/packages', () => {
return HttpResponse.json(
{
error: 'Not Found',
message: 'Organization not found',
},
{ status: 404 },
)
}),
]

/**
* Mock handler: Loading state (requests never resolve)
*/
export const mockOrgPackagesLoading = [
http.get('/api/registry/org/:org/packages', async () => {
// Delay indefinitely to show loading state
await new Promise(() => {})
return HttpResponse.json({ packages: [], count: 0 })
}),
http.post('https://*.algolia.net/1/indexes/*/objects', async () => {
// Delay indefinitely to show loading state
await new Promise(() => {})
return HttpResponse.json({ results: [] })
}),
]
Loading