fix all overlays

This commit is contained in:
larssand
2026-09-08 18:26:30 +02:00
parent d0889736d0
commit 8aedf83826
9 changed files with 197 additions and 4 deletions

65
tests/browser_overlays.js Normal file
View File

@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
export async function runOverlaySmoke({ evaluate }) {
const results = await evaluate(`(async () => {
const { renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } = await import('/src/overlays.js');
const { escapeHtml } = await import('/src/html_escape.js');
const { installListSorting } = await import('/src/list_sorting.js');
const frame = document.createElement('iframe');
frame.style.cssText = 'position:fixed;left:0;top:0;height:900px;border:0;z-index:99999';
document.body.append(frame);
const doc = frame.contentDocument;
const style = doc.createElement('link'); style.rel='stylesheet'; style.href='/src/styles.css';
const loaded = new Promise((resolve,reject) => {style.onload=resolve;style.onerror=reject;});
doc.head.append(style); await loaded;
const labels = {'table.pos':'Pos','table.driver':'Förare','table.laps':'Varv','table.result':'Resultat','table.gap':'Gap','table.ahead_gap':'Gap fram','table.own_delta':'Eget delta','table.best_lap':'Bästa varv'};
let layout='leaderboard';
const deps={t:key=>labels[key]||key, escapeHtml, formatLap:()=> '0:36.619', formatTeamActiveMemberLabel:()=> 'Test Driver', formatPredictedLapDelta:()=> '-', getVisiblePassings:()=>[], getEventName:()=> 'Club Practice', getSessionTypeLabel:()=> 'Practice', getStartModeLabel:()=> 'Mass start', getStatusLabel:()=> 'Running', getObsOverlayConfig:()=>({layout,rows:10,showClock:true,showLaps:true,showResult:true,showBest:true,showGap:true}), normalizeStartMode:v=>v, renderPositionGrid:()=>'', getSessionGridEntries:()=>[]};
const rows=[{key:'a',displayName:'Karl Bodell',laps:27,resultDisplay:'0:36.619',bestLapMs:36619},{key:'b',displayName:'Anders',laps:19,resultDisplay:'0:37.854',bestLapMs:37854}];
const results=[];
const tick=()=>new Promise(resolve=>setTimeout(resolve,0));
try {
for (const mode of ['leaderboard','tv','team']) {
for (const width of [390,900,1600]) {
frame.style.width=width+'px';
doc.body.innerHTML='<section class="overlay-shell"><div class="overlay-board"></div></section>';
const root=doc.querySelector('.overlay-board');
root.innerHTML=mode==='team' ? renderTeamOverlay(rows,{}, {},deps) : renderOverlayLeaderboard(rows,deps);
const stop=installListSorting(root); await tick();
const list=root.querySelector('.overlay-race-list');
const header=list.querySelector('.list-sort-toolbar');
const row=list.querySelector('.overlay-race-row');
const deltas=[...header.children].map((button,index)=>Math.abs(button.getBoundingClientRect().left-row.children[index].getBoundingClientRect().left));
for(let attempt=0;attempt<3 && !header.children[1].textContent.includes("▲");attempt++) header.children[1].click();
await tick();
const firstName=list.querySelector('.overlay-race-driver strong').textContent;
const firstPosition=list.querySelector('.pos-pill').textContent;
results.push({mode,width,aligned:deltas.every(delta=>delta<1),firstName,firstPosition,columns:header.children.length,tableScroll:list.scrollWidth>list.clientWidth});
stop();
}
}
for (layout of ['leaderboard','grid']) {
for (const width of [390,1600]) {
frame.style.width=width+'px';
doc.body.innerHTML='<section class="overlay-shell-obs"></section>';
const root=doc.body.firstElementChild;
root.innerHTML=renderObsOverlay({name:'Practice',eventId:'e',status:'running'},rows,{}, {remainingMs:300000},{},deps);
const stop=installListSorting(root);await tick();
results.push({mode:'obs-'+layout,width,controls:root.querySelectorAll('.list-sort-toolbar').length});
stop();
}
}
} finally {frame.remove();}
return results;
})()`);
for (const result of results) {
if (result.mode.startsWith("obs")) assert.equal(result.controls, 0, JSON.stringify(result));
else {
assert.equal(result.aligned, true, JSON.stringify(result));
assert.equal(result.columns, 8, JSON.stringify(result));
assert.equal(result.firstName, "Anders", JSON.stringify(result));
assert.equal(result.firstPosition, "2", JSON.stringify(result));
}
}
console.log("PASS overlay heading alignment at 390/900/1600px, sorting preserves race position, OBS has no sort controls");
}

View File

