mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 15:48:23 -05:00
Differ
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import type { FullConfig } from "@playwright/test";
|
||||
import { ensureE2eBuild } from "../scripts/ensure-e2e-build";
|
||||
import { ensureMigratedDb } from "../scripts/ensure-test-db";
|
||||
import {
|
||||
E2E_BASE_PORT,
|
||||
@@ -14,15 +15,6 @@ const MINIO_MARKER_FILE = ".e2e-minio-started";
|
||||
const STORAGE_BUCKET = "sendou";
|
||||
/** Anonymously listable only once the bucket exists and its public policy is set. */
|
||||
const MINIO_BUCKET_URL = `http://127.0.0.1:9000/${STORAGE_BUCKET}/`;
|
||||
const BUILD_MARKER_FILE = ".e2e-build-marker";
|
||||
const BUILD_INPUTS = [
|
||||
"app",
|
||||
"public",
|
||||
"package.json",
|
||||
"../../pnpm-lock.yaml",
|
||||
"vite.config.ts",
|
||||
"react-router.config.ts",
|
||||
];
|
||||
|
||||
declare global {
|
||||
var __E2E_SERVERS__: ChildProcess[];
|
||||
@@ -116,40 +108,6 @@ async function waitForServer(port: number, timeout = 120000): Promise<void> {
|
||||
throw new Error(`Server on port ${port} did not start within ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last e2e build can be reused when no build input changed since its marker
|
||||
* was written. Directory mtimes catch deletes and renames; a build made outside
|
||||
* the e2e flow (no marker, or output newer than the marker) forces a rebuild, as
|
||||
* does a marker from a different port setup. E2E_FORCE_BUILD=true overrides.
|
||||
*/
|
||||
function isBuildFresh(): boolean {
|
||||
if (process.env.E2E_FORCE_BUILD === "true") return false;
|
||||
if (!fs.existsSync(BUILD_MARKER_FILE)) return false;
|
||||
if (!fs.existsSync("build/server/index.js")) return false;
|
||||
|
||||
try {
|
||||
const marker = JSON.parse(fs.readFileSync(BUILD_MARKER_FILE, "utf8"));
|
||||
if (marker.siteDomain !== `http://localhost:${E2E_BASE_PORT}`) return false;
|
||||
if (marker.skalopWsUrl !== "") return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const markerMtime = fs.statSync(BUILD_MARKER_FILE).mtimeMs;
|
||||
if (fs.statSync("build/server/index.js").mtimeMs > markerMtime) return false;
|
||||
|
||||
try {
|
||||
const changedInput = execSync(
|
||||
`find ${BUILD_INPUTS.join(" ")} -newer ${BUILD_MARKER_FILE} -print -quit`,
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
return changedInput === "";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
const workerCount = config.workers;
|
||||
|
||||
@@ -161,37 +119,13 @@ async function globalSetup(config: FullConfig) {
|
||||
|
||||
// Build the app once with E2E test flag so VITE_E2E_TEST_RUN is embedded
|
||||
// Use port 6173 as the base - tests will rewrite URLs as needed
|
||||
if (isBuildFresh()) {
|
||||
// biome-ignore lint/suspicious/noConsole: CLI script output
|
||||
console.log(
|
||||
"Reusing existing build (no source changes since last e2e build)",
|
||||
);
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole: CLI script output
|
||||
console.log("Building the application...");
|
||||
fs.rmSync(BUILD_MARKER_FILE, { force: true });
|
||||
execSync("pnpm run build", {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_E2E_TEST_RUN: "true",
|
||||
VITE_SITE_DOMAIN: `http://localhost:${E2E_BASE_PORT}`,
|
||||
// Skalop is disconnected in e2e: all workers sharing one instance
|
||||
// cross-talk (identical seeded row ids -> colliding room names ->
|
||||
// spurious revalidations). When e2e tests for chat etc. are added
|
||||
// this needs an actual solution: one skalop (or stub) per worker
|
||||
// with a runtime-derived ws URL, since this build is shared.
|
||||
VITE_SKALOP_WS_URL: "",
|
||||
},
|
||||
});
|
||||
fs.writeFileSync(
|
||||
BUILD_MARKER_FILE,
|
||||
JSON.stringify({
|
||||
siteDomain: `http://localhost:${E2E_BASE_PORT}`,
|
||||
skalopWsUrl: "",
|
||||
}),
|
||||
);
|
||||
}
|
||||
const { reused } = ensureE2eBuild(`http://localhost:${E2E_BASE_PORT}`);
|
||||
// biome-ignore lint/suspicious/noConsole: CLI script output
|
||||
console.log(
|
||||
reused
|
||||
? "Reusing existing build (no source changes since last e2e build)"
|
||||
: "Built the application",
|
||||
);
|
||||
|
||||
// Prepare databases and start servers for each worker
|
||||
const serverPromises: Promise<void>[] = [];
|
||||
|
||||
@@ -9,17 +9,17 @@ export class WeaponParamsPage {
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.locators = {
|
||||
weaponHeaders: page.locator("th[class*='weaponHeader']"),
|
||||
weaponHeaders: page.getByTestId("weapon-header"),
|
||||
hideWeaponButtons: page.locator("[data-testid^='hide-weapon-']"),
|
||||
showAllWeaponsButton: page.getByTestId("show-all-weapons"),
|
||||
compareParamButtons: page.getByTestId("compare-param"),
|
||||
comparisonDialog: page.getByRole("dialog"),
|
||||
comparisonBars: page
|
||||
.getByRole("dialog")
|
||||
.locator("[class*='bars'] [class*='row']"),
|
||||
expandableRows: page.locator("[class*='expandableRow']"),
|
||||
.getByTestId("comparison-bar-row"),
|
||||
expandableRows: page.getByTestId("expandable-row"),
|
||||
patchHistoryTab: page.getByRole("tab", { name: /Patch history/ }),
|
||||
patchColumns: page.locator("[class*='column']"),
|
||||
patchColumns: page.getByTestId("patch-column"),
|
||||
subAndSpecialChangesSwitch: page.getByRole("switch", {
|
||||
name: /Show sub & special changes/,
|
||||
}),
|
||||
@@ -68,10 +68,10 @@ class ParamHistoryRow {
|
||||
|
||||
/** Shown while the row is collapsed, hidden once expanded */
|
||||
get historyBadge() {
|
||||
return this.root.locator("[class*='historyBadge']").first();
|
||||
return this.root.getByTestId("history-badge").first();
|
||||
}
|
||||
|
||||
async toggle() {
|
||||
await this.root.locator("td[class*='paramName']").click();
|
||||
await this.root.getByTestId("param-name").click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,23 +61,23 @@ class DamageComboList {
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.root.locator(".content, [class*='content']");
|
||||
return this.root.getByTestId("damage-combo-content");
|
||||
}
|
||||
|
||||
get damageValues() {
|
||||
return this.root.locator("[class*='damageValue']");
|
||||
return this.root.getByTestId("damage-value");
|
||||
}
|
||||
|
||||
get inkTimes() {
|
||||
return this.root.locator("[class*='inkTime']");
|
||||
return this.root.getByTestId("ink-time-label");
|
||||
}
|
||||
|
||||
get damageTypeLabels() {
|
||||
return this.root.locator("[class*='damageTypeLabel']");
|
||||
return this.root.getByTestId("damage-type-label");
|
||||
}
|
||||
|
||||
get filteredItems() {
|
||||
return this.root.locator("button[class*='filteredItem']");
|
||||
return this.root.getByTestId("filtered-item");
|
||||
}
|
||||
|
||||
get subDefenseSlider() {
|
||||
|
||||
@@ -26,13 +26,15 @@ export class MobileNav {
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.openPanelDialog = page.locator("[class*='panelDialog']:visible");
|
||||
this.openPanelDialog = page.locator(
|
||||
'[data-testid="mobile-nav-panel"]:visible',
|
||||
);
|
||||
this.locators = {
|
||||
menuPanel: page.getByLabel("Menu", { exact: true }),
|
||||
streamsHeading: page.locator("h3").filter({ hasText: "Streams" }),
|
||||
viewAllLink: page.getByRole("link", { name: "View all", exact: true }),
|
||||
youPanelUsername: page.locator("[class*='youPanelUsername']"),
|
||||
friendItems: this.openPanelDialog.locator("button[class*='listButton']"),
|
||||
youPanelUsername: page.getByTestId("you-panel-username"),
|
||||
friendItems: this.openPanelDialog.getByTestId("list-button"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,13 +52,13 @@ export class MobileNav {
|
||||
*/
|
||||
async switchPanel(panel: Panel) {
|
||||
await this.page
|
||||
.locator("[class*='ghostTab']:not([class*='ghostTabBar'])")
|
||||
.getByTestId("ghost-tab")
|
||||
.nth(PANELS.indexOf(panel))
|
||||
.dispatchEvent("click");
|
||||
}
|
||||
|
||||
async closePanel() {
|
||||
await this.page.locator("button:has(svg.lucide-x)").first().click();
|
||||
await this.page.getByTestId("panel-close-button").first().click();
|
||||
}
|
||||
|
||||
menuLink(name: string) {
|
||||
|
||||
@@ -17,17 +17,17 @@ export class SideNav {
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.root = page.locator("nav[class*='sideNav']:visible");
|
||||
this.root = page.locator('[data-testid="side-nav"]:visible');
|
||||
this.locators = {
|
||||
collapseButton: page.getByTestId("sidenav-collapse-button"),
|
||||
modalTrigger: page.getByTestId("sidenav-modal-trigger"),
|
||||
unseenRequestsBadge: page.getByRole("status", {
|
||||
name: /unseen friend request/,
|
||||
}),
|
||||
upcomingDivider: this.root.locator("[class*='upcomingDivider']"),
|
||||
upcomingDivider: this.root.getByTestId("upcoming-divider"),
|
||||
/** Every row of the sidebar's link lists, in render order: events, then streams. */
|
||||
listItems: this.root.locator("a[class*='listLink']"),
|
||||
friendItems: this.root.locator("button[class*='listButton']"),
|
||||
listItems: this.root.getByTestId("list-link"),
|
||||
friendItems: this.root.getByTestId("list-button"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,14 +41,15 @@ export class SideNav {
|
||||
}
|
||||
|
||||
viewAllLink(section: "Events" | "Friends") {
|
||||
return this.sectionHeading(section)
|
||||
.locator("xpath=..")
|
||||
return this.root
|
||||
.getByTestId("side-nav-header")
|
||||
.filter({ has: this.page.getByRole("heading", { name: section }) })
|
||||
.getByRole("link", { name: /View all/ });
|
||||
}
|
||||
|
||||
/** Text shown in place of a section's rows when it has none. */
|
||||
emptyText(text: string) {
|
||||
return this.page.locator("[class*='sideNavEmpty']:visible", {
|
||||
return this.page.locator('[data-testid="side-nav-empty"]:visible', {
|
||||
hasText: text,
|
||||
});
|
||||
}
|
||||
@@ -62,11 +63,11 @@ export class SideNav {
|
||||
}
|
||||
|
||||
itemSubtitle(name: string) {
|
||||
return this.eventItem(name).locator("span[class*='listLinkSubtitle']");
|
||||
return this.eventItem(name).getByTestId("list-item-subtitle");
|
||||
}
|
||||
|
||||
itemBadge(name: string) {
|
||||
return this.eventItem(name).locator("span[class*='listLinkBadge']");
|
||||
return this.eventItem(name).getByTestId("list-item-badge");
|
||||
}
|
||||
|
||||
/** The tier an upcoming tournament's stream row is pilled with. */
|
||||
@@ -79,7 +80,7 @@ export class SideNav {
|
||||
}
|
||||
|
||||
savedStreamIcon(streamName: string) {
|
||||
return this.streamItem(streamName).locator("svg.lucide-bookmark-check");
|
||||
return this.streamItem(streamName).getByTestId("stream-saved-icon");
|
||||
}
|
||||
|
||||
async saveStream(streamName: string) {
|
||||
@@ -117,8 +118,8 @@ export class FriendRow {
|
||||
this.page = page;
|
||||
this.trigger = root.getByRole("button", { name });
|
||||
this.locators = {
|
||||
subtitle: this.trigger.locator("span[class*='listLinkSubtitle']"),
|
||||
badge: this.trigger.locator("span[class*='listLinkBadge']"),
|
||||
subtitle: this.trigger.getByTestId("list-item-subtitle"),
|
||||
badge: this.trigger.getByTestId("list-item-badge"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export class TopNavMenus {
|
||||
this.locators = {
|
||||
// a closed menu renders the same links as an icon-only preview, so
|
||||
// anything asserted about an open menu has to be scoped to it
|
||||
openMenu: page.locator("[class*='menuContent']"),
|
||||
openMenu: page.getByTestId("menu-content"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user