provisional/phy-editor-feedback.md の合意事項と phy-editor-keybindings.md の割り当て案に沿って、PHY 画面を一から書き直した。 App.vue は共通のシェル(入出力・履歴・タブ)だけにし、PHY は src/ui/phy/ に、コンポーネント・composable・純粋モジュール(logic.ts, viewport.ts)を分けて置いた。 - 画面確認で見つかった不具合(読み込み時の全体表示、Shift+クリック、削除の確認の大きさ、Alt+↑、フォーカス中の二重描画、長押し中の選択など)を直し、キャンバス上の実際の操作で確かめる e2e を足した - 階層パネルでクラスターの子を余分な折りたたみで包まず、件数を配下のキー・部品の数にそろえた - 実装は Codex(Luna)。検証: vue-tsc 0件、Vitest 64件、Playwright 30件、prettier --check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbfgHT848s3JbuFw8MKFF8
476 lines
23 KiB
TypeScript
476 lines
23 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/");
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(16);
|
|
});
|
|
|
|
async function makeCluster(page: import("@playwright/test").Page) {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
await keys.nth(0).click();
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
await page.getByRole("button", { name: "クラスターにする" }).click();
|
|
return page.locator('[data-node-type="cluster"]').first();
|
|
}
|
|
|
|
test("ドラッグの刻みをUndo/Redoでき、コンソールにエラーがない", async ({ page }) => {
|
|
const errors: string[] = [];
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") errors.push(message.text());
|
|
});
|
|
await page.reload();
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(16);
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const before = await key.getAttribute("transform");
|
|
const box = await key.boundingBox();
|
|
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(box!.x + box!.width / 2 + 24, box!.y + box!.height / 2, { steps: 3 });
|
|
await page.mouse.up();
|
|
await expect.poll(() => key.getAttribute("transform")).not.toBe(before);
|
|
const after = await key.getAttribute("transform");
|
|
await page.getByRole("button", { name: "Undo" }).click();
|
|
await expect(key).toHaveAttribute("transform", before!);
|
|
await page.getByRole("button", { name: "Redo" }).click();
|
|
await expect(key).toHaveAttribute("transform", after!);
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
test("クラスター化で画面位置を保つ", async ({ page }) => {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
const centers = async () =>
|
|
keys.evaluateAll((es) =>
|
|
es.slice(0, 2).map((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
return [r.x + r.width / 2, r.y + r.height / 2];
|
|
}),
|
|
);
|
|
const before = await centers();
|
|
await keys.nth(0).click();
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
await page.getByRole("button", { name: "クラスターにする" }).click();
|
|
const after = await centers();
|
|
for (let i = 0; i < 2; i++) {
|
|
expect(after[i]![0]).toBeCloseTo(before[i]![0]!, 0);
|
|
expect(after[i]![1]).toBeCloseTo(before[i]![1]!, 0);
|
|
}
|
|
});
|
|
|
|
test("複数選択のドラッグが全件に適用される", async ({ page }) => {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
const before = await keys.evaluateAll((es) => es.slice(0, 2).map((e) => e.getAttribute("transform")));
|
|
await keys.nth(0).click();
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
const box = await keys.nth(0).boundingBox();
|
|
if (!box) throw new Error("key missing");
|
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(box.x + box.width / 2 + 27, box.y + box.height / 2 + 10, { steps: 3 });
|
|
await page.mouse.up();
|
|
const after = await keys.evaluateAll((es) => es.slice(0, 2).map((e) => e.getAttribute("transform")));
|
|
expect(after[0]).not.toBe(before[0]);
|
|
expect(after[1]).not.toBe(before[1]);
|
|
});
|
|
|
|
test("回転クラスターの矢印移動は親軸で動き、ドラッグもポインタに追従する", async ({ page }) => {
|
|
const cluster = await makeCluster(page);
|
|
const clusterId = await cluster.getAttribute("data-node-id");
|
|
await page.locator(`[data-tree-id="${clusterId}"]`).click();
|
|
await page.getByLabel("Rotation").fill("90");
|
|
await page.getByLabel("Rotation").dispatchEvent("change");
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const beforeBox = await key.boundingBox();
|
|
const beforeTransform = await key.getAttribute("transform");
|
|
await key.click();
|
|
const pane = page.locator(".canvas-pane");
|
|
await pane.focus();
|
|
await page.keyboard.press("ArrowRight");
|
|
const afterBox = await key.boundingBox();
|
|
expect(afterBox!.y).toBeGreaterThan(beforeBox!.y + 4);
|
|
const dragBox = await key.boundingBox();
|
|
await page.mouse.move(dragBox!.x + dragBox!.width / 2, dragBox!.y + dragBox!.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(dragBox!.x + dragBox!.width / 2 + 28, dragBox!.y + dragBox!.height / 2, { steps: 3 });
|
|
await page.mouse.up();
|
|
const dragAfterBox = await key.boundingBox();
|
|
expect(await key.getAttribute("transform")).not.toBe(beforeTransform);
|
|
expect(dragAfterBox!.x).toBeGreaterThan(dragBox!.x + 10);
|
|
expect(dragAfterBox!.y).toBeCloseTo(dragBox!.y, 0);
|
|
});
|
|
|
|
test("長押しでクラスターを選び、ダブルクリックでフォーカスしてEscで戻る", async ({ page }) => {
|
|
const cluster = await makeCluster(page);
|
|
const clusterId = await cluster.getAttribute("data-node-id");
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const b = await key.boundingBox();
|
|
await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2);
|
|
await page.mouse.down();
|
|
await expect(page.locator(".long-label")).toContainText("Cluster", { timeout: 2000 });
|
|
await page.mouse.up();
|
|
await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/);
|
|
await page.locator(`[data-tree-id="${clusterId}"]`).first().click();
|
|
const keyBox = await key.boundingBox();
|
|
await page.mouse.dblclick(keyBox!.x + keyBox!.width / 2, keyBox!.y + keyBox!.height / 2, { delay: 80 });
|
|
await expect(page.getByRole("button", { name: "root", exact: true })).toBeVisible();
|
|
const pane = page.locator(".canvas-pane");
|
|
await pane.focus();
|
|
await page.keyboard.press("Escape");
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("button", { name: "root", exact: true })).toHaveCount(0);
|
|
});
|
|
|
|
test("入れ子の回転クラスターをフォーカスできる", async ({ page }) => {
|
|
const outer = await makeCluster(page);
|
|
const outerId = await outer.getAttribute("data-node-id");
|
|
const childIds = await outer
|
|
.locator('[data-node-type="key"]')
|
|
.evaluateAll((es) => es.map((e) => e.getAttribute("data-node-id")));
|
|
await page.getByText("キー・部品 2件", { exact: true }).click();
|
|
await page.locator(`[data-tree-id="${childIds[0]}"]`).first().click();
|
|
await page
|
|
.locator(`[data-tree-id="${childIds[1]}"]`)
|
|
.first()
|
|
.click({ modifiers: ["Shift"] });
|
|
await page.getByRole("button", { name: "クラスターにする" }).click();
|
|
const inner = outer.locator('[data-node-type="cluster"]').first();
|
|
const innerId = await inner.getAttribute("data-node-id");
|
|
await page.locator(`[data-tree-id="${outerId}"]`).first().click();
|
|
await page.getByLabel("Rotation").fill("35");
|
|
await page.getByLabel("Rotation").dispatchEvent("change");
|
|
const key = inner.locator('[data-node-type="key"]').first();
|
|
await expect(key).toBeVisible();
|
|
await page.locator(`[data-tree-id="${outerId}"]`).first().click();
|
|
await page.locator(".canvas-pane").focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByRole("button", { name: "root", exact: true })).toBeVisible();
|
|
await expect(page.locator(`[data-node-id="${childIds[0]}"]`).last()).toBeVisible();
|
|
const focusedKey = page.locator(`[data-node-id="${childIds[0]}"]`).last();
|
|
const focusedBox = await focusedKey.boundingBox();
|
|
await page.mouse.move(focusedBox!.x + focusedBox!.width / 2, focusedBox!.y + focusedBox!.height / 2);
|
|
await page.mouse.down();
|
|
await page.waitForTimeout(700);
|
|
await expect(page.locator(`[data-node-id="${innerId}"] .group-sel.candidate`)).toBeVisible({
|
|
timeout: 1800,
|
|
});
|
|
await page.mouse.up();
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "rotated-long-press-in-focus.png") });
|
|
const clickBox = await focusedKey.boundingBox();
|
|
await page.mouse.dblclick(clickBox!.x + clickBox!.width / 2, clickBox!.y + clickBox!.height / 2, {
|
|
delay: 80,
|
|
});
|
|
await expect(page.locator(`[data-node-id="${innerId}"]`)).toHaveCount(1);
|
|
});
|
|
|
|
test("選択枠は画面上で見え、バリアントバッジで表示を切り替えられる", async ({ page }) => {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
await keys.first().click();
|
|
const outline = page.locator('[data-node-type="key"] .sel').first();
|
|
await expect(outline).toBeVisible();
|
|
expect(await outline.evaluate((e) => getComputedStyle(e).strokeWidth)).toBe("2px");
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
await page.getByRole("button", { name: "選択からバリアントグループ" }).click();
|
|
const group = page.locator('[data-node-type="variantGroup"]').first();
|
|
await page.getByLabel("Rotation").fill("35");
|
|
await page.getByLabel("Rotation").dispatchEvent("change");
|
|
const badge = group.locator(".badge");
|
|
await expect(badge).toBeVisible();
|
|
await badge.locator("rect").click();
|
|
await page
|
|
.locator(".popover")
|
|
.getByRole("button", { name: /Option B/ })
|
|
.click();
|
|
await expect(keys).toHaveCount(14);
|
|
await page.locator(".canvas-pane").focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.getByRole("button", { name: "root", exact: true })).toBeVisible();
|
|
await badge.locator("rect").click();
|
|
const focusedBadgeBox = await badge.boundingBox();
|
|
const focusedPopoverBox = await page.locator(".popover").boundingBox();
|
|
expect(
|
|
Math.hypot(focusedPopoverBox!.x - focusedBadgeBox!.x, focusedPopoverBox!.y - focusedBadgeBox!.y),
|
|
).toBeLessThanOrEqual(80);
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "rotated-badge-in-focus.png") });
|
|
await page
|
|
.locator(".popover")
|
|
.getByRole("button", { name: /Option A/ })
|
|
.click();
|
|
await expect(keys).toHaveCount(16);
|
|
});
|
|
|
|
test("削除確認は参照を列挙し、実行後はIDXからも消える", async ({ page }) => {
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const id = await key.getAttribute("data-node-id");
|
|
await key.click();
|
|
await page.getByRole("button", { name: "削除", exact: true }).click();
|
|
const confirm = page.locator(".confirm-float");
|
|
await expect(confirm).toContainText("参照");
|
|
await expect(confirm).toContainText(id!);
|
|
await confirm.getByRole("button", { name: "削除", exact: true }).click();
|
|
await expect(page.locator(`[data-node-id="${id}"]`)).toHaveCount(0);
|
|
await page.getByRole("button", { name: "IDX", exact: true }).click();
|
|
await expect(page.getByTestId(`idx-row-${id}`)).toHaveCount(0);
|
|
});
|
|
|
|
test("コピー貼り付けはIDを変えて1刻みずらし、KLE貼り付けはキーを追加する", async ({ page }) => {
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const id = await key.getAttribute("data-node-id");
|
|
const original = await key.getAttribute("transform");
|
|
await key.click();
|
|
const marker = await page.evaluate(() => {
|
|
const data = new DataTransfer();
|
|
window.dispatchEvent(
|
|
new ClipboardEvent("copy", { bubbles: true, cancelable: true, clipboardData: data }),
|
|
);
|
|
return data.getData("text/plain");
|
|
});
|
|
await page.evaluate((marker) => {
|
|
const data = new DataTransfer();
|
|
data.setData("text/plain", marker);
|
|
window.dispatchEvent(
|
|
new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: data }),
|
|
);
|
|
}, marker);
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(17);
|
|
const pasted = page.locator('[data-node-type="key"]').last();
|
|
expect(await pasted.getAttribute("data-node-id")).not.toBe(id);
|
|
expect(await pasted.getAttribute("transform")).not.toBe(original);
|
|
await page.evaluate(() => {
|
|
const data = new DataTransfer();
|
|
data.setData("text/plain", JSON.stringify([["Pasted"]]));
|
|
window.dispatchEvent(
|
|
new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: data }),
|
|
);
|
|
});
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(18);
|
|
});
|
|
|
|
test("カット貼り付けはIDとワールド位置を保つ", async ({ page }) => {
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
const id = await key.getAttribute("data-node-id");
|
|
const transform = await key.getAttribute("transform");
|
|
await key.click();
|
|
const marker = await page.evaluate(() => {
|
|
const data = new DataTransfer();
|
|
window.dispatchEvent(new ClipboardEvent("cut", { bubbles: true, cancelable: true, clipboardData: data }));
|
|
return data.getData("text/plain");
|
|
});
|
|
await expect(key).toHaveClass(/cut/);
|
|
await page.evaluate((value) => {
|
|
const data = new DataTransfer();
|
|
data.setData("text/plain", value);
|
|
window.dispatchEvent(
|
|
new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: data }),
|
|
);
|
|
}, marker);
|
|
const cutKey = page.locator(`[data-node-id="${id}"]`);
|
|
await expect(cutKey).toHaveAttribute("transform", transform!);
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(16);
|
|
});
|
|
|
|
test("プロジェクト保存読み込みとKLEから60キー新規を往復する", async ({ page }) => {
|
|
await page.locator('[data-node-type="key"]').first().click();
|
|
await page.getByLabel("Width").fill("1.5");
|
|
await page.getByLabel("Width").dispatchEvent("change");
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent("download"),
|
|
page.getByRole("button", { name: "プロジェクトを保存" }).click(),
|
|
]);
|
|
const savePath = path.join(test.info().outputDir, "project.json");
|
|
await download.saveAs(savePath);
|
|
const saved = await readFile(savePath);
|
|
const chooser = page.waitForEvent("filechooser");
|
|
await page.getByRole("button", { name: "プロジェクトを開く" }).click();
|
|
await (
|
|
await chooser
|
|
).setFiles({ name: "roundtrip.bkle.json", mimeType: "application/json", buffer: saved });
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(16);
|
|
await expect(page.locator('[data-node-type="key"] .key-face').first()).toHaveAttribute("width", "1.5");
|
|
const kle = await readFile(path.join(process.cwd(), "..", "samples", "kle", "cr0wn60.json"));
|
|
const kleChooser = page.waitForEvent("filechooser");
|
|
await page.getByRole("button", { name: "KLE JSONから新規" }).click();
|
|
await (await kleChooser).setFiles({ name: "cr0wn60.json", mimeType: "application/json", buffer: kle });
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(60);
|
|
});
|
|
|
|
test("unitSize不一致では追加と数値編集が無効", async ({ page }) => {
|
|
const [download] = await Promise.all([
|
|
page.waitForEvent("download"),
|
|
page.getByRole("button", { name: "プロジェクトを保存" }).click(),
|
|
]);
|
|
const savePath = path.join(test.info().outputDir, "project.json");
|
|
await download.saveAs(savePath);
|
|
const source = JSON.parse(await readFile(savePath, "utf8"));
|
|
source.phy.profiles.push({
|
|
...source.phy.profiles[0],
|
|
id: "alternate",
|
|
name: "Alternate",
|
|
unitSize: { x: 18, y: 18 },
|
|
});
|
|
const chooser = page.waitForEvent("filechooser");
|
|
await page.getByRole("button", { name: "プロジェクトを開く" }).click();
|
|
await (
|
|
await chooser
|
|
).setFiles({
|
|
name: "unit-mismatch.bkle.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify(source)),
|
|
});
|
|
await expect(page.getByText("unitSize が一致しないため編集できません。")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "キーを足す" })).toBeDisabled();
|
|
await page.locator('[data-node-type="key"]').first().click();
|
|
await expect(page.getByLabel("Width")).toBeDisabled();
|
|
const before = await page.locator('[data-node-type="key"] .key-face').first().getAttribute("width");
|
|
await page.getByLabel("Width").evaluate((e: HTMLInputElement) => {
|
|
e.value = "2";
|
|
e.dispatchEvent(new Event("change", { bubbles: true }));
|
|
});
|
|
await expect(page.locator('[data-node-type="key"] .key-face').first()).toHaveAttribute("width", before!);
|
|
});
|
|
|
|
test("KLE読込直後に全体表示し、cr0wn60の全キーがキャンバス内に収まる", async ({ page }) => {
|
|
const kle = await readFile(path.join(process.cwd(), "..", "samples", "kle", "cr0wn60.json"));
|
|
const chooser = page.waitForEvent("filechooser");
|
|
await page.getByRole("button", { name: "KLE JSONから新規" }).click();
|
|
await (await chooser).setFiles({ name: "cr0wn60.json", mimeType: "application/json", buffer: kle });
|
|
await expect(page.locator('[data-node-type="key"]')).toHaveCount(60);
|
|
const canvas = await page.locator("svg.canvas").boundingBox();
|
|
const boxes = await page.locator('[data-node-type="key"]').evaluateAll((es) =>
|
|
es.map((e) => {
|
|
const r = e.getBoundingClientRect();
|
|
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
}),
|
|
);
|
|
expect(canvas).not.toBeNull();
|
|
for (const b of boxes) {
|
|
expect(b.left).toBeGreaterThanOrEqual(canvas!.x);
|
|
expect(b.top).toBeGreaterThanOrEqual(canvas!.y);
|
|
expect(b.right).toBeLessThanOrEqual(canvas!.x + canvas!.width);
|
|
expect(b.bottom).toBeLessThanOrEqual(canvas!.y + canvas!.height);
|
|
}
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "cr0wn60-fit.png") });
|
|
});
|
|
|
|
test("キャンバス上のShift選択で2件をクラスター化し、選択解除と親ドラッグを行う", async ({ page }) => {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
await keys.nth(0).click();
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
await expect(page.locator(".sel")).toHaveCount(2);
|
|
await page.locator(".canvas-pane").focus();
|
|
await page.keyboard.press("Control+g");
|
|
const cluster = page.locator('[data-node-type="cluster"]').first();
|
|
await expect(cluster.locator('[data-node-type="key"]')).toHaveCount(2);
|
|
const clusterBefore = await cluster.getAttribute("transform");
|
|
const childrenBefore = await cluster
|
|
.locator('[data-node-type="key"]')
|
|
.evaluateAll((es) => es.map((e) => e.getAttribute("transform")));
|
|
const key = cluster.locator('[data-node-type="key"]').first();
|
|
const b = await key.boundingBox();
|
|
await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(b!.x + b!.width / 2 + 28, b!.y + b!.height / 2 + 5, { steps: 3 });
|
|
await page.mouse.up();
|
|
await expect(cluster).not.toHaveAttribute("transform", clusterBefore!);
|
|
expect(
|
|
await cluster
|
|
.locator('[data-node-type="key"]')
|
|
.evaluateAll((es) => es.map((e) => e.getAttribute("transform"))),
|
|
).toEqual(childrenBefore);
|
|
const third = keys.nth(3);
|
|
await third.click();
|
|
await third.click({ modifiers: ["Shift"] });
|
|
await expect(page.locator(".sel")).toHaveCount(0);
|
|
});
|
|
|
|
test("削除のキーボード確認とバリアントポップオーバーを対象の近くに置く", async ({ page }) => {
|
|
const key = page.locator('[data-node-type="key"]').first();
|
|
await key.click();
|
|
const keyBox = await key.boundingBox();
|
|
await page.locator(".canvas-pane").focus();
|
|
await page.keyboard.press("Delete");
|
|
const confirm = page.locator(".confirm-float");
|
|
const confirmBox = await confirm.boundingBox();
|
|
const pane = await page.locator(".canvas-pane").boundingBox();
|
|
expect(confirmBox!.width).toBeLessThanOrEqual(400);
|
|
expect(confirmBox!.x).toBeGreaterThanOrEqual(pane!.x);
|
|
expect(confirmBox!.y).toBeGreaterThanOrEqual(pane!.y);
|
|
expect(confirmBox!.x + confirmBox!.width).toBeLessThanOrEqual(pane!.x + pane!.width);
|
|
expect(confirmBox!.y + confirmBox!.height).toBeLessThanOrEqual(pane!.y + pane!.height);
|
|
const gapX = Math.max(
|
|
0,
|
|
confirmBox!.x - (keyBox!.x + keyBox!.width),
|
|
keyBox!.x - (confirmBox!.x + confirmBox!.width),
|
|
);
|
|
const gapY = Math.max(
|
|
0,
|
|
confirmBox!.y - (keyBox!.y + keyBox!.height),
|
|
keyBox!.y - (confirmBox!.y + confirmBox!.height),
|
|
);
|
|
expect(Math.hypot(gapX, gapY)).toBeLessThanOrEqual(150);
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "delete-confirm.png") });
|
|
await page.keyboard.press("Escape");
|
|
|
|
await keysSelectVariantGroup(page);
|
|
const badge = page.locator('[data-node-type="variantGroup"] .badge').first();
|
|
await badge.locator("rect").click();
|
|
const badgeBox = await badge.boundingBox();
|
|
const popoverBox = await page.locator(".popover").boundingBox();
|
|
expect(Math.hypot(popoverBox!.x - badgeBox!.x, popoverBox!.y - badgeBox!.y)).toBeLessThanOrEqual(80);
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "variant-popover.png") });
|
|
});
|
|
|
|
async function keysSelectVariantGroup(page: import("@playwright/test").Page) {
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
await keys.nth(0).click();
|
|
await keys.nth(1).click({ modifiers: ["Shift"] });
|
|
await page.getByRole("button", { name: "選択からバリアントグループ" }).click();
|
|
}
|
|
|
|
test("Alt+上矢印は選択を親へ上げ、フォーカス描画にID重複を作らない", async ({ page }) => {
|
|
const cluster = await makeCluster(page);
|
|
const clusterId = await cluster.getAttribute("data-node-id");
|
|
await page.locator(`[data-tree-id="${clusterId}"]`).click();
|
|
await page.getByLabel("Rotation").fill("35");
|
|
await page.getByLabel("Rotation").dispatchEvent("change");
|
|
const key = cluster.locator('[data-node-type="key"]').first();
|
|
const transform = await key.getAttribute("transform");
|
|
await key.click();
|
|
await page.locator(".canvas-pane").focus();
|
|
await page.keyboard.press("Alt+ArrowUp");
|
|
await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/);
|
|
await expect(key).toHaveAttribute("transform", transform!);
|
|
await page.keyboard.press("Enter");
|
|
const ids = await page
|
|
.locator("svg.canvas [data-node-id]")
|
|
.evaluateAll((es) => es.map((e) => e.getAttribute("data-node-id")));
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "focus-mode.png") });
|
|
const pane = await page.locator("svg.canvas").boundingBox();
|
|
await page.mouse.click(pane!.x + 8, pane!.y + 8);
|
|
await expect(page.locator(".sel")).toHaveCount(0);
|
|
});
|
|
|
|
test("長押し候補は離すまで選択を変えず、離すとクラスターを選ぶ", async ({ page }) => {
|
|
const cluster = await makeCluster(page);
|
|
const clusterId = await cluster.getAttribute("data-node-id");
|
|
const keys = page.locator('[data-node-type="key"]');
|
|
await keys.nth(4).click();
|
|
await expect(keys.nth(4).locator(".sel")).toBeVisible();
|
|
const b = await cluster.locator('[data-node-type="key"]').first().boundingBox();
|
|
await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2);
|
|
await page.mouse.down();
|
|
await expect(page.locator(".long-label")).toContainText("2/", { timeout: 1800 });
|
|
await expect(keys.nth(4).locator(".sel")).toBeVisible();
|
|
await page.screenshot({ path: path.join(test.info().outputDir, "long-press-candidate.png") });
|
|
await page.mouse.up();
|
|
await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/);
|
|
});
|
|
|
|
test("「その他」の部品はpartGroupを設定せず、選択後は追加メニューを戻す", async ({ page }) => {
|
|
const select = page.getByRole("combobox", { name: "部品を足す" });
|
|
await select.selectOption("その他");
|
|
await expect(select).toHaveValue("");
|
|
await expect(page.locator('[data-node-type="component"] .part-label').last()).toHaveText("part");
|
|
});
|