diff --git a/tests/e2e/calibration.spec.ts b/tests/e2e/calibration.spec.ts index ea1c0f2..be8323a 100644 --- a/tests/e2e/calibration.spec.ts +++ b/tests/e2e/calibration.spec.ts @@ -1,9 +1,15 @@ import { expect, test, type Locator, type Page } from "@playwright/test"; import { gotoSim } from "./helpers"; -// The sim boots with the whole calibration block at zero, so three edits -// (sensor top, angle top, gear ratio) are what it takes to reach "Calibrated". -const VALUE = /^-?\d+(\.\d+)?( counts| deg)?$/; +// Every simulated servo boots with a real servo's calibration table, so the +// card opens Calibrated and each row reads its seeded value in the row's unit. +const SEEDED: readonly (readonly [string, string])[] = [ + ["Sensor lowest", "5 counts"], + ["Sensor highest", "4095 counts"], + ["Angle lowest", "0.00 deg"], + ["Angle highest", "202.00 deg"], + ["Gear ratio", "254.64"], +]; async function openCalibration(page: Page, id: number): Promise { await gotoSim(page, [1, 2]); @@ -21,62 +27,50 @@ async function edit(card: Locator, row: string, from: string, text: string): Pro test("the card shows the status chip and one row per calibration register", async ({ page }) => { const card = await openCalibration(page, 1); - await card.getByText("Not calibrated").hover(); - await expect(page.getByRole("tooltip")).toContainText("sensor lowest must be below"); - for (const row of [ - "Sensor lowest", - "Sensor highest", - "Angle lowest", - "Angle highest", - "Gear ratio", - ]) { + await expect(card.getByText("Calibrated", { exact: true })).toBeVisible(); + for (const [row, value] of SEEDED) { const group = card.getByRole("group", { name: row }); - await expect(group.getByRole("button", { name: VALUE })).toBeVisible(); + await expect(group.getByRole("button", { name: value })).toBeVisible(); } await expect(card.getByText("raw_max 0x082")).toBeVisible(); }); -test("edits write through, the row shows the new value and the status follows", async ({ +test("edits write through, the row shows the new value and the status stays Calibrated", async ({ page, }) => { const card = await openCalibration(page, 1); - await edit(card, "Sensor highest", "0 counts", "4000"); + await edit(card, "Sensor highest", "4095 counts", "4000"); await expect( card .getByRole("group", { name: "Sensor highest" }) .getByRole("button", { name: "4000 counts" }), ).toBeVisible(); - await expect(card.getByText("Not calibrated")).toBeVisible(); + await expect(card.getByText("Calibrated", { exact: true })).toBeVisible(); - await edit(card, "Angle highest", "0.00 deg", "180"); + await edit(card, "Angle highest", "202.00 deg", "180"); await expect( card.getByRole("group", { name: "Angle highest" }).getByRole("button", { name: "180.00 deg" }), ).toBeVisible(); - await edit(card, "Gear ratio", "0.00", "1"); - await expect( - card.getByRole("group", { name: "Gear ratio" }).getByRole("button", { name: "1.00" }), - ).toBeVisible(); await expect(card.getByText("Calibrated", { exact: true })).toBeVisible(); }); test("an edit that inverts the sensor range is refused until cancelled", async ({ page }) => { const card = await openCalibration(page, 1); - await edit(card, "Sensor highest", "0 counts", "4000"); const lowest = card.getByRole("group", { name: "Sensor lowest" }); - await lowest.getByRole("button", { name: "0 counts" }).click(); + await lowest.getByRole("button", { name: "5 counts" }).click(); const input = page.getByRole("textbox"); await input.fill("4100"); await expect(page.getByText("sensor lowest must be below sensor highest")).toBeVisible(); await expect(page.getByRole("button", { name: "Apply" })).toBeDisabled(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(input).toBeHidden(); - await expect(lowest.getByRole("button", { name: "0 counts" })).toBeVisible(); + await expect(lowest.getByRole("button", { name: "5 counts" })).toBeVisible(); const highest = card.getByRole("group", { name: "Sensor highest" }); - await highest.getByRole("button", { name: "4000 counts" }).click(); + await highest.getByRole("button", { name: "4095 counts" }).click(); await input.fill("5000"); await expect(page.getByText("sensor range must sit inside 0 to 4095 counts")).toBeVisible(); await expect(page.getByRole("button", { name: "Apply" })).toBeDisabled(); await page.getByRole("button", { name: "Cancel" }).click(); - await expect(highest.getByRole("button", { name: "4000 counts" })).toBeVisible(); + await expect(highest.getByRole("button", { name: "4095 counts" })).toBeVisible(); }); diff --git a/tests/e2e/control.spec.ts b/tests/e2e/control.spec.ts index 31ed941..c7ad638 100644 --- a/tests/e2e/control.spec.ts +++ b/tests/e2e/control.spec.ts @@ -4,9 +4,13 @@ import { gotoSim } from "./helpers"; async function openLive(page: Page): Promise { await gotoSim(page, [1, 2]); await page.getByRole("button", { name: "ID 1" }).click(); + // The servo page proves the selection landed: leaving for Live while that + // navigation is still compiling on a cold dev server races the two. + await expect(page.getByRole("heading", { name: "ID 1" })).toBeVisible(); await page.getByRole("link", { name: "Live" }).click(); // A cold dev server compiles the Live route chunk on this first request. await expect(page.getByRole("region", { name: /^Motion/ })).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText("waiting for data")).toHaveCount(0); await expect(page.getByRole("combobox", { name: "Mode" })).toBeEnabled(); } @@ -41,9 +45,9 @@ test("each mode shows its own goal control and the warning is open loop only", a await expect(page.getByRole("alert")).toHaveCount(0); await expect(page.getByLabel("Commanded duty value")).toHaveCount(0); await pickMode(page, "Velocity"); - await expect(page.getByLabel("Goal readout")).toHaveText(/ counts\/s$/); + await expect(page.getByLabel("Goal readout")).toHaveText(/ deg\/s$/); await pickMode(page, "Position"); - await expect(page.getByLabel("Goal readout")).toHaveText(/ counts$/); + await expect(page.getByLabel("Goal readout")).toHaveText(/ deg$/); await pickMode(page, "Open loop"); await expect(page.getByLabel("Goal readout")).toHaveText(/ %$/); await expect(page.getByRole("alert")).toBeVisible(); @@ -54,11 +58,12 @@ test("a velocity goal reads back into the Motion goal readout in the mode's unit }) => { await openLive(page); await pickMode(page, "Velocity"); - await setGoal(page, "600"); - await expect(page.getByLabel("Goal readout")).toHaveText("600 counts/s"); + // Inside velocity_limit_cps, which caps the slider at 74 deg/s. + await setGoal(page, "50"); + await expect(page.getByLabel("Goal readout")).toHaveText("50 deg/s"); const motion = page.getByRole("region", { name: /^Motion/ }); await expect(motion).toBeVisible(); - await expect(page.getByLabel("Goal value")).toHaveText("600 counts/s"); + await expect(page.getByLabel("Goal value")).toHaveText("50 deg/s"); }); test("a current goal reads back in milliamps and the goal series moves to Electrical", async ({ @@ -71,17 +76,20 @@ test("a current goal reads back in milliamps and the goal series moves to Electr await expect(page.getByLabel("Goal value")).toHaveText("50 mA"); }); -test("position mode shows the goal in counts and the servo's answer to a goal it rejects", async ({ +test("position mode shows the goal in degrees and a goal past the rail stops at it", async ({ page, }) => { await openLive(page); await pickMode(page, "Position"); - await expect(page.getByLabel("Goal value")).toHaveText("0 counts"); - // The simulated servo's physical position limits are both 0, so any other - // goal fails the firmware's validator; the answer lands in the cluster. - await setGoal(page, "2000"); - await expect(page.getByText("servo answered Validation")).toBeVisible(); - await expect(page.getByLabel("Goal value")).toHaveText("0 counts"); + // goal_position boots at 0, one count below the calibrated sensor floor. + await expect(page.getByLabel("Goal value")).toHaveText("-0.2 deg"); + await setGoal(page, "100"); + await expect(page.getByLabel("Goal value")).toHaveText("100.0 deg"); + // The slider's range is the calibrated sensor span, which is also what the + // firmware validates against, so a goal beyond it clamps instead of nacking. + await setGoal(page, "500"); + await expect(page.getByLabel("Goal value")).toHaveText("202.0 deg"); + await expect(page.getByText(/servo answered/)).toHaveCount(0); }); test("torque on reads back on and the servo keeps answering without a fault", async ({ page }) => { @@ -91,7 +99,7 @@ test("torque on reads back on and the servo keeps answering without a fault", as await torque.click(); await expect(torque).toHaveAttribute("aria-checked", "true"); await page.waitForTimeout(1000); - await expect(page.getByRole("button", { name: "ID 1 raw", exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "ID 1", exact: true })).toBeVisible(); await expect(page.getByText(/servo answered/)).toHaveCount(0); }); diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts index 218af75..ca56189 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard.spec.ts @@ -3,7 +3,8 @@ import { gotoSim } from "./helpers"; const UID_1 = "c94b8419d1092aec87de5fd151ce290f"; const UID_2 = "30634f42dde2095340c8d84f67f28244"; -const READOUT = /^-?\d+(\.\d+)? (deg|counts)$/; +// The simulated fleet boots calibrated, so the position readout is degrees. +const READOUT = /^-?\d+(\.\d+)? deg$/; test("each simulated servo gets a card with its identity and a position readout", async ({ page, diff --git a/tests/e2e/live.spec.ts b/tests/e2e/live.spec.ts index d0a0f6f..435888b 100644 --- a/tests/e2e/live.spec.ts +++ b/tests/e2e/live.spec.ts @@ -1,26 +1,33 @@ import { expect, test, type Page } from "@playwright/test"; import { gotoSim } from "./helpers"; -const READOUT = /^-?\d+(\.\d+)? \S+$/; +// The simulated fleet boots calibrated, so the position family reads degrees. +const READOUT = /^-?\d+(\.\d+)? deg$/; async function openLive(page: Page): Promise { await gotoSim(page, [1, 2]); await page.getByRole("button", { name: "ID 1" }).click(); + // The servo page proves the selection landed: leaving for Live while that + // navigation is still compiling on a cold dev server races the two. + await expect(page.getByRole("heading", { name: "ID 1" })).toBeVisible(); await page.getByRole("link", { name: "Live" }).click(); // A cold dev server compiles the Live route chunk on this first request. await expect(page.getByRole("region", { name: /^Motion/ })).toBeVisible({ timeout: 15_000 }); + // Readouts are placeholders until the poll delivers its first sample. + await expect(page.getByText("waiting for data")).toHaveCount(0); } -test("the position readout is a number with a unit and moves with the replayed track", async ({ +test("the position readout is a number with a unit and the replayed track keeps moving", async ({ page, }) => { - await openLive(page); const position = page.getByLabel("Position value"); + await openLive(page); await expect(position).toHaveText(READOUT); - // The recorded pot sits at its rail most of the loop, so the second reading - // is whichever comes first that differs, not one a fixed delay later. - const first = (await position.textContent()) ?? ""; - await expect(position).not.toHaveText(first, { timeout: 5000 }); + // The recorded pot rests at a rail for seconds at a time, so movement is + // read off the shunt instead, which is noisy on every tick. + const current = page.getByLabel("Current value"); + const first = (await current.textContent()) ?? ""; + await expect(current).not.toHaveText(first, { timeout: 5000 }); await expect(position).toHaveText(READOUT); }); diff --git a/tests/e2e/manage.spec.ts b/tests/e2e/manage.spec.ts index 65b7777..44c7ecf 100644 --- a/tests/e2e/manage.spec.ts +++ b/tests/e2e/manage.spec.ts @@ -1,11 +1,9 @@ import { expect, test, type Locator, type Page } from "@playwright/test"; import { gotoSim } from "./helpers"; -// The simulated servos run the production stacks over a bus with no config -// store behind them, so SAVE and FACTORY reach the servo and come back -// `hardware` (firmware dispatch.rs: both need a store to write). REBOOT and -// ASSIGN need no flash and land for real. -const NO_FLASH = "servo answered Hardware"; +// The simulated servos run the production stacks over a bus with a config +// store behind each of them, so all four actions land for real: SAVE writes +// the store, FACTORY wipes it and reboots the servo onto board defaults. async function openManage(page: Page, id: number): Promise { await gotoSim(page, [1, 2]); @@ -56,8 +54,13 @@ test("Factory reset runs behind the confirm strip", async ({ page }) => { await card.getByRole("button", { name: "Factory reset" }).click(); await erase.click(); await expect(erase).toBeHidden(); - await expect(card.getByText(NO_FLASH)).toBeVisible(); + await expect(card.getByText("Erased. It comes back as ID 1 at 1 M.")).toBeVisible(); await expect(page.getByRole("button", { name: "ID 2" })).toBeVisible(); + // The seeded calibration is written into the servo's live table, never into + // its store, so the wipe reboots it onto board defaults and it reads blank. + await expect( + page.getByRole("region", { name: "Calibration" }).getByText("Not calibrated"), + ).toBeVisible(); }); test("Save settings reaches the servo while torque is off", async ({ page }) => { @@ -65,6 +68,6 @@ test("Save settings reaches the servo while torque is off", async ({ page }) => const save = card.getByRole("button", { name: "Save settings" }); await expect(save).toBeEnabled(); await save.click(); - await expect(card.getByText(NO_FLASH)).toBeVisible(); + await expect(card.getByText("Settings saved.")).toBeVisible(); await expect(page.getByRole("button", { name: "ID 2" })).toBeVisible(); });