// 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") }); for (const width of [1280, 390]) { test(`Prismana filter combines with search and survives grouping and navigation at ${width}px`, async ({ page }) => { test.setTimeout(45000); await page.setViewportSize({ width, height: 900 }); await page.goto(route); const prismana = page.getByRole("checkbox", { name: "Prismana uniquement", exact: true }); await expect(prismana).toBeVisible({ timeout: 30000 }); await expect(prismana).not.toBeChecked(); await prismana.focus(); await prismana.press("Space"); await expect(cards(page)).toHaveCount(27); await expect(page.locator('.aniimo-result:not([data-form="prismana_form"])')).toHaveCount(0); await expect(page.getByRole("tablist")).toHaveCount(0); expect(await page.locator('img[src="/static/icons/prismana.svg"]').evaluate((image) => image.complete && image.naturalWidth > 0)).toBe(true); await page.getByRole("searchbox").fill("iris"); 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(prismana).toBeChecked(); await expect(cards(page)).toHaveCount(3); await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click(); await expect(cards(page)).toHaveCount(3); await page.getByRole("searchbox").fill("emberpup"); await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible(); await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click(); await expect(prismana).not.toBeChecked(); await expect(cards(page)).toHaveCount(208); await expect(page.getByRole("button", { name: "Regrouper les variantes", exact: true })).toHaveAttribute("aria-pressed", "false"); expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true); }); } 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(5); }); test("Aniimo exposes its regions list", async ({ page }) => { await page.goto("/#/games/aniimo/lists"); await expect(page.getByRole("heading", { name: "Listes", exact: true })).toBeVisible(); await expect(page.locator("h2", { hasText: "Regions" })).toBeVisible(); await expect(page.getByText("14 éléments", { exact: true })).toBeVisible(); await expect(page.getByText("Champs de Cumulaine", { exact: true })).toBeVisible(); }); 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.locator("#aniimo-minimum-carry"); const crafting = page.locator("#aniimo-minimum-crafting"); await expect(abilities.getByRole("checkbox")).toHaveCount(0); await expect(abilities.getByRole("button", { name: "OU", exact: true })).toHaveCount(0); const minimumLevel = abilities.getByRole("button", { name: "Niveau minimum", exact: true }); const exactLevel = abilities.getByRole("button", { name: "Niveau exact", exact: true }); await expect(minimumLevel).toHaveAttribute("aria-pressed", "true"); await expect(exactLevel).toHaveAttribute("aria-pressed", "false"); await expect(carry).toHaveAccessibleName("Porter — Niveau minimum"); await expect(crafting).toHaveAccessibleName("Artisanat — Niveau minimum"); 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 exactLevel.click(); await expect(exactLevel).toHaveAttribute("aria-pressed", "true"); await expect(minimumLevel).toHaveAttribute("aria-pressed", "false"); await expect(carry).toHaveAccessibleName("Porter — Niveau exact"); 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(); await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click(); await expect(minimumLevel).toHaveAttribute("aria-pressed", "true"); await expect(exactLevel).toHaveAttribute("aria-pressed", "false"); }); 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(); });