feat: improve Drive file browsing and actions

This commit is contained in:
2026-10-09 18:39:27 +09:00
parent 73ac13e802
commit e8828cd1b5
6 changed files with 680 additions and 169 deletions
@@ -47,4 +47,6 @@ Home/sidebar and Workers are captured under identical owner/member/error/empty/l
The production shell scrolls `main` internally, so `fullPage` does not expand that scroll panel. Capture points use ordinary keyboard focus to show the actual form or the top denial state; no CSS/DOM screenshot override is applied. The responsive audit separately records document/main/table-region geometry, visible keyboard focus, and page errors across the full theme/viewport matrix. Raster checks wait for actual decoding, assert natural dimensions 1200×800, preserve the 3:2 aspect ratio, and constrain the rendered box by both the available width and 60vh height at all four widths/both themes.
The Drive browser suite also exercises the file-browser redesign at 1440/768/390/320px in light/dark themes: menu placement, native-dialog focus and Escape restoration, folder creation, search, and destination-folder navigation. Screenshots are written to `.svelte-kit/drive-redesign/` for the list, New menu, creation dialog, Markdown preview, and move dialog. Component tests cover failed/paged destination reads, exclusion of files and the moving folder itself, and pinned mutation revisions. These checks use the same fixture boundary, not a live Backend.
Parent and reviewer must open images and inspect context before asserting visual acceptance. In particular, a successful capture command is not sufficient and HTTP 200 does not excuse visible UI errors.
@@ -68,6 +68,7 @@ for (const scheme of ["light", "dark"]) {
label: "Rename / move pinned revision",
fullPage: true,
interaction: [
{ action: "click", selector: '.drive-menu > summary[aria-label="More actions"]' },
{ action: "click", selector: '.drive-toolbar button:has-text("Rename / move")' },
{
action: "wait",
@@ -82,6 +83,7 @@ for (const scheme of ["light", "dark"]) {
fullPage: true,
interaction: [
{ action: "click", selector: '.drive-form button:has-text("Cancel form")' },
{ action: "click", selector: '.drive-menu > summary[aria-label="More actions"]' },
{ action: "click", selector: '.drive-toolbar button:has-text("Replace file")' },
{
action: "wait",
@@ -244,6 +246,7 @@ for (const scheme of ["light", "dark"]) {
label: "Member after typed write denial",
fullPage: true,
interaction: [
{ action: "click", selector: '.drive-create-menu > summary' },
{
action: "click",
selector: '.drive-page button:has-text("New folder")',
@@ -261,12 +264,12 @@ for (const scheme of ["light", "dark"]) {
action: "wait",
ready: {
kind: "selector",
selector: '.drive-page:has-text("Read only / access denied by Backend")',
selector: '.drive-dialog:has-text("Read only / access denied by Backend")',
},
},
{
action: "press",
selector: ".drive-heading button",
selector: '.drive-dialog button[aria-label="Close dialog"]',
key: "Shift",
},
],
@@ -5,10 +5,14 @@ import { chromium, type Page, type Route } from "playwright";
import { Buffer } from "node:buffer";
import type { FixtureLog } from "../../../../tools/web-ux/drive-fixture/api.ts";
const root = join(dirname(fromFileUrl(import.meta.url)), "../../../..");
const ready = (page: Page) => page.locator('[data-drive-ready="true"]').waitFor();
const button = (page: Page, name: string) => page.getByRole("button", { name, exact: true });
const draft = (page: Page) => page.getByRole("textbox", { name: /^Draft \(expected revision/ });
const preview = (page: Page) => page.getByRole("article", { name: "Current saved content" });
const ready = (page: Page) =>
page.locator('[data-drive-ready="true"]').waitFor();
const button = (page: Page, name: string) =>
page.getByRole("button", { name, exact: true });
const draft = (page: Page) =>
page.getByRole("textbox", { name: /^Draft \(expected revision/ });
const preview = (page: Page) =>
page.getByRole("article", { name: "Current saved content" });
function gate() {
let release!: () => void;
const promise = new Promise<void>((resolve) => release = resolve);
@@ -180,7 +184,9 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
await draft(second).inputValue(),
"Second actor unsaved draft",
);
const casWrites = (await logs(origin)).filter((x) => x.path === "/mutate");
const casWrites = (await logs(origin)).filter((x) =>
x.path === "/mutate"
);
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedRevision]), [[
"3",
"1",
@@ -235,7 +241,7 @@ Deno.test("production Drive delayed old read cannot replace another file or work
await ready(page);
assert((await preview(page).textContent())!.includes("Second document"));
assertEquals(
await page.locator("main .drive-url a").getAttribute("href"),
await page.locator(".drive-details dd a").getAttribute("href"),
"/w/home-member/drive/4",
);
} finally {
@@ -321,7 +327,9 @@ Deno.test("production Drive delayed old save and status preserve destination dra
await button(page, "Save").click();
await page.getByText("Published — DB commit confirmed", { exact: false })
.last().waitFor();
const pinnedWrites = (await logs(origin)).filter((x) => x.path === "/mutate");
const pinnedWrites = (await logs(origin)).filter((x) =>
x.path === "/mutate"
);
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedRevision]), [
["3", "1"],
["4", "1"],
@@ -342,12 +350,15 @@ Deno.test("production Drive rename move delete recreate preserve stable identity
try {
await page.goto(`${origin}/w/home-owner/drive/3`);
await ready(page);
await page.getByLabel("More actions", { exact: true }).click();
await button(page, "Rename / move").click();
await page.getByRole("textbox", { name: "Name", exact: true }).fill(
"renamed.md",
);
await page.getByRole("textbox", { name: "Destination folder node ID" })
.fill("2");
await page.getByRole("dialog").getByRole("button", {
name: "資料",
exact: true,
}).click();
await button(page, "Apply rename / move").click();
await page.getByRole("heading", { name: "renamed.md", exact: true })
.waitFor();
@@ -361,6 +372,7 @@ Deno.test("production Drive rename move delete recreate preserve stable identity
await ready(page);
await page.getByRole("link", { name: "renamed.md", exact: true }).click();
await ready(page);
await page.getByLabel("More actions", { exact: true }).click();
await button(page, "Delete").click();
await page.getByRole("alert").filter({ hasText: /not[_ ]found/i })
.waitFor();
@@ -368,6 +380,8 @@ Deno.test("production Drive rename move delete recreate preserve stable identity
await rootFolder(page);
await page.getByRole("link", { name: "資料", exact: true }).click();
await ready(page);
await page.locator(".drive-menu > summary").filter({ hasText: /^New$/ })
.click();
await button(page, "New Markdown").click();
await page.getByRole("textbox", { name: "Name", exact: true }).fill(
"renamed.md",
@@ -445,6 +459,7 @@ Deno.test("production Drive upload response loss and navigation cancellation que
await transmitted.promise;
await button(page, "Cancel transfer / check outcome").click();
await button(page, "Check request result").waitFor();
await button(page, "Cancel form").click();
await page.getByRole("link", { name: "資料", exact: true }).click();
await page.getByRole("heading", { name: "資料", exact: true }).waitFor();
await ready(page);
@@ -607,20 +622,26 @@ Deno.test("production Drive denial empty pages truncation unsafe content and ima
await page.getByText("This file type is download-only.", { exact: false })
.waitFor();
assertEquals(
await page.locator("main svg, main iframe, main object").count(),
await page.locator(
"main svg:not([aria-hidden]), main iframe, main object",
).count(),
0,
);
await page.goto(`${origin}/w/home-owner/drive/5`);
await ready(page);
await page.waitForFunction(() => {
const image = document.querySelector("main .drive-image");
return image instanceof HTMLImageElement && image.complete && image.naturalWidth === 1200 &&
return image instanceof HTMLImageElement && image.complete &&
image.naturalWidth === 1200 &&
image.naturalHeight === 800;
});
assertEquals(
await page.getByRole("img", { name: "landscape.png", exact: true }).evaluate((image) =>
image instanceof HTMLImageElement ? [image.naturalWidth, image.naturalHeight] : []
),
await page.getByRole("img", { name: "landscape.png", exact: true })
.evaluate((image) =>
image instanceof HTMLImageElement
? [image.naturalWidth, image.naturalHeight]
: []
),
[1200, 800],
);
await page.goto(`${origin}/w/home-owner/drive/7`);
@@ -713,3 +734,154 @@ Deno.test("production Drive Markdown safe image placeholders preserve each image
await page.close();
}
}));
Deno.test("production Drive menus dialogs folder picker and search remain usable at every viewport", () =>
harness(async (origin, browser) => {
for (const colorScheme of ["light", "dark"] as const) {
for (const width of [1440, 768, 390, 320]) {
const context = await browser.newContext({
viewport: { width, height: 900 },
colorScheme,
reducedMotion: "reduce",
});
const page = await context.newPage();
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(String(error)));
const capture = async (surface: string) => {
const geometry = await page.evaluate(() => {
const main = document.querySelector("main")!;
const dialog = document.querySelector("dialog[open]");
return {
viewport: innerWidth,
document: document.documentElement.scrollWidth,
main: main.clientWidth,
mainScroll: main.scrollWidth,
dialog: dialog?.clientWidth,
dialogScroll: dialog?.scrollWidth,
};
});
assert(geometry.document <= width, JSON.stringify(geometry));
assert(
geometry.mainScroll <= geometry.main + 1,
JSON.stringify(geometry),
);
if (geometry.dialog) {
assert(
geometry.dialogScroll! <= geometry.dialog + 1,
JSON.stringify(geometry),
);
}
await page.screenshot({
path: join(
root,
`web/workspace/.svelte-kit/drive-redesign/${surface}-${colorScheme}-${width}.png`,
),
});
};
try {
const reset = await fetch(`${origin}/__fixture/reset`, {
method: "POST",
});
await reset.body?.cancel();
await page.goto(`${origin}/w/home-owner/drive`);
await ready(page);
const newMenu = page.locator(".drive-menu > summary").filter({
hasText: /^New$/,
});
assertEquals(await button(page, "New folder").isVisible(), false);
await capture("files");
await newMenu.press("Enter");
await button(page, "New folder").waitFor();
const menuBox = await page.locator(
".drive-create-menu .drive-menu-panel",
).boundingBox();
assert(
menuBox && menuBox.x >= 0 && menuBox.x + menuBox.width <= width,
JSON.stringify(menuBox),
);
await capture("menu");
await button(page, "New folder").click();
const dialog = page.getByRole("dialog", {
name: "New folder",
exact: true,
});
await dialog.waitFor();
assert(
await dialog.getByLabel("Name", { exact: true }).evaluate((el) =>
el === document.activeElement
),
);
await capture("create");
for (let i = 0; i < 8; i++) {
await page.keyboard.press("Tab");
// Native dialogs may cycle through browser chrome (reported as body),
// but background page controls must stay inert.
assert(
await dialog.evaluate((el) =>
el.contains(document.activeElement) ||
document.activeElement === document.body
),
"Modal must not focus background controls",
);
}
await page.keyboard.press("Escape");
await dialog.waitFor({ state: "hidden" });
await newMenu.locator(":scope:focus").waitFor();
await newMenu.press("Enter");
await button(page, "New folder").focus();
await page.keyboard.press("Escape");
assertEquals(await button(page, "New folder").isVisible(), false);
await newMenu.locator(":scope:focus").waitFor();
await newMenu.press("Enter");
await button(page, "New folder").click();
const folderName = `Created ${colorScheme} ${width}`;
await dialog.getByLabel("Name", { exact: true }).fill(folderName);
await button(page, "Create").click();
await dialog.waitFor({ state: "hidden" });
await page.getByRole("link", { name: folderName, exact: true })
.waitFor();
await page.getByRole("searchbox", { name: "Search Drive" }).fill(
folderName,
);
await button(page, "Search").click();
await button(page, "Show folder").waitFor();
await ready(page);
assertEquals(await page.locator(".drive-table tbody tr").count(), 1);
await button(page, "Show folder").click();
await ready(page);
await page.getByRole("link", { name: "README.md", exact: true })
.click();
await page.waitForURL("**/w/home-owner/drive/3");
await ready(page);
await preview(page).waitFor();
await capture("preview");
const more = page.getByLabel("More actions", { exact: true });
await more.click();
await button(page, "Rename / move").click();
const move = page.getByRole("dialog", {
name: "Rename / move",
exact: true,
});
await move.getByRole("button", { name: "資料", exact: true }).click();
await page.locator(".drive-folder-location strong").filter({
hasText: "資料",
}).waitFor();
await capture("move");
await button(page, "Parent folder").click();
await move.getByRole("button", { name: "資料", exact: true })
.waitFor();
assertEquals(
await move.getByRole("button", { name: "README.md", exact: true })
.count(),
0,
);
await page.keyboard.press("Escape");
await move.waitFor({ state: "hidden" });
await more.locator(":scope:focus").waitFor();
assertEquals(errors, []);
} finally {
await context.close();
}
}
}
}));