feat(lanes,ui): per-lane git surface — Changes panel, VS Code-style diff, branch-aware Add lane, redesigned tooltips (#34, #35)
- Changes card beside lane detail with five modes: Files tree, overview, Review (whole diff, per-file collapse), single file, commit history. - LaneDiffView: unified / side-by-side with independent scroll on both axes and mirrored scrollLeft, expand/collapse context, intra-line emphasis. - Staged-only commit box; git actions (fetch/pull/push/stage/unstage/ discard/commit/switch/branch-create/branch-delete) guarded in git-ops.js. - Server: git-diff.js + git-ops.js behind GET /api/lanes/:id/git/* and POST /api/lanes/:id/git/:action; gitFacts gains upstream/ahead/behind. - Add lane: new/existing branch, current branch, worktree list, prefill from the git bar's "New lane here"; /api/run/cwds gains a repo kind. - GlobalTooltip upgrades every native title into a themed anchored bubble; chart/info popovers use theme tokens.
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* @file LaneChangesPanel.test.tsx
|
||||
* @description Pins the Changes panel's five modes and its three ranges: the
|
||||
* overview lists files and nothing else, clicking one opens that file's diff
|
||||
* (and asks the server for THAT path), Review asks for every file's hunks at
|
||||
* once, the range toggle changes which comparison is requested, History opens
|
||||
* one commit, and Files quick-opens a whole file. Also pins the per-file git
|
||||
* writes — including discard needing two clicks before the server hears
|
||||
* `confirm` — and the degrade for a cwd git cannot read.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { LaneChangesPanel } from "../LaneChangesPanel";
|
||||
import { api } from "../../../lib/api";
|
||||
import type { LaneChanges } from "../../../lib/types";
|
||||
|
||||
vi.mock("../../../lib/api", () => ({
|
||||
api: {
|
||||
lanes: {
|
||||
changes: vi.fn(),
|
||||
gitAction: vi.fn(),
|
||||
gitLog: vi.fn(),
|
||||
gitFile: vi.fn(),
|
||||
gitLs: vi.fn(),
|
||||
gitTree: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const HUNK = {
|
||||
header: "@@ -1,2 +1,3 @@",
|
||||
context: "",
|
||||
oldStart: 1,
|
||||
newStart: 1,
|
||||
lines: [
|
||||
{ type: "ctx" as const, oldLine: 1, newLine: 1, text: "keep" },
|
||||
{ type: "del" as const, oldLine: 2, newLine: null, text: "old line" },
|
||||
{ type: "add" as const, oldLine: null, newLine: 2, text: "new line" },
|
||||
],
|
||||
};
|
||||
|
||||
function changes(over: Partial<Extract<LaneChanges, { available: true }>> = {}): LaneChanges {
|
||||
return {
|
||||
available: true,
|
||||
range: "work",
|
||||
base: "development",
|
||||
baseRef: "origin/development",
|
||||
sha: null,
|
||||
context: 3,
|
||||
truncated: false,
|
||||
files: [
|
||||
{
|
||||
path: "server/lib/git-ops.js",
|
||||
status: " M",
|
||||
untracked: false,
|
||||
unstaged: true,
|
||||
added: 12,
|
||||
deleted: 3,
|
||||
binary: false,
|
||||
},
|
||||
{ path: "notes.txt", status: "??", untracked: true, added: 4, deleted: 0, binary: false },
|
||||
{
|
||||
path: "docs/API.md",
|
||||
status: "M ",
|
||||
untracked: false,
|
||||
staged: true,
|
||||
added: 2,
|
||||
deleted: 1,
|
||||
binary: false,
|
||||
},
|
||||
],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(api.lanes.changes).mockReset();
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue(changes());
|
||||
vi.mocked(api.lanes.gitAction).mockReset();
|
||||
vi.mocked(api.lanes.gitAction).mockResolvedValue({ action: "stage", ok: true, output: "" });
|
||||
vi.mocked(api.lanes.gitLog).mockResolvedValue({
|
||||
available: true,
|
||||
commits: [
|
||||
{
|
||||
sha: "a".repeat(40),
|
||||
short: "aaaaaaa",
|
||||
subject: "feat: the commit",
|
||||
author: "t",
|
||||
date: "2026-09-22T01:00:00Z",
|
||||
refs: ["HEAD -> feat/thing"],
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(api.lanes.gitLs).mockResolvedValue({
|
||||
available: true,
|
||||
paths: ["src/main.ts"],
|
||||
truncated: false,
|
||||
total: 1,
|
||||
});
|
||||
vi.mocked(api.lanes.gitTree).mockResolvedValue({
|
||||
available: true,
|
||||
path: "",
|
||||
entries: [{ name: "src", path: "src", dir: true }],
|
||||
truncated: false,
|
||||
});
|
||||
vi.mocked(api.lanes.gitFile).mockResolvedValue({
|
||||
available: true,
|
||||
path: "src/main.ts",
|
||||
rev: "work",
|
||||
binary: false,
|
||||
truncated: false,
|
||||
lines: ["const whole = 1;"],
|
||||
});
|
||||
});
|
||||
|
||||
describe("LaneChangesPanel", () => {
|
||||
it("opens on the overview: a summary request, files listed, no diff bodies", async () => {
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
|
||||
expect(await screen.findByTestId("lane-changes-overview")).toBeInTheDocument();
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, { range: "work" });
|
||||
expect(screen.getByText("server/lib/git-ops.js")).toBeInTheDocument();
|
||||
expect(screen.getByText("+12")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/new line/)).toBeNull();
|
||||
});
|
||||
|
||||
it("opens one file's diff when its row is clicked, asking for that path only", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue(
|
||||
changes({
|
||||
files: [
|
||||
{
|
||||
path: "server/lib/git-ops.js",
|
||||
status: " M",
|
||||
untracked: false,
|
||||
added: 12,
|
||||
deleted: 3,
|
||||
binary: false,
|
||||
hunks: [HUNK],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
await user.click(screen.getByText("server/lib/git-ops.js"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "work",
|
||||
path: "server/lib/git-ops.js",
|
||||
context: 3,
|
||||
})
|
||||
);
|
||||
// The highlighter splits a line into one span per token, so assert on the
|
||||
// rendered text of the whole view rather than on a single node.
|
||||
const view = await screen.findByTestId("lane-diff-view");
|
||||
expect(view).toHaveTextContent("new line");
|
||||
// Both gutters are rendered, so a reviewer can cite a line number.
|
||||
expect(view).toHaveTextContent("keep");
|
||||
});
|
||||
|
||||
it("asks for every file's hunks in review mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Review" }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, { range: "work", full: true })
|
||||
);
|
||||
expect(await screen.findByTestId("lane-changes-review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("switches the comparison when the range toggle is used", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "vs base" }));
|
||||
await waitFor(() => expect(api.lanes.changes).toHaveBeenCalledWith(7, { range: "branch" }));
|
||||
});
|
||||
|
||||
it("says so, quietly, when the lane's directory is not a git repo", async () => {
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue({
|
||||
available: false,
|
||||
range: "work",
|
||||
files: [],
|
||||
});
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
expect(
|
||||
await screen.findByText("This lane's directory is not a readable git repository.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stages one file and asks the server for that path alone", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
// Both rows offer it (one modified, one untracked); the first is the
|
||||
// modified file the assertion below names.
|
||||
const [stage] = screen.getAllByRole("button", { name: "Stage this file" });
|
||||
await user.click(stage!);
|
||||
expect(api.lanes.gitAction).toHaveBeenCalledWith(7, "stage", {
|
||||
paths: ["server/lib/git-ops.js"],
|
||||
});
|
||||
});
|
||||
|
||||
it("needs two clicks to discard, and only the second one reaches the server", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
const [discard] = screen.getAllByRole("button", { name: "Discard this file's changes" });
|
||||
await user.click(discard!);
|
||||
expect(api.lanes.gitAction).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Click again to discard — this cannot be undone" })
|
||||
);
|
||||
expect(api.lanes.gitAction).toHaveBeenCalledWith(7, "discard", {
|
||||
paths: ["server/lib/git-ops.js"],
|
||||
confirm: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens a commit from history as a commit-range review", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "History" }));
|
||||
await user.click(await screen.findByText("feat: the commit"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "commit",
|
||||
sha: "a".repeat(40),
|
||||
full: true,
|
||||
})
|
||||
);
|
||||
expect(await screen.findByTestId("lane-changes-commit-chip")).toHaveTextContent("aaaaaaa");
|
||||
});
|
||||
|
||||
it("expands a folder in place, keeping its siblings visible, instead of navigating into it", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(api.lanes.gitTree).mockImplementation(async (_id, path) =>
|
||||
path === "src"
|
||||
? {
|
||||
available: true,
|
||||
path: "src",
|
||||
entries: [{ name: "main.ts", path: "src/main.ts", dir: false }],
|
||||
truncated: false,
|
||||
}
|
||||
: {
|
||||
available: true,
|
||||
path: "",
|
||||
entries: [{ name: "src", path: "src", dir: true }],
|
||||
truncated: false,
|
||||
}
|
||||
);
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Files" }));
|
||||
await user.click(await screen.findByText("src"));
|
||||
|
||||
expect(await screen.findByText("main.ts")).toBeInTheDocument();
|
||||
// The root's "src" row is still there — expanding did not replace the list.
|
||||
expect(screen.getByText("src")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("expands every folder at once, fetching whatever level wasn't cached yet", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(api.lanes.gitTree).mockImplementation(async (_id, path) => {
|
||||
if (path === "src")
|
||||
return {
|
||||
available: true,
|
||||
path: "src",
|
||||
entries: [{ name: "main.ts", path: "src/main.ts", dir: false }],
|
||||
truncated: false,
|
||||
};
|
||||
return {
|
||||
available: true,
|
||||
path: "",
|
||||
entries: [{ name: "src", path: "src", dir: true }],
|
||||
truncated: false,
|
||||
};
|
||||
});
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Files" }));
|
||||
await screen.findByText("src");
|
||||
await user.click(screen.getByRole("button", { name: "Expand all folders" }));
|
||||
|
||||
expect(await screen.findByText("main.ts")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Collapse all folders" }));
|
||||
expect(screen.queryByText("main.ts")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("src")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("quick-opens a tracked file as its whole content, not a diff", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Files" }));
|
||||
await user.type(await screen.findByLabelText("Find a file by path…"), "main");
|
||||
await user.click(await screen.findByText("src/main.ts"));
|
||||
|
||||
await waitFor(() => expect(api.lanes.gitFile).toHaveBeenCalledWith(7, "src/main.ts", "work"));
|
||||
expect(await screen.findByTestId("lane-file-view")).toHaveTextContent("const whole = 1;");
|
||||
});
|
||||
|
||||
it("switches the diff to side-by-side", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue(
|
||||
changes({
|
||||
files: [
|
||||
{
|
||||
path: "server/lib/git-ops.js",
|
||||
status: " M",
|
||||
untracked: false,
|
||||
added: 12,
|
||||
deleted: 3,
|
||||
binary: false,
|
||||
hunks: [HUNK],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
await user.click(screen.getByText("server/lib/git-ops.js"));
|
||||
expect(await screen.findByTestId("lane-diff-view")).toHaveAttribute("data-layout", "unified");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Side-by-side" }));
|
||||
expect(screen.getByTestId("lane-diff-view")).toHaveAttribute("data-layout", "split");
|
||||
});
|
||||
|
||||
it("expands and then collapses the context again", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
await user.click(screen.getByText("server/lib/git-ops.js"));
|
||||
await screen.findByRole("button", { name: "Show more surrounding lines" });
|
||||
|
||||
// Collapse is dead at the tightest step, and live once expanded — an
|
||||
// expanded diff that cannot be collapsed sends the reader back out of the
|
||||
// file to get the tight view.
|
||||
expect(screen.getByRole("button", { name: "Show fewer surrounding lines" })).toBeDisabled();
|
||||
await user.click(screen.getByRole("button", { name: "Show more surrounding lines" }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "work",
|
||||
path: "server/lib/git-ops.js",
|
||||
context: 12,
|
||||
})
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Show fewer surrounding lines" }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "work",
|
||||
path: "server/lib/git-ops.js",
|
||||
context: 3,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for more context when the diff is expanded", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
await user.click(screen.getByText("server/lib/git-ops.js"));
|
||||
await screen.findByText(/server\/lib\/git-ops\.js/);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Show more surrounding lines" }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "work",
|
||||
path: "server/lib/git-ops.js",
|
||||
context: 12,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("commits the index from the overview, never the whole tree", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
|
||||
// The box exists only because a file is staged — with a clean index there
|
||||
// is nothing this button could mean.
|
||||
const box = await screen.findByTestId("lane-changes-commit");
|
||||
await user.type(screen.getByLabelText("Commit message"), "fix: the staged half");
|
||||
await user.click(screen.getByRole("button", { name: "Commit 1 staged" }));
|
||||
|
||||
expect(box).toBeInTheDocument();
|
||||
expect(api.lanes.gitAction).toHaveBeenCalledWith(7, "commit", {
|
||||
message: "fix: the staged half",
|
||||
only: "staged",
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the commit box when nothing is staged", async () => {
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue(
|
||||
changes({
|
||||
files: [
|
||||
{
|
||||
path: "server/lib/git-ops.js",
|
||||
status: " M",
|
||||
untracked: false,
|
||||
unstaged: true,
|
||||
added: 1,
|
||||
deleted: 0,
|
||||
binary: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
expect(screen.queryByTestId("lane-changes-commit")).toBeNull();
|
||||
});
|
||||
|
||||
it("puts Files first in the mode row", async () => {
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
const modes = ["Files", "Overview", "Review", "History"].map((name) =>
|
||||
screen.getByRole("button", { name })
|
||||
);
|
||||
// Source order is the reading order here, so comparing positions pins it.
|
||||
for (let i = 1; i < modes.length; i += 1) {
|
||||
expect(
|
||||
modes[i - 1]!.compareDocumentPosition(modes[i]!) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("folds one file shut in review mode, and all of them at once", async () => {
|
||||
const user = userEvent.setup();
|
||||
const withHunks = changes({
|
||||
files: [
|
||||
{
|
||||
path: "server/lib/git-ops.js",
|
||||
status: " M",
|
||||
untracked: false,
|
||||
added: 1,
|
||||
deleted: 1,
|
||||
binary: false,
|
||||
hunks: [HUNK],
|
||||
},
|
||||
{
|
||||
path: "notes.txt",
|
||||
status: "??",
|
||||
untracked: true,
|
||||
added: 1,
|
||||
deleted: 0,
|
||||
binary: false,
|
||||
hunks: [HUNK],
|
||||
},
|
||||
],
|
||||
});
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
vi.mocked(api.lanes.changes).mockResolvedValue(withHunks);
|
||||
await user.click(screen.getByRole("button", { name: "Review" }));
|
||||
await screen.findByTestId("lane-changes-review");
|
||||
expect(screen.getAllByTestId("lane-diff-view")).toHaveLength(2);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Collapse server/lib/git-ops.js" }));
|
||||
expect(screen.getAllByTestId("lane-diff-view")).toHaveLength(1);
|
||||
// The header stays, so a folded file is still a row you can reopen.
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Expand server/lib/git-ops.js" })
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Collapse every file" }));
|
||||
expect(screen.queryAllByTestId("lane-diff-view")).toHaveLength(0);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Expand every file" }));
|
||||
expect(screen.getAllByTestId("lane-diff-view")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("opens a file only from its path, and folds it from anywhere else", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
await user.click(screen.getByRole("button", { name: "Review" }));
|
||||
await screen.findByTestId("lane-changes-review");
|
||||
|
||||
// Anywhere on the header but the path: fold, do not navigate.
|
||||
await user.click(screen.getByRole("button", { name: "Collapse server/lib/git-ops.js" }));
|
||||
expect(screen.getByTestId("lane-changes-review")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Expand server/lib/git-ops.js" })
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "server/lib/git-ops.js" }));
|
||||
await waitFor(() =>
|
||||
expect(api.lanes.changes).toHaveBeenCalledWith(7, {
|
||||
range: "work",
|
||||
path: "server/lib/git-ops.js",
|
||||
context: 3,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("goes back to the list it came from, at the offset it was left at", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LaneChangesPanel laneId={7} />);
|
||||
await screen.findByTestId("lane-changes-overview");
|
||||
await user.click(screen.getByRole("button", { name: "Review" }));
|
||||
await screen.findByTestId("lane-changes-review");
|
||||
|
||||
// jsdom lays nothing out, so the scrollable box is described by hand —
|
||||
// the restore deliberately waits until the list is tall enough again.
|
||||
const body = screen.getByTestId("lane-changes-body");
|
||||
Object.defineProperty(body, "scrollHeight", { value: 2000, configurable: true });
|
||||
Object.defineProperty(body, "clientHeight", { value: 400, configurable: true });
|
||||
body.scrollTop = 640;
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "server/lib/git-ops.js" }));
|
||||
await screen.findByRole("button", { name: /All files/ });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /All files/ }));
|
||||
// Back to REVIEW (where the file was opened from), not to the overview.
|
||||
expect(await screen.findByTestId("lane-changes-review")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("lane-changes-body").scrollTop).toBe(640);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user