Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 208 additions & 2 deletions app/src/components/Actions/Actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render, screen, act, fireEvent } from "@testing-library/react";
import { create } from "@bufbuild/protobuf";
import React from "react";

import { parser_pb } from "../../runme/client";
import { parser_pb, RunmeMetadataKey } from "../../runme/client";
import type { CellData } from "../../lib/notebookData";
import { Action } from "./Actions";

Expand Down Expand Up @@ -60,6 +60,47 @@ vi.mock("@runmedev/renderers", () => ({

vi.mock("../../contexts/CellContext", () => ({}));

// Fake streams for testing PID/exitCode callbacks through CellConsole subscriptions.
class FakeStreams {
private pidCbs = new Set<(pid: number) => void>();
private exitCbs = new Set<(code: number) => void>();
private stdoutCbs = new Set<(data: Uint8Array) => void>();
private stderrCbs = new Set<(data: Uint8Array) => void>();

stdout = {
subscribe: (cb: (data: Uint8Array) => void) => {
this.stdoutCbs.add(cb);
return { unsubscribe: () => this.stdoutCbs.delete(cb) };
},
};
stderr = {
subscribe: (cb: (data: Uint8Array) => void) => {
this.stderrCbs.add(cb);
return { unsubscribe: () => this.stderrCbs.delete(cb) };
},
};
pid = {
subscribe: (cb: (pid: number) => void) => {
this.pidCbs.add(cb);
return { unsubscribe: () => this.pidCbs.delete(cb) };
},
};
exitCode = {
subscribe: (cb: (code: number) => void) => {
this.exitCbs.add(cb);
return { unsubscribe: () => this.exitCbs.delete(cb) };
},
};

emitPid(pid: number) { this.pidCbs.forEach((cb) => cb(pid)); }
emitExitCode(code: number) { this.exitCbs.forEach((cb) => cb(code)); }
emitStdout(data: Uint8Array) { this.stdoutCbs.forEach((cb) => cb(data)); }

setCallback() {}
sendExecuteRequest() {}
close() {}
}
Comment on lines +63 to +102
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FakeStreams class is duplicated between CellConsole.test.tsx and Actions.test.tsx with identical implementation. Consider extracting this to a shared test utility file to reduce code duplication and ensure consistency across test files. This would also make it easier to maintain and extend the FakeStreams API in the future.

Copilot uses AI. Check for mistakes.

// Minimal stub CellData to drive runID changes.
class StubCellData {
snapshot: parser_pb.Cell;
Expand Down Expand Up @@ -103,13 +144,16 @@ class StubCellData {
this.runListeners.forEach((l) => l(id));
}

fakeStreams: FakeStreams | null = null;

getStreams() {
return null;
return this.fakeStreams;
}
addBefore() {}
addAfter() {}
remove() {}
run() {}
setRunner() {}
}

describe("Action component", () => {
Expand Down Expand Up @@ -189,4 +233,166 @@ describe("Action component", () => {
expect(updatedCell.kind).toBe(parser_pb.CellKind.MARKUP);
expect(updatedCell.languageId).toBe("markdown");
});

it("shows idle bracket [ ] before first run", () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-idle",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "echo hello",
});
const stub = new StubCellData(cell);

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);

const bracket = screen.getByTestId("cell-bracket");
expect(bracket.textContent).toBe("[ ]");
});

it("shows pending state immediately on run click", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-pend",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "echo hello",
});
const stub = new StubCellData(cell);
stub.fakeStreams = new FakeStreams();

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);

const cellCard = document.getElementById("cell-card-cell-pend")!;
expect(cellCard.getAttribute("data-exec-state")).toBe("idle");

await act(async () => {
fireEvent.click(screen.getByLabelText("Run code"));
});

expect(cellCard.getAttribute("data-exec-state")).toBe("pending");
const bracket = screen.getByTestId("cell-bracket");
expect(bracket.textContent).toBe("[*]");
});

it("transitions to running state on PID", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-run",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "echo hello",
});
const stub = new StubCellData(cell);
const streams = new FakeStreams();
stub.fakeStreams = streams;

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);

// Wait for CellConsole's useEffect to subscribe to streams
await act(async () => { await Promise.resolve(); });

// Click run to enter pending state
await act(async () => {
fireEvent.click(screen.getByLabelText("Run code"));
});

const cellCard = document.getElementById("cell-card-cell-run")!;
expect(cellCard.getAttribute("data-exec-state")).toBe("pending");

// Emit PID to transition to running
await act(async () => {
streams.emitPid(42);
});

expect(cellCard.getAttribute("data-exec-state")).toBe("running");
});

it("transitions to success state on exitCode 0", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-ok",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "echo hello",
});
cell.metadata[RunmeMetadataKey.Sequence] = "1";
const stub = new StubCellData(cell);
const streams = new FakeStreams();
stub.fakeStreams = streams;

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);
await act(async () => { await Promise.resolve(); });

// Run → PID → exit 0
await act(async () => { fireEvent.click(screen.getByLabelText("Run code")); });
await act(async () => { streams.emitPid(42); });
await act(async () => { streams.emitExitCode(0); });

const cellCard = document.getElementById("cell-card-cell-ok")!;
expect(cellCard.getAttribute("data-exec-state")).toBe("success");

const bracket = screen.getByTestId("cell-bracket");
expect(bracket.textContent).toBe("[1]");
});

it("transitions to error state on non-zero exitCode", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-err",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "exit 1",
});
const stub = new StubCellData(cell);
const streams = new FakeStreams();
stub.fakeStreams = streams;

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);
await act(async () => { await Promise.resolve(); });

await act(async () => { fireEvent.click(screen.getByLabelText("Run code")); });
await act(async () => { streams.emitPid(42); });
await act(async () => { streams.emitExitCode(1); });

const cellCard = document.getElementById("cell-card-cell-err")!;
expect(cellCard.getAttribute("data-exec-state")).toBe("error");

const bracket = screen.getByTestId("cell-bracket");
expect(bracket.textContent).toBe("[!]");
});

it("resets to pending on re-run after success", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-rerun",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {},
value: "echo hello",
});
const stub = new StubCellData(cell);
const streams = new FakeStreams();
stub.fakeStreams = streams;

render(<Action cellData={stub as unknown as CellData} isFirst={false} />);
await act(async () => { await Promise.resolve(); });

// First run → success
await act(async () => { fireEvent.click(screen.getByLabelText("Run code")); });
await act(async () => { streams.emitPid(42); });
await act(async () => { streams.emitExitCode(0); });

const cellCard = document.getElementById("cell-card-cell-rerun")!;
expect(cellCard.getAttribute("data-exec-state")).toBe("success");

// Re-run → should go back to pending
await act(async () => { fireEvent.click(screen.getByLabelText("Run code")); });
expect(cellCard.getAttribute("data-exec-state")).toBe("pending");
});
});
Loading