- axe is clean across every screen in both themes, phone and desktop. Fixed: the hidden file inputs (aria-hidden + tabIndex -1 — they are opened by labelled buttons, so they should not be in the accessibility tree), the heading order (deck/homework/smart-review cards are h2 under the page h1; LearningSection takes a level so it stays h2 on the library and h3 inside the settings dialog) and landmarks around the study and review feeds. - tests/contrast.mjs (new diagnostic): samples rendered pixels, because axe cannot judge text on blurred glass — it reports those nodes as incomplete. It skips disabled controls (1.4.3 exempts them) and content behind an open sheet. Result: no below-minimum text on any scanned screen. - The report control exists in the homework guide too: under each "Try it yourself" exercise, labelled "Report exercise", without a skip (there is no queue to skip). lib/question-reports now takes deckId/deckTitle directly. - The rail's two creation actions are buttons now (.nav-action) rather than quiet nav rows — once the library is full they were the only way to start something on desktop and read as just more navigation. "Make my homework" is "New homework" wherever it is an action (rail, empty state, share chooser, manifest shortcut); the page title and mode switch keep their wording. Version 0.1.6 (three task bumps), SW -> freezmo-v52.
115 lines
5.9 KiB
JavaScript
115 lines
5.9 KiB
JavaScript
/**
|
|
* Core regression: library, study feed, homework entry, review card, and the
|
|
* desktop shell + settings panel.
|
|
*/
|
|
import { BASE, DECK, launchBrowser, makeChecker, makePage, seed } from "./lib.mjs";
|
|
|
|
const { check, finish } = makeChecker();
|
|
const browser = await launchBrowser();
|
|
|
|
/* 1. desktop shell --------------------------------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 900, deviceScaleFactor: 1 });
|
|
await seed(page, { decks: [DECK], folders: [{ id: "fold_1", name: "Biology", createdAt: 1 }] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(900);
|
|
|
|
check("library title", await page.getByRole("heading", { name: "Your icebox" }).isVisible());
|
|
const sidebar = page.getByRole("complementary", { name: "Sidebar" });
|
|
for (const row of ["New deck", "New homework", "All decks", "Unorganized", "Biology", "Progress", "Exam mode", "Settings"]) {
|
|
check(`sidebar row: ${row}`, (await sidebar.getByText(row, { exact: true }).count()) >= 1);
|
|
}
|
|
check("desktop hides the tab bar", !(await page.locator("nav.tabbar").isVisible()));
|
|
check("desktop keeps the import button", await page.getByRole("button", { name: "Import JSON" }).isVisible());
|
|
|
|
await page.getByRole("button", { name: "Collapse sidebar" }).click();
|
|
await page.waitForTimeout(400);
|
|
check("sidebar collapses", await page.getByRole("button", { name: "Expand sidebar" }).isVisible());
|
|
await page.getByRole("button", { name: "Expand sidebar" }).click();
|
|
await page.waitForTimeout(400);
|
|
check("sidebar expands again", await page.getByRole("button", { name: "Collapse sidebar" }).isVisible());
|
|
|
|
// Settings from the sidebar footer.
|
|
await sidebar.getByRole("button", { name: "Settings" }).click();
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.waitFor({ timeout: 6000 });
|
|
await page.waitForTimeout(700);
|
|
check("settings: connection card", await dialog.getByText("Connection").isVisible());
|
|
check("settings: key field", await dialog.locator("#ollama-api-key").count() === 1);
|
|
check("settings: learning section", await dialog.getByText("Learning").isVisible());
|
|
check("settings: data section", await dialog.getByText("Data & backup").isVisible());
|
|
check("settings: sync section", await dialog.getByText("Sync").first().isVisible());
|
|
|
|
// Health panel: it can take a while while the Ollama probe answers.
|
|
const visionRow = dialog.getByText("Vision model");
|
|
await visionRow.waitFor({ timeout: 25_000 }).catch(() => {});
|
|
check("health panel present (vision row)", (await visionRow.count()) >= 1);
|
|
check("text model row", (await dialog.getByText("Text model").count()) >= 1);
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(500);
|
|
await context.close();
|
|
}
|
|
|
|
/* 2. library: tabs, deck card, menu, folders ------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser);
|
|
await seed(page, { decks: [DECK], folders: [{ id: "fold_1", name: "Biology", createdAt: 1 }] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(900);
|
|
|
|
check("library tabs present", await page.getByRole("link", { name: "Study Decks", exact: true }).isVisible());
|
|
check("deck card title", await page.getByText("Sky deck").first().isVisible());
|
|
check("deck meta line", (await page.getByText(/2 activities/).count()) >= 1);
|
|
check("folder pills visible with folders", (await page.locator(".folder-pill").count()) >= 1);
|
|
check("review card has no exam link", (await page.getByRole("link", { name: /Exam mode/ }).count()) === 0);
|
|
check("review card keeps due + mistakes", (await page.getByText(/Review due ·/).count()) >= 1 && (await page.getByText(/Mistakes ·/).count()) >= 1);
|
|
|
|
await page.getByRole("button", { name: "Deck actions" }).click();
|
|
await page.waitForTimeout(300);
|
|
for (const item of ["Rename", "Export JSON", "Share", "Regenerate deck", "Delete deck"]) {
|
|
check(`deck menu: ${item}`, (await page.getByRole("button", { name: item, exact: true }).count()) >= 1);
|
|
}
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(400);
|
|
|
|
await page.getByRole("link", { name: "Homework", exact: true }).click();
|
|
await page.waitForTimeout(600);
|
|
check("homework tab empty state", await page.getByRole("link", { name: "New homework", exact: true }).isVisible());
|
|
await context.close();
|
|
}
|
|
|
|
/* 3. study feed ------------------------------------------------------ */
|
|
{
|
|
const { context, page } = await makePage(browser);
|
|
await seed(page, { decks: [DECK] });
|
|
await page.goto(`${BASE}/deck/seed_deck`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(900);
|
|
const firstSlide = page.getByRole("region", { name: /Activity 1 of/ });
|
|
check("feed: first activity", await firstSlide.getByText("What colour is a clear daytime sky?").isVisible());
|
|
await page.getByRole("button", { name: "Green" }).click();
|
|
await page.waitForTimeout(500);
|
|
check("feed: wrong answer shows the correct one", await page.getByText(/Correct answer/i).first().isVisible());
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
await page.waitForTimeout(800);
|
|
await page.getByRole("button", { name: "Scattering" }).click();
|
|
await page.waitForTimeout(500);
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
await page.waitForTimeout(900);
|
|
check("feed: results appear", await page.getByText(/50%/).first().isVisible());
|
|
check("feed: results summary", (await page.getByText(/You got 1 of 2 right/).count()) >= 1);
|
|
await context.close();
|
|
}
|
|
|
|
/* 4. homework entry -------------------------------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser);
|
|
await seed(page, { decks: [] });
|
|
await page.goto(`${BASE}/homework`, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(800);
|
|
check("homework: step headline", await page.getByRole("heading", { name: "The assignment" }).isVisible());
|
|
check("homework: empty placeholder", await page.getByText("Nothing added yet").isVisible());
|
|
await context.close();
|
|
}
|
|
|
|
await browser.close();
|
|
finish();
|