Add Aniimo guides and link game cards to toolbox checklists
Some checks failed
Deploy Sokko G / deploy (push) Failing after 3s

This commit is contained in:
Shinuwa 2026-09-23 20:05:19 +02:00
parent f0797ccf14
commit 13434f72f6
278 changed files with 10025 additions and 57 deletions

344
e2e/aniimo.spec.js Normal file
View file

@ -0,0 +1,344 @@
// Rôle : vérifie les parcours Aniimo, les variantes, les cartes et les seuils au clavier.
import { test, expect } from "@playwright/test";
const route = "/#/games/aniimo/aniimos";
const cards = (page) => page.locator(".aniimo-result");
const section = (page, name) => page.getByRole("group", { name, exact: true }).filter({ has: page.locator("legend") });
test("Aniimo matches MHW card motion, surfaces and filter controls", async ({ page }) => {
test.setTimeout(45000);
async function styles(locator, properties) {
return locator.evaluate((element, keys) => {
const css = globalThis.getComputedStyle(element);
return Object.fromEntries(keys.map((key) => [key, css.getPropertyValue(key)]));
}, properties);
}
const motionProperties = ["transition-duration", "transition-timing-function", "transform-style"];
const faceProperties = ["background-image", "border-radius", "backface-visibility"];
const indicatorProperties = ["width", "height", "background-color", "opacity"];
const filterProperties = ["background-color", "border-radius", "min-height", "padding"];
await page.goto("/#/games/mhwilds");
await expect(page.locator(".game-home-card").first()).toBeVisible();
const homeCard = await page.locator(".game-home-card").first().boundingBox();
await page.goto("/#/games/aniimo");
await expect(page.locator(".aniimo-home-card")).toBeVisible();
const aniimoHomeCard = await page.locator(".aniimo-home-card").boundingBox();
expect(aniimoHomeCard.width).toBeCloseTo(homeCard.width, 1);
expect(aniimoHomeCard.height).toBeCloseTo(homeCard.height, 1);
await page.goto("/#/games/mhwilds/monsters");
await expect(page.locator(".monster-card").first()).toBeVisible({ timeout: 15000 });
const motion = await styles(page.locator(".mhwilds-card-inner").first(), motionProperties);
const face = await styles(page.locator(".mhwilds-card-front").first(), faceProperties);
const indicator = await styles(page.locator(".mhwilds-flip-indicator").first(), indicatorProperties);
const filter = await styles(page.locator(".filter-logic"), filterProperties);
await page.goto(route);
const card = cards(page).first();
await expect(card).toBeVisible();
expect(await styles(card.locator(".aniimo-card-inner"), motionProperties)).toEqual(motion);
expect(await styles(card.locator(".aniimo-front"), faceProperties)).toEqual(face);
expect(await styles(card.locator(".aniimo-front .aniimo-flip"), indicatorProperties)).toEqual(indicator);
expect(await styles(page.locator(".filter-logic").first(), filterProperties)).toEqual(filter);
await expect(card.locator(".aniimo-face .aniimo-flip")).toHaveCount(2);
await expect(card.locator(".aniimo-card > .aniimo-flip")).toHaveCount(0);
await card.getByRole("button", { name: /Afficher les capacités/ }).press("Enter");
await expect(card.getByRole("button", { name: /Afficher le portrait/ })).toBeFocused();
await expect(card.locator(".aniimo-front")).toHaveAttribute("inert", "");
await card.getByRole("button", { name: /Afficher le portrait/ }).press("Enter");
await expect(card.getByRole("button", { name: /Afficher les capacités/ })).toBeFocused();
});
test("catalogue, overview and deferred Aniimo loading preserve filters across navigation", async ({ page }) => {
const requests = [];
page.on("request", (request) => { if (request.url().includes("/data/aniimo/")) requests.push(request.url()); });
await page.goto("/#/games");
await expect(page.getByRole("heading", { name: "Jeux disponibles" })).toBeVisible();
expect(requests).toHaveLength(0);
await page.locator('a.game-card-cover[href="#/games/aniimo"]').click();
await expect(page.getByRole("heading", { name: "Aniimo", exact: true })).toBeVisible();
const cover = page.locator('.aniimo-home-card img');
await expect(cover).toHaveAttribute("src", "/static/img/aniimo/emberpup__basic_form.png");
await expect(cover).toHaveCSS("object-fit", "cover");
await expect(cover).toHaveCSS("object-position", "50% 38%");
await expect(cover).toHaveCSS("height", "120px");
await page.locator('a[href="#/games/aniimo/aniimos"]').click();
await expect(cards(page)).toHaveCount(88);
await expect(page.getByRole("status")).toHaveText("88 / 88 Aniimos · 208 / 208 variantes");
const switchLabel = page.getByRole("button", { name: "Regrouper les variantes", exact: true }).locator("span:not(.ui-icon)");
expect(await switchLabel.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
await page.getByRole("searchbox", { name: "Nom" }).fill("EMBERPUP");
await expect(cards(page)).toHaveCount(1);
await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click();
await expect(cards(page)).toHaveCount(3);
await page.locator('.game-breadcrumb a[href="#/games/aniimo"]').click();
await page.locator('a[href="#/games/aniimo/aniimos"]').click();
await expect(cards(page)).toHaveCount(3);
await expect(page.getByRole("searchbox")).toHaveValue("EMBERPUP");
expect(requests).toHaveLength(3);
});
test("tabs, flip, matching variants and reset remain independent", async ({ page }) => {
await page.goto(route);
await page.getByRole("searchbox").fill("emberpup");
const card = cards(page).first();
await expect(card.getByRole("tab")).toHaveCount(3);
await card.getByRole("button", { name: /Afficher les capacités/ }).click();
await expect(card.locator(".aniimo-back")).toHaveAttribute("aria-hidden", "false");
await card.getByRole("tab", { name: "Hautes-plaines", exact: true }).click();
await expect(card).toHaveAttribute("data-form", "highlands");
await expect(card.getByRole("button", { name: /Afficher le portrait/ })).toHaveAttribute("aria-pressed", "true");
await expect(card.locator(".aniimo-back")).toHaveAttribute("aria-hidden", "false");
await expect(card.locator(".aniimo-back .aniimo-attributes").first()).toContainText("Roche");
await card.getByRole("tab", { name: "Forme de base", exact: true }).focus();
await page.keyboard.press("ArrowRight");
await expect(card).toHaveAttribute("data-form", "mountain_woods_form");
await expect(card.locator(".aniimo-back")).toHaveAttribute("aria-hidden", "false");
await section(page, "Éléments").getByRole("checkbox", { name: "Roche", exact: true }).check();
await expect(card.getByRole("tab")).toHaveCount(0);
await expect(card).toHaveAttribute("data-form", "highlands");
await section(page, "Éléments").getByRole("checkbox", { name: "Eau", exact: true }).check();
await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible();
await section(page, "Éléments").getByRole("button", { name: "OU", exact: true }).click();
await expect(cards(page)).toHaveCount(1);
await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click();
await expect(cards(page)).toHaveCount(1);
await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click();
await expect(cards(page)).toHaveCount(208);
await expect(page.getByRole("button", { name: "Regrouper les variantes", exact: true })).toHaveAttribute("aria-pressed", "false");
});
test("ability levels use independent native ranges and retain name constraints", async ({ page }) => {
await page.goto(route);
const abilities = section(page, "Compétences de logis");
const carry = page.getByRole("slider", { name: "Porter — Niveau minimum" });
const crafting = page.getByRole("slider", { name: "Artisanat — Niveau minimum" });
await expect(abilities.getByRole("checkbox")).toHaveCount(0);
await expect(carry).toHaveValue("0");
await expect(carry).toHaveAttribute("min", "0");
await carry.focus();
await carry.press("ArrowRight");
await carry.press("ArrowRight");
await carry.press("ArrowRight");
await expect(carry).toHaveValue("3");
await expect(crafting).toHaveValue("0");
await crafting.focus();
await crafting.press("ArrowRight");
await crafting.press("ArrowRight");
await expect(crafting).toHaveValue("2");
await expect(crafting).toHaveCSS("outline-style", "solid");
await abilities.getByRole("button", { name: "OU", exact: true }).click();
await expect(section(page, "Éléments").getByRole("button", { name: "ET", exact: true })).toHaveAttribute("aria-pressed", "true");
await carry.press("Home");
await crafting.press("Home");
await expect(carry).toHaveAttribute("aria-valuetext", "Sans filtre");
await expect(cards(page)).toHaveCount(88);
await page.getByRole("searchbox").fill("does-not-exist");
await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible();
});
test("seven tabs support keyboard navigation and fall back after filtering", async ({ page }) => {
await page.goto(route);
await page.getByRole("searchbox").fill("iris");
const card = cards(page).filter({ has: page.getByRole("heading", { name: "Iris", exact: true }) });
await expect(card.getByRole("tab")).toHaveCount(7);
const plateau = card.getByRole("tab", { name: "Forme des plateaux", exact: true });
await expect(plateau).toHaveText("PLA");
await plateau.hover();
await expect(page.getByRole("tooltip", { name: "Forme des plateaux", exact: true })).toBeVisible();
await page.mouse.move(0, 0);
await plateau.focus();
await expect(page.getByRole("tooltip", { name: "Forme des plateaux", exact: true })).toBeVisible();
const firstTab = await card.getByRole("tab").first().boundingBox();
const secondTab = await card.getByRole("tab").nth(1).boundingBox();
expect(secondTab.x).toBeGreaterThanOrEqual(firstTab.x + firstTab.width);
expect(await card.getByRole("tab").nth(1).evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
await card.getByRole("tab").first().focus();
await page.keyboard.press("End");
await expect(card).toHaveAttribute("data-form", "prismana_form");
await page.keyboard.press("Home");
await expect(card).toHaveAttribute("data-form", "basic_form");
await card.getByRole("tab", { name: "Forme prismana", exact: true }).click();
await expect(card).toHaveAttribute("data-form", "prismana_form");
await section(page, "Éléments").getByRole("checkbox", { name: "Herbe", exact: true }).check();
await expect(card).toHaveAttribute("data-form", "prismana_form");
await section(page, "Éléments").getByRole("checkbox", { name: "Herbe", exact: true }).uncheck();
await page.getByRole("searchbox").fill("emberpup");
await expect(cards(page)).toHaveAttribute("data-form", "basic_form");
await section(page, "Éléments").getByRole("checkbox", { name: "Roche", exact: true }).check();
await expect(cards(page)).toHaveAttribute("data-form", "highlands");
await section(page, "Éléments").getByRole("checkbox", { name: "Roche", exact: true }).uncheck();
await expect(cards(page)).toHaveAttribute("data-form", "highlands");
});
for (const width of [1280, 390]) {
test(`uniform cards, visible variant tabs and scrollable backs at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 844 });
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto(route);
await expect(cards(page)).toHaveCount(88);
const sizes = await page.locator(".aniimo-card").evaluateAll((elements) => elements.map((element) => {
const { width, height } = element.getBoundingClientRect();
return { width, height };
}));
expect(Math.max(...sizes.map((size) => size.width)) - Math.min(...sizes.map((size) => size.width))).toBeLessThan(1);
expect(Math.max(...sizes.map((size) => size.height)) - Math.min(...sizes.map((size) => size.height))).toBeLessThan(1);
await expect(page.locator(".aniimo-front .aniimo-element-icons").first()).toHaveText("");
await expect(page.locator(".aniimo-front .aniimo-element-icons img").first()).toHaveAttribute("alt", "Feu");
await page.getByRole("searchbox").fill("emberpup");
const longFormCard = cards(page).first();
const baseHeight = (await longFormCard.boundingBox()).height;
await longFormCard.getByRole("tab", { name: "Forme des montagnes boisées", exact: true }).click();
const form = longFormCard.locator(".aniimo-front .aniimo-form");
await expect(form).toHaveCSS("white-space", "normal");
expect(await form.evaluate((element) => element.scrollHeight <= element.clientHeight)).toBe(true);
expect((await longFormCard.boundingBox()).height).toBeCloseTo(baseHeight, 1);
await longFormCard.getByRole("tab", { name: "Forme de base", exact: true }).click();
await page.getByRole("searchbox").fill("");
const icons = page.locator(".aniimo-front .aniimo-element-icons").first();
const rowBox = await icons.boundingBox();
const iconBox = await icons.locator("img").boundingBox();
expect(iconBox.x + iconBox.width / 2).toBeCloseTo(rowBox.x + rowBox.width / 2, 1);
await page.getByRole("searchbox").fill("iris");
for (const name of ["Iris", "Valsiris"]) {
const card = cards(page).filter({ has: page.getByRole("heading", { name, exact: true }) });
const tabs = card.getByRole("tablist");
await expect(card.getByRole("tab")).toHaveCount(7);
await expect(card.getByRole("tab").last()).toHaveAccessibleName("Forme prismana");
expect(await tabs.evaluate((element) => {
const box = element.getBoundingClientRect();
return element.scrollWidth <= element.clientWidth && [...element.children].every((tab) => {
const bounds = tab.getBoundingClientRect();
return bounds.left >= box.left && bounds.right <= box.right && tab.scrollWidth <= tab.clientWidth;
});
})).toBe(true);
await expect(card.getByRole("tab").last()).toHaveCSS("background-image", /linear-gradient/);
}
await page.getByRole("searchbox").fill("fenmane");
const card = cards(page).first();
const before = await card.locator(".aniimo-card").boundingBox();
await card.getByRole("button", { name: /Afficher les capacités/ }).click();
await card.getByRole("tab", { name: "Forme prismana", exact: true }).click();
await expect(card.locator(".aniimo-back")).toHaveAttribute("aria-hidden", "false");
const after = await card.locator(".aniimo-card").boundingBox();
expect(after.height).toBeCloseTo(before.height, 1);
expect(after.width).toBeCloseTo(before.width, 1);
const details = card.locator(".aniimo-back-details");
await expect(details).toHaveCSS("overflow-y", "auto");
await expect(details.locator(".aniimo-abilities li")).toHaveCount(4);
if (width === 1280) {
expect(await details.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
await details.focus();
await details.press("End");
await expect.poll(() => details.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
}
await card.getByRole("button", { name: /Afficher le portrait/ }).click();
expect((await card.locator(".aniimo-card").boundingBox()).height).toBeCloseTo(before.height, 1);
});
}
test("portrait geometry, stable faces and mobile layout", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(route);
await page.getByRole("searchbox").fill("emberpup");
const card = cards(page).first();
await card.scrollIntoViewIfNeeded();
const portrait = card.locator(".aniimo-portrait img");
await expect(portrait).toHaveCSS("object-fit", "cover");
await expect(portrait).toHaveCSS("object-position", "50% 50%");
await expect(portrait).toHaveJSProperty("naturalWidth", 512);
const size = await portrait.boundingBox();
expect(size.height).toBeLessThanOrEqual(250);
expect(size.height).toBeGreaterThan(240);
const before = await card.boundingBox();
expect(before.height).toBeLessThan(520);
expect(before.width).toBeLessThanOrEqual(280);
await card.getByRole("button", { name: /Afficher les capacités/ }).press("Enter");
await expect(card.locator(".aniimo-back")).toHaveAttribute("aria-hidden", "false");
expect((await card.boundingBox()).height).toBeCloseTo(before.height, 1);
const summary = card.locator(".aniimo-back-summary > div");
const elementBox = await summary.nth(0).boundingBox();
const roleBox = await summary.nth(1).boundingBox();
expect(elementBox.y).toBeCloseTo(roleBox.y, 1);
expect(elementBox.x + elementBox.width).toBeLessThan(roleBox.x);
await expect(card.getByRole("progressbar")).toHaveCount(6);
await expect(card.getByRole("progressbar", { name: "PV", exact: true })).toHaveAttribute("aria-valuenow", "67");
await expect(card.getByRole("progressbar", { name: "PV", exact: true })).toHaveAttribute("aria-valuemax", "130");
await expect(card.getByRole("progressbar", { name: "PV", exact: true })).toHaveCSS("height", "4px");
await expect(card.locator(".aniimo-stat-value")).toHaveText(["67", "90", "60", "57", "40", "68"]);
await expect(card.locator(".aniimo-stat-total")).toHaveText("Total382");
expect(await card.locator(".aniimo-card-inner").evaluate((element) => Number.parseFloat(globalThis.getComputedStyle(element).transitionDuration))).toBeLessThan(0.001);
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true);
});
test("desktop grid shows five columns with the multiple icon and complete stats", async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(route);
await expect(cards(page)).toHaveCount(88);
const columns = await page.locator(".aniimo-grid").evaluate((element) => globalThis.getComputedStyle(element).gridTemplateColumns.split(" ").length);
expect(columns).toBe(5);
await expect(page.locator(".ui-icon-multiple")).toHaveCSS("mask-image", /multiples\.svg/);
const card = cards(page).first();
await card.getByRole("button", { name: /Afficher les capacités/ }).click();
await card.locator(".aniimo-back-details").evaluate((element) => { element.scrollTop = element.scrollHeight; });
await expect(card.getByRole("progressbar")).toHaveCount(6);
await page.screenshot({ path: test.info().outputPath("aniimo-stats-desktop.png") });
});
test("filter categories share one scroll and retain their selections when collapsed", async ({ page }) => {
await page.goto(route);
const elements = section(page, "Éléments");
const roles = section(page, "Rôles");
const abilities = section(page, "Compétences de logis");
await expect(page.locator(".aniimo-filter-options")).toHaveCount(3);
for (const grid of await page.locator(".aniimo-chip-options").all()) {
expect(await grid.evaluate((element) => globalThis.getComputedStyle(element).gridTemplateColumns.split(" ").length)).toBe(2);
}
const groups = await page.locator(".aniimo-ability-filter").evaluateAll((rows) => rows.map((row) => row.dataset.abilityGroup));
expect(groups).toEqual([...groups].sort());
await expect(page.locator(".aniimo-filter-toggle").first()).toHaveText("Rôles");
await expect(roles.getByRole("checkbox", { name: "SOIN", exact: true })).toHaveCount(1);
await expect(roles.getByRole("checkbox", { name: "SOUTIEN", exact: true })).toHaveCount(1);
const elementScroll = elements.getByRole("region", { name: "Éléments", exact: true });
const scroll = page.locator(".aniimo-filter-sections");
await expect(scroll).toHaveCSS("overflow-y", "auto");
for (const options of await page.locator(".aniimo-filter-options").all()) {
await expect(options).toHaveCSS("overflow-y", "visible");
expect(await options.evaluate((element) => element.scrollHeight <= element.clientHeight)).toBe(true);
}
await scroll.evaluate((element) => { element.scrollTop = element.scrollHeight; });
expect(await scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
await elements.getByRole("checkbox", { name: "Roche", exact: true }).check();
const results = await page.getByRole("status").textContent();
const toggle = elements.getByRole("button", { name: "Éléments", exact: true });
await toggle.click();
await expect(toggle).toHaveAttribute("aria-expanded", "false");
await expect(elementScroll).toBeHidden();
await expect(toggle.locator(".aniimo-selected-count")).toHaveText("1");
await expect(page.getByRole("status")).toHaveText(results);
await expect(roles.getByRole("region")).toBeVisible();
await expect(abilities.getByRole("region")).toBeVisible();
await toggle.press("Enter");
await expect(elements.getByRole("checkbox", { name: "Roche", exact: true })).toBeChecked();
const slider = abilities.getByRole("slider", { name: "Porter — Niveau minimum" });
await slider.focus();
await slider.press("End");
await abilities.getByRole("button", { name: "Compétences de logis", exact: true }).click();
await expect(slider).toBeHidden();
await abilities.getByRole("button", { name: "Compétences de logis", exact: true }).click();
await expect(slider).toHaveValue("4");
});
test("pending and failed data requests show loading and error states", async ({ page }) => {
let releaseRequests;
const pending = new Promise((resolve) => { releaseRequests = resolve; });
await page.route("**/data/aniimo/aniimos.json", async (route) => {
await pending;
await route.fulfill({ status: 503, body: "unavailable" });
});
await page.goto(route);
await expect(page.getByRole("heading", { name: "Chargement des Aniimos…" })).toBeVisible();
releaseRequests();
await expect(page.getByRole("heading", { name: "Impossible de charger Aniimo" })).toBeVisible();
});

245
e2e/guide-checklist.spec.js Normal file
View file

@ -0,0 +1,245 @@
// Rôle : vérifie copie, liaison de session et progression des checklists dans les guides.
import { test, expect } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resetSokkoStorage } from "./helpers/storage.js";
import { importChecklistText } from "../website/src/features/toolboxes/modules/textImport/basic.js";
import { normalizeChecklistData } from "../website/src/features/toolboxes/storage/modules/checklist.js";
const route = "/#/games/aniimo/aniimos";
const sessionKey = "sokkog:guide-checklists";
const selector = (page) => page.getByRole("combobox", { name: "Checklist associée", exact: true });
const hiddenSwitch = (page) => page.getByRole("button", { name: "Masquer les cochés", exact: true });
const json = (path) => JSON.parse(readFileSync(`website/public/data/${path}`, "utf8"));
const row = (label, qtyCurrent = 0) => ({ id: label, label, qtyCurrent });
async function seed(page, gameId = "aniimo", lists = {
capture: [row("Loufeuteau")],
perfect: [row("Loufeuteau", 1)],
forms: [row("Loufeuteau — Forme de base", 1), row("Emberpup — Mountain Woods Form")],
unrelated: [row("Unrelated")]
}) {
await resetSokkoStorage(page);
await page.evaluate(async ({ gameId, lists }) => {
await new Promise((resolve, reject) => {
const request = globalThis.indexedDB.open("sokkog", 1);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(["kv", "modules"], "readwrite");
tx.objectStore("kv").put([
{ id: "guide", name: "Guide toolbox", modules: Object.keys(lists).map((id) => ({ id, type: "checklist", title: id })), moduleColumns: 1 },
{ id: "other", name: "Other toolbox", modules: [{ id: "outside", type: "checklist", title: "Outside" }] }
], "toolboxes");
tx.objectStore("kv").put({ [gameId]: "guide" }, "links");
for (const [id, items] of Object.entries(lists)) tx.objectStore("modules").put({ key: `guide:${id}`, data: { items } });
tx.objectStore("modules").put({ key: "other:outside", data: { items: [{ id: "outside", label: "Loufeuteau", qtyCurrent: 1 }] } });
tx.oncomplete = () => { db.close(); resolve(); };
tx.onerror = () => reject(tx.error);
};
request.onerror = () => reject(request.error);
});
}, { gameId, lists });
await page.reload();
}
async function readModule(page, moduleId) {
return page.evaluate(async (moduleId) => new Promise((resolve, reject) => {
const request = globalThis.indexedDB.open("sokkog", 1);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction("modules", "readonly");
const get = tx.objectStore("modules").get(`guide:${moduleId}`);
get.onsuccess = () => resolve(get.result.data);
tx.oncomplete = () => db.close();
tx.onerror = () => reject(tx.error);
};
}), moduleId);
}
async function openDrawer(page) {
const drawer = page.locator("#toolbox-drawer");
if (!await drawer.locator(".drawer-panel").isVisible()) await page.locator(".topbar-actions button").click();
await expect(drawer.locator(".drawer-panel")).toBeVisible();
return drawer;
}
async function closeDrawer(page) {
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
}
async function choose(page, id) {
await openDrawer(page);
await selector(page).selectOption(id);
await closeDrawer(page);
}
async function toggleHidden(page) {
await openDrawer(page);
await hiddenSwitch(page).click();
await closeDrawer(page);
}
async function assertLink(page, id, hidden) {
await openDrawer(page);
await expect(selector(page)).toHaveValue(id);
if (id) await expect(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden));
else await expect(hiddenSwitch(page)).toHaveCount(0);
await closeDrawer(page);
}
test("Aniimo copies visible species or forms and round-trips through checklist import", async ({ page, context }) => {
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await seed(page);
await page.goto(route);
await page.getByRole("searchbox").fill("emberpup");
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
const copy = page.locator(".results-copy-button");
await expect(copy).toHaveAccessibleName(/sans les variantes/);
await expect(page.locator('[data-copy-target="true"]')).toHaveText("1 / 88 Aniimos");
await copy.click();
await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Loufeuteau:1");
await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click();
await expect(copy).toHaveAccessibleName(/toutes les variantes/);
await expect(page.locator('[data-copy-target="true"]')).toHaveText("3 / 208 variantes");
await copy.click();
const text = await page.evaluate(() => globalThis.navigator.clipboard.readText());
const imported = importChecklistText(text, { uid: (prefix) => `${prefix}-${Math.random()}`, normalizeChecklistData });
expect(imported.items.map((item) => item.label)).toEqual(["Loufeuteau — Forme de base", "Loufeuteau — Forme des montagnes boisées", "Loufeuteau — Hautes-plaines"]);
expect(imported.items.every((item) => item.qtyTarget === 1 && item.qtyCurrent === 0)).toBe(true);
await choose(page, "forms");
await toggleHidden(page);
await copy.click();
expect(await page.evaluate(() => globalThis.navigator.clipboard.readText())).not.toContain("Forme de base");
await choose(page, "perfect");
await expect(copy).toBeDisabled();
await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible();
});
for (const width of [1280, 390]) {
test(`drawer linking, live completion and session restoration at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await seed(page);
await page.goto(route);
await page.getByRole("searchbox").fill("emberpup");
const card = page.locator(".aniimo-result");
await openDrawer(page);
await expect(selector(page)).toHaveValue("");
await expect(selector(page).locator("option")).toHaveCount(4);
const before = await readModule(page, "forms");
await selector(page).selectOption("forms");
const panel = page.locator(".guide-checklist-controls");
const bounds = await panel.boundingBox();
const selectBounds = await selector(page).boundingBox();
expect(selectBounds.x + selectBounds.width / 2).toBeCloseTo(bounds.x + bounds.width / 2, 0);
await page.screenshot({ path: test.info().outputPath("linked-drawer.png") });
await closeDrawer(page);
await expect(card).toHaveAttribute("data-checklist-complete", "true");
await expect(card.locator(".aniimo-front .guide-checklist-mark")).toHaveAccessibleName("Coché");
await toggleHidden(page);
await expect(card.getByRole("tab")).toHaveCount(2);
await expect(card).toHaveAttribute("data-form", "mountain_woods_form");
expect(await readModule(page, "forms")).toEqual(before);
await choose(page, "capture");
await assertLink(page, "capture", true);
await expect(card.getByRole("tab")).toHaveCount(3);
const drawer = await openDrawer(page);
await drawer.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: "Retirer capture", exact: true }) }).getByRole("checkbox", { name: "Terminer Loufeuteau", exact: true }).check();
await closeDrawer(page);
await expect(card).toHaveCount(0);
await page.reload();
await assertLink(page, "capture", true);
await page.getByRole("searchbox").fill("emberpup");
await expect(card).toHaveCount(0);
await openDrawer(page);
await hiddenSwitch(page).press("Space");
await closeDrawer(page);
await expect(card).toHaveAttribute("data-checklist-complete", "true");
await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click();
await openDrawer(page);
await expect(selector(page)).toHaveValue("capture");
await page.getByRole("button", { name: "Dissocier", exact: true }).click();
await expect(selector(page)).toHaveValue("");
await expect(hiddenSwitch(page)).toHaveCount(0);
await closeDrawer(page);
await expect(page.locator('[data-checklist-complete="true"]')).toHaveCount(0);
expect(await page.evaluate((key) => globalThis.sessionStorage.getItem(key), sessionKey)).toBeNull();
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true);
});
}
for (const [gameId, category, cardClass, dataPath, dataKey] of [
["mhwilds", "monsters", ".monster-card", "mhwilds/monsters.json", "monsters"],
["mhwilds", "endemic", ".endemic-card", "mhwilds/endemic_life.json", "endemicLife"],
["diablo4", "affixes", ".diablo4-affix-card", "diablo4/affixes_types.json", "affixes"]
]) {
test(`${gameId}/${category} links from its drawer and hides completed cards`, async ({ page }) => {
const item = json(dataPath)[dataKey][0];
const label = gameId === "mhwilds" ? json("mhwilds/i18n/fr.json")[item.name] : item.label;
await seed(page, gameId, { progress: [row(label, 1)] });
await page.goto(`/#/games/${gameId}/${category}`);
await expect(page.locator(cardClass).first()).toBeVisible();
const count = await page.locator(cardClass).count();
await choose(page, "progress");
await expect(page.locator(`${cardClass}[data-checklist-complete="true"]`)).toHaveCount(1);
await toggleHidden(page);
await expect(page.locator(cardClass)).toHaveCount(count - 1);
await page.setViewportSize({ width: 390, height: 844 });
await page.reload();
await assertLink(page, "progress", true);
await expect(page.locator(cardClass)).toHaveCount(count - 1);
expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true);
});
}
test("renames retain a zero-match selection; changing toolbox clears the association", async ({ page }) => {
await seed(page);
await page.goto(route);
await choose(page, "capture");
const drawer = await openDrawer(page);
const checklist = drawer.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: "Retirer capture", exact: true }) });
await checklist.getByRole("button", { name: "Modifier Loufeuteau", exact: true }).click();
await checklist.getByRole("textbox", { name: "Libellé", exact: true }).fill("Unrelated now");
await checklist.getByRole("textbox", { name: "Libellé", exact: true }).press("Enter");
await expect(selector(page)).toHaveValue("capture");
await expect(page.getByText("Cette checklist ne contient plus de correspondance avec le guide.")).toBeVisible();
await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Lier une toolbox" });
await dialog.getByRole("combobox").selectOption("other");
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
await expect(selector(page)).toHaveValue("");
await expect(selector(page).locator("option")).toHaveCount(2);
expect(await page.evaluate((key) => globalThis.sessionStorage.getItem(key), sessionKey)).toBeNull();
});
test("deleting a selected checklist clears its link immediately", async ({ page }) => {
await seed(page);
await page.goto(route);
await choose(page, "capture");
await toggleHidden(page);
const drawer = await openDrawer(page);
await drawer.getByRole("button", { name: "Retirer capture", exact: true }).click();
await page.getByRole("dialog").getByRole("button", { name: "Retirer", exact: true }).click();
await expect(selector(page)).toHaveValue("");
await expect(hiddenSwitch(page)).toHaveCount(0);
await page.reload();
await assertLink(page, "", false);
expect(await page.evaluate((key) => globalThis.sessionStorage.getItem(key), sessionKey)).toBeNull();
});
test("unavailable session storage retains links across navigation; overview has no guide controls", async ({ page }) => {
await seed(page);
await page.addInitScript((key) => {
for (const method of ["getItem", "setItem", "removeItem"]) {
const original = globalThis.Storage.prototype[method];
globalThis.Storage.prototype[method] = function (name, ...args) {
if (name === key) throw new Error("Session storage unavailable");
return original.call(this, name, ...args);
};
}
}, sessionKey);
await page.reload();
await page.goto(route);
await choose(page, "perfect");
await toggleHidden(page);
await page.goto("/#/games/aniimo");
await openDrawer(page);
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
await closeDrawer(page);
await page.goto(route);
await assertLink(page, "perfect", true);
});

