diff --git a/e2e/accessibility-focus.e2e.ts b/e2e/accessibility-focus.e2e.ts index cf9df1a4..d32b3029 100644 --- a/e2e/accessibility-focus.e2e.ts +++ b/e2e/accessibility-focus.e2e.ts @@ -25,7 +25,7 @@ async function expectNoSeriousAccessibilityViolations(page: Page) { async function openStableDemo(page: Page) { await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page.getByRole("heading", { name: "Demo timetable" })).toBeVisible(); await expect(page.getByRole("button", { name: "Update timetable" })).toBeEnabled(); } diff --git a/e2e/accessibility.e2e.ts b/e2e/accessibility.e2e.ts index 7f3f5aea..7625494c 100644 --- a/e2e/accessibility.e2e.ts +++ b/e2e/accessibility.e2e.ts @@ -37,7 +37,7 @@ test("core release journey has no serious or critical automatic a11y violations" await expectLanding(page); await expectNoSeriousAccessibilityViolations(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page.getByRole("heading", { name: "Demo timetable" })).toBeVisible(); // The timetable shell becomes visible before demo initialization finishes. // Scan the stable interactive state rather than the brief disabled/loading transition. diff --git a/e2e/adversarial-smoke-routes.e2e.ts b/e2e/adversarial-smoke-routes.e2e.ts index 70bd7213..653c6be2 100644 --- a/e2e/adversarial-smoke-routes.e2e.ts +++ b/e2e/adversarial-smoke-routes.e2e.ts @@ -1,46 +1,39 @@ import { expect, test } from "@playwright/test"; import { watchForAppFailures } from "./helpers"; -test("Adversarially smoke-test live routes across UTM, Guelph, McMaster, York, and Brock", async ({ - browser, - baseURL, -}) => { - test.skip(test.info().project.name !== "chromium"); - if (!baseURL) throw new Error("Playwright baseURL is required"); - - const targets = [ - { id: "uoft", query: "", name: "University of Toronto", sampleCourse: "DEM101H5" }, - { - id: "guelph", - query: "?university=guelph", - name: "University of Guelph", - sampleCourse: "CIS 1300", - }, - { - id: "mcmaster", - query: "?university=mcmaster", - name: "McMaster University", - sampleCourse: "COMPSCI 1MD3", - }, - { id: "york", query: "?university=york", name: "York University", sampleCourse: "EECS 1022" }, - { - id: "brock", - query: "?university=brock", - name: "Brock University", - sampleCourse: "COSC 1P02", - }, - ]; - - for (const target of targets) { - console.log(`Smoke testing UI route for ${target.name} (${target.id})...`); - const context = await browser.newContext({ baseURL }); - const page = await context.newPage(); +const targets = [ + { id: "uoft", query: "", name: "University of Toronto", sampleCourse: "DEM101H5" }, + { + id: "guelph", + query: "?university=guelph", + name: "University of Guelph", + sampleCourse: "CIS 1300", + }, + { + id: "mcmaster", + query: "?university=mcmaster", + name: "McMaster University", + sampleCourse: "COMPSCI 1MD3", + }, + { id: "york", query: "?university=york", name: "York University", sampleCourse: "EECS 1022" }, + { + id: "brock", + query: "?university=brock", + name: "Brock University", + sampleCourse: "COSC 1P02", + }, +] as const; + +for (const target of targets) { + test(`adversarial route smoke test for ${target.name}`, async ({ page, baseURL }) => { + test.skip(test.info().project.name !== "chromium"); + if (!baseURL) throw new Error("Playwright baseURL is required"); const failures = watchForAppFailures(page, baseURL); await page.goto(`/${target.query}`); // Click demo button to load campus schedule - const demoButton = page.getByRole("button", { name: "Try a demo" }); + const demoButton = page.getByRole("button", { name: /Try the .* demo/ }); await expect(demoButton).toBeVisible(); await demoButton.click(); @@ -59,6 +52,5 @@ test("Adversarially smoke-test live routes across UTM, Guelph, McMaster, York, a ).toBeVisible(); failures.assertClean(); - await context.close(); - } -}); + }); +} diff --git a/e2e/and66.e2e.ts b/e2e/and66.e2e.ts index c3a75c78..5768b871 100644 --- a/e2e/and66.e2e.ts +++ b/e2e/and66.e2e.ts @@ -38,15 +38,13 @@ test("AND-66 first-run landing keeps activation clear on a narrow phone", async await page.setViewportSize({ width: 360, height: 740 }); await expectLanding(page); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); await expect( page.getByText( "Plan classes, buildings, routes, and time between classes across the Mississauga campus.", ), ).toBeVisible(); - await expect(page.getByRole("button", { name: "Try a demo" })).toBeVisible(); + await expect(page.getByRole("button", { name: /Try the .* demo/ })).toBeVisible(); await expect(page.getByRole("link", { name: "Need help importing?" })).toHaveAttribute( "href", "/acorn-import", diff --git a/e2e/clickable-gaps.e2e.ts b/e2e/clickable-gaps.e2e.ts index 9a0b9771..54c9c6c5 100644 --- a/e2e/clickable-gaps.e2e.ts +++ b/e2e/clickable-gaps.e2e.ts @@ -15,7 +15,7 @@ test("a timetable gap opens Gap Plan with that exact interval selected", async ( const mobile = isMobileProject(testInfo.project.name); await page.goto("/"); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); if (mobile) { const nav = page.getByRole("navigation", { name: "Main" }); diff --git a/e2e/core.e2e.ts b/e2e/core.e2e.ts index c5c90c28..7c94593d 100644 --- a/e2e/core.e2e.ts +++ b/e2e/core.e2e.ts @@ -13,11 +13,9 @@ const twoTermFixturePath = path.join( test("landing page is usable without an account", async ({ page }, testInfo) => { const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await expect(page).toHaveTitle( - "Gapwise for University of Toronto Mississauga — Timetable & Campus Navigation", - ); + await expect(page).toHaveTitle("Gapwise for University of Toronto Mississauga"); await expect( - page.getByText("For University of Toronto Mississauga", { exact: true }), + page.getByText("Gapwise for University of Toronto Mississauga", { exact: true }), ).toBeVisible(); await expect(page.getByRole("button", { name: "Import ACORN" })).toBeVisible(); guard.assertClean(); @@ -28,9 +26,7 @@ test("bare fragments are removed without enabling hash routing", async ({ page } const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await page.goto("/#"); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); await expect.poll(() => page.url().endsWith("#")).toBe(false); await page.evaluate(() => { @@ -54,18 +50,14 @@ test("path navigation, refresh, and browser history use the SPA fallback", async await expect(page.getByRole("heading", { name: "Page not found" })).toBeVisible(); await page.getByRole("link", { name: "Go home" }).click(); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); expect(new URL(page.url()).pathname).toBe("/"); expect(new URL(page.url()).hash).toBe(""); await page.goBack(); await expect(page.getByRole("heading", { name: "Page not found" })).toBeVisible(); await page.goForward(); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); guard.assertClean(); }); @@ -118,7 +110,7 @@ test("route-driven navigation preserves a loaded timetable through history", asy const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable$/); await expect(page.getByRole("heading", { name: "Demo timetable" })).toBeVisible(); @@ -151,9 +143,7 @@ test("route-driven navigation preserves a loaded timetable through history", asy await page.getByRole("link", { name: "Gapwise home" }).click(); await expect(page).toHaveURL(/\/$/); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); await page.goBack(); await expect(page).toHaveURL(/\/route\/?$/); @@ -167,7 +157,7 @@ test("timetable export offers available terms and downloads light and dark PNGs" test.skip(testInfo.project.name !== "chromium", "PNG export coverage runs once in Chromium"); const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable$/); await page.getByRole("button", { name: "Account settings" }).click(); @@ -276,7 +266,7 @@ test("campus-day arrival settings route transit and parking through the map", as test.skip(testInfo.project.name !== "chromium", "commuter route coverage runs once"); const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await page .getByRole("group", { name: "View mode" }) .getByRole("button", { name: "Day route" }) @@ -546,7 +536,10 @@ test("mobile term switching selects a scheduled weekday", async ({ page }, testI ].join("\r\n"), ), }); - await page.getByRole("link", { name: "Timetable" }).click(); + await page + .getByRole("navigation", { name: "Main" }) + .getByRole("link", { name: "Timetable", exact: true }) + .click(); const weekdays = page.getByRole("group", { name: "Weekday" }); await weekdays.getByRole("button", { name: /^Mon/ }).click(); await expect(page.getByRole("heading", { name: "Monday", exact: true })).toBeVisible(); @@ -565,8 +558,11 @@ test("mobile weekday buttons expose native keyboard selection state", async ({ test.skip(testInfo.project.name !== "mobile-chromium", "mobile keyboard regression runs once"); const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); - await page.getByRole("link", { name: "Timetable" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); + await page + .getByRole("navigation", { name: "Main" }) + .getByRole("link", { name: "Timetable", exact: true }) + .click(); const weekdays = page.getByRole("group", { name: "Weekday" }); const monday = weekdays.getByRole("button", { name: /^Mon/ }); @@ -591,9 +587,7 @@ test("malformed calendar fails safely with a useful error", async ({ page }, tes }); await expect(page.getByRole("alert")).toContainText("doesn't look like a calendar export"); - const landingHeading = isMobileProject(testInfo.project.name) - ? "Start with your timetable." - : "Make every gap on campus count."; + const landingHeading = "Make every gap at UTM count."; await expect(page.getByRole("heading", { name: landingHeading })).toBeVisible(); guard.assertClean(); }); @@ -603,7 +597,7 @@ test("desktop demo moves between timetable, gaps, and route", async ({ page }, t const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page.getByRole("heading", { name: "Demo timetable" })).toBeVisible(); const visibleGapWindows = page.getByTestId("gap-window"); await expect(visibleGapWindows.first()).toBeVisible(); @@ -624,7 +618,7 @@ test("mobile demo exposes the primary timetable and map navigation", async ({ pa const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); const nav = page.getByRole("navigation", { name: "Main" }); await expect(nav).toBeVisible(); await expect(nav.getByRole("link", { name: "Today" })).toBeVisible(); diff --git a/e2e/day-route-map.e2e.ts b/e2e/day-route-map.e2e.ts index 83058c1d..7abfef7c 100644 --- a/e2e/day-route-map.e2e.ts +++ b/e2e/day-route-map.e2e.ts @@ -35,7 +35,7 @@ test.describe("map-first Day Route", () => { }); await page.goto("/"); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); if (test.info().project.name === "mobile-chromium") { const nav = page.getByRole("navigation", { name: "Main" }); diff --git a/e2e/destination-checker.e2e.ts b/e2e/destination-checker.e2e.ts index 411a5ae7..52841e30 100644 --- a/e2e/destination-checker.e2e.ts +++ b/e2e/destination-checker.e2e.ts @@ -11,7 +11,7 @@ test("Today checks canonical destinations inside a live gap on mobile Safari", a await page.clock.setFixedTime(new Date("2026-09-07T15:30:00.000Z")); await page.goto("/today"); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: "Try a demo", exact: true }).click(); await expect(page).toHaveURL(/\/today$/); await expect(page.getByRole("heading", { name: "Can I go there?" })).toBeVisible(); await expect( diff --git a/e2e/entrance-marker-anchor-contract.e2e.ts b/e2e/entrance-marker-anchor-contract.e2e.ts index 619b6207..bdd7f49c 100644 --- a/e2e/entrance-marker-anchor-contract.e2e.ts +++ b/e2e/entrance-marker-anchor-contract.e2e.ts @@ -8,7 +8,7 @@ test("entrance geographic anchor stays dimensionless and MapLibre-owned", async const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await page .getByRole("group", { name: "View mode" }) .getByRole("button", { name: "Day route" }) diff --git a/e2e/entrance-marker-stability.e2e.ts b/e2e/entrance-marker-stability.e2e.ts index b6a09d53..14f0c039 100644 --- a/e2e/entrance-marker-stability.e2e.ts +++ b/e2e/entrance-marker-stability.e2e.ts @@ -185,9 +185,9 @@ test("entrance markers keep MapLibre projection isolated from interactive stylin const projectionUrl = new URL(page.url()); projectionUrl.searchParams.set("e2eMapProjection", "1"); await page.goto(projectionUrl.toString()); - await expect(page.getByRole("button", { name: "Try a demo" })).toBeVisible(); + await expect(page.getByRole("button", { name: /Try the .* demo/ })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await page .getByRole("group", { name: "View mode" }) .getByRole("button", { name: "Day route" }) diff --git a/e2e/gap-plan-mobile-scroll.e2e.ts b/e2e/gap-plan-mobile-scroll.e2e.ts index f4a9101d..20129441 100644 --- a/e2e/gap-plan-mobile-scroll.e2e.ts +++ b/e2e/gap-plan-mobile-scroll.e2e.ts @@ -8,7 +8,7 @@ test("mobile gap tool sheets keep their content vertically scrollable", async ({ const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); const nav = page.getByRole("navigation", { name: "Main" }); await expect(nav).toBeVisible(); await nav.getByRole("link", { name: "Timetable" }).click(); diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 81f639ef..9abaa727 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -74,12 +74,7 @@ export function watchForAppFailures(page: Page, baseURL: string) { export async function expectLanding(page: Page) { await page.goto("/"); - const mobile = (page.viewportSize()?.width ?? 1280) < 768; - await expect( - page.getByRole("heading", { - name: mobile ? "Start with your timetable." : "Make every gap on campus count.", - }), - ).toBeVisible(); - await expect(page.getByRole("button", { name: "Try a demo" })).toBeVisible(); + await expect(page.locator(".university-home h1, .global-home h1").first()).toBeVisible(); + await expect(page.locator(".university-home-primary").first()).toBeVisible(); await expect(page.locator("#ics-file")).toHaveCount(1); } diff --git a/e2e/mcgill-university.e2e.ts b/e2e/mcgill-university.e2e.ts index 3d408b99..463cc78a 100644 --- a/e2e/mcgill-university.e2e.ts +++ b/e2e/mcgill-university.e2e.ts @@ -10,7 +10,7 @@ test("McGill calendar import preserves downtown campus search and routing", asyn const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "mcgill")); - await expect(page.getByText("For McGill University", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for McGill University", { exact: true })).toBeVisible(); await expect(page.locator(".brand-scope-pill").first()).toHaveText("McGill"); await page @@ -49,7 +49,7 @@ test("McGill demo remains usable on the mobile campus map", async ({ page, baseU const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "mcgill")); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); await expect(page.getByText("COMP 202").first()).toBeVisible(); diff --git a/e2e/mobile-header-nav.e2e.ts b/e2e/mobile-header-nav.e2e.ts index 7a2cf769..6ad5fb8e 100644 --- a/e2e/mobile-header-nav.e2e.ts +++ b/e2e/mobile-header-nav.e2e.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -test.describe("mobile header and secondary navigation", () => { +test.describe("public header and university hero", () => { const viewports = [ { width: 320, height: 568, name: "narrow-320" }, { width: 375, height: 667, name: "iphone-se-375" }, @@ -13,7 +13,7 @@ test.describe("mobile header and secondary navigation", () => { ]; for (const vp of viewports) { - test(`header and secondary nav never overlap on ${vp.name} (${vp.width}x${vp.height})`, async ({ + test(`header and university hero fit on ${vp.name} (${vp.width}x${vp.height})`, async ({ page, }) => { await page.setViewportSize({ width: vp.width, height: vp.height }); @@ -21,48 +21,29 @@ test.describe("mobile header and secondary navigation", () => { await page.waitForLoadState("networkidle"); const header = page.locator(".desktop-app-header"); - const nav = page.locator(".product-story-nav"); + const hero = page.locator(".university-home-hero"); await expect(header).toBeVisible(); - await expect(nav).toBeVisible(); + await expect(hero).toBeVisible(); const headerBox = await header.boundingBox(); - const navBox = await nav.boundingBox(); + const heroBox = await hero.boundingBox(); expect(headerBox).not.toBeNull(); - expect(navBox).not.toBeNull(); - - console.log(`[${vp.name}] header:`, headerBox, "nav:", navBox); - - // Primary header and secondary navigation must NEVER overlap. - // The secondary nav top must be at or below the primary header bottom. - expect(navBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height - 0.5); - - // Secondary navigation links should not wrap - const linksContainer = page.locator(".product-story-nav > div"); - const isSingleLine = await linksContainer.evaluate((el) => { - const links = Array.from(el.querySelectorAll("a")); - if (links.length < 2) return true; - const firstTop = links[0].getBoundingClientRect().top; - return links.every((link) => Math.abs(link.getBoundingClientRect().top - firstTop) < 4); - }); - expect(isSingleLine).toBe(true); + expect(heroBox).not.toBeNull(); + expect(heroBox!.y).toBeGreaterThanOrEqual(headerBox!.y + headerBox!.height - 0.5); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual( + vp.width, + ); + await expect(page.locator(".brand-lockup")).toContainText("Gapwise"); // Check after scrolling await page.evaluate(() => window.scrollTo(0, 400)); await page.waitForTimeout(100); const headerBoxScrolled = await header.boundingBox(); - const navBoxScrolled = await nav.boundingBox(); - expect(headerBoxScrolled).not.toBeNull(); - expect(navBoxScrolled).not.toBeNull(); - - console.log(`[${vp.name} scrolled] header:`, headerBoxScrolled, "nav:", navBoxScrolled); - - expect(navBoxScrolled!.y).toBeGreaterThanOrEqual( - headerBoxScrolled!.y + headerBoxScrolled!.height - 0.5, - ); + expect(headerBoxScrolled!.y).toBe(0); }); } }); diff --git a/e2e/print-export.e2e.ts b/e2e/print-export.e2e.ts index 33f85558..f1d138a4 100644 --- a/e2e/print-export.e2e.ts +++ b/e2e/print-export.e2e.ts @@ -5,7 +5,7 @@ test("unified timetable export exposes the print-ready vector flow", async ({ pa test.skip(testInfo.project.name !== "chromium", "print export coverage runs once in Chromium"); const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); await expectLanding(page); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable$/); await expect(page.getByRole("button", { name: "Print-ready" })).toHaveCount(0); diff --git a/e2e/pwa.e2e.ts b/e2e/pwa.e2e.ts index 29fecc02..d993c384 100644 --- a/e2e/pwa.e2e.ts +++ b/e2e/pwa.e2e.ts @@ -71,9 +71,7 @@ test("PWA metadata, service worker, and cached app shell are functional", async await context.setOffline(true); try { await page.reload({ waitUntil: "domcontentloaded" }); - await expect( - page.getByRole("heading", { name: "Make every gap on campus count." }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Make every gap at UTM count." })).toBeVisible(); expect(await page.evaluate(() => Boolean(navigator.serviceWorker?.controller))).toBe(true); } finally { await context.setOffline(false); diff --git a/e2e/site-roles.e2e.ts b/e2e/site-roles.e2e.ts index f3f1a701..4724ee0a 100644 --- a/e2e/site-roles.e2e.ts +++ b/e2e/site-roles.e2e.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; -import { watchForAppFailures } from "./helpers"; +import manifest from "../universities.json" with { type: "json" }; +import { editionUrl, watchForAppFailures } from "./helpers"; test("global homepage presents the multi-university Gapwise ecosystem", async ({ page, @@ -21,6 +22,29 @@ test("global homepage presents the multi-university Gapwise ecosystem", async ({ failures.assertClean(); }); +test("reserved hosts publish an honest planned state and canonicalize globally", async ({ + page, + baseURL, +}) => { + test.skip(test.info().project.name !== "chromium"); + if (!baseURL) throw new Error("Playwright baseURL is required"); + const failures = watchForAppFailures(page, baseURL); + + await page.goto(editionUrl(baseURL, "harvard")); + await expect( + page.getByRole("heading", { name: "Harvard University edition coming soon" }), + ).toBeVisible(); + await expect(page).toHaveTitle("Gapwise — University Timetable & Campus Navigation"); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + "href", + "https://gapwise.ca/", + ); + await expect(page.locator('meta[name="robots"]')).toHaveAttribute("content", "noindex, nofollow"); + await expect(page.getByText(/preparing campus data and timetable adapters/)).toBeVisible(); + await expect(page.getByText(/Mapped buildings|Routing segments/)).toHaveCount(0); + failures.assertClean(); +}); + test("global university chooser finds registry aliases and direct campus editions", async ({ page, baseURL, @@ -110,7 +134,10 @@ test("University of Toronto hub links to three distinct campus editions", async ["University of Toronto St. George", "https://utsg.gapwise.ca"], ["University of Toronto Scarborough", "https://utsc.gapwise.ca"], ] as const) { - await expect(page.getByRole("link", { name: new RegExp(name) })).toHaveAttribute("href", host); + await expect(page.getByRole("link", { name: new RegExp(name) }).first()).toHaveAttribute( + "href", + host, + ); } failures.assertClean(); }); @@ -129,7 +156,7 @@ test("U of T campus editions retain campus-scoped import experiences", async ({ ["utsc", "University of Toronto Scarborough"], ] as const) { await page.goto(`/?campus=${campus}`); - await expect(page.getByText(`For ${name}`, { exact: true })).toBeVisible(); + await expect(page.getByText(`Gapwise for ${name}`, { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Import ACORN" })).toBeVisible(); } failures.assertClean(); @@ -186,7 +213,7 @@ test("narrow UTM header preserves the full brand and contains its controls", asy await page.goto("/?campus=utm"); await expect( - page.getByText("For University of Toronto Mississauga", { exact: true }), + page.getByText("Gapwise for University of Toronto Mississauga", { exact: true }), ).toBeVisible(); const header = await page.evaluate(() => { const brand = document.querySelector(".brand-lockup"); @@ -219,3 +246,97 @@ test("narrow UTM header preserves the full brand and contains its controls", asy await page.screenshot({ path: test.info().outputPath("utm-320.png"), fullPage: false }); failures.assertClean(); }); + +const supportedEditions = [ + ["uoft", "Three campuses. One clearer university day."], + ["carleton", "Navigate Carleton. Plan your schedule. Get to class."], + ["tmu", "Move through downtown campus with your day in view."], + ["queens", "Plan Queen's days from first lecture to last walk."], + ["laurier", "Make the walk between Laurier classes part of the plan."], + ["york", "Keep your Keele campus day connected."], + ["mcmaster", "Your McMaster schedule, mapped to campus."], + ["western", "Plan the space between classes at Western."], + ["guelph", "Build a calmer day across the Guelph campus."], + ["uottawa", "See your uOttawa day before you cross campus."], + ["brock", "Turn your Brock schedule into a campus plan."], + ["ubc", "Plan the distance between UBC Vancouver classes."], + ["waterloo", "Keep your Waterloo schedule and campus route together."], + ["mcgill", "Connect your McGill timetable to downtown campus."], +] as const; + +test("every supported university edition renders isolated registry-driven identity and metadata", async ({ + page, + baseURL, +}) => { + test.skip(test.info().project.name !== "chromium"); + test.setTimeout(90_000); + if (!baseURL) throw new Error("Playwright baseURL is required"); + const failures = watchForAppFailures(page, baseURL); + + for (const [id, headline] of supportedEditions) { + const expectedTitle = manifest.universities.find((university) => university.id === id) + ?.marketing.seoTitle; + expect(expectedTitle).toBeTruthy(); + await page.goto(`/?university=${id}`); + await expect(page.getByRole("heading", { name: headline })).toBeVisible(); + await expect(page).toHaveTitle(expectedTitle!); + await expect(page.locator(".university-home-stats")).toBeVisible(); + for (const [otherId, otherHeadline] of supportedEditions) { + if (otherId !== id) + await expect(page.getByRole("heading", { name: otherHeadline })).toHaveCount(0); + } + } + failures.assertClean(); +}); + +test("campus aliases render distinct U of T editions without identity leaks", async ({ + page, + baseURL, +}) => { + test.skip(test.info().project.name !== "chromium"); + if (!baseURL) throw new Error("Playwright baseURL is required"); + const failures = watchForAppFailures(page, baseURL); + const campuses = [ + ["utm", "Make every gap at UTM count.", "University of Toronto Mississauga"], + ["utsg", "Plan the distance between downtown classes.", "University of Toronto St. George"], + ["utsc", "See your next move across UTSC.", "University of Toronto Scarborough"], + ] as const; + + for (const [campus, headline, identity] of campuses) { + await page.goto(`/?campus=${campus}`); + await expect(page.getByRole("heading", { name: headline })).toBeVisible(); + await expect(page).toHaveTitle(new RegExp(`Gapwise for ${identity}`)); + for (const [otherCampus, otherHeadline] of campuses) { + if (otherCampus !== campus) + await expect(page.getByRole("heading", { name: otherHeadline })).toHaveCount(0); + } + } + failures.assertClean(); +}); + +for (const viewport of [ + { width: 390, height: 844 }, + { width: 1440, height: 900 }, +]) { + test(`university hero is usable without horizontal overflow at ${viewport.width}px`, async ({ + page, + baseURL, + }) => { + test.skip(test.info().project.name !== "chromium"); + if (!baseURL) throw new Error("Playwright baseURL is required"); + await page.setViewportSize(viewport); + const failures = watchForAppFailures(page, baseURL); + await page.goto("/?university=carleton"); + await expect(page.getByRole("button", { name: "Try the Carleton demo" })).toBeVisible(); + const layout = await page.evaluate(() => ({ + documentWidth: document.documentElement.scrollWidth, + viewportWidth: window.innerWidth, + ctaBottom: document + .querySelector(".university-home-primary") + ?.getBoundingClientRect().bottom, + })); + expect(layout.documentWidth).toBeLessThanOrEqual(layout.viewportWidth); + if (viewport.width >= 1000) expect(layout.ctaBottom).toBeLessThan(viewport.height); + failures.assertClean(); + }); +} diff --git a/e2e/trust-auth-academic.e2e.ts b/e2e/trust-auth-academic.e2e.ts index 71a9ab52..7590a5d5 100644 --- a/e2e/trust-auth-academic.e2e.ts +++ b/e2e/trust-auth-academic.e2e.ts @@ -115,7 +115,7 @@ test("academic planning journey accepts work and exposes safe block actions", as const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL)); const mobile = isMobileProject(testInfo.project.name); await page.goto("/"); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); if (mobile) { const nav = page.getByRole("navigation", { name: "Main" }); diff --git a/e2e/ubc-university.e2e.ts b/e2e/ubc-university.e2e.ts index 3d60a4e1..cbdb51f1 100644 --- a/e2e/ubc-university.e2e.ts +++ b/e2e/ubc-university.e2e.ts @@ -16,7 +16,9 @@ test("UBC Workday paste import preserves the Vancouver campus through timetable const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "ubc")); - await expect(page.getByText("For University of British Columbia", { exact: true })).toBeVisible(); + await expect( + page.getByText("Gapwise for University of British Columbia", { exact: true }), + ).toBeVisible(); await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC"); await page.getByRole("button", { name: "Paste Workday View My Courses rows" }).click(); @@ -69,8 +71,10 @@ test("UBC has its own demo timetable and usable mobile campus map", async ({ pag const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "ubc")); - await expect(page.getByText("For University of British Columbia", { exact: true })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await expect( + page.getByText("Gapwise for University of British Columbia", { exact: true }), + ).toBeVisible(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); await expect(page.getByText("CPSC 110").first()).toBeVisible(); diff --git a/e2e/university-context.e2e.ts b/e2e/university-context.e2e.ts index 871c677f..bf37a38b 100644 --- a/e2e/university-context.e2e.ts +++ b/e2e/university-context.e2e.ts @@ -10,9 +10,9 @@ test("Carleton uses the canonical product screens and campus dataset", async ({ const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "carleton")); - await expect(page.getByText("For Carleton University", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for Carleton University", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: /Import Carleton/ })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); if (test.info().project.name === "mobile-chromium") { await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); @@ -44,9 +44,7 @@ test("explicit UTM local host preserves the campus edition", async ({ page, base if (!baseURL) throw new Error("Playwright baseURL is required"); const failures = watchForAppFailures(page, baseURL); await page.goto("/"); - await expect(page).toHaveTitle( - "Gapwise for University of Toronto Mississauga — Timetable & Campus Navigation", - ); + await expect(page).toHaveTitle("Gapwise for University of Toronto Mississauga"); await expect(page.getByRole("button", { name: "Import ACORN" })).toBeVisible(); failures.assertClean(); }); @@ -133,12 +131,12 @@ test("Direct URL refresh preserves university context in local development", asy const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "carleton")); - await expect(page.getByText("For Carleton University", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for Carleton University", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: /Import Carleton/ })).toBeVisible(); // Refresh page await page.reload(); - await expect(page.getByText("For Carleton University", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for Carleton University", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: /Import Carleton/ })).toBeVisible(); failures.assertClean(); }); @@ -154,12 +152,12 @@ test("TMU, Queen's, and Laurier use canonical product screens and single-campus // TMU await page.goto(editionUrl(baseURL, "tmu")); await expect( - page.getByText("For Toronto Metropolitan University", { exact: true }), + page.getByText("Gapwise for Toronto Metropolitan University", { exact: true }), ).toBeVisible(); await expect( page.getByRole("button", { name: /Import (MyServiceHub|pasted schedule)/ }), ).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); if (test.info().project.name === "mobile-chromium") { await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); @@ -168,9 +166,9 @@ test("TMU, Queen's, and Laurier use canonical product screens and single-campus // Queen's await page.goto(editionUrl(baseURL, "queens")); - await expect(page.getByText("For Queen's University", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for Queen's University", { exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: /Import (SOLUS|pasted schedule)/ })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); if (test.info().project.name === "mobile-chromium") { await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); @@ -179,9 +177,11 @@ test("TMU, Queen's, and Laurier use canonical product screens and single-campus // Laurier await page.goto(editionUrl(baseURL, "laurier")); - await expect(page.getByText("For Wilfrid Laurier University", { exact: true })).toBeVisible(); + await expect( + page.getByText("Gapwise for Wilfrid Laurier University", { exact: true }), + ).toBeVisible(); await expect(page.getByRole("button", { name: /Import (LORIS|pasted schedule)/ })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); if (test.info().project.name === "mobile-chromium") { await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); diff --git a/e2e/uoft-campus-inference.e2e.ts b/e2e/uoft-campus-inference.e2e.ts index 3c0eabdd..270d523a 100644 --- a/e2e/uoft-campus-inference.e2e.ts +++ b/e2e/uoft-campus-inference.e2e.ts @@ -26,19 +26,16 @@ async function openDayRoute(page: Page, mobile: boolean) { } } -test("ACORN building evidence activates the correct UTM, UTSG, and UTSC products", async ({ - page, - baseURL, -}) => { - test.skip(test.info().project.name !== "chromium"); - if (!baseURL) throw new Error("Playwright baseURL is required"); - const failures = watchForAppFailures(page, baseURL); +for (const [name, campus, firstCode] of [ + ["acorn-utm-sanitized.ics", "UTM", "MN"], + ["acorn-utsg-sanitized.ics", "UTSG", "BA"], + ["acorn-utsc-sanitized.ics", "UTSC", "HL"], +] as const) { + test(`ACORN building evidence activates the ${campus} product`, async ({ page, baseURL }) => { + test.skip(test.info().project.name !== "chromium"); + if (!baseURL) throw new Error("Playwright baseURL is required"); + const failures = watchForAppFailures(page, baseURL); - for (const [name, campus, firstCode] of [ - ["acorn-utm-sanitized.ics", "UTM", "MN"], - ["acorn-utsg-sanitized.ics", "UTSG", "BA"], - ["acorn-utsc-sanitized.ics", "UTSC", "HL"], - ] as const) { await importFixture(page, name); await openDayRoute(page, false); @@ -67,10 +64,10 @@ test("ACORN building evidence activates the correct UTM, UTSG, and UTSC products await expect(page.getByRole("searchbox", { name: `Search ${campus} buildings` })).toBeVisible(); await page.reload(); await expect(page.getByRole("searchbox", { name: `Search ${campus} buildings` })).toBeVisible(); - } - failures.assertClean(); -}); + failures.assertClean(); + }); +} test("mixed and ambiguous ACORN locations preserve the explicit campus edition", async ({ page, diff --git a/e2e/waterloo-university.e2e.ts b/e2e/waterloo-university.e2e.ts index 721ccd5c..ffb9b1e9 100644 --- a/e2e/waterloo-university.e2e.ts +++ b/e2e/waterloo-university.e2e.ts @@ -16,7 +16,7 @@ test("Waterloo Quest import preserves the canonical campus through timetable and const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "waterloo")); - await expect(page.getByText("For University of Waterloo", { exact: true })).toBeVisible(); + await expect(page.getByText("Gapwise for University of Waterloo", { exact: true })).toBeVisible(); await expect(page.locator(".brand-scope-pill").first()).toHaveText("Waterloo"); await page.getByRole("button", { name: "Paste Quest Class Schedule rows" }).click(); @@ -70,8 +70,8 @@ test("Waterloo has its own demo timetable and usable mobile campus map", async ( const failures = watchForAppFailures(page, baseURL); await page.goto(editionUrl(baseURL, "waterloo")); - await expect(page.getByText("For University of Waterloo", { exact: true })).toBeVisible(); - await page.getByRole("button", { name: "Try a demo" }).click(); + await expect(page.getByText("Gapwise for University of Waterloo", { exact: true })).toBeVisible(); + await page.getByRole("button", { name: /Try the .* demo/ }).click(); await expect(page).toHaveURL(/\/timetable/); await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click(); await expect(page.getByText("MATH 135").first()).toBeVisible(); diff --git a/scripts/build-seo-pages.ts b/scripts/build-seo-pages.ts index f6f18193..551e74f0 100644 --- a/scripts/build-seo-pages.ts +++ b/scripts/build-seo-pages.ts @@ -6,7 +6,24 @@ const SITE_ORIGIN = "https://gapwise.ca"; const GITHUB_ORGANIZATION = "https://github.com/GapwiseHQ"; const GITHUB_CORE = `${GITHUB_ORGANIZATION}/gapwise`; +type UniversityMarketing = { + campusName: string; + headline: string; + description: string; + seoTitle: string; + seoDescription: string; +}; + const universitiesManifest = JSON.parse(await readFile("universities.json", "utf8")) as { + sites: Array<{ + role: string; + canonicalHost: string; + universityId?: string; + campusId?: string; + name?: string; + shortName?: string; + presentation?: { marketing?: UniversityMarketing }; + }>; universities: Array<{ id: string; name: string; @@ -15,6 +32,7 @@ const universitiesManifest = JSON.parse(await readFile("universities.json", "utf hosts: string[]; campuses: string[]; enabledFeatures?: { routing?: boolean }; + marketing: UniversityMarketing; }>; }; const supportedUniversityCount = universitiesManifest.universities.length; @@ -30,48 +48,37 @@ export type UniversityContext = { origin: string; routable: boolean; campusName?: string; + marketing: UniversityMarketing; }; +const uoftUniversity = universitiesManifest.universities.find( + (university) => university.id === "uoft", +)!; +const uoftHub = universitiesManifest.sites.find( + (site) => site.role === "university-hub" && site.universityId === "uoft", +)!; export const UOFT_CONTEXT: UniversityContext = { id: "uoft", - name: "University of Toronto", - shortName: "U of T", - origin: "https://uoft.gapwise.ca", + name: uoftUniversity.name, + shortName: uoftUniversity.shortName, + origin: `https://${uoftHub.canonicalHost}`, routable: true, + marketing: uoftHub.presentation?.marketing ?? uoftUniversity.marketing, }; -const UOFT_CAMPUS_EDITIONS = [ - { - campusId: "utm", - context: { - ...UOFT_CONTEXT, - name: "University of Toronto Mississauga", - shortName: "UTM", - origin: "https://utm.gapwise.ca", - campusName: "Mississauga campus", - }, - }, - { - campusId: "utsg", - context: { - ...UOFT_CONTEXT, - name: "University of Toronto St. George", - shortName: "UTSG", - origin: "https://utsg.gapwise.ca", - campusName: "St. George campus", - }, - }, - { - campusId: "utsc", +const UOFT_CAMPUS_EDITIONS = universitiesManifest.sites + .filter((site) => site.role === "campus-edition" && site.universityId === "uoft") + .map((site) => ({ + campusId: site.campusId!, context: { ...UOFT_CONTEXT, - name: "University of Toronto Scarborough", - shortName: "UTSC", - origin: "https://utsc.gapwise.ca", - campusName: "Scarborough campus", + name: site.name!, + shortName: site.shortName!, + origin: `https://${site.canonicalHost}`, + campusName: site.presentation!.marketing!.campusName, + marketing: site.presentation!.marketing!, }, - }, -] as const satisfies ReadonlyArray<{ campusId: string; context: UniversityContext }>; + })) satisfies ReadonlyArray<{ campusId: string; context: UniversityContext }>; function socialImageUrl(uniContext?: UniversityContext) { if (!uniContext) return `${SITE_ORIGIN}/og-gapwise.png`; @@ -380,6 +387,7 @@ function metadata(page: SeoPage, uniContext?: UniversityContext) { : ""; return ` + ${title} @@ -400,7 +408,8 @@ function metadata(page: SeoPage, uniContext?: UniversityContext) { - ${schema}`; + ${schema} + `; } function fallback(page: SeoPage, uniContext?: UniversityContext) { @@ -560,11 +569,10 @@ function getUniversityPage(page: SeoPage, uni: UniversityContext): SeoPage { if (page.path === "/") { return { path: "/", - title: `Gapwise for ${uni.name} — Timetable & Campus Navigation`, - description: `Gapwise is a free and open-source timetable, campus navigation, and student planning platform for ${uni.name} students.`, - heading: "Make the time between classes count.", - detail: - "Import your class schedule, understand the usable time between classes, and explore source-backed campus maps. Guest mode and a demo work without an account.", + title: uni.marketing.seoTitle, + description: uni.marketing.seoDescription, + heading: uni.marketing.headline, + detail: uni.marketing.description, sections: [ { title: "Your timetable, connected to campus context", @@ -693,6 +701,7 @@ for (const uni of universitiesManifest.universities) { shortName: uni.shortName, origin: uniOrigin, routable: Boolean(uni.enabledFeatures?.routing), + marketing: uni.marketing, }; const uniSitemapUrl = `${uniOrigin}/sitemap.xml`; diff --git a/scripts/check-seo-output.ts b/scripts/check-seo-output.ts index 8845454b..9d97aaf0 100644 --- a/scripts/check-seo-output.ts +++ b/scripts/check-seo-output.ts @@ -41,7 +41,21 @@ function requireReference( } const universityManifest = JSON.parse(await readFile("universities.json", "utf8")) as { - universities: Array<{ id: string; name: string; hosts: string[]; status: string }>; + sites: Array<{ + role: string; + universityId?: string; + campusId?: string; + name?: string; + canonicalHost: string; + presentation?: { marketing?: { seoTitle: string; seoDescription: string } }; + }>; + universities: Array<{ + id: string; + name: string; + hosts: string[]; + status: string; + marketing: { seoTitle: string; seoDescription: string }; + }>; }; const supportedUniversityEntries = universityManifest.universities.filter( (university) => university.status === "supported", @@ -185,23 +199,15 @@ requireText(robots, "Disallow: /api/", "robots.txt"); requireText(robots, "Disallow: /oauth/", "robots.txt"); requireText(robots, "Sitemap: https://gapwise.ca/sitemap.xml", "robots.txt"); -const UOFT_CAMPUS_EDITIONS = [ - { - id: "utm", - name: "University of Toronto Mississauga", - origin: "https://utm.gapwise.ca", - }, - { - id: "utsg", - name: "University of Toronto St. George", - origin: "https://utsg.gapwise.ca", - }, - { - id: "utsc", - name: "University of Toronto Scarborough", - origin: "https://utsc.gapwise.ca", - }, -] as const; +const UOFT_CAMPUS_EDITIONS = universityManifest.sites + .filter((site) => site.role === "campus-edition" && site.universityId === "uoft") + .map((site) => ({ + id: site.campusId!, + name: site.name!, + origin: `https://${site.canonicalHost}`, + seoTitle: site.presentation!.marketing!.seoTitle, + seoDescription: site.presentation!.marketing!.seoDescription, + })); const CAMPUS_EDITION_PATHS = [ "/about", @@ -229,7 +235,7 @@ for (const campus of UOFT_CAMPUS_EDITIONS) { requireText( campusHome, - `Gapwise for ${campus.name} — Timetable & Campus Navigation`, + `${escapeHtml(campus.seoTitle)}`, `${campus.id} homepage title`, ); requireText( @@ -250,7 +256,7 @@ for (const campus of UOFT_CAMPUS_EDITIONS) { requireText(campusRobots, `Sitemap: ${campus.origin}/sitemap.xml`, `${campus.id} robots.txt`); requireText(campusSitemap, `${campus.origin}/`, `${campus.id} sitemap`); - const expectedDescription = `Gapwise is a free and open-source timetable, campus navigation, and student planning platform for ${campus.name} students.`; + const expectedDescription = escapeHtml(campus.seoDescription); requireText( campusHome, `name="description" content="${expectedDescription}"`, @@ -462,6 +468,9 @@ const ALL_UNIVERSITY_IDS = supportedUniversityEntries.map((university) => univer const ALL_UNIVERSITY_NAMES = Object.fromEntries( supportedUniversityEntries.map((university) => [university.id, university.name]), ) as Record; +const ALL_UNIVERSITY_TITLES = Object.fromEntries( + supportedUniversityEntries.map((university) => [university.id, university.marketing.seoTitle]), +) as Record; const ALL_UNIVERSITY_ORIGINS = Object.fromEntries( supportedUniversityEntries.map((university) => [university.id, `https://${university.hosts[0]}`]), ) as Record; @@ -517,7 +526,7 @@ for (const uniId of ALL_UNIVERSITY_IDS) { ``, `${uniId} twitter:image:alt`, ); - const expectedTitle = `Gapwise for ${escapedName} — Timetable & Campus Navigation`; + const expectedTitle = escapeHtml(ALL_UNIVERSITY_TITLES[uniId]!); requireText(html, ``, `${uniId} og:title`); requireText(html, `${expectedTitle}`, `${uniId} title`); diff --git a/src/components/MarketingLandingImpl.tsx b/src/components/MarketingLandingImpl.tsx index 4a7f22b0..15984677 100644 --- a/src/components/MarketingLandingImpl.tsx +++ b/src/components/MarketingLandingImpl.tsx @@ -1,490 +1,12 @@ -import { Link } from "@tanstack/react-router"; -import { useEffect, useRef, useState } from "react"; -import { UploadPanel } from "@/components/UploadPanel"; -import type { MarketingLandingProps } from "./MarketingLanding"; +import { activeSite } from "@/universities/registry"; import { GlobalMarketingHome } from "./GlobalMarketingHome"; -import { - activeUniversity, - activeSite, - SITES, - supportedUniversities, - canonicalUrlForUniversity, -} from "@/universities/registry"; -import "./marketing-landing.css"; - -function ProductMark() { - return