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
130 lines
6.4 KiB
TypeScript
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");
|
|
});
|