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
86 changes: 80 additions & 6 deletions app/src/components/Actions/Actions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, act, fireEvent } from "@testing-library/react";
import { clone, create } from "@bufbuild/protobuf";
import React from "react";
import { APPKERNEL_RUNNER_NAME, APPKERNEL_RUNNER_LABEL } from "../../lib/runtime/appKernel";

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

const editorMockState = vi.hoisted(() => ({
onChangeHandlers: [] as Array<(value: string) => void>,
}));

vi.mock("./Editor", () => ({
default: ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => {
editorMockState.onChangeHandlers.push(onChange);
return <textarea data-testid="editor-proxy" readOnly value={value} />;
},
}));

// Minimal mocks for contexts Action consumes
vi.mock("../../contexts/OutputContext", () => ({
useOutput: () => ({
Expand Down Expand Up @@ -42,6 +59,12 @@ vi.mock("../../contexts/NotebookContext", () => ({
}),
}));

vi.mock("../../contexts/NotebookStoreContext", () => ({
useNotebookStore: () => ({
store: null,
}),
}));

vi.mock("../../contexts/CurrentDocContext", () => ({
useCurrentDoc: () => ({
getCurrentDoc: () => null,
Expand Down Expand Up @@ -129,6 +152,10 @@ class StubCellData {
}

describe("Action component", () => {
beforeEach(() => {
editorMockState.onChangeHandlers.length = 0;
});

it("updates CellConsole key when runID changes", async () => {
const cell = create(parser_pb.CellSchema,{
refId: "cell-1",
Expand All @@ -140,9 +167,9 @@ describe("Action component", () => {
},
value: "echo hi",
});
const stub = new StubCellData(cell) as unknown as CellData;
const stub = new StubCellData(cell);

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

const first = screen.getByTestId("cell-console") as HTMLElement;
const firstKey = first.dataset.runkey;
Expand All @@ -169,9 +196,9 @@ describe("Action component", () => {
},
value: "echo hi",
});
const stub = new StubCellData(cell) as unknown as CellData;
const stub = new StubCellData(cell);

render(<Action cellData={stub} isFirst={false} />);
render(<Action cellData={stub as unknown as CellData} isFirst={false} />);
expect(screen.getByTestId("cell-console")).toBeTruthy();

await act(async () => {
Expand All @@ -182,6 +209,53 @@ describe("Action component", () => {
expect(screen.queryByTestId("cell-console")).toBeNull();
});

it("preserves latest outputs when edit callback uses stale closure", async () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-stale-closure",
kind: parser_pb.CellKind.CODE,
languageId: "bash",
outputs: [],
metadata: {
[RunmeMetadataKey.LastRunID]: "run-0",
},
value: "echo hi",
});
const stub = new StubCellData(cell);

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

const staleOnChange = editorMockState.onChangeHandlers[0];
expect(staleOnChange).toBeTypeOf("function");

await act(async () => {
const withOutput = clone(parser_pb.CellSchema, stub.snapshot);
withOutput.outputs = [
create(parser_pb.CellOutputSchema, {
items: [
create(parser_pb.CellOutputItemSchema, {
mime: MimeType.VSCodeNotebookStdOut,
type: "Buffer",
}),
],
}),
];
stub.update(withOutput);
await Promise.resolve();
});

await act(async () => {
staleOnChange?.("echo edited");
await Promise.resolve();
});

const latestUpdateCall = stub.update.mock.calls.at(-1);
const updatedCell = latestUpdateCall?.[0] as parser_pb.Cell;
expect(updatedCell.outputs).toHaveLength(1);
expect(updatedCell.outputs[0]?.items[0]?.mime).toBe(
MimeType.VSCodeNotebookStdOut,
);
});

it("shows language selector in markdown edit mode and converts to code language", () => {
const cell = create(parser_pb.CellSchema, {
refId: "cell-md",
Expand Down
16 changes: 12 additions & 4 deletions app/src/components/Actions/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useSyncExternalStore,
} from "react";

import { create } from "@bufbuild/protobuf";
import { clone, create } from "@bufbuild/protobuf";
import { Button, ScrollArea, Tabs, Text } from "@radix-ui/themes";
import { useParams } from "react-router-dom";

Expand Down Expand Up @@ -568,7 +568,11 @@ export function Action({
return;
}

const updatedCell = create(parser_pb.CellSchema, cell);
const latestCell = cellData.snapshot;
if (!latestCell) {
return;
}
const updatedCell = clone(parser_pb.CellSchema, latestCell);
if (nextValue === "markdown") {
setMarkdownEditRequest((request) => request + 1);
updatedCell.kind = parser_pb.CellKind.MARKUP;
Expand All @@ -588,7 +592,7 @@ export function Action({
setPid(null);
setExitCode(null);
},
[cell, selectedLanguage, updateCellLocal],
[cell, cellData, selectedLanguage, updateCellLocal],
);

// Determine if this cell is a markdown cell (either MARKUP kind or CODE with markdown language)
Expand Down Expand Up @@ -739,7 +743,11 @@ export function Action({
fontSize={fontSettings.fontSize}
fontFamily={fontSettings.fontFamily}
onChange={(v) => {
const updated = create(parser_pb.CellSchema, cell);
const latestCell = cellData.snapshot;
if (!latestCell) {
return;
}
const updated = clone(parser_pb.CellSchema, latestCell);
updated.value = v;
updateCellLocal(updated);
}}
Expand Down
Loading