- Timezone: curated USelectMenu dropdown (app/config/timezones.js), preserves unknown saved values
- Profile save now uses useToast() for success/error; remove inline save banner
- Nav onboarding dot nudged down 1px for optical alignment with lowercase text
- Onboarding: skip a suggestion with POST /api/onboarding/track {skip}; member.onboarding.skipped map; does not affect graduation
- CirclePicker takes :saved-value so 'Current' badge stays until save completes
- PrivacyToggle is binary (USwitch labeled Private); member schema enum reduced to ['members','private']; zod coerces legacy 'public'
- New /member/payment-setup page: HelcimPay $0 verify + update-contribution, wired from account.vue via requiresPaymentSetup redirect
- Helcim portal: NUXT_PUBLIC_HELCIM_PORTAL_URL env + account.vue 'Manage billing in Helcim' link
- Migration script: scripts/migrate-privacy-public-to-members.js
197 lines
5.6 KiB
JavaScript
197 lines
5.6 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const { mockBoardPostExists } = vi.hoisted(() => ({
|
|
mockBoardPostExists: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../../server/utils/auth.js', () => ({
|
|
requireAuth: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../../server/utils/mongoose.js', () => ({
|
|
connectDB: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../../server/models/boardPost.js', () => ({
|
|
default: { exists: mockBoardPostExists }
|
|
}))
|
|
|
|
import { requireAuth } from '../../../server/utils/auth.js'
|
|
import handler from '../../../server/api/onboarding/status.get.js'
|
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
|
|
|
describe('GET /api/onboarding/status', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockBoardPostExists.mockResolvedValue(null)
|
|
})
|
|
|
|
// 1.1: Default state for new member — all false, completedAt null
|
|
it('returns all goals false for a new member with no data', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: false,
|
|
wikiClicked: false,
|
|
},
|
|
})
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result).toEqual({
|
|
goals: {
|
|
hasProfileTags: false,
|
|
hasVisitedEvent: false,
|
|
hasEngagedBoard: false,
|
|
hasClickedWiki: false,
|
|
},
|
|
skipped: {
|
|
profileTags: false,
|
|
visitEvent: false,
|
|
board: false,
|
|
wiki: false,
|
|
},
|
|
completedAt: null,
|
|
})
|
|
})
|
|
|
|
// Skip flags surface from member.onboarding.skipped
|
|
it('returns skipped map from member document', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: false,
|
|
wikiClicked: false,
|
|
skipped: { profileTags: true, wiki: true },
|
|
},
|
|
})
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.skipped).toEqual({
|
|
profileTags: true,
|
|
visitEvent: false,
|
|
board: false,
|
|
wiki: true,
|
|
})
|
|
})
|
|
|
|
// 1.2: hasProfileTags true when craft tags present
|
|
it('hasProfileTags is true when member has craft tags', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: ['game-design'],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: false,
|
|
wikiClicked: false,
|
|
},
|
|
})
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.goals.hasProfileTags).toBe(true)
|
|
})
|
|
|
|
// 1.3: hasProfileTags false when no craft tags
|
|
it('hasProfileTags is false when member has no craft tags', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: false,
|
|
wikiClicked: false,
|
|
},
|
|
})
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.goals.hasProfileTags).toBe(false)
|
|
})
|
|
|
|
// 1.5: hasEngagedBoard true when visited AND has a BoardPost
|
|
it('hasEngagedBoard is true when page visited and member has posted', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: true,
|
|
wikiClicked: false,
|
|
},
|
|
})
|
|
mockBoardPostExists.mockResolvedValue({ _id: 'post-1' })
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.goals.hasEngagedBoard).toBe(true)
|
|
expect(mockBoardPostExists).toHaveBeenCalledWith({ author: 'member-1' })
|
|
})
|
|
|
|
// 1.6: hasEngagedBoard false when visited but no posts
|
|
it('hasEngagedBoard is false when page visited but member has no posts', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: false,
|
|
boardPageVisited: true,
|
|
wikiClicked: false,
|
|
},
|
|
})
|
|
mockBoardPostExists.mockResolvedValue(null)
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.goals.hasEngagedBoard).toBe(false)
|
|
})
|
|
|
|
// 1.9: Maps stored booleans directly
|
|
it('maps stored onboarding booleans directly for visit and wiki goals', async () => {
|
|
requireAuth.mockResolvedValue({
|
|
_id: 'member-1',
|
|
craftTags: [],
|
|
onboarding: {
|
|
completedAt: null,
|
|
eventPageVisited: true,
|
|
boardPageVisited: false,
|
|
wikiClicked: true,
|
|
},
|
|
})
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
const result = await handler(event)
|
|
|
|
expect(result.goals.hasVisitedEvent).toBe(true)
|
|
expect(result.goals.hasEngagedBoard).toBe(false)
|
|
expect(result.goals.hasClickedWiki).toBe(true)
|
|
})
|
|
|
|
// 1.11: Requires auth (401)
|
|
it('returns 401 when not authenticated', async () => {
|
|
requireAuth.mockRejectedValue(
|
|
createError({ statusCode: 401, statusMessage: 'Authentication required' })
|
|
)
|
|
|
|
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
|
|
|
|
await expect(handler(event)).rejects.toMatchObject({ statusCode: 401 })
|
|
})
|
|
})
|