View file

@ -2,6 +2,23 @@
import { test, expect } from "@playwright/test";
import { mountStyles, computed, expectPseudo } from "./fixtures/compiled-styles.js";
test("native ranges support public sizes, keyboard thresholds and disabled states", async ({ page }) => {
for (const [size, height] of [["md", 44], ["sm", 36]]) {
await mountStyles(page, `<label style="display:block;padding:24px;width:260px">Niveau minimum<input class="nds-range" data-size="${size}" type="range" min="1" max="4" step="1" value="1"></label>`);
const range = page.getByRole("slider", { name: "Niveau minimum" });
await expect(range).toHaveCSS("height", `${height}px`);
await range.focus();
await range.press("End");
await expect(range).toHaveValue("4");
await range.press("ArrowLeft");
await expect(range).toHaveValue("3");
await expect(range).toHaveCSS("outline-style", "solid");
await range.evaluate((element) => { element.disabled = true; });
await expect(range).toBeDisabled();
await expect(range).toHaveCSS("cursor", "not-allowed");
}
});
test("fields, selects and date triggers keep public sizes and keyboard focus", async ({ page }) => {
for (const [size, height, textareaHeight] of [["md", 44, 108], ["sm", 36, 88], ["xs", 32, 72]]) {
await mountStyles(page, `<div style="padding:24px;display:grid;gap:12px;width:300px">

View file

@ -48,12 +48,25 @@ test("badges, notices, progress and chips expose meaningful visual states", asyn
await expect(progress.locator("span")).toHaveCSS("width", "99px");
await progress.locator("span").evaluate((element) => { element.style.width = "100%"; });
await expect(progress.locator("span")).toHaveCSS("width", "198px");
await progress.evaluate((element) => { element.dataset.size = "sm"; });
await expect(progress).toHaveCSS("height", "4px");
}
const chip = page.locator(".nds-chip");
await expect(chip).toHaveCSS("background-color", "rgba(5, 7, 17, 0.32)");
await chip.locator("input").check();
await page.mouse.move(0, 0);
await expect(chip).toHaveCSS("background-color", "rgba(139, 92, 246, 0.14)");
await chip.evaluate((element) => {
element.dataset.size = "sm";
const image = globalThis.document.createElement("img");
image.src = "/static/icons/multiples.svg";
image.alt = "";
element.append(image);
});
await expect(chip).toHaveCSS("min-height", "30px");
await expect(chip).toHaveCSS("font-size", "12px");
await expect(chip.locator("img")).toHaveCSS("width", "16px");
await expect(chip.locator("img")).toHaveCSS("height", "16px");
});
for (const width of [1280, 390]) {