Skip to content
Open
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
86 changes: 86 additions & 0 deletions app/pages/profile/[identity]/index.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Profile from './index.vue'
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
import { expect, userEvent } from 'storybook/test'
import { pageDecorator } from '../../../../.storybook/decorators'
import {
mockAuthSessionHandler,
mockPackageLikesHandler,
mockProfile,
mockProfileHandle,
mockProfileHandler,
mockProfileLikesHandler,
mockUpdateProfileHandler,
} from '../../../storybook/mocks/handlers/profile'
import { renderPageAt } from '../../../storybook/render-page'

const PROFILE_PATH = `/profile/${mockProfileHandle}`

const meta = {
component: Profile,

render: renderPageAt(Profile, PROFILE_PATH),

beforeEach({ msw }) {
msw.use(
mockProfileHandler(),
mockProfileLikesHandler(),
mockPackageLikesHandler,
mockAuthSessionHandler(),
mockUpdateProfileHandler,
)
},

parameters: {
layout: 'fullscreen',
},

decorators: [pageDecorator],
} satisfies Meta<typeof Profile>

export default meta
type Story = StoryObj<typeof meta>

/** Public profile with profile metadata and several liked packages. */
export const Default: Story = {}

/** Authenticated as the profile owner so the edit action is available. */
export const Owner: Story = {
beforeEach({ msw }) {
msw.use(mockAuthSessionHandler(mockProfileHandle))
},
}

/** Owner profile with the edit form opened and its editable controls visible. */
export const Editing: Story = {
...Owner,
play: async ({ canvas }) => {
await userEvent.click(await canvas.findByRole('button', { name: /edit/i }))

await expect(canvas.getByText(/display name/i)).toBeVisible()
await expect(await canvas.findByDisplayValue(mockProfile.displayName)).toBeVisible()
await expect(canvas.getByText(/description/i)).toBeVisible()
await expect(canvas.getByDisplayValue(mockProfile.description ?? '')).toBeVisible()
await expect(canvas.getByText(/website/i)).toBeVisible()
await expect(canvas.getByDisplayValue(mockProfile.website ?? '')).toBeVisible()
await expect(canvas.getByRole('button', { name: /cancel/i })).toBeVisible()
await expect(canvas.getByRole('button', { name: /save/i })).toBeVisible()
},
}

/** Missing npmx profile record with no likes; authenticated as another user so the invite section appears. */
export const Invite: Story = {
beforeEach({ msw }) {
msw.use(
mockProfileHandler({ recordExists: false }),
mockProfileLikesHandler([]),
mockAuthSessionHandler('other-user.test'),
)
},
}

/** Existing profile with no liked packages and no invite section. */
export const WithoutLikes: Story = {
beforeEach({ msw }) {
msw.use(mockProfileLikesHandler([]))
},
}
65 changes: 65 additions & 0 deletions app/storybook/mocks/handlers/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { http, HttpResponse } from 'msw'
import type { NPMXProfile, PackageLikes } from '#shared/types/social'

export const mockProfileHandle = 'mock-steward'

export const mockProfile: NPMXProfile = {
displayName: mockProfileHandle,
description: 'Maintains small tools for package metadata, docs, and release workflows.',
website: `https://github.com/${mockProfileHandle}`,
handle: mockProfileHandle,
recordExists: true,
}

const mockLikedPackages = ['https://npmx.dev/package/nuxt', 'https://npmx.dev/package/vitest']

function createMockAuthSession(handle: string) {
return {
did: `did:plc:${handle}`,
handle,
pds: 'https://bsky.social',
}
}

export function mockProfileHandler(overrides: Partial<NPMXProfile> = {}) {
return http.get(`/api/social/profile/${mockProfileHandle}`, () =>
HttpResponse.json({
...mockProfile,
...overrides,
}),
)
}

export function mockProfileLikesHandler(likes = mockLikedPackages) {
return http.get(`/api/social/profile/${mockProfileHandle}/likes`, () =>
HttpResponse.json({
cursor: null,
likes,
}),
)
}

export const mockPackageLikesHandler = http.get('/api/social/likes/:pkg', ({ params }) => {
const pkg = String(params.pkg)
const packageLikesByName: Record<string, number | undefined> = {
nuxt: 128,
vitest: 96,
}
const response: PackageLikes = {
totalLikes: packageLikesByName[pkg] ?? 12,
userHasLiked: false,
topLikedRank: null,
}

return HttpResponse.json(response)
})

export function mockAuthSessionHandler(handle: string | null = null) {
return http.get('/api/auth/session', () =>
HttpResponse.json(handle ? createMockAuthSession(handle) : null),
)
}

export const mockUpdateProfileHandler = http.put(`/api/social/profile/${mockProfileHandle}`, () =>
HttpResponse.text('ok'),
)
26 changes: 26 additions & 0 deletions app/storybook/render-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { clearNuxtData, useRouter } from '#app'
import { PageRouteSymbol } from '#app/components/injections'
import { h, provide, shallowReactive, Suspense } from 'vue'
import type { Component } from 'vue'
import type { RouteLocationRaw } from 'vue-router'

export function renderPageAt(component: Component, path: RouteLocationRaw) {
return () => ({
setup() {
clearNuxtData()

const router = useRouter()
const routeForStory = shallowReactive(router.resolve(path))
provide(PageRouteSymbol, routeForStory)

if (router.currentRoute.value.fullPath !== routeForStory.fullPath) {
router.replace(path).catch(() => {})
}

return () =>
h(Suspense, null, {
default: () => h(component),
})
},
})
}
Loading