Files
workinf_Blender_Wasm/web/tests/e2e/responsive-layout.spec.ts
mes123456 a69e41457a
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Add full Blender desktop parity validation workflow
2026-09-08 05:18:12 +08:00

144 lines
9.2 KiB
TypeScript

import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
const viewports = [
{ name: "desktop-1440x900", width: 1440, height: 900 },
{ name: "desktop-1280x720", width: 1280, height: 720 },
{ name: "mobile-narrow", width: 360, height: 640 },
{ name: "mobile-extra-narrow", width: 320, height: 568 },
] as const;
test.describe("M7-14 responsive layout", () => {
for (const viewport of viewports) {
test(`${viewport.name} keeps controls inside their layout bands`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
const report = await page.evaluate(() => {
const visible = (element: Element): element is HTMLElement => {
const node = element as HTMLElement;
const style = getComputedStyle(node);
return style.display !== "none" && style.visibility !== "hidden" && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
};
const bands = [".topbar", ".workspace-toolbar", ".storage-budget-panel", ".status-bar"]
.map((selector) => document.querySelector<HTMLElement>(selector))
.filter((element): element is HTMLElement => Boolean(element) && visible(element));
const bandViolations = bands.flatMap((band) => {
const bandRect = band.getBoundingClientRect();
const horizontalConstrained = !["auto", "scroll", "hidden"].includes(getComputedStyle(band).overflowX);
return [...band.querySelectorAll<HTMLElement>("button, input, select, output, span")]
.filter(visible)
.filter((control) => {
const rect = control.getBoundingClientRect();
return rect.top < bandRect.top - 1 || rect.bottom > bandRect.bottom + 1 || (horizontalConstrained && (rect.left < bandRect.left - 1 || rect.right > bandRect.right + 1));
})
.map((control) => `${band.className}:${control.textContent?.trim() || control.getAttribute("aria-label") || control.tagName}`);
});
const textOverflow = [...document.querySelectorAll<HTMLElement>(".topbar button, .topbar select, .workspace-toolbar button, .workspace-toolbar > span, .storage-budget-panel span, .storage-budget-panel output, .editor-header button")]
.filter(visible)
.filter((element) => element.scrollWidth > element.clientWidth + 1)
.map((element) => element.textContent?.trim() || element.getAttribute("aria-label") || element.tagName);
const root = document.documentElement;
return { bandViolations, textOverflow, rootScrollWidth: root.scrollWidth, viewportWidth: window.innerWidth };
});
expect(report.bandViolations, JSON.stringify(report)).toEqual([]);
expect(report.textOverflow, JSON.stringify(report)).toEqual([]);
expect(report.rootScrollWidth, JSON.stringify(report)).toBeLessThanOrEqual(report.viewportWidth);
});
}
test("desktop mirrors Blender 5.2 Layout workspace regions", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
await expect(page.locator(".workspace-tab")).toHaveText([
"Layout", "Modeling", "Sculpting", "UV Editing", "Texture Paint", "Shading",
"Animation", "Rendering", "Compositing", "Geometry Nodes", "Scripting",
]);
const geometry = await page.evaluate(() => {
const rect = (selector: string) => {
const bounds = document.querySelector<HTMLElement>(selector)?.getBoundingClientRect();
if (!bounds) throw new Error(`Missing ${selector}`);
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, bottom: bounds.bottom };
};
const propertyTabs = document.querySelector<HTMLElement>(".property-tabs");
return {
viewport: rect(".viewport-area"),
outliner: rect(".outliner-area"),
properties: rect(".properties-area"),
timeline: rect(".timeline-area"),
status: rect(".status-bar"),
propertyTabsDirection: propertyTabs ? getComputedStyle(propertyTabs).flexDirection : "missing",
};
});
expect(geometry.viewport.width / 1440).toBeGreaterThan(0.75);
expect(geometry.outliner.x).toBeCloseTo(geometry.viewport.width + 1, 0);
expect(geometry.outliner.height).toBeLessThan(geometry.properties.height);
expect(geometry.properties.bottom).toBeCloseTo(geometry.status.y, 0);
expect(geometry.timeline.x).toBe(0);
expect(geometry.timeline.width).toBeCloseTo(geometry.viewport.width, 0);
expect(geometry.timeline.height).toBeGreaterThanOrEqual(96);
expect(geometry.timeline.height).toBeLessThanOrEqual(120);
expect(geometry.propertyTabsDirection).toBe("column");
});
test("desktop cycles every Blender 5.2 workspace with its source editor set", async ({ page }) => {
const expected = {
Layout: { mode: "Object Mode", menus: "View Select Add Object", editors: ["3D Viewport", "Outliner", "Properties", "Timeline"] },
Modeling: { mode: "Edit Mode", menus: "View Select Add Mesh Vertex Edge Face UV", editors: ["3D Viewport", "Outliner", "Properties"] },
Sculpting: { mode: "Sculpt Mode", menus: "Sculpt Mask Face Sets View", editors: ["3D Viewport", "Outliner", "Properties"] },
"UV Editing": { mode: "Edit Mode", menus: "View Select Image UV", editors: ["3D Viewport", "Image Editor", "Outliner", "Properties"] },
"Texture Paint": { mode: "Texture Paint Mode", menus: "View Paint Mask Face Selection", editors: ["3D Viewport", "Image Editor", "Outliner", "Properties"] },
Shading: { mode: "Object Mode", menus: "View Select Add Node", editors: ["3D Viewport", "File Browser", "Image Editor", "Outliner", "Properties", "Shader Editor"] },
Animation: { mode: "Object Mode", menus: "View Select Marker Channel Key", editors: ["3D Viewport", "3D Viewport", "Dope Sheet", "Outliner", "Properties"] },
Rendering: { mode: "Object Mode", menus: "View Image", editors: ["Properties", "Render Result", "Timeline"] },
Compositing: { mode: "Object Mode", menus: "View Select Add Node", editors: ["Compositor", "Image Editor", "Properties", "Timeline"] },
"Geometry Nodes": { mode: "Object Mode", menus: "View Select Add Node", editors: ["3D Viewport", "Geometry Nodes", "Outliner", "Properties", "Spreadsheet", "Timeline"] },
Scripting: { mode: "Object Mode", menus: "View Text Templates", editors: ["3D Viewport", "Info", "Outliner", "Outliner", "Properties", "Python Console", "Text Editor"] },
} as const;
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await expect(page.getByTestId("engine-status")).toContainText("ready, open a .blend file", { timeout: 20_000 });
for (const [workspace, contract] of Object.entries(expected)) {
await page.getByRole("button", { name: workspace, exact: true }).click();
await expect(page.locator("main.blender-app")).toHaveAttribute("data-workspace", workspace);
await expect(page.locator(".mode-chip")).toHaveText(contract.mode);
await expect(page.locator(".workspace-editor-menus")).toHaveText(contract.menus);
const report = await page.evaluate(() => {
const visibleAreas = [...document.querySelectorAll<HTMLElement>(".workspace-grid > .area-frame")].filter((area) => {
const bounds = area.getBoundingClientRect();
return getComputedStyle(area).display !== "none" && bounds.width > 0 && bounds.height > 0;
});
const overlaps: string[] = [];
for (let leftIndex = 0; leftIndex < visibleAreas.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < visibleAreas.length; rightIndex += 1) {
const left = visibleAreas[leftIndex].getBoundingClientRect();
const right = visibleAreas[rightIndex].getBoundingClientRect();
if (Math.min(left.right, right.right) - Math.max(left.left, right.left) > 1 && Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top) > 1) {
overlaps.push(`${visibleAreas[leftIndex].dataset.editor}/${visibleAreas[rightIndex].dataset.editor}`);
}
}
}
return {
editors: visibleAreas.map((area) => area.dataset.editor).sort(),
overlaps,
rootScrollWidth: document.documentElement.scrollWidth,
viewportWidth: window.innerWidth,
};
});
expect(report.editors, workspace).toEqual([...contract.editors].sort());
expect(report.overlaps, workspace).toEqual([]);
expect(report.rootScrollWidth, workspace).toBeLessThanOrEqual(report.viewportWidth);
if (await page.locator(".workspace-editor-surface").count() > 0) {
const accessibility = await new AxeBuilder({ page }).include(".workspace-editor-surface").withTags(["wcag2aa", "wcag21aa", "wcag22aa"]).analyze();
expect(accessibility.violations.filter((violation) => violation.impact === "critical" || violation.impact === "serious").map((violation) => violation.id), workspace).toEqual([]);
}
}
});
});