Files
Beyond-KLE/mock/e2e/idx.spec.ts
T
ewdernaandClaude Opus 5.5 c180642fa8 mock: IDX Editorを作り直す(T-31)
provisional/idx-editor-feedback.md の合意事項と T-31 の仮決定に沿って、IdxPanel を捨てて ui/idx/ に一から書き直した。
並びのリストは置かず、左にバリアントのツリー、中央のキャンバスでドラッグして並べ替え、右にプリセット・取り込み・未登録の登録を置いた。

- order の操作(隠れたキーのくっつき先、挿入、入れ替え、連番の並べ直し)を domain/idxOrder.ts に、
  走査・半身の分割・行の割り当てのずらし・マトリクス順を domain/idxScan.ts に、純粋関数として置いた。行と列の判定は rows.ts をそのまま使う
- QMK keyboard.json・VIA定義・KLE JSON の取り込みは、位置と回転で表示中のキーに照合する(io/layoutImport.ts、domain/idxImport.ts)
- 回転したキーでも出力番号を正立させる
- Codex の設定ファイルが壊れていて起動できなかったため、実装は Claude が直接行った
- 検証: vue-tsc 0件、Vitest 101件(うち新規17件)、Playwright 48件(IDX 8件を書き直し)、prettier --check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GzX13A7HseQz9n6etGbFDg
2026-10-25 10:59:44 +09:00

