Skip to content

Commit 0cedde9

Browse files
authored
Merge branch 'main' into copilot/add-waitlisted-proposal-status
2 parents 2c4d982 + fcae930 commit 0cedde9

84 files changed

Lines changed: 1673 additions & 339 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/copilot-setup-steps.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,3 @@ jobs:
4646
4747
- name: Install dependencies
4848
run: pnpm install --frozen-lockfile
49-
50-
- name: Install Playwright browsers
51-
run: pnpm exec playwright install --with-deps chromium
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
vi.mock('@/lib/auth', () => ({
2+
getAuthSession: vi.fn().mockResolvedValue(null),
3+
}))
4+
5+
import { isClientError } from '@/server/trpc'
6+
7+
describe('tRPC error handler', () => {
8+
describe('isClientError', () => {
9+
it.each([
10+
'NOT_FOUND',
11+
'BAD_REQUEST',
12+
'UNAUTHORIZED',
13+
'FORBIDDEN',
14+
'PARSE_ERROR',
15+
])('should classify %s as a client error', (code) => {
16+
expect(isClientError(code)).toBe(true)
17+
})
18+
19+
it.each(['INTERNAL_SERVER_ERROR', 'TIMEOUT', 'TOO_MANY_REQUESTS'])(
20+
'should not classify %s as a client error',
21+
(code) => {
22+
expect(isClientError(code)).toBe(false)
23+
},
24+
)
25+
})
26+
})

__tests__/components/admin/sponsor-crm/SponsorContractView.test.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,20 @@ describe('SponsorContractView', () => {
141141
})
142142

143143
it('shows portal section when registration not complete', () => {
144-
renderView({ registrationComplete: false })
144+
renderView({ registrationComplete: false, status: 'closed-won' })
145145
expect(screen.getByTestId('portal-section')).toBeInTheDocument()
146146
})
147147

