Files
galaxy-game/ui/frontend/tests/calculator-tab.test.ts
T
Ilia Denisov b01a60e42b
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m48s
Tests · Go / test (pull_request) Successful in 2m1s
Tests · UI / test (pull_request) Successful in 2m34s
fix(ui): F8-06 calculator polish — drop delete-class button, reserve lock slot
- Remove the `delete <ship_class_name>` button (and `deleteClass`,
  `canDelete`, `.delete` CSS, `game.calculator.action.delete` i18n key)
  from the calculator. Delete-class lives in the ship-classes table —
  the broader rework will land under #53.
- Bombing and cargo-capacity rows now reserve a hidden lock-slot
  placeholder so their value column lines up vertically with the
  mass/speed/attack/defence rows (which carry a lock button).
2026-05-26 19:10:59 +02:00

668 lines
23 KiB
TypeScript

// Component coverage for the Phase 30 ship-class calculator: forward
// results, single-target goal-seek wired through a mounted component, the
// Create flow against a real OrderDraftStore, and the planet area. The
// math itself is covered by `calc-model.test.ts` and the Go parity tests;
// here we assert the component renders and orchestrates them.
import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render } from "@testing-library/svelte";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
// The calculator reads `page.params.id` to scope its long-lived state to
// the active game; stub a stable id so the component test has a router.
vi.mock("$app/state", () => ({ page: { params: { id: "calc-test-game" } } }));
import { i18n } from "../src/lib/i18n/index.svelte";
import CalculatorTab from "../src/lib/sidebar/calculator-tab.svelte";
import { calculatorState } from "../src/lib/calculator/calc-state.svelte";
import { CORE_CONTEXT_KEY, CoreHolder } from "../src/lib/core-context.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import {
SELECTION_CONTEXT_KEY,
SelectionStore,
} from "../src/lib/selection.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "../src/lib/rendered-report.svelte";
import type { GameReport, ReportPlanet } from "../src/api/game-state";
import type { Core } from "../src/platform/core/index";
import { makeFakeCore } from "./fake-core";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { IDBPDatabase } from "idb";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let draft: OrderDraftStore;
const LOCAL_PLANET: ReportPlanet = {
number: 17,
name: "Castle",
x: 100,
y: 100,
kind: "local",
owner: "me",
size: 1000,
resources: 10,
industryStockpile: 0,
materialsStockpile: 100,
industry: 1000,
population: 1000,
colonists: 0,
production: "Cruiser",
freeIndustry: 1000,
};
function makeReport(over: Partial<GameReport> = {}): GameReport {
return {
localPlayerDrive: 1.2,
localPlayerWeapons: 1.5,
localPlayerShields: 1,
localPlayerCargo: 1,
localShipClass: [],
planets: [],
...over,
} as unknown as GameReport;
}
function mount(opts: {
core?: Core | null;
report?: GameReport;
selection?: SelectionStore;
} = {}) {
const holder = new CoreHolder();
holder.set(opts.core === undefined ? makeFakeCore() : opts.core);
const selection = opts.selection ?? new SelectionStore();
const report = opts.report ?? makeReport();
const source: RenderedReportSource = {
get report() {
return report;
},
};
const context = new Map<unknown, unknown>([
[RENDERED_REPORT_CONTEXT_KEY, source],
[ORDER_DRAFT_CONTEXT_KEY, draft],
[CORE_CONTEXT_KEY, holder],
[SELECTION_CONTEXT_KEY, selection],
]);
return render(CalculatorTab, { context });
}
async function setBlock(
ui: { getByTestId(id: string): HTMLElement },
key: string,
value: number,
): Promise<void> {
await fireEvent.input(ui.getByTestId(`calculator-block-${key}`), {
target: { value: String(value) },
});
}
beforeEach(async () => {
dbName = `galaxy-calculator-${crypto.randomUUID()}`;
db = await openGalaxyDB(dbName);
draft = new OrderDraftStore();
await draft.init({ cache: new IDBCache(db), gameId: GAME_ID });
i18n.resetForTests("en");
// The calculator state is a module singleton shared across cases.
calculatorState.reset();
});
afterEach(async () => {
draft.dispose();
db.close();
await new Promise<void>((resolve) => {
const req = indexedDB.deleteDatabase(dbName);
req.onsuccess = () => resolve();
req.onerror = () => resolve();
req.onblocked = () => resolve();
});
});
describe("calculator-tab", () => {
test("computes results once the blocks are valid", async () => {
const ui = mount();
// All-zero blocks are invalid: results read as unavailable.
expect(ui.getByTestId("calculator-out-emptyMass")).toHaveTextContent("—");
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
// empty mass = 10 + 5 + 5 = 20.
expect(ui.getByTestId("calculator-out-emptyMass")).toHaveTextContent("20");
});
test("locking attack back-solves the weapons block", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "armament", 2);
await setBlock(ui, "weapons", 5);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
await fireEvent.input(ui.getByTestId("calculator-locked-attack"), {
target: { value: "30" },
});
// weapons = 30 / weaponsTech(1.5) = 20, shown read-only.
const weapons = ui.getByTestId("calculator-block-weapons");
expect(weapons).toHaveValue(20);
expect(weapons).toHaveAttribute("readonly");
});
test("flags an unreachable speed target as infeasible", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-speedEmpty"));
// ceiling is 20 * driveTech(1.2) = 24; 100 is unreachable.
await fireEvent.input(ui.getByTestId("calculator-locked-speedEmpty"), {
target: { value: "100" },
});
const locked = ui.getByTestId("calculator-locked-speedEmpty");
expect(locked).toHaveAttribute("title", expect.stringMatching(/cannot be reached/i));
});
test("create adds a ship-class command once the name is valid", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
const create = ui.getByTestId("calculator-create");
expect(create).toBeDisabled();
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Cruiser" },
});
expect(create).not.toBeDisabled();
await fireEvent.click(create);
expect(draft.commands).toHaveLength(1);
expect(draft.commands[0]).toMatchObject({
kind: "createShipClass",
name: "Cruiser",
drive: 10,
shields: 5,
cargo: 5,
});
});
test("planet area prompts for a selection when none is active", () => {
const ui = mount();
expect(ui.getByTestId("calculator-planet-none")).toBeInTheDocument();
});
test("planet area shows build stats for a selected own planet", async () => {
const selection = new SelectionStore();
selection.selectPlanet(17);
const ui = mount({
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
expect(ui.getByTestId("calculator-planet-name")).toHaveTextContent("Castle");
expect(
ui.getByTestId("calculator-ships-per-turn"),
).not.toHaveTextContent("—");
});
test("zero cargo disables the load toggle", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 0);
expect(ui.getByTestId("calculator-load-full")).toBeDisabled();
expect(ui.getByTestId("calculator-load-custom")).toBeDisabled();
});
test("full load shows the cargo capacity", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
// A fresh design starts with cargo 0, which pins load to empty;
// pick full now that there is a hold.
await fireEvent.click(ui.getByTestId("calculator-load-full"));
// capacity = cargoTech(1) * (5 + 25/20) = 6.25.
expect(ui.getByTestId("calculator-full-capacity")).toHaveTextContent("6.25");
});
test("flags a custom load above cargo capacity", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-load-custom"));
await fireEvent.input(ui.getByTestId("calculator-custom-load"), {
target: { value: "100" },
});
expect(ui.getByTestId("calculator-custom-load")).toHaveAttribute(
"aria-invalid",
"true",
);
});
test("marks an invalid block value with aria-invalid", async () => {
const ui = mount();
// 0.5 is neither 0 nor ≥ 1.
await setBlock(ui, "drive", 0.5);
expect(ui.getByTestId("calculator-block-drive")).toHaveAttribute(
"aria-invalid",
"true",
);
});
test("disables the speed lock when drive is zero", async () => {
const ui = mount();
await setBlock(ui, "drive", 0);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
expect(ui.getByTestId("calculator-lock-speedEmpty")).toBeDisabled();
});
test("displays computed values rounded up to three decimals", async () => {
const ui = mount();
await setBlock(ui, "drive", 7);
await setBlock(ui, "shields", 3);
await setBlock(ui, "cargo", 1);
// empty mass = 11; max speed = 11 * driveTech... use a value that is
// not already 3-decimal: speedEmpty = 20*7*1.2 / 11 = 15.2727…
// ceil to 3 → 15.273.
expect(ui.getByTestId("calculator-out-speedEmpty")).toHaveTextContent(
"15.273",
);
});
test("tech defaults render as a number with an open-lock affordance", () => {
const ui = mount();
// Default state: no override → number + open lock, no input.
expect(ui.getByTestId("calculator-tech-value-drive")).toHaveTextContent(
"1.2",
);
expect(
ui.getByTestId("calculator-tech-override-drive"),
).toBeInTheDocument();
expect(ui.queryByTestId("calculator-tech-drive")).toBeNull();
});
test("clicking the open tech lock reveals the input + closed lock", async () => {
const ui = mount();
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
// Now an input is rendered and the lock turned closed (reset).
expect(ui.getByTestId("calculator-tech-drive")).toHaveValue(1.2);
expect(ui.getByTestId("calculator-tech-reset-drive")).toBeInTheDocument();
expect(ui.queryByTestId("calculator-tech-value-drive")).toBeNull();
});
test("flags a tech override below the player's current tech", async () => {
const ui = mount();
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
// Player drive is 1.2; setting 0.5 is below the floor.
await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
target: { value: "0.5" },
});
expect(ui.getByTestId("calculator-tech-drive")).toHaveAttribute(
"aria-invalid",
"true",
);
});
test("smart step jumps from 0 to 1 on ArrowUp for ship blocks", async () => {
const ui = mount();
const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
drive.focus();
await fireEvent.keyDown(drive, { key: "ArrowUp" });
expect(drive).toHaveValue(1);
await fireEvent.keyDown(drive, { key: "ArrowUp" });
expect(drive).toHaveValue(1.1);
await fireEvent.keyDown(drive, { key: "ArrowDown" });
expect(drive).toHaveValue(1);
await fireEvent.keyDown(drive, { key: "ArrowDown" });
expect(drive).toHaveValue(0);
});
test("regression: speed lock works at the ceiling with all-zero non-drive blocks", async () => {
const ui = mount();
await setBlock(ui, "drive", 1);
// Override drive tech to 1 so the ceiling math is plain.
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
await fireEvent.input(ui.getByTestId("calculator-tech-drive"), {
target: { value: "1" },
});
// With D=1, W=A=S=C=0 the only achievable speed is 20*driveTech=20.
expect(ui.getByTestId("calculator-out-speedEmpty")).toHaveTextContent("20");
await fireEvent.click(ui.getByTestId("calculator-lock-speedEmpty"));
const locked = ui.getByTestId("calculator-locked-speedEmpty");
expect(locked).toHaveValue(20);
// The lock is feasible — no infeasible title and no red error class.
expect(locked).not.toHaveAttribute(
"title",
expect.stringMatching(/cannot be reached/i),
);
});
test("planet MAT defaults to a value + open lock and opens an input on click", async () => {
const selection = new SelectionStore();
selection.selectPlanet(17);
const ui = mount({
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
// Initial state: the MAT shows the planet's value via the number cell
// and an open lock; no input until the override is activated.
expect(
ui.getByTestId("calculator-planet-mat-value"),
).toHaveTextContent("100");
expect(
ui.getByTestId("calculator-mat-override"),
).toBeInTheDocument();
expect(ui.queryByTestId("calculator-planet-mat")).toBeNull();
await fireEvent.click(ui.getByTestId("calculator-mat-override"));
expect(ui.getByTestId("calculator-planet-mat")).toHaveValue(100);
expect(ui.getByTestId("calculator-mat-reset")).toBeInTheDocument();
});
test("flags a modernization target below the player's current tech", async () => {
const ui = mount();
await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
// Player drive is 1.2; the target is seeded with the same value.
await fireEvent.input(ui.getByTestId("calculator-target-drive"), {
target: { value: "0.5" },
});
expect(ui.getByTestId("calculator-target-drive")).toHaveAttribute(
"aria-invalid",
"true",
);
});
test("armament Arrow keys step the integer block by ±1 (clamped at 0)", async () => {
const ui = mount();
const armament = ui.getByTestId(
"calculator-block-armament",
) as HTMLInputElement;
armament.focus();
await fireEvent.keyDown(armament, { key: "ArrowUp" });
expect(armament).toHaveValue(1);
await fireEvent.keyDown(armament, { key: "ArrowUp" });
expect(armament).toHaveValue(2);
await fireEvent.keyDown(armament, { key: "ArrowDown" });
expect(armament).toHaveValue(1);
await fireEvent.keyDown(armament, { key: "ArrowDown" });
expect(armament).toHaveValue(0);
// Clamped at zero — another ArrowDown is a no-op.
await fireEvent.keyDown(armament, { key: "ArrowDown" });
expect(armament).toHaveValue(0);
});
test("renders unoverridden tech as a 3-decimal label (matches the report)", () => {
// Player drive tech 1.2 → "1.200" via the shared ceil3 formatter,
// always padded to three decimals (calculator labels are column-
// aligned with the report).
const ui = mount();
const tech = ui.getByTestId("calculator-tech-value-drive");
expect((tech.textContent ?? "").trim()).toBe("1.200");
});
test("planet MAT label renders through the 3-decimal formatter", () => {
const selection = new SelectionStore();
selection.selectPlanet(17);
const ui = mount({
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
// Planet MAT is 100 → "100.000" through the shared formatter; the
// label is monospaced + right-aligned via the existing `.mat-val`
// rule. Integer MAT pads to three decimals like every other label.
const mat = ui.getByTestId("calculator-planet-mat-value");
expect((mat.textContent ?? "").trim()).toBe("100.000");
});
test("derived results pad to three decimals (integer empty mass)", async () => {
// Integer-valued outputs read with the same trailing zeros as
// fractional ones — column-aligned tabular display.
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
const mass = ui.getByTestId("calculator-out-emptyMass");
expect((mass.textContent ?? "").trim()).toBe("20.000");
});
test("number inputs refuse a fourth decimal as the user types", async () => {
const selection = new SelectionStore();
selection.selectPlanet(17);
const ui = mount({
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
// MAT input: typed "12.3456" must clamp to "12.345" on input.
await fireEvent.click(ui.getByTestId("calculator-mat-override"));
const mat = ui.getByTestId("calculator-planet-mat") as HTMLInputElement;
await fireEvent.input(mat, { target: { value: "12.3456" } });
expect(mat.value).toBe("12.345");
expect(mat.valueAsNumber).toBeCloseTo(12.345, 9);
// Custom-load input on a ship with a non-zero cargo: typed
// "1.2345" must clamp to "1.234".
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-load-custom"));
const load = ui.getByTestId("calculator-custom-load") as HTMLInputElement;
await fireEvent.input(load, { target: { value: "1.2345" } });
expect(load.value).toBe("1.234");
});
test("tech and target-tech inputs cap at three decimals", async () => {
const ui = mount();
// Tech override input.
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
const tech = ui.getByTestId("calculator-tech-drive") as HTMLInputElement;
await fireEvent.input(tech, { target: { value: "2.9999" } });
expect(tech.value).toBe("2.999");
// Modernization target tech input.
await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
const target = ui.getByTestId(
"calculator-target-drive",
) as HTMLInputElement;
await fireEvent.input(target, { target: { value: "3.1416" } });
expect(target.value).toBe("3.141");
});
test("lock value input caps at three decimals", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
const lock = ui.getByTestId(
"calculator-locked-attack",
) as HTMLInputElement;
await fireEvent.input(lock, { target: { value: "0.1234" } });
expect(lock.value).toBe("0.123");
});
test("ship-block input caps at three decimals", async () => {
const ui = mount();
const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
await fireEvent.input(drive, { target: { value: "1.2345" } });
expect(drive.value).toBe("1.234");
});
test("lock spinner step is replaced by ArrowUp/ArrowDown (±0.001)", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
const locked = ui.getByTestId(
"calculator-locked-attack",
) as HTMLInputElement;
// Lock value is seeded from outputs.attack (0 with no weapons).
const start = Number(locked.value);
locked.focus();
await fireEvent.keyDown(locked, { key: "ArrowUp" });
expect(Number(locked.value)).toBeCloseTo(start + 0.001, 9);
await fireEvent.keyDown(locked, { key: "ArrowDown" });
expect(Number(locked.value)).toBeCloseTo(start, 9);
});
test("flags the lock as infeasible when the back-solved block falls in (0, 1)", async () => {
// attack lock → weapons = targetAttack / weaponsTech. weaponsTech
// is 1.5; a target of 0.5 would force weapons = 0.333… which
// fails the DWSC rule (must be 0 or ≥ 1).
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "armament", 2);
await setBlock(ui, "weapons", 5);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
await fireEvent.input(ui.getByTestId("calculator-locked-attack"), {
target: { value: "0.5" },
});
const locked = ui.getByTestId("calculator-locked-attack");
expect(locked).toHaveAttribute(
"title",
expect.stringMatching(/cannot be reached/i),
);
// The claimed block is not back-solved into the invalid (0, 1)
// range — the weapons input keeps the user's typed value (5).
expect(ui.getByTestId("calculator-block-weapons")).toHaveValue(5);
});
test("dropdown selection loads the class immediately (no blur needed)", async () => {
const ui = mount({
report: makeReport({
localShipClass: [
{
name: "Scout",
drive: 3,
armament: 0,
weapons: 0,
shields: 2,
cargo: 1,
},
],
} as unknown as GameReport),
});
// A datalist option click sets the whole value at once — Firefox
// reports no `inputType`, Chromium reports "insertReplacementText".
// Simulate the latter; the calculator should load before any
// `change` event.
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Scout" },
inputType: "insertReplacementText",
});
expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
expect(ui.getByTestId("calculator-block-shields")).toHaveValue(2);
});
test("dropdown selection asks before discarding manual edits", async () => {
const ui = mount({
report: makeReport({
localShipClass: [
{
name: "Scout",
drive: 3,
armament: 0,
weapons: 0,
shields: 2,
cargo: 1,
},
],
} as unknown as GameReport),
});
// The user has hand-edited the design.
await setBlock(ui, "drive", 7);
const confirm = vi.spyOn(window, "confirm").mockReturnValue(false);
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Scout" },
inputType: "insertReplacementText",
});
expect(confirm).toHaveBeenCalledTimes(1);
// The user said no — the manual edits stay.
expect(ui.getByTestId("calculator-block-drive")).toHaveValue(7);
// The name field is reverted to the previously loaded class (or
// empty), so the field does not pretend the load happened.
expect(ui.getByTestId("calculator-name")).toHaveValue("");
confirm.mockReturnValue(true);
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Scout" },
inputType: "insertReplacementText",
});
// Confirmed — the class is now loaded.
expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
confirm.mockRestore();
});
test("dropdown selection loads silently when the design is clean", async () => {
const ui = mount({
report: makeReport({
localShipClass: [
{
name: "Scout",
drive: 3,
armament: 0,
weapons: 0,
shields: 2,
cargo: 1,
},
],
} as unknown as GameReport),
});
const confirm = vi.spyOn(window, "confirm");
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Scout" },
inputType: "insertReplacementText",
});
expect(confirm).not.toHaveBeenCalled();
expect(ui.getByTestId("calculator-block-drive")).toHaveValue(3);
confirm.mockRestore();
});
test("does not render a delete-class button after loading a class", async () => {
const ui = mount({
report: makeReport({
localShipClass: [
{
name: "Scout",
drive: 3,
armament: 0,
weapons: 0,
shields: 2,
cargo: 1,
},
],
} as unknown as GameReport),
});
await fireEvent.input(ui.getByTestId("calculator-name"), {
target: { value: "Scout" },
inputType: "insertReplacementText",
});
// The loaded class state used to render a `delete <name>` button;
// the calculator no longer owns delete-class — issue #53 will.
expect(ui.queryByTestId("calculator-delete")).toBeNull();
});
test("bombing and cargo-capacity rows reserve the lock slot for column alignment", () => {
const ui = mount();
for (const id of ["calculator-out-bombing", "calculator-out-cargo-capacity"]) {
const cell = ui.getByTestId(id).parentElement;
expect(cell).not.toBeNull();
// A hidden placeholder occupies the same width as the lock button
// on the mass/speed/attack/defence rows, so the value column does
// not drift right on the rows without a lock.
expect(cell!.querySelector(".lock-slot")).not.toBeNull();
}
});
});