@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
export async function runRaceSetupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause }) {
const click = id => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
const submit = id => evaluate(`document.getElementById(${JSON.stringify(id)}).requestSubmit()`);
const findEvent = async name => (await savedState()).events.find(event => event.name === name);
async function editEvent(event, view, readyForm) {
await click("event-edit-" + event.id);
await fill("eventEditForm", { name: event.name + " Updated" });
await submit("eventEditForm");
await waitFor(async () => Boolean(await findEvent(event.name + " Updated")), "event name saved");
await navigate(view, readyForm);
await click("event-edit-" + event.id);
assert.equal(await evaluate('document.getElementById("eventEditForm").elements.namedItem("name").value'), event.name + " Updated");
await click("eventEditCancel");
await click("event-manage-" + event.id);
}
await navigate("events", "eventForm");
await fill("eventForm", { name: "Browser Sponsor", date: "2026-09-08", classId: "test_class" });
await submit("eventForm");
await waitFor(async () => Boolean(await findEvent("Browser Sponsor")), "create sponsor event");
const sponsor = await findEvent("Browser Sponsor");
await editEvent(sponsor, "events", "eventForm");
await fill("sessionForm", { name: "Sponsor Heat", type: "heat", durationMin: "5", startMode: "mass" });
await submit("sessionForm");
await waitFor(async () => (await savedState()).sessions.some(session => session.name === "Sponsor Heat" && session.eventId === sponsor.id), "sponsor session");
console.log("PASS sponsor-event create/edit/reload and session creation");
await navigate("race_setup", "raceWizardStepForm");
await fill("raceWizardStepForm", { name: "Browser Endurance", date: "2026-09-08", classId: "test_class", presetId: "endurance" });
await click("raceWizardNext");
await click("wizardSelectAllParticipants");
await click("raceWizardNext");
await evaluate('document.querySelector("#raceWizardPlanForm [name=createTeamRace]").checked = true');
await fill("raceWizardPlanForm", { teamRaceDurationMin: "45" });
await click("raceWizardNext");
await click("raceWizardCreate");
await waitFor(async () => Boolean(await findEvent("Browser Endurance")), "wizard race creation");
const race = await findEvent("Browser Endurance");
await navigate("race_setup", "raceWizardStepForm");
await editEvent(race, "race_setup", "raceWizardStepForm");
const current = await savedState();
const driver = current.drivers.find(item => item.name === "Browser Driver Updated");
const car = current.cars.find(item => item.name === "Browser Car Updated");
const raceState = () => findEvent("Browser Endurance Updated");
await fill("teamForm", { teamName: "Browser Team" });
await evaluate(`document.querySelector('[form="teamForm"][name="teamDriverIds"][value="${driver.id}"]').checked=true`);
await submit("teamForm");
await waitFor(async () => (await raceState()).raceConfig.teams.some(team => team.name === "Browser Team"), "team create");
const team = (await raceState()).raceConfig.teams.find(item => item.name === "Browser Team");
assert.deepEqual(team.driverIds, [driver.id]);
await click("team-edit-" + team.id);
await fill("teamEditForm", { teamName: "Browser Team Updated" });
await evaluate(`document.querySelector('#teamEditForm [name="teamCarIds"][value="${car.id}"]').checked=true`);
await pause(5500);
assert.equal(await evaluate('document.getElementById("teamEditForm").elements.namedItem("teamName").value'), "Browser Team Updated");
await click("teamEditSave");
await waitFor(async () => (await raceState()).raceConfig.teams.some(item => item.name === "Browser Team Updated" && item.carIds.includes(car.id)), "team membership update");
await navigate("race_setup", "raceWizardStepForm");
await click("event-manage-" + race.id);
await click("team-edit-" + team.id);
assert.equal(await evaluate('document.getElementById("teamEditForm").elements.namedItem("teamName").value'), "Browser Team Updated");
assert.ok(await evaluate(`document.querySelector('#teamEditForm [name="teamCarIds"][value="${car.id}"]').checked`));
await click("teamEditCancel");
await fill("sessionForm", { name: "Editable Practice", type: "open_practice", durationMin: "5" });
await submit("sessionForm");
await waitFor(async () => (await savedState()).sessions.some(session => session.name === "Editable Practice"), "practice create");
const session = (await savedState()).sessions.find(item => item.name === "Editable Practice");
await click("session-edit-" + session.id);
await fill("sessionEditForm", { name: "Edited Team Session", type: "team_race", durationMin: "45", followUpSec: "30" });
await submit("sessionEditForm");
await waitFor(async () => (await savedState()).sessions.some(item => item.id === session.id && item.type === "team_race" && item.durationMin === 45 && item.followUpSec === 30), "session edit type and duration");
await fill("raceFormatForm", { minLapSec: "30", maxLapSec: "120" });
await submit("raceFormatForm");
await waitFor(async () => (await raceState()).raceConfig.minLapMs === 30000 && (await raceState()).raceConfig.maxLapMs === 120000, "lap limits saved");
await navigate("race_setup", "raceWizardStepForm");
await click("event-manage-" + race.id);
await click("session-edit-" + session.id);
assert.equal(await evaluate('document.getElementById("sessionEditForm").elements.namedItem("type").value'), "team_race");
await click("sessionEditCancel");
assert.equal(await evaluate('document.getElementById("raceFormatForm").elements.namedItem("minLapSec").value'), "30.0");
await click("team-delete-" + team.id);
await waitFor(async () => !(await raceState()).raceConfig.teams.some(item => item.id === team.id), "team deletion");
console.log("PASS race wizard/edit/reload, team create/member-edit/delete, session type/duration and lap limits");
}

View File

@@ -1,3 +1,5 @@
import { runOverlaySmoke } from "./browser_overlays.js";
import { runRaceSetupSmoke } from "./browser_race_setup.js";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
@@ -105,6 +107,7 @@ try {
assert.ok(await evaluate(`document.getElementById("view").textContent.includes(${JSON.stringify(fields.name + " Updated")})`));
console.log("PASS create/edit/reload and draft retention: " + collection);
}
await runRaceSetupSmoke({ evaluate, navigate, fill, savedState, waitFor, pause });
await navigate("classes", "classForm");
await send("Network.emulateNetworkConditions", { offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1 });
await fill("classForm", { name: "Offline Draft" });
@@ -115,6 +118,7 @@ try {
await evaluate('document.getElementById("syncStatusRetry").click()');
await waitFor(async () => (await savedState()).classes.some(item => item.name === "Offline Draft"), "retry persisted draft");
await waitFor(async () => (await status()) === "saved", "confirmed saved status");
await runOverlaySmoke({ evaluate });
assert.deepEqual(errors, []);
console.log("PASS offline save failure, local draft retention, retry and confirmed saved status");
} finally {