Files
Beyond-KLE/mock/e2e/elc.spec.ts
T
ewdernaandClaude Opus 5.5 fccbc28bee mock: 仮決定に沿ってモックを実装する(PHY Editor・IDX・ELC・MAP・Generator)
provisional/decisions.md の仮決定(T-1〜T-28)に従い、mock/ に Vue 3 + Vite + TypeScript のモックを置いた。
T-3 の1〜3(回転したクラスター内のドラッグと保存の往復、IDX・MAPとの連携、ELCと出力の対応表)まで。疑似デバイスとの往復(T-24)は未着手。

- KLE JSONの入出力は、KLE本家の serial.js の deserialize の結果を期待値とする fixture(tests/fixtures/kle-oracle.json)で照合した。本家のコードは取り込んでいない(T-7)
- 基本キーコードの表は QMK の data/constants/keycodes/keycodes_0.0.1_basic.hjson(1e340cd)から抽出した
- 実装は Codex(Luna)、レビューは Codex(Sol)。検証: Vitest 53件、Playwright 20件、vue-tsc、vite build
- TypeScript は vue-tsc が 7 系に未対応のため 6 系に固定した
- 操作性の改善(ELCの提供者の表示、一括配線とGeneratorのピン設定の引き継ぎ、キー内の文字のはみ出し)は別途行う

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AATqjRyp6rrnwTNdYw8XBT
2026-09-25 22:42:30 +09:00