148+
it('shows warning instead of portal when sponsor is not closed-won', () => {
149+
renderView({ registrationComplete: false, status: 'negotiating' })
150+
expect(
151+
screen.getByText(
152+
/Move the sponsor to Closed Won before sending registration/,
153+
),
154+
).toBeInTheDocument()
155+
expect(screen.queryByTestId('portal-section')).not.toBeInTheDocument()
156+
})
157+
148158
it('shows completion message when registration is done', () => {
149159
renderView({
150160
registrationComplete: true,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { isScheduleInPast, isScheduleToday } from '@/lib/program/time-utils'
2+
3+
describe('program/time-utils.ts', () => {
4+
describe('isScheduleInPast', () => {
5+
it('should return true for dates in the past', () => {
6+
const currentTime = new Date('2025-10-28T12:00:00')
7+
const scheduleDate = '2025-10-27'
8+
expect(isScheduleInPast(scheduleDate, currentTime)).toBe(true)
9+
})
10+
11+
it('should return false for today', () => {
12+
const currentTime = new Date('2025-10-27T12:00:00')
13+
const scheduleDate = '2025-10-27'
14+
expect(isScheduleInPast(scheduleDate, currentTime)).toBe(false)
15+
})
16+
17+
it('should return false for dates in the future', () => {
18+
const currentTime = new Date('2025-10-27T12:00:00')
19+
const scheduleDate = '2025-10-28'
20+
expect(isScheduleInPast(scheduleDate, currentTime)).toBe(false)
21+
})
22+
23+
it('should handle dates far in the past', () => {
24+
const currentTime = new Date('2025-10-27T12:00:00')
25+
const scheduleDate = '2024-01-01'
26+
expect(isScheduleInPast(scheduleDate, currentTime)).toBe(true)
27+
})
28+
29+
it('should handle dates far in the future', () => {
30+
const currentTime = new Date('2025-10-27T12:00:00')
31+
const scheduleDate = '2026-12-31'
32+
expect(isScheduleInPast(scheduleDate, currentTime)).toBe(false)
33+
})
34+
35+
it('should ignore time of day (only compare dates)', () => {
36+
// Morning
37+
const morningTime = new Date('2025-10-28T08:00:00')
38+
expect(isScheduleInPast('2025-10-27', morningTime)).toBe(true)
39+
40+
// Evening
41+
const eveningTime = new Date('2025-10-28T23:59:59')
42+
expect(isScheduleInPast('2025-10-27', eveningTime)).toBe(true)
43+
44+
// Today should still be false regardless of time
45+
expect(isScheduleInPast('2025-10-28', morningTime)).toBe(false)
46+
expect(isScheduleInPast('2025-10-28', eveningTime)).toBe(false)
47+
})
48+
})
49+
50+
describe('isScheduleToday', () => {
51+
it('should return true when schedule date matches current date', () => {
52+
const currentTime = new Date('2025-10-27T12:00:00')
53+
const scheduleDate = '2025-10-27'
54+
expect(isScheduleToday(scheduleDate, currentTime)).toBe(true)
55+
})
56+
57+
it('should return false when schedule date is in the past', () => {
58+
const currentTime = new Date('2025-10-28T12:00:00')
59+
const scheduleDate = '2025-10-27'
60+
expect(isScheduleToday(scheduleDate, currentTime)).toBe(false)
61+
})
62+
63+
it('should return false when schedule date is in the future', () => {
64+
const currentTime = new Date('2025-10-27T12:00:00')
65+
const scheduleDate = '2025-10-28'
66+
expect(isScheduleToday(scheduleDate, currentTime)).toBe(false)
67+
})
68+
})
69+
70+
describe('integration: isScheduleInPast and isScheduleToday', () => {
71+
it('should have mutually exclusive results for past and today', () => {
72+
const currentTime = new Date('2025-10-27T12:00:00')
73+
const yesterdayDate = '2025-10-26'
74+
const todayDate = '2025-10-27'
75+
const tomorrowDate = '2025-10-28'
76+
77+
// Yesterday
78+
expect(isScheduleInPast(yesterdayDate, currentTime)).toBe(true)
79+
expect(isScheduleToday(yesterdayDate, currentTime)).toBe(false)
80+
81+
// Today
82+
expect(isScheduleInPast(todayDate, currentTime)).toBe(false)
83+
expect(isScheduleToday(todayDate, currentTime)).toBe(true)
84+
85+
// Tomorrow
86+
expect(isScheduleInPast(tomorrowDate, currentTime)).toBe(false)
87+
expect(isScheduleToday(tomorrowDate, currentTime)).toBe(false)
88+
})
89+
})
90+
})

__tests__/lib/proposal/schemas.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,75 @@ describe('ProposalInputSchema (strict)', () => {
121121
expect(result.success).toBe(false)
122122
})
123123

124-
it('requires capacity for workshop formats', () => {
124+
it('accepts workshop format with capacity', () => {
125+
const result = ProposalInputSchema.safeParse({
126+
...fullProposal,
127+
format: Format.workshop_120,
128+
capacity: 30,
129+
})
130+
expect(result.success).toBe(true)
131+
})
132+
133+
it('accepts workshop format without capacity', () => {
134+
const result = ProposalInputSchema.safeParse({
135+
...fullProposal,
136+
format: Format.workshop_120,
137+
})
138+
expect(result.success).toBe(true)
139+
})
140+
141+
it('accepts workshop format with prerequisites', () => {
125142
const result = ProposalInputSchema.safeParse({
126143
...fullProposal,
127144
format: Format.workshop_120,
145+
capacity: 30,
146+
prerequisites: 'Bring a computer with Docker installed',
147+
})
148+
expect(result.success).toBe(true)
149+
})
150+
151+
it('accepts non-workshop format without prerequisites', () => {
152+
const result = ProposalInputSchema.safeParse({
153+
...fullProposal,
154+
format: Format.presentation_40,
155+
prerequisites: undefined,
156+
})
157+
expect(result.success).toBe(true)
158+
})
159+
160+
it('rejects prerequisites for non-workshop formats', () => {
161+
const result = ProposalInputSchema.safeParse({
162+
...fullProposal,
163+
format: Format.presentation_40,
164+
prerequisites: 'Should not be allowed',
128165
})
129166
expect(result.success).toBe(false)
130167
})
131168

132-
it('accepts workshop format with capacity', () => {
169+
it('normalizes empty prerequisites to undefined', () => {
170+
const result = ProposalInputSchema.safeParse({
171+
...fullProposal,
172+
format: Format.workshop_120,
173+
capacity: 30,
174+
prerequisites: ' ',
175+
})
176+
expect(result.success).toBe(true)
177+
if (result.success) {
178+
expect(result.data.prerequisites).toBeUndefined()
179+
}
180+
})
181+
182+
it('trims whitespace from prerequisites', () => {
133183
const result = ProposalInputSchema.safeParse({
134184
...fullProposal,
135185
format: Format.workshop_120,
136186
capacity: 30,
187+
prerequisites: ' Docker required ',
137188
})
138189
expect(result.success).toBe(true)
190+
if (result.success) {
191+
expect(result.data.prerequisites).toBe('Docker required')
192+
}
139193
})
140194

141195
it('rejects missing title', () => {

__tests__/lib/proposal/validation.test.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -103,42 +103,6 @@ describe('validateProposalForm', () => {
103103
expect(errors.tos).toBe(PROPOSAL_VALIDATION_MESSAGES.TOS_REQUIRED)
104104
})
105105

106-
it('returns capacity error for workshop_120 without capacity', () => {
107-
const errors = validateProposalForm(
108-
makeValidProposal({ format: Format.workshop_120 }),
109-
)
110-
expect(errors.capacity).toBe(PROPOSAL_VALIDATION_MESSAGES.CAPACITY_REQUIRED)
111-
})
112-
113-
it('returns capacity error for workshop_240 without capacity', () => {
114-
const errors = validateProposalForm(
115-
makeValidProposal({ format: Format.workshop_240 }),
116-
)
117-
expect(errors.capacity).toBe(PROPOSAL_VALIDATION_MESSAGES.CAPACITY_REQUIRED)
118-
})
119-
120-
it('accepts workshop format with capacity', () => {
121-
const errors = validateProposalForm(
122-
makeValidProposal({ format: Format.workshop_120, capacity: 30 }),
123-
)
124-
expect(errors.capacity).toBeUndefined()
125-
})
126-
127-
it('does not return capacity error for non-workshop formats', () => {
128-
const errors = validateProposalForm(
129-
makeValidProposal({ format: Format.presentation_40 }),
130-
)
131-
expect(errors.capacity).toBeUndefined()
132-
})
133-
134-
it('suppresses capacity check when requireCapacity is false', () => {
135-
const errors = validateProposalForm(
136-
makeValidProposal({ format: Format.workshop_120 }),
137-
{ requireCapacity: false },
138-
)
139-
expect(errors.capacity).toBeUndefined()
140-
})
141-
142106
it('returns multiple errors simultaneously', () => {
143107
const errors = validateProposalForm(
144108
makeValidProposal({
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { speakerImageUrl } from '@/lib/sanity/client'
3+
4+
describe('speakerImageUrl', () => {
5+
const sanityCdnUrl =
6+
'https://cdn.sanity.io/images/testproject/production/abc123-200x200.jpg'
7+
const githubAvatarUrl = 'https://avatars.githubusercontent.com/u/12345?v=4'
8+
const linkedinAvatarUrl =
9+
'https://media.licdn.com/dms/image/v2/abc123/profile-photo.jpg'
10+
11+
it('should transform Sanity CDN URLs through the image builder', () => {
12+
const result = speakerImageUrl(sanityCdnUrl)
13+
14+
expect(result).toContain('cdn.sanity.io')
15+
})
16+
17+
it('should return GitHub avatar URLs unchanged', () => {
18+
expect(speakerImageUrl(githubAvatarUrl)).toBe(githubAvatarUrl)
19+
})
20+
21+
it('should return LinkedIn avatar URLs unchanged', () => {
22+
expect(speakerImageUrl(linkedinAvatarUrl)).toBe(linkedinAvatarUrl)
23+
})
24+
25+
it('should not transform non-Sanity URLs even with custom options', () => {
26+
const result = speakerImageUrl(githubAvatarUrl, {
27+
width: 800,
28+
height: 800,
29+
fit: 'crop',
30+
})
31+
32+
expect(result).toBe(githubAvatarUrl)
33+
})
34+
35+
it('should not transform plain http URLs', () => {
36+
const httpUrl = 'http://example.com/photo.jpg'
37+
expect(speakerImageUrl(httpUrl)).toBe(httpUrl)
38+
})
39+
})

0 commit comments

Comments
 (0)