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); });