Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 85 additions & 9 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e2e/and66.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ test("AND-66 first-run landing keeps activation clear on a narrow phone", async
).toBeVisible();
await expect(
page.getByText(
"One precise workspace for your U of T timetable, the time between classes, and source-backed campus context where available.",
"Plan classes, buildings, routes, and time between classes across the Mississauga campus.",
),
).toBeVisible();
await expect(page.getByRole("button", { name: "Try a demo" })).toBeVisible();
Expand Down
47 changes: 17 additions & 30 deletions e2e/core.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ 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 — University Timetable & Campus Navigation");
await expect(page.getByText("For University of Toronto", { exact: true })).toBeVisible();
await expect(page).toHaveTitle(
"Gapwise for University of Toronto Mississauga — Timetable & Campus Navigation",
);
await expect(
page.getByText("For University of Toronto Mississauga", { exact: true }),
).toBeVisible();
await expect(page.getByRole("button", { name: "Import ACORN" })).toBeVisible();
guard.assertClean();
});
Expand Down Expand Up @@ -241,44 +245,27 @@ test("campus explorer supports public building deep links and local search", asy
guard.assertClean();
});

test("a first-time visitor can browse buildings on all three campus maps", async ({
test("a first-time visitor can browse buildings on the campus edition", async ({
page,
}, testInfo) => {
test.skip(
!["chromium", "mobile-chromium"].includes(testInfo.project.name),
"tri-campus map journey runs in desktop and phone Chromium",
"campus map journey runs in desktop and phone Chromium",
);
const guard = watchForAppFailures(page, String(testInfo.project.use.baseURL));

await page.goto("/route");
await expect(page.getByRole("heading", { name: "Choose a campus map" })).toBeVisible();

for (const [campus, campusId, query, building, buildingCode] of [
["UTSG", "utsg", "Bahen", "Bahen Centre for Information Technology", "BA"],
["UTSC", "utsc", "Science Wing", "Science Wing", "SW"],
["UTM", "utm", "Maanjiwe", "Maanjiwe nendamowinan", "MN"],
]) {
await page.getByRole("button", { name: campus, exact: true }).click();
const search = page.getByRole("searchbox", { name: `Search ${campus} buildings` });
await expect(search).toBeVisible();
await search.fill(query);
await search.press("Enter");
await expect(page.getByRole("heading", { name: building })).toBeVisible();
expect(new URL(page.url()).searchParams.get("campus")).toBe(campusId);
expect(new URL(page.url()).searchParams.get("building")).toBe(buildingCode);

if (campus === "UTSG") {
await page.reload();
await expect(page.getByRole("heading", { name: building })).toBeVisible();
await expect(page.getByRole("searchbox", { name: "Search UTSG buildings" })).toBeVisible();
}
}
const search = page.getByRole("searchbox", { name: "Search UTM buildings" });
await expect(search).toBeVisible();
await search.fill("Maanjiwe");
await search.press("Enter");
await expect(page.getByRole("heading", { name: "Maanjiwe nendamowinan" })).toBeVisible();
expect(new URL(page.url()).searchParams.get("campus")).toBe("utm");
expect(new URL(page.url()).searchParams.get("building")).toBe("MN");

await page.goBack();
await expect(page.getByRole("heading", { name: "Science Wing" })).toBeVisible();
await expect(page.getByRole("searchbox", { name: "Search UTSC buildings" })).toBeVisible();
await page.goForward();
await page.reload();
await expect(page.getByRole("heading", { name: "Maanjiwe nendamowinan" })).toBeVisible();
await expect(page.getByRole("searchbox", { name: "Search UTM buildings" })).toBeVisible();

guard.assertClean();
});
Expand Down
25 changes: 22 additions & 3 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,38 @@ export function isMobileProject(projectName: string) {
return projectName.startsWith("mobile-");
}

export function editionUrl(baseURL: string, host: string, path = "/") {
const url = new URL(path, baseURL);
const isLocal =
url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname.endsWith(".localhost");
if (isLocal) url.hostname = `${host}.localhost`;
else url.searchParams.set("university", host);
return url.toString();
}

export function watchForAppFailures(page: Page, baseURL: string) {
const failures: string[] = [];
const appOrigin = new URL(baseURL).origin;
const appUrl = new URL(baseURL);
const ignoredLocalInstrumentationPaths = [
"/_vercel/insights/",
"/_vercel/speed-insights/",
"/api/telemetry",
];

const isIgnoredLocalInstrumentationRequest = (url: URL) =>
url.origin === appOrigin &&
isFirstParty(url) &&
ignoredLocalInstrumentationPaths.some((path) => url.pathname.startsWith(path));

const isFirstParty = (url: URL) =>
url.origin === appOrigin ||
(appUrl.hostname.endsWith(".localhost") &&
url.hostname.endsWith(".localhost") &&
url.protocol === appUrl.protocol &&
url.port === appUrl.port);

page.on("pageerror", (error) => {
failures.push(`pageerror: ${error.message}`);
});
Expand All @@ -34,15 +53,15 @@ export function watchForAppFailures(page: Page, baseURL: string) {
page.on("response", (response) => {
const url = new URL(response.url());
if (isIgnoredLocalInstrumentationRequest(url)) return;
if (url.origin === appOrigin && response.status() >= 400) {
if (isFirstParty(url) && response.status() >= 400) {
failures.push(`HTTP ${response.status()}: ${url.pathname}`);
}
});

page.on("requestfailed", (request) => {
const url = new URL(request.url());
if (isIgnoredLocalInstrumentationRequest(url)) return;
if (url.origin !== appOrigin) return;
if (!isFirstParty(url)) return;
failures.push(`request failed: ${url.pathname} (${request.failure()?.errorText ?? "unknown"})`);
});

Expand Down
8 changes: 4 additions & 4 deletions e2e/mcgill-university.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { watchForAppFailures } from "./helpers";
import { editionUrl, watchForAppFailures } from "./helpers";

test("McGill calendar import preserves downtown campus search and routing", async ({
page,
Expand All @@ -9,7 +9,7 @@ test("McGill calendar import preserves downtown campus search and routing", asyn
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=mcgill&campus=mcgill-downtown");
await page.goto(editionUrl(baseURL, "mcgill"));
await expect(page.getByText("For McGill University", { exact: true })).toBeVisible();
await expect(page.locator(".brand-scope-pill").first()).toHaveText("McGill");

Expand Down Expand Up @@ -37,7 +37,7 @@ test("McGill calendar import preserves downtown campus search and routing", asyn
await search.fill("LEA");
await expect(page.getByTestId("building-search-result").first()).toContainText("Leacock");

await page.goto("/route?university=mcgill&campus=mcgill-downtown");
await page.goto(editionUrl(baseURL, "mcgill", "/route"));
await expect(page.locator(".brand-scope-pill").first()).toHaveText("McGill");
await expect(page.getByRole("searchbox", { name: "Search McGill buildings" })).toBeVisible();
failures.assertClean();
Expand All @@ -48,7 +48,7 @@ test("McGill demo remains usable on the mobile campus map", async ({ page, baseU
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=mcgill&campus=mcgill-downtown");
await page.goto(editionUrl(baseURL, "mcgill"));
await page.getByRole("button", { name: "Try a demo" }).click();
await expect(page).toHaveURL(/\/timetable/);
await page.getByRole("group", { name: "Weekday" }).getByRole("button", { name: /Mon/ }).click();
Expand Down
150 changes: 150 additions & 0 deletions e2e/site-roles.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { expect, test } from "@playwright/test";
import { watchForAppFailures } from "./helpers";

test("global homepage presents the multi-university Gapwise ecosystem", 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("/?site=global");
await expect(
page.getByRole("heading", { name: "Make every gap on campus count." }),
).toBeVisible();
await expect(page.locator(".global-home-facts")).toContainText("14supported universities");
await expect(page.getByRole("link", { name: /Choose your university/ })).toBeVisible();
await expect(page.getByText("For University of Toronto", { exact: true })).toHaveCount(0);
await expect(page.getByText("This edition", { exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: /Search campus and classes/ })).toHaveCount(0);
failures.assertClean();
});

test("University of Toronto hub links to three distinct campus editions", 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("/?site=uoft-hub");
await expect(
page.getByText("Gapwise for the University of Toronto", { exact: true }),
).toBeVisible();
await expect(page.getByRole("button", { name: /Search campus and classes/ })).toHaveCount(0);
for (const [name, host] of [
["University of Toronto Mississauga", "https://utm.gapwise.ca"],
["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);
}
failures.assertClean();
});

test("U of T campus editions retain campus-scoped import experiences", 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 [campus, name] of [
["utm", "University of Toronto Mississauga"],
["utsg", "University of Toronto St. George"],
["utsc", "University of Toronto Scarborough"],
] as const) {
await page.goto(`/?campus=${campus}`);
await expect(page.getByText(`For ${name}`, { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Import ACORN" })).toBeVisible();
}
failures.assertClean();
});

for (const width of [320, 360, 375, 390, 393, 430]) {
test(`mobile public header and global hero fit at ${width}px`, async ({ page, baseURL }) => {
test.skip(test.info().project.name !== "chromium");
if (!baseURL) throw new Error("Playwright baseURL is required");
await page.setViewportSize({ width, height: 844 });
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?site=global");
await expect(
page.getByRole("heading", { name: "Make every gap on campus count." }),
).toBeVisible();
const layout = await page.evaluate(() => {
const brand = document.querySelector<HTMLElement>(".brand-lockup");
const heading = document.querySelector<HTMLElement>("#global-home-title");
const brandRect = brand?.getBoundingClientRect();
const headingRect = heading?.getBoundingClientRect();
return {
viewportWidth: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
bodyWidth: document.body.scrollWidth,
brandText: brand?.innerText.trim(),
brandLeft: brandRect?.left,
brandRight: brandRect?.right,
headingHeight: headingRect?.height,
};
});
expect(layout.brandText).toBe("Gapwise");
expect(layout.documentWidth).toBeLessThanOrEqual(layout.viewportWidth);
expect(layout.bodyWidth).toBeLessThanOrEqual(layout.viewportWidth);
expect(layout.brandLeft).toBeGreaterThanOrEqual(0);
expect(layout.brandRight).toBeLessThanOrEqual(width);
expect(layout.headingHeight).toBeLessThan(260);
await page.screenshot({
path: test.info().outputPath(`global-${width}.png`),
fullPage: false,
});
failures.assertClean();
});
}

test("narrow UTM header preserves the full brand and contains its controls", async ({
page,
baseURL,
}) => {
test.skip(test.info().project.name !== "chromium");
if (!baseURL) throw new Error("Playwright baseURL is required");
await page.setViewportSize({ width: 320, height: 844 });
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?campus=utm");
await expect(
page.getByText("For University of Toronto Mississauga", { exact: true }),
).toBeVisible();
const header = await page.evaluate(() => {
const brand = document.querySelector<HTMLElement>(".brand-lockup");
const brandRect = brand?.getBoundingClientRect();
const controls = [...document.querySelectorAll<HTMLElement>(".desktop-app-header button")]
.filter((control) => control.getClientRects().length > 0)
.map((control) => {
const rect = control.getBoundingClientRect();
return { left: rect.left, right: rect.right, width: rect.width, height: rect.height };
});
return {
viewportWidth: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
brandText: brand?.innerText.trim(),
brandLeft: brandRect?.left,
brandRight: brandRect?.right,
controls,
};
});
expect(header.brandText).toBe("Gapwise");
expect(header.documentWidth).toBeLessThanOrEqual(header.viewportWidth);
expect(header.brandLeft).toBeGreaterThanOrEqual(0);
expect(header.brandRight).toBeLessThanOrEqual(320);
for (const control of header.controls) {
expect(control.left).toBeGreaterThanOrEqual(0);
expect(control.right).toBeLessThanOrEqual(320);
expect(control.width).toBeGreaterThanOrEqual(44);
expect(control.height).toBeGreaterThanOrEqual(44);
}
await page.screenshot({ path: test.info().outputPath("utm-320.png"), fullPage: false });
failures.assertClean();
});
8 changes: 4 additions & 4 deletions e2e/ubc-university.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { watchForAppFailures } from "./helpers";
import { editionUrl, watchForAppFailures } from "./helpers";

const workdayFixture = readFileSync(
new URL("../tests/fixtures/ubc-workday-sanitized.tsv", import.meta.url),
Expand All @@ -15,7 +15,7 @@ test("UBC Workday paste import preserves the Vancouver campus through timetable
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=ubc&campus=ubc-vancouver");
await page.goto(editionUrl(baseURL, "ubc"));
await expect(page.getByText("For University of British Columbia", { exact: true })).toBeVisible();
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");

Expand Down Expand Up @@ -57,7 +57,7 @@ test("UBC Workday paste import preserves the Vancouver campus through timetable
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();

await page.goto("/route?university=ubc&campus=ubc-vancouver");
await page.goto(editionUrl(baseURL, "ubc", "/route"));
await expect(page.locator(".brand-scope-pill").first()).toHaveText("UBC");
await expect(page.getByRole("searchbox", { name: "Search UBC buildings" })).toBeVisible();
failures.assertClean();
Expand All @@ -68,7 +68,7 @@ test("UBC has its own demo timetable and usable mobile campus map", async ({ pag
if (!baseURL) throw new Error("Playwright baseURL is required");
const failures = watchForAppFailures(page, baseURL);

await page.goto("/?university=ubc&campus=ubc-vancouver");
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).toHaveURL(/\/timetable/);
Expand Down
Loading
Loading