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
171 lines
8.4 KiB
TypeScript
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);
|
|
});
|