Cover remaining Sass families and fix autocomplete hover
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
77f357e056
commit
e52d6e66eb
13 changed files with 617 additions and 286 deletions
|
|
@ -1,14 +1,9 @@
|
|||
// Rôle : vérifie les surfaces de cartes à partir du Sass compilé et des styles calculés.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { compile } from "sass";
|
||||
|
||||
let css;
|
||||
test.beforeAll(() => {
|
||||
css = compile("website/src/styles/main.scss").css;
|
||||
});
|
||||
import { mountStyles } from "./fixtures/compiled-styles.js";
|
||||
|
||||
async function mountCards(page) {
|
||||
await page.setContent(`
|
||||
await mountStyles(page, `
|
||||
<main style="display:grid;gap:24px;padding:24px">
|
||||
<article id="global" class="nds-card" data-variant="interactive">
|
||||
<div class="nds-card-cover">Cover</div><button>Open</button>
|
||||
|
|
@ -20,6 +15,7 @@ async function mountCards(page) {
|
|||
<div class="mhwilds-card-art">Art</div><div class="mhwilds-card-body">Endemic</div>
|
||||
</article>
|
||||
<article id="monster" class="mhwilds-card monster-card" tabindex="0">
|
||||
<span class="mhwilds-flip-indicator">Flip</span>
|
||||
<div class="mhwilds-card-inner">
|
||||
<div class="mhwilds-card-face mhwilds-card-front"><div class="mhwilds-card-art">Front</div></div>
|
||||
<div class="mhwilds-card-face mhwilds-card-back"><div class="mhwilds-card-body">Back</div></div>
|
||||
|
|
@ -27,8 +23,6 @@ async function mountCards(page) {
|
|||
</article>
|
||||
</main>
|
||||
`);
|
||||
await page.addStyleTag({ content: css });
|
||||
await page.mouse.move(0, 0);
|
||||
}
|
||||
|
||||
async function expectSurface(card, { hovered = false, face = false } = {}) {
|
||||
|
|
@ -78,6 +72,8 @@ for (const width of [1280, 390]) {
|
|||
await global.getByRole("button").focus();
|
||||
await expectSurface(global, { hovered: true });
|
||||
const monster = page.locator("#monster");
|
||||
await expect(monster.locator(".mhwilds-flip-indicator")).toHaveCSS("pointer-events", "none");
|
||||
await expect(monster.locator(".mhwilds-flip-indicator")).toHaveCSS("width", "34px");
|
||||
for (const face of await monster.locator(".mhwilds-card-face").all()) {
|
||||
await expectSurface(face, { face: true });
|
||||
await expect(face).toHaveCSS("backface-visibility", "hidden");
|
||||
|
|
|
|||
32
e2e/fixtures/compiled-styles.js
Normal file
32
e2e/fixtures/compiled-styles.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Rôle : monte le CSS complet compilé et lit les styles calculés des fixtures visuelles.
|
||||
import { compile } from "sass";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
let css;
|
||||
export async function mountStyles(page, html) {
|
||||
css ??= compile("website/src/styles/main.scss").css;
|
||||
await page.setContent(`<!doctype html>${html}`);
|
||||
await page.addStyleTag({ content: css });
|
||||
await page.mouse.move(0, 0);
|
||||
}
|
||||
|
||||
export function computed(locator, property, pseudo = null) {
|
||||
return locator.evaluate((element, { property, pseudo }) =>
|
||||
element.ownerDocument.defaultView.getComputedStyle(element, pseudo).getPropertyValue(property),
|
||||
{ property, pseudo });
|
||||
}
|
||||
|
||||
export async function expectPseudo(locator, pseudo, property, value) {
|
||||
await expect.poll(() => computed(locator, property, pseudo)).toEqual(value);
|
||||
}
|
||||
|
||||
export async function expectFits(locator) {
|
||||
await expect(locator).toBeVisible();
|
||||
const viewport = locator.page().viewportSize();
|
||||
await expect.poll(async () => (await locator.boundingBox()).x).toBeGreaterThanOrEqual(-1);
|
||||
await expect.poll(async () => {
|
||||
const bounds = await locator.boundingBox();
|
||||
return bounds.x + bounds.width;
|
||||
}).toBeLessThanOrEqual(viewport.width + 1);
|
||||
expect(await locator.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
146
e2e/nds-controls.spec.js
Normal file
146
e2e/nds-controls.spec.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Rôle : vérifie les tailles et états rendus des familles de contrôles NDS.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { mountStyles, computed, expectPseudo } from "./fixtures/compiled-styles.js";
|
||||
|
||||
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">
|
||||
<input class="nds-input" type="text" data-size="${size}" placeholder="Name">
|
||||
<input class="nds-time" type="time" data-size="${size}">
|
||||
<select class="nds-select" data-size="${size}"><option>Choice</option></select>
|
||||
<textarea class="nds-textarea" data-size="${size}"></textarea>
|
||||
<button class="nds-select-trigger" data-size="${size}">Choice</button>
|
||||
<button class="nds-date-trigger" data-size="${size}">Date</button>
|
||||
</div>`);
|
||||
for (const field of await page.locator("input, select, textarea, button").all()) {
|
||||
const isTextarea = await field.evaluate((element) => element.tagName === "TEXTAREA");
|
||||
await expect(field).toHaveCSS("min-height", `${isTextarea ? textareaHeight : height}px`);
|
||||
await field.focus();
|
||||
await expect(field).toHaveCSS("border-top-color", "rgb(139, 92, 246)");
|
||||
await expect(field).toHaveCSS("box-shadow", "rgba(139, 92, 246, 0.16) 0px 0px 0px 3px");
|
||||
await expect(field).toHaveCSS("outline-style", "solid");
|
||||
await field.evaluate((element) => { element.disabled = true; });
|
||||
await expect(field).toHaveCSS("cursor", "not-allowed");
|
||||
}
|
||||
await expect(page.locator("textarea")).toHaveCSS("resize", "vertical");
|
||||
await expect(page.locator("select")).toHaveCSS("appearance", "none");
|
||||
await expect(page.locator("select")).toHaveCSS("background-image", /data:image\/svg/);
|
||||
expect(await computed(page.locator(".nds-date-trigger"), "mask-image", "::after")).toContain("data:image/svg");
|
||||
await expectPseudo(page.locator(".nds-date-trigger"), "::after", "pointer-events", "none");
|
||||
}
|
||||
});
|
||||
|
||||
test("text and icon buttons retain dimensions, variants, active and joined edges", async ({ page }) => {
|
||||
await mountStyles(page, `<div style="padding:24px;display:flex;flex-wrap:wrap;gap:12px">
|
||||
${[["md", 40], ["sm", 34], ["xs", 30], ["xxs", 26], ["micro", 22]].map(([size]) =>
|
||||
`<button class="nds-icon-button" data-size="${size}"><i class="ui-icon"></i></button>`).join("")}
|
||||
${["md", "sm", "xs"].map((size) => `<button class="nds-button" data-size="${size}">${size}</button>`).join("")}
|
||||
${["primary", "secondary", "ghost", "dashed", "danger"].map((variant) => `<button class="nds-button" data-variant="${variant}">${variant}</button>`).join("")}
|
||||
<button id="edge" class="nds-icon-button" data-edge="left" data-border="joined">L</button>
|
||||
</div>`);
|
||||
for (const [size, button, icon] of [["md", 40, 21], ["sm", 34, 19], ["xs", 30, 17], ["xxs", 26, 15], ["micro", 22, 12]]) {
|
||||
const target = page.locator(`.nds-icon-button[data-size="${size}"]`);
|
||||
await expect(target).toHaveCSS("width", `${button}px`);
|
||||
await expect(target).toHaveCSS("height", `${button}px`);
|
||||
await expect(target.locator("i")).toHaveCSS("width", `${icon}px`);
|
||||
await expect(target.locator("i")).toHaveCSS("height", `${icon}px`);
|
||||
await target.hover();
|
||||
await expect(target).toHaveCSS("background-image", /139, 92, 246/);
|
||||
await expect(target).toHaveCSS("width", `${button}px`);
|
||||
}
|
||||
await page.mouse.move(0, 0);
|
||||
for (const [size, height] of [["md", 42], ["sm", 34], ["xs", 30]]) {
|
||||
await expect(page.locator(`.nds-button[data-size="${size}"]`)).toHaveCSS("min-height", `${height}px`);
|
||||
}
|
||||
await expect(page.locator('[data-variant="primary"]')).toHaveCSS("background-image", /246, 196, 83/);
|
||||
await expect(page.locator('[data-variant="danger"]')).toHaveCSS("color", "rgb(251, 113, 133)");
|
||||
await expect(page.locator('[data-variant="ghost"]')).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(page.locator('[data-variant="dashed"]')).toHaveCSS("border-top-style", "dashed");
|
||||
const toggle = page.locator('[data-variant="secondary"]');
|
||||
await toggle.evaluate((element) => element.setAttribute("aria-pressed", "true"));
|
||||
await expect(toggle).toHaveCSS("background-image", /246, 196, 83/);
|
||||
await toggle.evaluate((element) => { element.disabled = true; });
|
||||
await expect(toggle).toHaveCSS("cursor", "not-allowed");
|
||||
await expect(page.locator("#edge")).toHaveCSS("border-top-right-radius", "0px");
|
||||
await expect(page.locator("#edge")).toHaveCSS("border-right-color", "rgba(0, 0, 0, 0)");
|
||||
});
|
||||
|
||||
test("switches and native checkboxes expose checked and disabled states at each size", async ({ page }) => {
|
||||
for (const [size, height, width, trackHeight, off, on] of [["md", 34, 34, 18, 3, 17], ["lg", 40, 30, 16, 3, 15], ["sm", 28, 24, 14, 2, 12], ["xs", 26, 18, 12, 2, 7]]) {
|
||||
await mountStyles(page, `<button style="margin:24px" class="nds-switch" data-size="${size}" aria-pressed="false"><span class="ui-icon"></span><i></i></button>`);
|
||||
const control = page.locator("button");
|
||||
const track = control.locator("i");
|
||||
await expect(control).toHaveCSS("min-height", `${height}px`);
|
||||
await expect(track).toHaveCSS("width", `${width}px`);
|
||||
await expect(track).toHaveCSS("height", `${trackHeight}px`);
|
||||
await expectPseudo(track, "::before", "left", `${off}px`);
|
||||
await control.evaluate((element) => element.setAttribute("aria-pressed", "true"));
|
||||
await expectPseudo(track, "::before", "left", `${on}px`);
|
||||
await expectPseudo(track, "::before", "background-color", "rgb(246, 196, 83)");
|
||||
}
|
||||
await mountStyles(page, `<div style="padding:24px">
|
||||
${["md", "sm", "xs"].map((size) => `<input type="checkbox" class="nds-checkbox" data-size="${size}" aria-label="${size}">`).join("")}
|
||||
<label class="nds-native-switch"><input type="checkbox"><span></span><b>Native</b></label>
|
||||
</div>`);
|
||||
for (const [size, dimension] of [["md", 20], ["sm", 18], ["xs", 14]]) {
|
||||
const checkbox = page.getByRole("checkbox", { name: size, exact: true });
|
||||
await expect(checkbox).toHaveCSS("width", `${dimension}px`);
|
||||
await expect(checkbox).toHaveCSS("height", `${dimension}px`);
|
||||
await expectPseudo(checkbox, "::before", "transform", "matrix(0, 0, 0, 0, 0, 0)");
|
||||
await checkbox.check();
|
||||
await expectPseudo(checkbox, "::before", "transform", "matrix(1, 0, 0, 1, 0, 0)");
|
||||
await checkbox.evaluate((element) => { element.disabled = true; });
|
||||
await expect(checkbox).toHaveCSS("cursor", "not-allowed");
|
||||
}
|
||||
await page.getByText("Native", { exact: true }).click();
|
||||
const native = page.locator(".nds-native-switch");
|
||||
await expect(native.locator("input")).toBeChecked();
|
||||
await expectPseudo(native.locator("span"), "::before", "left", "19px");
|
||||
await native.locator("input").evaluate((element) => { element.disabled = true; });
|
||||
await expect(native).toHaveCSS("cursor", "not-allowed");
|
||||
});
|
||||
|
||||
test("steppers, radio groups and compound fields retain compact geometry", async ({ page }) => {
|
||||
for (const [size, buttonSize, optionHeight] of [["md", 28, 34], ["sm", 18, 28], ["xs", 16, 26]]) {
|
||||
await mountStyles(page, `<div style="padding:24px">
|
||||
<div class="nds-quantity-stepper" data-size="${size}"><button>-</button><span class="nds-quantity-stepper-value"><input type="number" value="1" class="nds-quantity-stepper-input"></span><button>+</button></div>
|
||||
<div class="nds-radio-button-group" data-size="${size}"><button>A</button><button class="active">B</button></div>
|
||||
<div class="nds-compound-toggle-field"><button class="active">Hold</button><input type="number" class="nds-compound-toggle-field-input"></div>
|
||||
</div>`);
|
||||
for (const button of await page.locator(".nds-quantity-stepper button").all()) {
|
||||
await expect(button).toHaveCSS("width", `${buttonSize}px`);
|
||||
await expect(button).toHaveCSS("height", `${buttonSize}px`);
|
||||
}
|
||||
await expect(page.locator(".nds-radio-button-group > button").first()).toHaveCSS("min-height", `${optionHeight}px`);
|
||||
await expect(page.locator(".nds-radio-button-group > .active")).toHaveCSS("background-image", /246, 196, 83/);
|
||||
await expect(page.locator(".nds-compound-toggle-field-input")).toHaveCSS("min-height", "26px");
|
||||
await expect(page.locator(".nds-compound-toggle-field-input")).toHaveCSS("width", "34px");
|
||||
await expect(page.locator(".nds-compound-toggle-field > button")).toHaveCSS("background-image", /246, 196, 83/);
|
||||
}
|
||||
});
|
||||
|
||||
test("palettes, inline fields and autocomplete menus render their specialised states", async ({ page }) => {
|
||||
await mountStyles(page, `<div style="padding:24px">
|
||||
<button class="nds-swatch" style="--swatch-color:#22d3ee"></button>
|
||||
<button class="nds-swatch" data-variant="rainbow"></button>
|
||||
<button class="nds-large-icon-picker-trigger"><i class="ui-icon"></i></button>
|
||||
<input class="nds-inline-input" type="text" value="Inline">
|
||||
<textarea class="nds-textarea" data-variant="ghost">Memo</textarea>
|
||||
<div class="nds-autocomplete-field"><input class="nds-input" type="text"><div class="nds-autocomplete-menu"><button class="nds-autocomplete-option">Choice</button></div></div>
|
||||
</div>`);
|
||||
await expectPseudo(page.locator(".nds-swatch").first(), "::before", "background-color", "rgb(34, 211, 238)");
|
||||
expect(await computed(page.locator('[data-variant="rainbow"]'), "background-image", "::before")).toContain("conic-gradient");
|
||||
await expect(page.locator(".nds-large-icon-picker-trigger")).toHaveCSS("width", "100px");
|
||||
await expect(page.locator(".nds-large-icon-picker-trigger i")).toHaveCSS("width", "72px");
|
||||
await expect(page.locator(".nds-inline-input")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await page.locator(".nds-inline-input").focus();
|
||||
await expect(page.locator(".nds-inline-input")).toHaveCSS("border-top-color", "rgb(139, 92, 246)");
|
||||
await expect(page.locator("textarea")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(page.locator(".nds-autocomplete-menu")).toHaveCSS("position", "absolute");
|
||||
await expect(page.locator(".nds-autocomplete-menu")).toHaveCSS("overflow-y", "auto");
|
||||
await page.locator(".nds-autocomplete-option").hover();
|
||||
await expect(page.locator(".nds-autocomplete-option")).toHaveCSS("background-color", "rgba(246, 196, 83, 0.1)");
|
||||
await page.mouse.move(0, 0);
|
||||
await page.locator(".nds-autocomplete-option").evaluate((element) => element.classList.add("is-active"));
|
||||
await expect(page.locator(".nds-autocomplete-option")).toHaveCSS("background-color", "rgba(246, 196, 83, 0.1)");
|
||||
});
|
||||
151
e2e/nds-patterns.spec.js
Normal file
151
e2e/nds-patterns.spec.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Rôle : vérifie les surfaces, feedbacks, overlays et préférences visuelles compilés.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { mountStyles, computed, expectPseudo, expectFits } from "./fixtures/compiled-styles.js";
|
||||
|
||||
test("panels, tool surfaces and heroes keep layered non-intercepting decoration", async ({ page }) => {
|
||||
await mountStyles(page, `<main>
|
||||
<section class="nds-panel"><button>Panel</button></section>
|
||||
<section class="nds-tool-panel" data-variant="interactive"><button>Tool</button></section>
|
||||
<section class="nds-filter-panel"><button>Filter</button></section>
|
||||
<header class="nds-page-hero"><h1>Hero</h1></header>
|
||||
<div class="nds-toolbar"><button>A</button><button>B</button></div>
|
||||
<div class="nds-section-heading"><h2>Heading</h2><button>Action</button></div>
|
||||
<div class="nds-empty-state">Empty</div>
|
||||
</main>`);
|
||||
for (const selector of [".nds-panel", ".nds-tool-panel", ".nds-filter-panel", ".nds-page-hero"]) {
|
||||
const surface = page.locator(selector);
|
||||
await expect(surface).toHaveCSS("border-top-width", "1px");
|
||||
await expect(surface).toHaveCSS("overflow", "hidden");
|
||||
await expectPseudo(surface, "::before", "pointer-events", "none");
|
||||
await expectPseudo(surface, "::after", "pointer-events", "none");
|
||||
expect(await computed(surface, "background-image", "::after")).toContain("gradient");
|
||||
await expect(surface.locator(":scope > *")).toHaveCSS("z-index", "1");
|
||||
}
|
||||
await expect(page.locator(".nds-tool-panel")).toHaveCSS("contain", "paint");
|
||||
await page.locator(".nds-tool-panel").hover();
|
||||
await expect(page.locator(".nds-tool-panel")).toHaveCSS("box-shadow", /139, 92, 246/);
|
||||
await expect(page.locator(".nds-filter-panel")).toHaveCSS("backdrop-filter", "blur(10px)");
|
||||
await expect(page.locator(".nds-page-hero")).toHaveCSS("min-height", "180px");
|
||||
await expect(page.locator(".nds-toolbar")).toHaveCSS("flex-wrap", "wrap");
|
||||
await expect(page.locator(".nds-section-heading")).toHaveCSS("justify-content", "space-between");
|
||||
await expect(page.locator(".nds-empty-state")).toHaveCSS("min-height", "132px");
|
||||
await expect(page.locator(".nds-empty-state")).toHaveCSS("align-content", "center");
|
||||
});
|
||||
|
||||
test("badges, notices, progress and chips expose meaningful visual states", async ({ page }) => {
|
||||
const tones = { success: "rgb(52, 211, 153)", warning: "rgb(251, 191, 36)", danger: "rgb(251, 113, 133)" };
|
||||
await mountStyles(page, `<main>
|
||||
${Object.keys(tones).map((tone) => `<span class="nds-badge" data-variant="${tone}">${tone}</span><div class="nds-inline-notice" data-variant="${tone}">${tone}</div><div class="nds-progress" data-variant="${tone}" style="width:200px"><span style="width:50%"></span></div>`).join("")}
|
||||
<label class="nds-chip"><input type="checkbox">Choice</label>
|
||||
</main>`);
|
||||
for (const [tone, color] of Object.entries(tones)) {
|
||||
await expect(page.locator(`.nds-badge[data-variant="${tone}"]`)).toHaveCSS("color", color);
|
||||
await expect(page.locator(`.nds-inline-notice[data-variant="${tone}"]`)).toHaveCSS("color", color);
|
||||
const progress = page.locator(`.nds-progress[data-variant="${tone}"]`);
|
||||
await expect(progress).toHaveCSS("height", "8px");
|
||||
await expect(progress).toHaveCSS("overflow", "hidden");
|
||||
expect(await computed(progress.locator("span"), "background-image")).toContain(color);
|
||||
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");
|
||||
}
|
||||
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)");
|
||||
});
|
||||
|
||||
for (const width of [1280, 390]) {
|
||||
test(`overlays fit, scroll internally and preserve stacking at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
await mountStyles(page, `<div class="confirm-modal-root">
|
||||
<div class="nds-modal-backdrop"></div>
|
||||
<section class="nds-modal confirm-modal"><h2>Confirm</h2><div style="height:1200px">Long content</div><footer class="nds-modal-footer"><button>Close</button></footer></section>
|
||||
</div>`);
|
||||
const modal = page.locator(".nds-modal");
|
||||
await expectFits(modal);
|
||||
await expect(modal).toHaveCSS("overflow-y", "auto");
|
||||
expect((await modal.boundingBox()).height).toBeLessThanOrEqual(752);
|
||||
await modal.evaluate((element) => { element.scrollTop = 300; });
|
||||
expect(await modal.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
||||
await expect(page.locator(".confirm-modal-root")).toHaveCSS("overscroll-behavior", "contain");
|
||||
await expect(page.locator(".nds-modal-backdrop")).toHaveCSS("position", "fixed");
|
||||
await expect(page.locator(".nds-modal-backdrop")).toHaveCSS("backdrop-filter", "blur(6px)");
|
||||
await page.locator("body").evaluate((element) => element.classList.add("is-modal-open"));
|
||||
await expect(page.locator("body")).toHaveCSS("overflow", "hidden");
|
||||
|
||||
await mountStyles(page, `<div class="drawer" aria-hidden="false"><div class="drawer-backdrop"></div>
|
||||
<section class="drawer-panel nds-drawer-panel"><div style="height:1400px">Drawer</div></section>
|
||||
<div class="drawer-resize-handle nds-drawer-resize-handle"></div></div>`);
|
||||
const drawer = page.locator(".drawer-panel");
|
||||
await expectFits(drawer);
|
||||
await expect(drawer).toHaveCSS("top", width <= 760 ? "110px" : "68px");
|
||||
await expect(drawer).toHaveCSS("position", "fixed");
|
||||
await expect(drawer).toHaveCSS("overflow-y", "auto");
|
||||
await expect(drawer).toHaveCSS("backdrop-filter", "blur(10px)");
|
||||
await expect(page.locator(".drawer-resize-handle")).toHaveCSS("display", width <= 760 ? "none" : "block");
|
||||
await page.locator(".drawer").evaluate((element) => element.setAttribute("aria-hidden", "true"));
|
||||
await expect(drawer).toBeHidden();
|
||||
|
||||
await mountStyles(page, `<div class="tool-add-modal-root"><section class="nds-modal tool-add-modal">Tools</section></div>`);
|
||||
await expectFits(page.locator(".tool-add-modal"));
|
||||
await expect(page.locator(".tool-add-modal-root")).toHaveCSS("overscroll-behavior", "contain");
|
||||
await expect(page.locator(".tool-add-modal-root")).toHaveCSS("z-index", "130");
|
||||
});
|
||||
|
||||
test(`toast positions, popovers and reduced motion render correctly at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
await mountStyles(page, `<div class="nds-popover">Popover</div>
|
||||
<div class="nds-toast-stack notification-toast-stack" data-position="right"><div class="nds-toast notification-toast"><span>Notification</span><button>OK</button></div></div>`);
|
||||
const stack = page.locator(".nds-toast-stack");
|
||||
const toast = page.locator(".nds-toast");
|
||||
await expect(stack).toHaveCSS("position", "fixed");
|
||||
await expect(stack).toHaveCSS("pointer-events", "none");
|
||||
await expect(toast).toHaveCSS("pointer-events", "auto");
|
||||
await expect(toast).toHaveCSS("background-image", /conic-gradient/);
|
||||
await expect.poll(async () => (await toast.boundingBox()).x + (await toast.boundingBox()).width).toBeLessThanOrEqual(width);
|
||||
await expect(page.locator(".nds-popover")).toHaveCSS("overflow", "hidden");
|
||||
await expect(page.locator(".nds-popover")).toHaveCSS("background-color", "rgba(7, 10, 24, 0.96)");
|
||||
await stack.evaluate((element) => { element.dataset.position = "left"; element.classList.add("is-left"); });
|
||||
await expect(stack).toHaveCSS("left", "20px");
|
||||
await expect(toast).toHaveCSS("animation-name", /notification-toast-enter-left/);
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await expect(toast).toHaveCSS("animation-duration", "1e-05s");
|
||||
await expect(toast).toHaveCSS("animation-iteration-count", "1");
|
||||
await expectFits(toast);
|
||||
});
|
||||
}
|
||||
|
||||
test("event sizes preserve metadata density and light/dark contrast", async ({ page }) => {
|
||||
await mountStyles(page, `<main>${["md", "sm", "xs"].map((size) => `<div class="nds-event-surface" data-size="${size}"><button><strong>Title</strong><span>12:30</span></button></div>`).join("")}</main>`);
|
||||
for (const [size, height] of [["md", 44], ["sm", 22], ["xs", 10]]) {
|
||||
const event = page.locator(`[data-size="${size}"]`);
|
||||
await expect(event).toHaveCSS("min-height", `${height}px`);
|
||||
await expect(event.locator("span")).toHaveCSS("display", size === "md" ? "block" : "none");
|
||||
await expect(event.locator("strong")).toHaveCSS("text-overflow", "ellipsis");
|
||||
const light = await computed(event.locator("button"), "color");
|
||||
await event.evaluate((element) => { element.dataset.variant = "dark"; element.style.setProperty("--nds-event-color", "#f6c453"); });
|
||||
await expect.poll(() => computed(event.locator("button"), "color")).not.toBe(light);
|
||||
await event.locator("button").hover();
|
||||
await expect(event.locator("button")).toHaveCSS("background-color", size === "xs" ? "rgba(0, 0, 0, 0.18)" : "rgba(0, 0, 0, 0.14)");
|
||||
}
|
||||
});
|
||||
|
||||
test("scrollbars and nebula layers compile without blocking content", async ({ page }) => {
|
||||
const classes = ["themed-scrollbar", "legacy-scrollbar", "nds-scrollbox", "module-content", "table-grid-wrap", "notepad-surface", "annotation-marker-list", "goal-tree-view-wrap", "filter-options"];
|
||||
await mountStyles(page, classes.map((name) => `<div class="${name}" style="height:60px;overflow:auto"><div style="height:500px">Scroll</div></div>`).join(""));
|
||||
for (const name of classes) {
|
||||
const box = page.locator(`.${name}`);
|
||||
await expect(box).toHaveCSS("scrollbar-width", "thin");
|
||||
await expect(box).toHaveCSS("scrollbar-color", "rgb(88, 55, 174) rgba(0, 0, 0, 0)");
|
||||
await expectPseudo(box, "::-webkit-scrollbar", "width", "6px");
|
||||
await box.evaluate((element) => { element.scrollTop = 100; });
|
||||
expect(await box.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
||||
}
|
||||
for (const pseudo of ["::before", "::after"]) {
|
||||
await expectPseudo(page.locator("body"), pseudo, "position", "fixed");
|
||||
await expectPseudo(page.locator("body"), pseudo, "pointer-events", "none");
|
||||
expect(await computed(page.locator("body"), "background-image", pseudo)).toContain("radial-gradient");
|
||||
}
|
||||
});
|
||||
95
e2e/style-layouts.spec.js
Normal file
95
e2e/style-layouts.spec.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// 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);
|
||||
}
|
||||
|
||||
for (const width of [1280, 900, 390]) {
|
||||
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);
|
||||
await columns(page.locator(".home-onboarding-layout"), width <= 760 ? 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");
|
||||
const side = page.locator(".sidebar");
|
||||
await expect(side).toHaveCSS("display", width <= 760 ? "none" : "flex");
|
||||
await expect(page.locator(".sidebar-mascot")).toHaveCSS("display", width <= 1024 ? "none" : "block");
|
||||
for (const link of await page.locator(".sidebar-about-link").all()) {
|
||||
await expect(link).toHaveCSS("align-items", "center");
|
||||
if (width <= 1024) await expect(link).toHaveCSS("justify-content", "center");
|
||||
}
|
||||
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 <= 760 ? 1 : 2);
|
||||
expect(await page.locator("body").evaluate((element) => element.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
});
|
||||
}
|
||||
|
||||
for (const width of [1280, 390]) {
|
||||
test(`game grids and toolbox columns adapt without horizontal overflow at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await mountStyles(page, `<main>
|
||||
<div class="game-layout"><aside class="game-filters"><div class="filter-panel nds-filter-panel">Filter</div></aside><div class="game-grid"><article>Game</article><article>Game</article></div></div>
|
||||
<div class="modules" data-layout-cols="2"><div class="module-column"><article class="module">A</article></div><div class="module-column"><article class="module">B</article></div></div>
|
||||
<div class="calculator-module"><div>Formula</div><div class="calculator-result"><strong>12</strong></div></div>
|
||||
<div class="task-planner-module"><div class="task-planner-settings"><label>Reset</label><label>Time</label></div></div>
|
||||
<div class="equipment-planner-summary"><div class="equipment-planner-summary-source-grid"><div>Equipment</div><div>Materials</div></div></div>
|
||||
<div class="toolbox-actions-row"><div>Actions</div><div>Preferences</div></div>
|
||||
</main>`);
|
||||
await columns(page.locator(".game-layout"), width <= 760 ? 1 : 2);
|
||||
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(".calculator-module"), width <= 760 ? 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, `<main>
|
||||
<section class="toolbox-cover-editor"><div class="toolbox-cover-crop-frame"></div></section>
|
||||
<div class="toolbox-icon-picker"><button class="toolbox-icon-picker-button"><img alt="Icon"></button>
|
||||
<div class="toolbox-icon-picker-menu"><button>A</button><button class="toolbox-cover-menu-action nds-button">Cover</button></div></div>
|
||||
<div class="toolbox-game-icon"><img alt="Game"></div><a class="toolbox-hero-link">Game page</a>
|
||||
<p class="dialogue-line"><i>Aside</i></p>
|
||||
</main>`);
|
||||
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");
|
||||
});
|
||||
}
|
||||
135
e2e/tool-style-states.spec.js
Normal file
135
e2e/tool-style-states.spec.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// Rôle : vérifie les états visuels des outils, leurs pickers et les masques d'icônes compilés.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { mountStyles, computed, expectPseudo, expectFits } from "./fixtures/compiled-styles.js";
|
||||
|
||||
test("table cells distinguish selection, move targets, editing and errors", async ({ page }) => {
|
||||
await mountStyles(page, `<div class="table-module"><div class="table-grid-wrap"><input type="text" class="table-cell-input" value="12"></div></div>`);
|
||||
await expect(page.locator(".table-module")).toHaveCSS("display", "grid");
|
||||
const cell = page.locator("input");
|
||||
await expect(cell).toHaveCSS("caret-color", "rgba(0, 0, 0, 0)");
|
||||
await cell.evaluate((element) => element.classList.add("is-selected"));
|
||||
await expect(cell).toHaveCSS("box-shadow", /34, 211, 238/);
|
||||
await cell.evaluate((element) => { element.className = "table-cell-input is-move-target"; });
|
||||
await expect(cell).toHaveCSS("box-shadow", /246, 196, 83/);
|
||||
await expect(cell).toHaveCSS("cursor", "move");
|
||||
await cell.evaluate((element) => { element.className = "table-cell-input is-editing"; });
|
||||
await cell.focus();
|
||||
await expect(cell).not.toHaveCSS("caret-color", "rgba(0, 0, 0, 0)");
|
||||
await cell.evaluate((element) => { element.blur(); element.className = "table-cell-input has-error"; });
|
||||
await expect(cell).toHaveCSS("color", "rgb(254, 202, 202)");
|
||||
});
|
||||
|
||||
test("goal nodes and NDS reorder indicators follow orientation and placement", async ({ page }) => {
|
||||
await mountStyles(page, `<main>
|
||||
<div class="goal-tree-node is-drop-target" data-reorder-orientation="vertical"><div class="goal-tree-node-card"><div class="goal-tree-node-content-row"><span>1</span><span>Goal</span></div></div></div>
|
||||
<div class="nds-reorder-indicator" style="width:200px;height:60px"></div>
|
||||
</main>`);
|
||||
const node = page.locator(".goal-tree-node");
|
||||
const card = page.locator(".goal-tree-node-card");
|
||||
const indicator = page.locator(".nds-reorder-indicator");
|
||||
await expect(card).toHaveCSS("border-top-color", "rgba(246, 196, 83, 0.48)");
|
||||
await expect(page.locator(".goal-tree-node-content-row")).toHaveCSS("display", "grid");
|
||||
for (const axis of ["vertical", "horizontal"]) {
|
||||
for (const placement of ["before", "after"]) {
|
||||
await node.evaluate((element, { axis, placement }) => { element.dataset.reorderOrientation = axis; element.classList.toggle("drop-after", placement === "after"); }, { axis, placement });
|
||||
await indicator.evaluate((element, { axis, placement }) => { element.dataset.axis = axis; element.dataset.placement = placement; }, { axis, placement });
|
||||
const side = axis === "vertical" ? (placement === "before" ? "top" : "bottom") : (placement === "before" ? "left" : "right");
|
||||
const thickness = axis === "vertical" ? "height" : "width";
|
||||
await expectPseudo(card, "::after", side, "-8px");
|
||||
await expectPseudo(card, "::after", thickness, "3px");
|
||||
await expectPseudo(indicator, "::before", side, "0px");
|
||||
await expectPseudo(indicator, "::before", thickness, "3px");
|
||||
await expectPseudo(card, "::after", "pointer-events", "none");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("calendar picker opens on focus and alert controls retain blocked states", async ({ page }) => {
|
||||
await mountStyles(page, `<main><form class="calendar-event-form">
|
||||
<div class="calendar-month-date-dropdown"><button type="button" class="calendar-month-date-trigger">Date</button><div class="calendar-month-date-picker"><button type="button">1</button></div></div>
|
||||
<div class="timer-alert-mode-switch calendar-alert-mode-switch"><button type="button" class="timer-alert-mode-button">Alert</button></div>
|
||||
<button type="button" class="tool-split-scroll-toggle"><span class="ui-icon"></span><i></i></button>
|
||||
</form></main>`);
|
||||
await expect(page.locator("form")).toHaveCSS("display", "grid");
|
||||
await expect(page.locator("form")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
const picker = page.locator(".calendar-month-date-picker");
|
||||
await expect(picker).toHaveCSS("opacity", "0");
|
||||
await expect(picker).toHaveCSS("pointer-events", "none");
|
||||
await page.locator(".calendar-month-date-trigger").focus();
|
||||
await expect(picker).toHaveCSS("opacity", "1");
|
||||
await expect(picker).toHaveCSS("pointer-events", "auto");
|
||||
expect((await computed(picker, "grid-template-columns")).split(" ")).toHaveLength(7);
|
||||
const alert = page.locator(".timer-alert-mode-switch");
|
||||
await expect(alert).toHaveCSS("align-self", "start");
|
||||
await alert.evaluate((element) => element.classList.add("is-blocked"));
|
||||
await expect(alert.locator("button")).toHaveCSS("opacity", "0.2");
|
||||
await expect(alert.locator("button")).toHaveCSS("cursor", "not-allowed");
|
||||
const scroll = page.locator(".tool-split-scroll-toggle");
|
||||
await scroll.focus();
|
||||
await expect(picker).toHaveCSS("opacity", "0");
|
||||
await expectPseudo(scroll.locator("i"), "::before", "left", "3px");
|
||||
await scroll.evaluate((element) => element.classList.add("active"));
|
||||
await expectPseudo(scroll.locator("i"), "::before", "left", "17px");
|
||||
});
|
||||
|
||||
test("combo tokens distinguish analog, directional and held inputs", async ({ page }) => {
|
||||
await mountStyles(page, `<main>
|
||||
<div class="combo-hold-field"><div class="combo-hold-controls"><button class="active">Hold</button><input type="number" value="1"></div></div>
|
||||
<span id="analog" class="combo-input-token combo-input-direction combo-device-n64 combo-value-stick-up">Up</span>
|
||||
<span id="direction" class="combo-input-token combo-input-direction combo-device-n64 combo-value-up">Up</span>
|
||||
<span class="combo-input-token combo-value-grab has-hold"><i class="ui-icon"></i></span>
|
||||
</main>`);
|
||||
await expect(page.locator(".combo-hold-controls")).toHaveCSS("width", "92px");
|
||||
await expect(page.locator(".combo-hold-controls button")).toHaveCSS("background-image", /246, 196, 83/);
|
||||
await expect(page.locator("#analog")).toHaveCSS("color", "rgb(165, 243, 252)");
|
||||
await expect(page.locator("#direction")).toHaveCSS("color", "rgb(248, 217, 130)");
|
||||
await expect(page.locator("#direction")).toHaveCSS("border-radius", "10px");
|
||||
await expect(page.locator(".combo-value-grab .ui-icon")).toHaveCSS("width", "24px");
|
||||
await expect(page.locator(".has-hold")).toHaveCSS("box-shadow", /7px/);
|
||||
});
|
||||
|
||||
for (const width of [1280, 390]) {
|
||||
test(`text exchange and image annotation keep usable bounds at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await mountStyles(page, `<main>
|
||||
<div class="text-import-modal-dialog nds-modal"><form class="text-import-modal-form"><textarea class="has-error"></textarea><footer class="text-import-actions"><button>Import</button><button>Export</button></footer></form></div>
|
||||
<div id="image" style="position:relative;width:300px;height:200px"><button class="annotation-marker">1</button></div>
|
||||
</main>`);
|
||||
await expectFits(page.locator(".text-import-modal-dialog"));
|
||||
await expect(page.locator(".text-import-actions")).toHaveCSS("flex-wrap", "wrap");
|
||||
await expect(page.locator("textarea")).toHaveCSS("border-top-color", "rgba(248, 113, 113, 0.75)");
|
||||
const marker = page.locator(".annotation-marker");
|
||||
for (const position of ["0%", "50%", "100%"]) {
|
||||
await marker.evaluate((element, position) => { element.style.setProperty("--marker-x", position); element.style.setProperty("--marker-y", position); }, position);
|
||||
const box = await marker.boundingBox();
|
||||
const frame = await page.locator("#image").boundingBox();
|
||||
expect(box.width).toBe(30);
|
||||
expect(box.height).toBe(30);
|
||||
expect(box.x).toBeGreaterThanOrEqual(frame.x);
|
||||
expect(box.y).toBeGreaterThanOrEqual(frame.y);
|
||||
expect(box.x + box.width).toBeLessThanOrEqual(frame.x + frame.width);
|
||||
expect(box.y + box.height).toBeLessThanOrEqual(frame.y + frame.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("public icon masks resolve to decodable assets instead of empty shapes", async ({ page }) => {
|
||||
const icons = ["drag", "dropdown", "enter", "calculator", "scrollable", "hide", "eye-open", "eye-closed", "chevron-down", "chevron-up", "flip", "add", "add-row", "add-column", "remove-row", "remove-column", "table", "map", "grab", "shield", "gun"];
|
||||
const modules = ["abacus", "calculator", "columns", "table", "chest-armor"];
|
||||
await page.route("**/__style-icons", (route) => route.fulfill({ contentType: "text/html", body: "<!doctype html><html><body></body></html>" }));
|
||||
await page.goto("/__style-icons");
|
||||
await mountStyles(page, `<main>${icons.map((name) => `<i class="ui-icon ui-icon-${name}"></i>`).join("")}${modules.map((name) => `<i class="module-icon-svg module-icon-${name}"></i>`).join("")}</main>`);
|
||||
for (const icon of await page.locator("i").all()) {
|
||||
await expect(icon).toHaveCSS("mask-size", "contain");
|
||||
const mask = await computed(icon, "mask-image");
|
||||
expect(mask).toMatch(/^url\(".+"\)$/);
|
||||
const url = JSON.parse(mask.slice(4, -1));
|
||||
const decoded = await icon.evaluate(async (element, url) => {
|
||||
const image = new element.ownerDocument.defaultView.Image();
|
||||
image.src = url;
|
||||
await image.decode();
|
||||
return image.naturalWidth > 0 && image.naturalHeight > 0;
|
||||
}, url);
|
||||
expect(decoded, url).toBe(true);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue