88 lines
4.9 KiB
TypeScript
88 lines
4.9 KiB
TypeScript
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");
|
|
});
|
|
});
|