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
107 changes: 107 additions & 0 deletions app/pages/org/[org].stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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() {
useRouter().replace('/org/npmx')
},
template: '<Org />',
}),
}

/**
* 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() {
useRouter().replace('/org/single-org')
},
template: '<Org />',
}),
}

/**
* 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() {
useRouter().replace('/org/empty-org')
},
template: '<Org />',
}),
}

/**
* 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() {
useRouter().replace('/org/nonexistent-org')
},
template: '<Org />',
}),
}

/**
* 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() {
useRouter().replace('/org/npmx')
},
template: '<Org />',
}),
}
2 changes: 1 addition & 1 deletion app/pages/org/[org].vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ definePageMeta({
const route = useRoute('org')
const router = useRouter()

const orgName = computed(() => route.params.org.toLowerCase())
const orgName = computed(() => (route.params.org ?? '').toLowerCase())

const { isConnected } = useConnector()

Expand Down
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: '[email protected]',
},
],
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