From b7c1d55bba3bf6bb556590fe3529cb4fd73695bc Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Wed, 12 Aug 2026 22:25:07 +0000 Subject: [PATCH 1/3] test(website): keep upcoming-events coverage when nothing is scheduled The upcoming list is derived from the wall clock, so it empties out once the newest configured event ends. Every assertion about those rows then scaled to zero and the section's behaviour silently stopped being tested. Cover the rows in a component test that stubs the derived list, so row markup, localization, and link targets stay verified whatever the date. Keep the e2e clock-honest and have it assert the list container renders even with no rows. Also correct the comment on the derivation: the events islands re-evaluate it in the browser on hydration, so classification is not fixed at build time. --- apps/website/e2e/events.spec.ts | 3 + apps/website/src/data/events.ts | 13 +-- .../events/UpcomingEventsSection.test.ts | 94 +++++++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 apps/website/src/templates/events/UpcomingEventsSection.test.ts diff --git a/apps/website/e2e/events.spec.ts b/apps/website/e2e/events.spec.ts index 8e878b668c2..349dc89ecd5 100644 --- a/apps/website/e2e/events.spec.ts +++ b/apps/website/e2e/events.spec.ts @@ -165,6 +165,9 @@ test.describe('Events page — desktop @smoke', () => { for (const [path, locale] of LOCALES) { await page.goto(path) const section = upcomingSection(page, locale) + // Every configured event ages out eventually, so the row assertions + // scale to zero — the list itself has to render either way. + await expect(section.getByRole('list')).toBeAttached() const rows = section.locator('li') await expect(rows).toHaveCount(upcomingEvents.length) diff --git a/apps/website/src/data/events.ts b/apps/website/src/data/events.ts index feedd944b6b..a99a614c952 100644 --- a/apps/website/src/data/events.ts +++ b/apps/website/src/data/events.ts @@ -399,15 +399,16 @@ const events: readonly ComfyEvent[] = [ } ] -// The site is statically built, so classification is fixed at build time: an -// event moves between the upcoming and past sections on the next deploy. -const BUILD_NOW = new Date() +// Evaluated once per module load: at build time for the pre-rendered HTML, and +// again in the browser when the events islands hydrate. An event therefore +// leaves the upcoming section as soon as it ends, without waiting for a deploy. +const NOW = new Date() -export const upcomingEvents = deriveUpcomingEvents(events, BUILD_NOW) +export const upcomingEvents = deriveUpcomingEvents(events, NOW) -export const pastEvents = derivePastEvents(events, BUILD_NOW) +export const pastEvents = derivePastEvents(events, NOW) -export const featuredEvents = deriveFeaturedEvents(events, BUILD_NOW) +export const featuredEvents = deriveFeaturedEvents(events, NOW) export const watchablePastEvents: readonly ComfyEvent[] = pastEvents.filter( (event) => eventVideoId(event) diff --git a/apps/website/src/templates/events/UpcomingEventsSection.test.ts b/apps/website/src/templates/events/UpcomingEventsSection.test.ts new file mode 100644 index 00000000000..80ea8e4e376 --- /dev/null +++ b/apps/website/src/templates/events/UpcomingEventsSection.test.ts @@ -0,0 +1,94 @@ +// @vitest-environment happy-dom +import { render, screen } from '@testing-library/vue' +import { describe, expect, it, vi } from 'vitest' + +import type * as EventsData from '../../data/events' + +import UpcomingEventsSection from './UpcomingEventsSection.vue' + +// `upcomingEvents` is derived from the wall clock, so it empties out once the +// newest configured event has ended and the e2e coverage of these rows goes +// quiet with it. Stubbing the list keeps the row markup, localization, and +// link targets covered whatever the date and whatever happens to be scheduled. +const { streamedEvent, externalEvent } = vi.hoisted(() => ({ + streamedEvent: { + id: 'streamed-event', + category: 'livestream', + title: { en: 'Streamed Event', 'zh-CN': '直播活动' }, + description: { en: 'Watch it live.', 'zh-CN': '在线观看。' }, + location: { en: 'Online', 'zh-CN': '线上' }, + dateLabel: { + en: 'September 1, 2026 · 10AM PT', + 'zh-CN': '2026年9月1日 · 上午10点(PT)' + }, + startDateTime: '2026-09-01T10:00:00-07:00', + liveVideoId: 'live123' + } satisfies EventsData.ComfyEvent, + externalEvent: { + id: 'external-event', + category: 'community', + title: { en: 'External Event', 'zh-CN': '外部活动' }, + description: { en: 'Hosted elsewhere.', 'zh-CN': '由他方举办。' }, + location: { en: 'San Francisco', 'zh-CN': '旧金山' }, + dateLabel: { en: 'September 8, 2026', 'zh-CN': '2026年9月8日' }, + startDateTime: '2026-09-08T10:00:00-07:00', + link: { + href: { en: 'https://lu.ma/comfy-sf', 'zh-CN': 'https://lu.ma/comfy-sf' }, + newTab: true + } + } satisfies EventsData.ComfyEvent +})) + +vi.mock('../../data/events', async (importOriginal) => ({ + ...(await importOriginal()), + upcomingEvents: [streamedEvent, externalEvent] +})) + +describe('UpcomingEventsSection', () => { + it('renders a row per upcoming event with its title, blurb, location and date', () => { + render(UpcomingEventsSection) + + const rows = screen.getAllByRole('listitem') + expect(rows).toHaveLength(2) + + for (const [index, event] of [streamedEvent, externalEvent].entries()) { + const row = rows[index] + expect(row.textContent).toContain(event.title.en) + expect(row.textContent).toContain(event.description.en) + expect(row.textContent).toContain(event.location.en) + expect(row.textContent).toContain(event.dateLabel.en) + } + }) + + it('links streamed events to their own page and the rest straight out', () => { + render(UpcomingEventsSection) + + const streamedLink = screen.getByRole('link', { + name: `${streamedEvent.title.en} — Livestream` + }) + expect(streamedLink.getAttribute('href')).toBe('/events/streamed-event') + expect(streamedLink.getAttribute('target')).toBeNull() + + const externalLink = screen.getByRole('link', { + name: `${externalEvent.title.en} — Livestream` + }) + expect(externalLink.getAttribute('href')).toBe(externalEvent.link.href.en) + expect(externalLink.getAttribute('target')).toBe('_blank') + expect(externalLink.getAttribute('rel')).toBe('noopener noreferrer') + }) + + it('localizes rows and event-page links for the zh-CN page', () => { + render(UpcomingEventsSection, { props: { locale: 'zh-CN' } }) + + expect(screen.getByText(streamedEvent.title['zh-CN'])).toBeTruthy() + expect(screen.getByText(streamedEvent.dateLabel['zh-CN'])).toBeTruthy() + expect(screen.queryByText(streamedEvent.title.en)).toBeNull() + + const streamedLink = screen.getByRole('link', { + name: `${streamedEvent.title['zh-CN']} — 直播` + }) + expect(streamedLink.getAttribute('href')).toBe( + '/zh-CN/events/streamed-event' + ) + }) +}) From d6977c94ac305255fd2c1a58a9b77079a4bf1587 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Wed, 12 Aug 2026 22:30:32 +0000 Subject: [PATCH 2/3] docs(website): tighten the events derivation comment The list is re-derived on hydration, so an event drops out on the first page load after it ends -- not the instant it ends. --- apps/website/src/data/events.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/website/src/data/events.ts b/apps/website/src/data/events.ts index a99a614c952..82b78c938eb 100644 --- a/apps/website/src/data/events.ts +++ b/apps/website/src/data/events.ts @@ -399,9 +399,10 @@ const events: readonly ComfyEvent[] = [ } ] -// Evaluated once per module load: at build time for the pre-rendered HTML, and +// Sampled once per module load: at build time for the pre-rendered HTML, and // again in the browser when the events islands hydrate. An event therefore -// leaves the upcoming section as soon as it ends, without waiting for a deploy. +// leaves the upcoming section on the first page load after it ends, rather than +// on the next deploy; a page left open keeps the list it hydrated with. const NOW = new Date() export const upcomingEvents = deriveUpcomingEvents(events, NOW) From e7c3b2509747ecd0f98ebba725db840451219170 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Wed, 12 Aug 2026 22:42:32 +0000 Subject: [PATCH 3/3] test(website): cover the empty upcoming-events derivation Read the stubbed list through a getter so a test can empty it, and assert the list element survives an empty derivation. That is the state the live page is in whenever the schedule runs dry, and the e2e only reaches it by accident of the calendar. --- .../events/UpcomingEventsSection.test.ts | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/website/src/templates/events/UpcomingEventsSection.test.ts b/apps/website/src/templates/events/UpcomingEventsSection.test.ts index 80ea8e4e376..b80127b031b 100644 --- a/apps/website/src/templates/events/UpcomingEventsSection.test.ts +++ b/apps/website/src/templates/events/UpcomingEventsSection.test.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom import { render, screen } from '@testing-library/vue' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as EventsData from '../../data/events' @@ -10,8 +10,8 @@ import UpcomingEventsSection from './UpcomingEventsSection.vue' // newest configured event has ended and the e2e coverage of these rows goes // quiet with it. Stubbing the list keeps the row markup, localization, and // link targets covered whatever the date and whatever happens to be scheduled. -const { streamedEvent, externalEvent } = vi.hoisted(() => ({ - streamedEvent: { +const { streamedEvent, externalEvent, stub } = vi.hoisted(() => { + const streamedEvent = { id: 'streamed-event', category: 'livestream', title: { en: 'Streamed Event', 'zh-CN': '直播活动' }, @@ -23,8 +23,8 @@ const { streamedEvent, externalEvent } = vi.hoisted(() => ({ }, startDateTime: '2026-09-01T10:00:00-07:00', liveVideoId: 'live123' - } satisfies EventsData.ComfyEvent, - externalEvent: { + } satisfies EventsData.ComfyEvent + const externalEvent = { id: 'external-event', category: 'community', title: { en: 'External Event', 'zh-CN': '外部活动' }, @@ -37,12 +37,26 @@ const { streamedEvent, externalEvent } = vi.hoisted(() => ({ newTab: true } } satisfies EventsData.ComfyEvent -})) + const upcomingEvents: readonly EventsData.ComfyEvent[] = [ + streamedEvent, + externalEvent + ] + return { streamedEvent, externalEvent, stub: { upcomingEvents } } +}) + +vi.mock('../../data/events', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + get upcomingEvents() { + return stub.upcomingEvents + } + } +}) -vi.mock('../../data/events', async (importOriginal) => ({ - ...(await importOriginal()), - upcomingEvents: [streamedEvent, externalEvent] -})) +beforeEach(() => { + stub.upcomingEvents = [streamedEvent, externalEvent] +}) describe('UpcomingEventsSection', () => { it('renders a row per upcoming event with its title, blurb, location and date', () => { @@ -77,6 +91,17 @@ describe('UpcomingEventsSection', () => { expect(externalLink.getAttribute('rel')).toBe('noopener noreferrer') }) + // The live page sits in this state whenever the schedule runs dry, so the + // list has to survive an empty derivation rather than disappear with it. + it('keeps the list in place when nothing is upcoming', () => { + stub.upcomingEvents = [] + + render(UpcomingEventsSection) + + expect(screen.getByRole('list')).toBeTruthy() + expect(screen.queryAllByRole('listitem')).toHaveLength(0) + }) + it('localizes rows and event-page links for the zh-CN page', () => { render(UpcomingEventsSection, { props: { locale: 'zh-CN' } })