171 lines
8.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
import path from "node:path";
import { readFile } from "node:fs/promises";
async function projectJson(page: import("@playwright/test").Page) {
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByRole("button", { name: "プロジェクトを保存" }).click(),
]);
const file = path.join(test.info().outputDir, download.suggestedFilename());
await download.saveAs(file);
return JSON.parse(await readFile(file, "utf8"));
}
async function loadProject(page: import("@playwright/test").Page, project: unknown) {
const chooser = page.waitForEvent("filechooser");
await page.getByRole("button", { name: "プロジェクトを開く" }).click();
await (
await chooser
).setFiles({
name: "case.bkle.json",
mimeType: "application/json",
buffer: Buffer.from(JSON.stringify(project)),
});
}
test("provider, key terminals and diode edit wiring and show collisions", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "ELC" }).click();
await page.getByTestId("provider-id").fill("MCU");
await page.getByRole("button", { name: "MCUを追加・更新" }).click();
await expect(page.getByText(/MCU · mcu · RP2040/)).toBeVisible();
const view = page.locator(".wiring-box [data-testid=keyboard-view]");
const first = view.locator(".kv-key").nth(0);
await first.click();
await page.locator(".edit-form select").nth(0).selectOption("MCU.GP0");
await page.locator(".edit-form select").nth(1).selectOption("MCU.GP1");
await page.locator(".edit-form select").nth(2).selectOption("a-to-b");
await page.getByRole("button", { name: "配線を保存" }).click();
await expect(page.getByTestId("unwired-count")).toHaveText("15");
await expect(first).toHaveClass(/ok/);
await expect(first).toContainText("GP0/GP1");
await view.locator(".kv-key").nth(1).click();
await page.locator(".edit-form select").nth(0).selectOption("MCU.GP0");
await page.locator(".edit-form select").nth(1).selectOption("MCU.GP1");
await page.locator(".edit-form select").nth(2).selectOption("a-to-b");
await page.getByRole("button", { name: "配線を保存" }).click();
await expect(page.getByText("wiring-collision", { exact: false }).first()).toBeVisible();
await expect(view.locator(".kv-key.error")).toHaveCount(2);
});
test("batch wiring applies selected keys in row-major PHY order", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "ELC" }).click();
await page.getByTestId("provider-id").fill("MCU");
await page.getByRole("button", { name: "MCUを追加・更新" }).click();
const views = page.locator(".wiring-box [data-testid=keyboard-view]");
const batchView = views.nth(1);
await batchView.locator(".kv-key").nth(0).click();
await batchView.locator(".kv-key").nth(1).click();
await page.getByTestId("batch-row-select").selectOption("MCU.GP0");
await page.getByRole("button", { name: "rowへ追加" }).click();
await page.getByTestId("batch-col-select").selectOption("MCU.GP1");
await page.getByRole("button", { name: "colへ追加" }).click();
await page.getByTestId("batch-col-select").selectOption("MCU.GP2");
await page.getByRole("button", { name: "colへ追加" }).click();
await page.getByTestId("batch-wire").click();
await expect(page.getByTestId("batch-preview")).toBeVisible();
await expect(page.getByTestId("batch-preview").locator("tbody tr").nth(0)).toContainText("MCU.GP0");
await page.getByTestId("batch-confirm").click();
await expect(page.getByTestId("unwired-count")).toHaveText("14");
await expect(views.first().locator(".kv-key").nth(0)).toContainText("GP0/GP1");
await expect(views.first().locator(".kv-key").nth(1)).toContainText("GP0/GP2");
});
test("batch wiring previews overwritten assignments and blocks insufficient capacity", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "ELC" }).click();
await page.getByTestId("provider-id").fill("MCU");
await page.getByRole("button", { name: "MCUを追加・更新" }).click();
const view = page.locator(".wiring-box [data-testid=keyboard-view]");
await view.locator(".kv-key").first().click();
await page.locator(".edit-form select").nth(0).selectOption("MCU.GP5");
await page.locator(".edit-form select").nth(1).selectOption("MCU.GP6");
await page.getByRole("button", { name: "配線を保存" }).click();
const batchView = page.locator(".wiring-box [data-testid=keyboard-view]").nth(1);
for (let i = 0; i < 3; i++) await batchView.locator(".kv-key").nth(i).click();
await page.getByTestId("batch-row-select").selectOption("MCU.GP0");
await page.getByRole("button", { name: "rowへ追加" }).click();
await page.getByTestId("batch-col-select").selectOption("MCU.GP1");
await page.getByRole("button", { name: "colへ追加" }).click();
await page.getByTestId("batch-wire").click();
await expect(page.getByTestId("batch-capacity-error")).toBeVisible();
await expect(page.getByTestId("batch-confirm")).toBeDisabled();
await page.getByTestId("batch-col-select").selectOption("MCU.GP2");
await page.getByRole("button", { name: "colへ追加" }).click();
await page.getByTestId("batch-wire").click();
const rows = page.getByTestId("batch-preview").locator("tbody tr");
await expect(rows).toHaveCount(3);
await expect(rows.first()).toContainText("上書き");
});
test("diode direction is part of collision identity and unknown MCU parts accept free pin names", async ({
page,
}) => {
await page.goto("/");
let source = await projectJson(page);
const [k1, k2] = source.idx.order;
source.elc.providers = [{ id: "MCU", kind: "mcu", part: "RP2040" }];
source.elc.keys = [
{ key: k1, detect: "contact", terminals: { a: "MCU.GP0", b: "MCU.GP1" }, diode: "a-to-b" },
{ key: k2, detect: "contact", terminals: { a: "MCU.GP0", b: "MCU.GP1" }, diode: "b-to-a" },
];
await loadProject(page, source);
await page.getByRole("button", { name: "ELC" }).click();
await expect(page.getByText("wiring-collision", { exact: false })).toHaveCount(0);
source.elc.keys[1].diode = "a-to-b";
await loadProject(page, source);
await page.getByRole("button", { name: "ELC" }).click();
await expect(page.getByText("wiring-collision", { exact: false }).first()).toBeVisible();
await page.getByTestId("provider-id").fill("ALT");
await page.locator("input[list=mcu-parts]").fill("Unknown-MCU");
await page.getByRole("button", { name: "MCUを追加・更新" }).click();
await expect(page.getByText("未知の品番はピン名を自由入力します。")).toBeVisible();
const key = page.locator(".wiring-box .kv-key").nth(2);
await key.click();
await page.getByLabel("a端子自由入力").fill("ALT.PIN_X");
await page.getByLabel("b端子自由入力").fill("ALT.PIN_Y");
await page.getByRole("button", { name: "配線を保存" }).click();
await expect(page.getByTestId("unwired-count")).toHaveText("13");
});
test("LED chain candidates include only per-key LED keys and @led parts across all variants", async ({
page,
}) => {
await page.goto("/");
const source = await projectJson(page);
const keys = [] as any[];
const walk = (nodes: any[]) =>
nodes.forEach((n) => {
if (n.type === "key") keys.push(n);
else if (n.type === "cluster") walk(n.children);
else if (n.type === "variantGroup") n.children.forEach((v: any) => walk(v.children));
});
walk(source.phy.root);
keys[0].perKeyLED = true;
keys[1].perKeyLED = false;
source.phy.root.push({
type: "component",
id: "cmp-test-led",
transform: { x: 0, y: 0, rotation: 0 },
partGroup: "@led",
});
source.phy.root.push({
type: "component",
id: "cmp-test-other",
transform: { x: 0, y: 0, rotation: 0 },
partGroup: "@screw",
});
await loadProject(page, source);
await page.getByRole("button", { name: "ELC" }).click();
await page.getByLabel("チェーンID").fill("LED1");
await page.getByLabel("データピン").fill("MCU.DATA");
await page.getByRole("button", { name: "作成" }).click();
await expect(page.getByTestId("led-candidate").locator(`option[value="${keys[0].id}"]`)).toHaveCount(1);
await expect(page.getByTestId("led-candidate").locator('option[value="cmp-test-led"]')).toHaveCount(1);
await expect(page.getByTestId("led-candidate").locator(`option[value="${keys[1].id}"]`)).toHaveCount(0);
await expect(page.getByTestId("led-candidate").locator('option[value="cmp-test-other"]')).toHaveCount(0);
});