Skip to content

Commit 296fc5c

Browse files
test(website): keep upcoming-events coverage when nothing is scheduled (Comfy-Org#15160)
*PR Created by the Glary-Bot Agent* --- ## Summary The "Upcoming events" list on `/events` is derived from the wall clock, so it empties out once the newest configured event ends — which silently reduced every assertion about those rows to a no-op. This restores that coverage in a form that does not depend on the calendar. ## Changes - **What**: - New `UpcomingEventsSection.test.ts` stubs the derived `upcomingEvents` list (`vi.mock` + `importOriginal`) and covers what the e2e can no longer see: one row per event, localized title/blurb/location/date, and the streamed-vs-external link branch (`/events/{id}` vs the external href with `target=_blank` + `rel`), including the zh-CN event-page href. - `events.spec.ts` now asserts the list container renders even when there are no rows, so a zero-row page is still a real assertion rather than a vacuous `toHaveCount(0)`. - Corrected the comment on the derivation (`BUILD_NOW` → `NOW`): the events islands re-evaluate it in the browser on hydration, so classification is **not** fixed at build time as the old comment claimed. ## Review Focus **Why prod is empty is not a bug in the fetch/render logic.** The data is not remote — it is the hard-coded array in `src/data/events.ts`. The only dynamic input is the clock: `deriveUpcomingEvents` keeps events whose end (start + 1h default) is still in the future. The newest configured event, `video-model-showdown`, ended `2026-08-12T18:00Z`, and nothing is scheduled after it, so `upcomingEvents` is `[]` and the `ul` renders as `<!--[--><!--]-->`. A locally built `dist/events/index.html` reproduces the reported prod markup byte-for-byte. The page needs a content refresh, not a code fix. **Two findings worth separate follow-ups (deliberately not changed here):** 1. `<UpcomingEventsSection client:visible />` ships the full events array plus `new Date()` to the client, so the upcoming/past split is re-derived from the *visitor's* clock at hydration. Freezing the browser clock to `2026-08-01` against the current build makes 2 rows appear where the build rendered 0, along with a burst of Vue `Hydration text/children mismatch` warnings — i.e. real visitors on a build older than an event's end get a visible post-hydration content shift on `/events`. Making it deterministic per-deploy is a behaviour change (a finished livestream would linger as "upcoming" until the next deploy), so it needs a product call. 2. With zero upcoming events the section still renders its heading over an empty card (first screenshot). An empty state — hiding the section, or a "nothing scheduled yet" line — would be an improvement, but that is a design decision. Follows up Comfy-Org#15150, which stopped the CI failure by guarding the one unguarded test; this addresses the coverage hole that guard left behind. ## Screenshots Both taken against a local production build. The first is today's real clock (reproduces prod); the second freezes the browser clock to `2026-08-01`, showing the render logic itself is healthy. ## Verification - `pnpm test:unit` (website): 42 files, 399 tests passing, including the 3 new ones. - `pnpm typecheck` (astro check): 0 errors, 0 warnings. - oxfmt, oxlint `--type-aware`, and eslint clean on the changed files (also via pre-commit hooks). - `playwright test events.spec.ts --project=desktop --project=mobile`: 7 passed, 2 skipped (the two time-dependent tests, correctly skipped with zero upcoming events). - Mutation-checked rather than assumed: inverting the link branch fails 2 of the new tests, deleting the location text fails a third, and replacing the `ul` with a `div` fails the new e2e assertion — which previously passed silently. ## Screenshots ![Upcoming events section on a local production build with the real clock: heading over an empty card, reproducing the empty ul reported on prod](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/a13b72487621d011f2a2accee147afd95038136721ed1c44890d8816aeb483cd/pr-images/1786574131744-00a7b135-0bac-48f8-b344-0644294b936a.png) ![Same build with the browser clock frozen to 2026-08-01: two upcoming event rows render with location, date, Add to calendar, and Livestream links](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/a13b72487621d011f2a2accee147afd95038136721ed1c44890d8816aeb483cd/pr-images/1786574132210-2700dd84-d951-4e6f-81c7-ddb39420ad62.png) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
1 parent 1e16aa9 commit 296fc5c

3 files changed

Lines changed: 130 additions & 6 deletions

File tree

apps/website/e2e/events.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ test.describe('Events page — desktop @smoke', () => {
185185
for (const [path, locale] of LOCALES) {
186186
await page.goto(path)
187187
const section = upcomingSection(page, locale)
188+
// Every configured event ages out eventually, so the row assertions
189+
// scale to zero — the list itself has to render either way.
190+
await expect(section.getByRole('list')).toBeAttached()
188191
const rows = section.locator('li')
189192
await expect(rows).toHaveCount(upcomingEvents.length)
190193

apps/website/src/data/events.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -576,15 +576,17 @@ const events: readonly ComfyEvent[] = [
576576
}
577577
]
578578

579-
// The site is statically built, so classification is fixed at build time: an
580-
// event moves between the upcoming and past sections on the next deploy.
581-
const BUILD_NOW = new Date()
579+
// Sampled once per module load: at build time for the pre-rendered HTML, and
580+
// again in the browser when the events islands hydrate. An event therefore
581+
// leaves the upcoming section on the first page load after it ends, rather than
582+
// on the next deploy; a page left open keeps the list it hydrated with.
583+
const NOW = new Date()
582584

583-
export const upcomingEvents = deriveUpcomingEvents(events, BUILD_NOW)
585+
export const upcomingEvents = deriveUpcomingEvents(events, NOW)
584586

585-
export const pastEvents = derivePastEvents(events, BUILD_NOW)
587+
export const pastEvents = derivePastEvents(events, NOW)
586588

587-
export const featuredEvents = deriveFeaturedEvents(events, BUILD_NOW)
589+
export const featuredEvents = deriveFeaturedEvents(events, NOW)
588590

589591
export const watchablePastEvents: readonly ComfyEvent[] = pastEvents.filter(
590592
(event) => eventVideoId(event)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// @vitest-environment happy-dom
2+
import { render, screen } from '@testing-library/vue'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import type * as EventsData from '../../data/events'
6+
7+
import UpcomingEventsSection from './UpcomingEventsSection.vue'
8+
9+
// `upcomingEvents` is derived from the wall clock, so it empties out once the
10+
// newest configured event has ended and the e2e coverage of these rows goes
11+
// quiet with it. Stubbing the list keeps the row markup, localization, and
12+
// link targets covered whatever the date and whatever happens to be scheduled.
13+
const { streamedEvent, externalEvent, stub } = vi.hoisted(() => {
14+
const streamedEvent = {
15+
id: 'streamed-event',
16+
category: 'livestream',
17+
title: { en: 'Streamed Event', 'zh-CN': '直播活动' },
18+
description: { en: 'Watch it live.', 'zh-CN': '在线观看。' },
19+
location: { en: 'Online', 'zh-CN': '线上' },
20+
dateLabel: {
21+
en: 'September 1, 2026 · 10AM PT',
22+
'zh-CN': '2026年9月1日 · 上午10点(PT)'
23+
},
24+
startDateTime: '2026-09-01T10:00:00-07:00',
25+
liveVideoId: 'live123'
26+
} satisfies EventsData.ComfyEvent
27+
const externalEvent = {
28+
id: 'external-event',
29+
category: 'community',
30+
title: { en: 'External Event', 'zh-CN': '外部活动' },
31+
description: { en: 'Hosted elsewhere.', 'zh-CN': '由他方举办。' },
32+
location: { en: 'San Francisco', 'zh-CN': '旧金山' },
33+
dateLabel: { en: 'September 8, 2026', 'zh-CN': '2026年9月8日' },
34+
startDateTime: '2026-09-08T10:00:00-07:00',
35+
link: {
36+
href: { en: 'https://lu.ma/comfy-sf', 'zh-CN': 'https://lu.ma/comfy-sf' },
37+
newTab: true
38+
}
39+
} satisfies EventsData.ComfyEvent
40+
const upcomingEvents: readonly EventsData.ComfyEvent[] = [
41+
streamedEvent,
42+
externalEvent
43+
]
44+
return { streamedEvent, externalEvent, stub: { upcomingEvents } }
45+
})
46+
47+
vi.mock('../../data/events', async (importOriginal) => {
48+
const actual = await importOriginal<typeof EventsData>()
49+
return {
50+
...actual,
51+
get upcomingEvents() {
52+
return stub.upcomingEvents
53+
}
54+
}
55+
})
56+
57+
beforeEach(() => {
58+
stub.upcomingEvents = [streamedEvent, externalEvent]
59+
})
60+
61+
describe('UpcomingEventsSection', () => {
62+
it('renders a row per upcoming event with its title, blurb, location and date', () => {
63+
render(UpcomingEventsSection)
64+
65+
const rows = screen.getAllByRole('listitem')
66+
expect(rows).toHaveLength(2)
67+
68+
for (const [index, event] of [streamedEvent, externalEvent].entries()) {
69+
const row = rows[index]
70+
expect(row.textContent).toContain(event.title.en)
71+
expect(row.textContent).toContain(event.description.en)
72+
expect(row.textContent).toContain(event.location.en)
73+
expect(row.textContent).toContain(event.dateLabel.en)
74+
}
75+
})
76+
77+
it('links streamed events to their own page and the rest straight out', () => {
78+
render(UpcomingEventsSection)
79+
80+
const streamedLink = screen.getByRole('link', {
81+
name: `${streamedEvent.title.en} — Livestream`
82+
})
83+
expect(streamedLink.getAttribute('href')).toBe('/events/streamed-event')
84+
expect(streamedLink.getAttribute('target')).toBeNull()
85+
86+
const externalLink = screen.getByRole('link', {
87+
name: `${externalEvent.title.en} — Livestream`
88+
})
89+
expect(externalLink.getAttribute('href')).toBe(externalEvent.link.href.en)
90+
expect(externalLink.getAttribute('target')).toBe('_blank')
91+
expect(externalLink.getAttribute('rel')).toBe('noopener noreferrer')
92+
})
93+
94+
// The live page sits in this state whenever the schedule runs dry, so the
95+
// list has to survive an empty derivation rather than disappear with it.
96+
it('keeps the list in place when nothing is upcoming', () => {
97+
stub.upcomingEvents = []
98+
99+
render(UpcomingEventsSection)
100+
101+
expect(screen.getByRole('list')).toBeTruthy()
102+
expect(screen.queryAllByRole('listitem')).toHaveLength(0)
103+
})
104+
105+
it('localizes rows and event-page links for the zh-CN page', () => {
106+
render(UpcomingEventsSection, { props: { locale: 'zh-CN' } })
107+
108+
expect(screen.getByText(streamedEvent.title['zh-CN'])).toBeTruthy()
109+
expect(screen.getByText(streamedEvent.dateLabel['zh-CN'])).toBeTruthy()
110+
expect(screen.queryByText(streamedEvent.title.en)).toBeNull()
111+
112+
const streamedLink = screen.getByRole('link', {
113+
name: `${streamedEvent.title['zh-CN']} — 直播`
114+
})
115+
expect(streamedLink.getAttribute('href')).toBe(
116+
'/zh-CN/events/streamed-event'
117+
)
118+
})
119+
})

0 commit comments

Comments
 (0)