// Rôle : vérifie les layouts responsive réels et les contraintes de placement compilées. import { test, expect } from "@playwright/test"; import { mountStyles, computed, expectFits } from "./fixtures/compiled-styles.js"; async function columns(locator, count) { await expect(locator).toHaveCSS("display", "grid"); await expect.poll(async () => (await computed(locator, "grid-template-columns")).split(" ").length).toBe(count); } test("calculator layout follows the tool width rather than its location", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mountStyles(page, `
Formula
Current
Result
12
Entries
`); const tool = page.locator('[data-module-type="calculator"]'); const calculator = tool.locator(".calculator-module"); const card = tool.locator(".calculator-card"); await expect(tool).toHaveCSS("container-type", "inline-size"); await columns(calculator, 2); await columns(card, 1); await tool.evaluate((element) => { element.style.width = "700px"; }); await columns(calculator, 1); await columns(card, 2); await expect(tool.locator(".calculator-field")).toHaveCSS("grid-column-start", "1"); await expect(tool.locator(".calculator-result-field")).toHaveCSS("grid-column-start", "2"); await expect(tool.locator(".calculator-result")).toHaveCSS("height", "36px"); await expect(tool.locator(".calculator-result")).toHaveCSS("min-height", "36px"); const resultValue = tool.locator(".calculator-result strong"); const compactResultFontSize = Number.parseFloat(await computed(resultValue, "font-size")); await expect(tool.locator(".calculator-result")).toHaveCSS("padding-top", "7px"); await tool.locator(".calculator-result-field").evaluate((element) => { element.classList.add("has-current"); }); await expect.poll(async () => Number.parseFloat(await computed(tool.locator(".calculator-result"), "height"))).toBeGreaterThan(36); await expect.poll(async () => Number.parseFloat(await computed(resultValue, "font-size"))).toBeGreaterThan(compactResultFontSize); await expect(tool.locator(".calculator-result")).toHaveCSS("padding-top", "12px"); await tool.evaluate((element) => { element.style.width = "480px"; }); await columns(card, 1); await expect(tool.locator(".calculator-result")).toHaveCSS("min-height", "72px"); }); test("toolbox columns follow the toolbox content width", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 720 }); await mountStyles(page, `
Layout
A
B
`); const toolbox = page.locator(".toolbox-page"); const modules = toolbox.locator(".modules"); await columns(modules, 2); await expect(toolbox.locator(".modules-toolbar")).toBeVisible(); await toolbox.evaluate((element) => { element.style.width = "899px"; }); await columns(modules, 1); await expect(toolbox.locator(".modules-toolbar")).toBeHidden(); await expect(toolbox.locator(".module-column").first()).toHaveCSS("display", "contents"); }); for (const width of [1920, 1280, 1025, 1024, 960, 768]) { test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await page.goto("/"); const onboarding = page.locator(".home-onboarding"); await expectFits(onboarding); const hero = page.locator(".hero"); const heroContent = hero.locator(":scope > div").first(); const heroStats = hero.locator(".hero-stats"); await columns(hero, width <= 1024 ? 1 : 2); const [heroContentBounds, heroStatsBounds] = await Promise.all([heroContent.boundingBox(), heroStats.boundingBox()]); if (width <= 1024) { expect(heroStatsBounds.y).toBeGreaterThanOrEqual(heroContentBounds.y + heroContentBounds.height); expect(heroStatsBounds.x).toBeCloseTo(heroContentBounds.x, 0); } else { expect(heroStatsBounds.x).toBeGreaterThan(heroContentBounds.x + heroContentBounds.width); } await columns(page.locator(".home-onboarding-layout"), width <= 1024 ? 1 : 2); await expect(page.locator(".home-onboarding-slider figcaption")).toHaveCSS("position", "absolute"); await expect(page.locator(".home-onboarding-dots")).toHaveCSS("display", "flex"); await expect(page.locator(".home-onboarding-slider img")).toHaveCSS("object-fit", "cover"); await expect(page.locator(".home-visual")).toHaveCSS("display", width <= 1024 ? "none" : "grid"); await expect(page.locator(".origin-copy")).toHaveCSS("border-right-width", width <= 1024 ? "0px" : "2px"); await columns(page.locator(".origin-section"), width <= 1024 ? 1 : 2); const side = page.locator(".sidebar"); await expect(side).toHaveCSS("display", "flex"); { const responsiveCompact = width <= 1024; if (responsiveCompact) await expect(side).toHaveAttribute("data-variant", "compact"); else await expect(side).not.toHaveAttribute("data-variant"); await expect(side).toHaveCSS("width", responsiveCompact ? "72px" : "248px"); await expect(page.locator(".sidebar-mascot")).toHaveCSS("display", responsiveCompact ? "none" : "block"); const secondaryLinks = page.locator(".sidebar-secondary-links"); await expect(secondaryLinks).toHaveCSS("display", "grid"); await expect(page.locator(".sidebar-about-link")).toHaveCount(3); await expect(page.locator(".sidebar-about-link strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block"); await expect(page.locator(".sidebar-note")).toHaveCSS("display", "block"); await expect(page.locator(".sidebar-storage-actions")).toHaveCSS("flex-direction", responsiveCompact ? "column" : "row"); await expect(page.locator(".sidebar-action-button")).toHaveCount(2); await expect(page.locator(".nav-item")).toHaveCount(3); await expect(page.locator(".nav-item strong").first()).toHaveCSS("display", responsiveCompact ? "none" : "block"); const brand = page.locator(".brand"); const brandMark = page.locator(".brand-mark"); await expect(brandMark).toHaveCSS("animation-name", "none"); await expect(brandMark).toHaveCSS("transform", "none"); await brand.hover(); await expect.poll(async () => brandMark.evaluate((element) => globalThis.getComputedStyle(element).transform)).not.toBe("none"); if (!responsiveCompact) { const storageActions = await page.locator(".sidebar-action-button").evaluateAll((elements) => elements.map((element) => element.getBoundingClientRect())); expect(storageActions[1].x).toBeGreaterThan(storageActions[0].x); expect(storageActions[1].y).toBeCloseTo(storageActions[0].y, 0); const secondaryBox = await secondaryLinks.boundingBox(); const storageBox = await page.locator(".sidebar-note").boundingBox(); expect(secondaryBox.y + secondaryBox.height).toBeLessThan(storageBox.y); } } await page.goto("/#/about"); await expectFits(page.locator(".about-page")); await expect(page.locator(".about-story-section")).toHaveCSS("display", "grid"); await columns(page.locator(".about-tools-list"), width <= 760 ? 1 : 2); await expect(page.locator(".about-tool-item .module-icon").first()).toHaveCSS("width", "34px"); await page.goto("/#/library"); await expectFits(page.locator(".library-page")); await columns(page.locator(".library-doc-layout"), width <= 1024 ? 1 : 2); await columns(page.locator(".library-example-layout.has-legend").first(), width <= 1024 ? 1 : 2); expect(await page.locator("body").evaluate((element) => element.scrollWidth)).toBeLessThanOrEqual(width); }); } for (const width of [640, 390]) { test(`below the validated width the compact sidebar remains available without phone navigation at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 720 }); await page.goto("/#/games/aniimo/catalog"); await expect(page.locator(".sidebar")).toBeVisible(); await expect(page.locator(".sidebar")).toHaveCSS("width", "72px"); await expect(page.locator(".mobile-nav")).toHaveCount(0); await expect(page.locator(".route-content-view")).toBeVisible(); await expect(page.locator(".compact-game-view-switch")).toBeVisible(); expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); }); } for (const width of [1280, 768]) { test(`game grids and toolbox columns adapt without horizontal overflow at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await mountStyles(page, `
Game
Game
A
B
Formula
12
Equipment
Materials
Actions
Preferences
`); await columns(page.locator(".game-layout"), 1); await expect(page.locator(".game-grid")).toHaveCSS("display", "grid"); await columns(page.locator(".modules"), width <= 760 ? 1 : 2); await expect(page.locator(".module-column").first()).toHaveCSS("display", width <= 760 ? "contents" : "grid"); await columns(page.locator(".layout-calculator .calculator-module"), width <= 820 ? 1 : 2); await expect(page.locator(".calculator-result")).toHaveCSS("min-height", "72px"); await expect(page.locator(".task-planner-module")).toHaveCSS("display", "grid"); await columns(page.locator(".task-planner-settings"), width <= 760 ? 1 : 2); await expect(page.locator(".equipment-planner-summary")).toHaveCSS("padding", "12px"); await columns(page.locator(".equipment-planner-summary-source-grid"), width <= 760 ? 1 : 2); await page.locator(".equipment-planner-summary-source-grid").evaluate((element) => element.classList.add("columns-1")); await columns(page.locator(".equipment-planner-summary-source-grid"), 1); await expect(page.locator(".toolbox-actions-row")).toHaveCSS("flex-direction", width <= 760 ? "column" : "row"); await page.locator(".modules").evaluate((element) => { element.dataset.layoutCols = "1"; }); await columns(page.locator(".modules"), 1); await expectFits(page.locator("main")); }); test(`cover crop and icon picker respect image geometry at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await mountStyles(page, `
Game
Game page

Aside

`); const frame = page.locator(".toolbox-cover-crop-frame"); await expectFits(frame); const bounds = await frame.boundingBox(); expect(bounds.width / bounds.height).toBeCloseTo(620 / 248, 2); await expect(frame).toHaveCSS("touch-action", "none"); await expect(frame).toHaveCSS("overflow", "hidden"); const icon = page.locator(".toolbox-icon-picker-button"); await expect(icon.locator("img")).toHaveCSS("object-fit", "contain"); await expect(icon.locator("img")).toHaveCSS("width", "72px"); expect(await computed(icon.locator("img"), "border-radius")).toBe(await computed(icon, "border-radius")); await icon.evaluate((element) => element.classList.add("has-custom-cover")); await expect(icon.locator("img")).toHaveCSS("object-fit", "cover"); await expect(page.locator(".toolbox-game-icon img")).toHaveCSS("object-fit", "cover"); await expect(page.locator(".toolbox-cover-menu-action")).toHaveCSS("grid-column", "1 / -1"); await expect(page.locator(".toolbox-icon-picker-menu")).toHaveCSS("position", "absolute"); await expect(page.locator(".toolbox-hero-link")).toHaveCSS("display", "flex"); await expect(page.locator(".dialogue-line i")).toHaveCSS("font-style", "italic"); }); }