130 lines
6.4 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const canvasKeys = (page: Page) => page.locator('[data-testid="idx-canvas"] .key');
const numberOf = (page: Page, i: number) => canvasKeys(page).nth(i).getByTestId("idx-number");
/** Drags key `from` onto key `to`, releasing at `fraction` of the target's width. */
async function dragKey(page: Page, from: number, to: number, fraction: number) {
const a = (await canvasKeys(page).nth(from).locator("rect").boundingBox())!;
const b = (await canvasKeys(page).nth(to).locator("rect").boundingBox())!;
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2);
await page.mouse.down();
await page.mouse.move(a.x + a.width / 2 + 10, a.y + a.height / 2, { steps: 3 });
await page.mouse.move(b.x + b.width * fraction, b.y + b.height / 2, { steps: 5 });
await page.mouse.up();
}
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "IDX", exact: true }).click();
await expect(canvasKeys(page)).toHaveCount(16);
});
test("drops at the edge of a key insert before or after it, and undo restores", async ({ page }) => {
await dragKey(page, 0, 3, 0.9);
await expect(numberOf(page, 0)).toHaveText("3");
await expect(numberOf(page, 3)).toHaveText("2");
await expect(numberOf(page, 1)).toHaveText("0");
await dragKey(page, 0, 1, 0.1);
await expect(numberOf(page, 0)).toHaveText("0");
await page.getByRole("button", { name: "Undo" }).click();
await expect(numberOf(page, 0)).toHaveText("3");
await page.getByRole("button", { name: "Undo" }).click();
await expect(numberOf(page, 0)).toHaveText("0");
await expect(numberOf(page, 3)).toHaveText("3");
});
test("a drop in the middle of a key swaps the two and leaves the others", async ({ page }) => {
await dragKey(page, 0, 5, 0.5);
await expect(numberOf(page, 0)).toHaveText("5");
await expect(numberOf(page, 5)).toHaveText("0");
await expect(numberOf(page, 1)).toHaveText("1");
await expect(numberOf(page, 4)).toHaveText("4");
});
test("a key added in PHY is registered at the end and can be dragged to the front", async ({ page }) => {
await page.getByRole("button", { name: "PHY", exact: true }).click();
await page.getByRole("button", { name: "キーを足す" }).click();
await page.getByRole("button", { name: "IDX", exact: true }).click();
await expect(numberOf(page, 16)).toHaveText("16");
await dragKey(page, 16, 0, 0.1);
await expect(numberOf(page, 16)).toHaveText("0");
await expect(numberOf(page, 0)).toHaveText("1");
});
test("the variant tree switches the shown configuration without touching other keys' order", async ({
page,
}) => {
await page.getByRole("button", { name: "PHY", exact: true }).click();
const keys = page.locator('[data-node-type="key"]');
const firstId = await keys.nth(0).getAttribute("data-node-id");
await keys.nth(0).click();
await keys.nth(1).click({ modifiers: ["Shift"] });
await page.getByRole("button", { name: "選択からバリアントグループ" }).click();
await page.getByRole("button", { name: "IDX", exact: true }).click();
const optionA = page.locator(".tree").getByRole("button", { name: /Option A/ });
const optionB = page.locator(".tree").getByRole("button", { name: /Option B/ });
await expect(optionA).toHaveAttribute("aria-pressed", "true");
await expect(page.locator(`[data-testid="idx-canvas"] [data-key-id="${firstId}"]`)).toHaveCount(1);
await optionB.click();
await expect(optionB).toHaveAttribute("aria-pressed", "true");
await expect(page.locator(`[data-testid="idx-canvas"] [data-key-id="${firstId}"]`)).toHaveCount(0);
await expect(canvasKeys(page)).toHaveCount(14);
await expect(numberOf(page, 0)).toHaveText("0");
});
test("a column-major preset previews, applies as one undo step", async ({ page }) => {
await page.getByLabel("走査").selectOption({ label: "列優先" });
await page.getByRole("button", { name: "プレビュー" }).click();
await expect(numberOf(page, 4)).toHaveText("1");
await expect(page.locator(".scan-line")).toHaveCount(4);
await page.getByRole("button", { name: "適用" }).click();
await expect(numberOf(page, 4)).toHaveText("1");
await expect(numberOf(page, 1)).toHaveText("4");
await page.getByRole("button", { name: "Undo" }).click();
await expect(numberOf(page, 1)).toHaveText("1");
});
test("the preview shifts a picked column by one row and Esc discards it", async ({ page }) => {
await page.getByRole("button", { name: "プレビュー" }).click();
await canvasKeys(page).nth(0).click();
await page.keyboard.press("ArrowDown");
// column 0 now belongs one row lower: row 0 is keys 1..3
await expect(numberOf(page, 1)).toHaveText("0");
await expect(numberOf(page, 0)).toHaveText("3");
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "プレビュー" })).toBeVisible();
await expect(numberOf(page, 0)).toHaveText("0");
});
test("a range selection renumbers from its first key's position", async ({ page }) => {
const k2 = (await canvasKeys(page).nth(2).locator("rect").boundingBox())!;
const k15 = (await canvasKeys(page).nth(15).locator("rect").boundingBox())!;
await page.mouse.move(k2.x + 5, k2.y - 20);
await page.mouse.down();
await page.mouse.move(k15.x + k15.width + 20, k15.y + k15.height + 20, { steps: 6 });
await page.mouse.up();
await expect(page.getByText("選択範囲(8)")).toBeVisible();
await page.getByText("選択範囲(8)").click();
await page.getByLabel("走査").selectOption({ label: "列優先" });
await page.getByRole("button", { name: "プレビュー" }).click();
await page.getByRole("button", { name: "適用" }).click();
// 0, 1, then the right half column by column, then the rest of the left half
await expect(numberOf(page, 2)).toHaveText("2");
await expect(numberOf(page, 6)).toHaveText("3");
await expect(numberOf(page, 3)).toHaveText("6");
await expect(numberOf(page, 4)).toHaveText("10");
});
test("importing a QMK layout copies its key order onto matching keys", async ({ page }) => {
const layout = [0, 1, 2, 3].flatMap((x) => [0, 1, 2, 3].map((y) => ({ matrix: [y, x], x, y })));
await page.getByTestId("idx-import-file").setInputFiles({
name: "keyboard.json",
mimeType: "application/json",
buffer: Buffer.from(JSON.stringify({ layouts: { LAYOUT: { layout } } })),
});
await expect(page.getByTestId("idx-import-result")).toContainText("16件を照合");
await expect(numberOf(page, 4)).toHaveText("1");
await expect(numberOf(page, 1)).toHaveText("4");
});