|
22 | 22 | const { selectHotIssueCards } = await server.ssrLoadModule("/src/data/hotIssueSelection.ts"); |
23 | 23 | const { selectNewsTrackingCards } = await server.ssrLoadModule("/src/data/newsTracking.ts"); |
24 | 24 | const { selectLatestCivicWatchItems } = await server.ssrLoadModule("/src/data/civicWatchFeed.ts"); |
| 25 | + const { getEditorialSeries } = await server.ssrLoadModule("/src/data/editorialSeries.ts"); |
| 26 | + const { getColumnsNewestFirst } = await server.ssrLoadModule("/src/data/columns.ts"); |
| 27 | + const { selectHomeStandaloneColumns } = await server.ssrLoadModule("/src/data/homeColumnSelection.ts"); |
| 28 | + |
25 | 29 |
|
26 | 30 | assert.equal(canonicalHomePath("/en/columns/example/?utm_source=test#top"), "/columns/example"); |
27 | 31 | assert.equal(getHomeTopic("/en/news/olympic-park-protest-115-days/"), |
|
56 | 60 | assert.deepEqual(selectLatestCivicWatchItems(civic, new Set(["/en/news/olympic-park-protest-115-days/"]), 8).map((item) => item.to), |
57 | 61 | [fixtures[1].to, fixtures[3].to], "Civic-watch exclusions must canonicalize aliases"); |
58 | 62 |
|
| 63 | + // A new published chapter automatically leaves the standalone homepage |
| 64 | + // column desk even if it is not the most recently published series chapter. |
| 65 | + const standaloneFixtures = [ |
| 66 | + { slug: "series-chapter-1" }, |
| 67 | + { slug: "series-chapter-2" }, |
| 68 | + { slug: "standalone-1" }, |
| 69 | + { slug: "standalone-2" }, |
| 70 | + { slug: "standalone-3" }, |
| 71 | + ]; |
| 72 | + const serializedFixtures = [{ chapters: [ |
| 73 | + { path: "/columns/series-chapter-1" }, |
| 74 | + { path: "/en/columns/series-chapter-2/?ref=series" }, |
| 75 | + ] }]; |
| 76 | + assert.deepEqual( |
| 77 | + selectHomeStandaloneColumns(standaloneFixtures, serializedFixtures, new Set(), 2).map((c) => c.slug), |
| 78 | + ["standalone-1", "standalone-2"], |
| 79 | + "Published series chapters must never take slots in the homepage column desk", |
| 80 | + ); |
| 81 | + assert.deepEqual( |
| 82 | + selectHomeStandaloneColumns(standaloneFixtures, serializedFixtures, new Set([getHomeTopic("/columns/standalone-1")]), 2).map((c) => c.slug), |
| 83 | + ["standalone-2", "standalone-3"], |
| 84 | + "Excluded chapters and claimed articles must be skipped before limiting", |
| 85 | + ); |
| 86 | + assert.equal(standaloneFixtures.length, 5, "Filtering cannot delete published column records"); |
| 87 | + |
59 | 88 | // The carousel intentionally adds off-screen DOM clones for wraparound; |
60 | 89 | // these are one desk, not repeat placements. Deduplicate within a section, |
61 | 90 | // then reject any article identity found in another homepage section. |
|
68 | 97 | React.createElement(LanguageProvider, null, React.createElement(Home)))); |
69 | 98 | const sections = [...html.matchAll(/<section\b[^>]*>[\s\S]*?<\/section>/g)]; |
70 | 99 | assert(sections.length >= 6, "Homepage sections must render"); |
| 100 | + |
| 101 | + // Series cards display an actual chapter headline without linking directly |
| 102 | + // to its article. Checking article hrefs alone misses that visible repeat. |
| 103 | + const language = route === "/" ? "ko" : "en"; |
| 104 | + const series = getEditorialSeries(language); |
| 105 | + const serializedPaths = new Set(series.flatMap((item) => |
| 106 | + item.chapters.map((chapter) => canonicalHomePath(chapter.path)), |
| 107 | + )); |
| 108 | + assert(serializedPaths.size > 0, `${route}: expected published editorial series chapters`); |
| 109 | + assert(getColumnsNewestFirst().some((column) => |
| 110 | + serializedPaths.has(`/columns/${column.slug}`)), |
| 111 | + `${route}: regression scenario must contain series chapters in the full columns archive`); |
| 112 | + const columnsSection = sections.find((section) => section[0].includes('>COLUMNS</p>')); |
| 113 | + assert(columnsSection, `${route}: standalone column desk must exist`); |
| 114 | + const renderedColumns = [...columnsSection[0].matchAll(/<a\b[^>]*href="([^"]+)"/g)] |
| 115 | + .map((match) => canonicalHomePath(match[1])) |
| 116 | + .filter((path) => /^\/columns\/[^/]+$/.test(path)); |
| 117 | + assert(renderedColumns.length >= 3, `${route}: excluding series must preserve a useful standalone column desk`); |
| 118 | + assert(renderedColumns.every((path) => !serializedPaths.has(path)), |
| 119 | + `${route}: serialized article repeated in standalone columns: ${renderedColumns.filter((path) => serializedPaths.has(path)).join(", ")}`); |
| 120 | + const seriesSection = sections.find((section) => section[0].includes('id="editorial-series-title"')); |
| 121 | + assert(seriesSection && seriesSection[0].includes('href="/series/'), |
| 122 | + `${route}: editorial series navigation must remain available`); |
| 123 | + |
71 | 124 | const seen = new Map(); |
72 | 125 | let checked = 0; |
73 | 126 | for (const [sectionIndex, match] of sections.entries()) { |
|
0 commit comments