fix(ui): unify heading help, table sizing and navigation contracts
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
@@ -1,20 +1,28 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function install(page: Page) {
|
||||
async function install(page: Page, recipientData = false, fieldCount = 0) {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", error => { errors.push(error.message); console.error("Attachment fixture:", error.message); });
|
||||
let revision = 1;
|
||||
const zip = { enabled: true, archives: [{ id: "zip-1", name: "recipient.zip", method: "zip_standard", password_enabled: true }] };
|
||||
let raw = { campaign: { name: "Fixture" }, template: { subject: "Fixture", text: "" }, server: {},
|
||||
...(recipientData ? { recipients: { allow_individual_to: true },
|
||||
fields: Array.from({ length: fieldCount }, (_, index) => ({ name: `field_${index + 1}`, label: `Field ${index + 1}`, type: "string", can_override: true })),
|
||||
entries: { defaults: {}, inline: [
|
||||
{ id: "recipient-1", name: "Fixture recipient", email: "recipient@example.test", channel_policy: "mail",
|
||||
print_target: { target: "Dispatch fixture", channel: "internal_mail" } }
|
||||
] } } : {}),
|
||||
attachments: { base_paths: [{ id: "source-1", name: "Source", path: ".", source: "managed:user:user-1", allow_individual: true }],
|
||||
global: [], zip } };
|
||||
const writes: Record<string, any>[] = [];
|
||||
const mutations: string[] = [];
|
||||
const version = () => ({ id: "version-attachments", campaign_id: "campaign-attachments", version_number: 1,
|
||||
edit_revision: revision, strong_etag: `"version-attachments:${revision}"`, editor_state: {},
|
||||
current_flow: "manual", current_step: "files", workflow_state: "editing", is_complete: false,
|
||||
updated_at: "2026-09-07T10:00:00Z", raw_json: raw });
|
||||
await page.route((url) => url.pathname.startsWith("/api/"), async route => {
|
||||
const request = route.request(); const url = new URL(request.url());
|
||||
if (request.method() !== "GET") mutations.push(`${request.method()} ${url.pathname}`);
|
||||
if (request.method() === "GET") {
|
||||
if (url.pathname.endsWith("/workspace/delta")) return route.fulfill({ json: {
|
||||
campaign: { id: "campaign-attachments", name: "Fixture", current_version_id: "version-attachments", status: "draft" },
|
||||
@@ -38,9 +46,10 @@ async function install(page: Page) {
|
||||
}
|
||||
return route.abort();
|
||||
});
|
||||
await page.goto("/?campaign-attachments");
|
||||
await expect(page.locator("#campaign-attachment-sources .chooser-display-input")).toBeEnabled();
|
||||
return { writes, errors, zip };
|
||||
await page.goto(`/?campaign-attachments${recipientData ? "&recipient-data" : ""}`);
|
||||
if (recipientData) await expect(page.locator('.recipient-profiles-table-surface .data-grid-body-cell[data-column-id="recipients"]')).toContainText("recipient@example.test");
|
||||
else await expect(page.locator("#campaign-attachment-sources .chooser-display-input")).toBeEnabled();
|
||||
return { writes, mutations, errors, zip };
|
||||
}
|
||||
|
||||
test("actual Files chooser opens repeatedly from attachment path by click and keyboard", async ({ page }) => {
|
||||
@@ -82,3 +91,134 @@ test("attachment source corrections save without changing or reauthorizing legac
|
||||
await expect(page.locator('#campaign-attachment-sources input[placeholder="Campaign files"]')).toHaveValue("Updated source name");
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("global attachment labels grow across the fixed source column without changing campaign data", async ({ page }) => {
|
||||
const fixture = await install(page);
|
||||
const card = page.locator("#campaign-global-attachments");
|
||||
const label = card.locator('.data-grid-header-cell[data-column-id="label"]');
|
||||
const basePath = card.locator('.data-grid-header-cell[data-column-id="base_path"]');
|
||||
const pattern = card.locator('.data-grid-header-cell[data-column-id="file_filter"]');
|
||||
const measured = (column: typeof label) => column.evaluate((element) => element.getBoundingClientRect().width);
|
||||
await expect(label).toBeVisible();
|
||||
const handle = label.getByRole("separator");
|
||||
await handle.scrollIntoViewIfNeeded();
|
||||
const initialLabel = await measured(label);
|
||||
const initialBasePath = await measured(basePath);
|
||||
const initialPattern = await measured(pattern);
|
||||
const bounds = (await handle.boundingBox())!;
|
||||
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(bounds.x + bounds.width / 2 + 80, bounds.y + bounds.height / 2, { steps: 6 });
|
||||
await expect.poll(() => measured(label)).toBeCloseTo(initialLabel + 80, 0);
|
||||
await page.mouse.up();
|
||||
await expect.poll(() => measured(label)).toBeCloseTo(initialLabel + 80, 0);
|
||||
await expect.poll(() => measured(basePath)).toBeCloseTo(initialBasePath, 0);
|
||||
await expect.poll(() => measured(pattern)).toBeCloseTo(initialPattern, 0);
|
||||
await handle.press("Shift+ArrowRight");
|
||||
await expect.poll(() => measured(label)).toBeCloseTo(initialLabel + 120, 0);
|
||||
await page.reload();
|
||||
await expect.poll(() => measured(label)).toBeCloseTo(initialLabel + 120, 0);
|
||||
expect(fixture.writes).toHaveLength(0);
|
||||
expect(fixture.mutations).toHaveLength(0);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("recipient and delivery columns grow past the old caps across fixed neighbors and keep personal widths", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 2048, height: 1100 });
|
||||
const fixture = await install(page, true);
|
||||
const grid = page.locator(".recipient-profiles-table-surface");
|
||||
await expect(grid.locator('.data-grid-body-cell[data-column-id="delivery"]')).toContainText("Internal mail: Dispatch fixture");
|
||||
const column = (id: string) => grid.locator(`.data-grid-header-cell[data-column-id="${id}"]`);
|
||||
const measured = (id: string) => column(id).evaluate((element) => element.getBoundingClientRect().width);
|
||||
const fixedWidths = { active: await measured("active"), attachments: await measured("attachments") };
|
||||
const expectedWidths: Record<string, number> = {};
|
||||
for (const [id, oldMaximum] of [["recipients", 640], ["delivery", 480]] as const) {
|
||||
const handle = column(id).getByRole("separator");
|
||||
await grid.locator(".data-grid-scroll-region").evaluate((element, columnId) => {
|
||||
const header = element.querySelector<HTMLElement>(`.data-grid-header-cell[data-column-id="${columnId}"]`)!;
|
||||
element.scrollLeft = Math.max(0, header.offsetLeft - element.clientWidth / 4);
|
||||
}, id);
|
||||
await handle.scrollIntoViewIfNeeded();
|
||||
const initial = await measured(id);
|
||||
const target = Math.max(oldMaximum + 80, initial + 80);
|
||||
const bounds = (await handle.boundingBox())!;
|
||||
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(bounds.x + bounds.width / 2 + target - initial, bounds.y + bounds.height / 2, { steps: 6 });
|
||||
await expect.poll(() => measured(id)).toBeCloseTo(target, 0);
|
||||
await page.mouse.up();
|
||||
await expect.poll(() => measured(id)).toBeCloseTo(target, 0);
|
||||
await handle.press("Shift+ArrowRight");
|
||||
expectedWidths[id] = target + 40;
|
||||
await expect.poll(() => measured(id)).toBeCloseTo(expectedWidths[id], 0);
|
||||
for (const fixed of ["active", "attachments"] as const) await expect.poll(() => measured(fixed)).toBeCloseTo(fixedWidths[fixed], 0);
|
||||
}
|
||||
expect(await grid.locator(".data-grid-scroll-region").evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(true);
|
||||
await page.reload();
|
||||
for (const id of ["recipients", "delivery"]) await expect.poll(() => measured(id)).toBeCloseTo(expectedWidths[id], 0);
|
||||
await expect(grid.locator('.data-grid-body-cell[data-column-id="delivery"]')).toContainText("Internal mail: Dispatch fixture");
|
||||
for (const fixed of ["active", "attachments"] as const) await expect.poll(() => measured(fixed)).toBeCloseTo(fixedWidths[fixed], 0);
|
||||
expect(fixture.writes).toHaveLength(0);
|
||||
expect(fixture.mutations).toHaveLength(0);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("ultrawide recipient grids keep balanced initial widths and resize in both directions across fixed columns", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 3085, height: 1200 });
|
||||
const fixture = await install(page, true, 3);
|
||||
const grid = page.locator(".recipient-profiles-table-surface");
|
||||
const region = grid.locator(".data-grid-scroll-region");
|
||||
const column = (id: string) => grid.locator(`.data-grid-header-cell[data-column-id="${id}"]`);
|
||||
const width = (id: string) => column(id).evaluate(element => element.getBoundingClientRect().width);
|
||||
await expect.poll(() => width("recipients")).toBeLessThanOrEqual(640.01);
|
||||
await expect.poll(() => width("delivery")).toBeLessThanOrEqual(480.01);
|
||||
const fixed = { active: await width("active"), attachments: await width("attachments") };
|
||||
const ready = async (id: string) => {
|
||||
const handle = column(id).getByRole("separator");
|
||||
await handle.scrollIntoViewIfNeeded();
|
||||
return handle;
|
||||
};
|
||||
const keyResize = async (id: string, grow: boolean, count = 1) => {
|
||||
const handle = await ready(id);
|
||||
for (let step = 0; step < count; step += 1) await handle.press(grow ? "Shift+ArrowRight" : "Shift+ArrowLeft");
|
||||
};
|
||||
|
||||
// All right-hand field columns must be adjustable beyond their old 360px
|
||||
// presentation caps, and shrinking must not silently snap back afterwards.
|
||||
for (const id of ["field-field_1", "field-field_2", "field-field_3"]) {
|
||||
const initial = await width(id);
|
||||
await keyResize(id, true, 6);
|
||||
await expect.poll(() => width(id)).toBeCloseTo(initial + 240, 0);
|
||||
await keyResize(id, false, 2);
|
||||
await expect.poll(() => width(id)).toBeCloseTo(initial + 160, 0);
|
||||
}
|
||||
const recipientStart = await width("recipients");
|
||||
await keyResize("recipients", true, 10);
|
||||
await expect.poll(() => width("recipients")).toBeCloseTo(recipientStart + 400, 0);
|
||||
const recipientHandle = await ready("recipients");
|
||||
const bounds = (await recipientHandle.boundingBox())!;
|
||||
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(bounds.x + bounds.width / 2 - 160, bounds.y + bounds.height / 2, { steps: 8 });
|
||||
await expect.poll(() => width("recipients")).toBeCloseTo(recipientStart + 240, 0);
|
||||
await page.mouse.up();
|
||||
await expect.poll(() => width("recipients")).toBeCloseTo(recipientStart + 240, 0);
|
||||
|
||||
const deliveryStart = await width("delivery");
|
||||
await keyResize("delivery", true, 3);
|
||||
await keyResize("delivery", false, 2);
|
||||
await expect.poll(() => width("delivery")).toBeCloseTo(deliveryStart + 40, 0);
|
||||
const savedWidths = await Promise.all(["recipients", "delivery", "field-field_1", "field-field_2", "field-field_3"].map(width));
|
||||
await page.reload();
|
||||
for (const [index, id] of ["recipients", "delivery", "field-field_1", "field-field_2", "field-field_3"].entries()) {
|
||||
await expect.poll(() => width(id)).toBeCloseTo(savedWidths[index], 0);
|
||||
}
|
||||
await (await ready("delivery")).focus();
|
||||
const deliveryBounds = (await column("delivery").boundingBox())!;
|
||||
const viewport = (await region.boundingBox())!;
|
||||
expect(deliveryBounds.x + deliveryBounds.width).toBeGreaterThan(viewport.x);
|
||||
expect(deliveryBounds.x).toBeLessThan(viewport.x + viewport.width);
|
||||
for (const id of ["active", "attachments"] as const) await expect.poll(() => width(id)).toBeCloseTo(fixed[id], 0);
|
||||
expect(fixture.mutations).toHaveLength(0);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user