Skip to content

Commit 5863d5a

Browse files
Refactor: Extract Console modal into a new component
This change moves the console modal logic into a dedicated component, improving code organization and reusability. Co-authored-by: anders.hafreager <anders.hafreager@cognite.com>
1 parent 7f4c50c commit 5863d5a

3 files changed

Lines changed: 413 additions & 101 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { render, screen, waitFor } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
4+
import ConsoleModal from "./ConsoleModal";
5+
import type { StoreModel } from "../store/model";
6+
import { useStoreState, useStoreActions } from "../hooks";
7+
8+
// Mock the Console component
9+
vi.mock("../containers/Console", () => ({
10+
default: vi.fn(() => <div data-testid="console">Console Content</div>),
11+
}));
12+
13+
// Mock the hooks
14+
const mockSetShowConsole = vi.fn();
15+
const mockSetPreferredView = vi.fn();
16+
17+
vi.mock("../hooks", () => ({
18+
useStoreState: vi.fn(),
19+
useStoreActions: vi.fn(),
20+
}));
21+
22+
describe("ConsoleModal", () => {
23+
let mockClipboard: { writeText: ReturnType<typeof vi.fn> };
24+
let createElementSpy: ReturnType<typeof vi.spyOn>;
25+
let appendChildSpy: ReturnType<typeof vi.spyOn>;
26+
let removeChildSpy: ReturnType<typeof vi.spyOn>;
27+
let clickSpy: ReturnType<typeof vi.fn>;
28+
let createObjectURLSpy: ReturnType<typeof vi.spyOn>;
29+
let revokeObjectURLSpy: ReturnType<typeof vi.spyOn>;
30+
let execCommandSpy: ReturnType<typeof vi.spyOn>;
31+
let showConsoleValue: boolean;
32+
let lammpsOutputValue: string[];
33+
34+
beforeEach(() => {
35+
// Mock clipboard API
36+
mockClipboard = {
37+
writeText: vi.fn().mockResolvedValue(undefined),
38+
};
39+
Object.defineProperty(navigator, "clipboard", {
40+
value: mockClipboard,
41+
writable: true,
42+
configurable: true,
43+
});
44+
45+
// Mock document methods for download
46+
createElementSpy = vi.spyOn(document, "createElement");
47+
appendChildSpy = vi.spyOn(document.body, "appendChild");
48+
removeChildSpy = vi.spyOn(document.body, "removeChild");
49+
clickSpy = vi.fn();
50+
execCommandSpy = vi.spyOn(document, "execCommand").mockReturnValue(true);
51+
52+
// Mock URL methods
53+
createObjectURLSpy = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:url");
54+
revokeObjectURLSpy = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {});
55+
56+
// Mock link element
57+
const mockLink = {
58+
href: "",
59+
download: "",
60+
click: clickSpy,
61+
} as Partial<HTMLAnchorElement> as HTMLAnchorElement;
62+
createElementSpy.mockReturnValue(mockLink);
63+
64+
// Setup store mocks - useStoreState is called multiple times with different selectors
65+
vi.mocked(useStoreState).mockImplementation((selector: (state: StoreModel) => unknown) => {
66+
// Check which property is being accessed by calling the selector with a mock state
67+
const mockState = {
68+
simulation: {
69+
showConsole: showConsoleValue,
70+
lammpsOutput: lammpsOutputValue,
71+
},
72+
} as StoreModel;
73+
return selector(mockState);
74+
});
75+
76+
vi.mocked(useStoreActions).mockImplementation(() => ({
77+
simulation: {
78+
setShowConsole: mockSetShowConsole,
79+
},
80+
app: {
81+
setPreferredView: mockSetPreferredView,
82+
},
83+
}));
84+
});
85+
86+
afterEach(() => {
87+
vi.clearAllMocks();
88+
vi.restoreAllMocks();
89+
});
90+
91+
describe("rendering", () => {
92+
it("should not render when showConsole is false", () => {
93+
// Arrange
94+
showConsoleValue = false;
95+
lammpsOutputValue = [];
96+
97+
// Act
98+
const { container } = render(<ConsoleModal />);
99+
100+
// Assert
101+
expect(container.firstChild).toBeNull();
102+
});
103+
104+
it("should render modal when showConsole is true", () => {
105+
// Arrange
106+
showConsoleValue = true;
107+
lammpsOutputValue = ["log line 1", "log line 2"];
108+
109+
// Act
110+
render(<ConsoleModal />);
111+
112+
// Assert
113+
expect(screen.getByTestId("console")).toBeInTheDocument();
114+
expect(screen.getByText("Download logs")).toBeInTheDocument();
115+
expect(screen.getByText("Copy logs")).toBeInTheDocument();
116+
expect(screen.getByText("Analyze in notebook")).toBeInTheDocument();
117+
expect(screen.getByText("Close")).toBeInTheDocument();
118+
});
119+
});
120+
121+
describe("download logs", () => {
122+
it("should download logs as text file when Download logs button is clicked", async () => {
123+
// Arrange
124+
showConsoleValue = true;
125+
lammpsOutputValue = ["line 1", "line 2", "line 3"];
126+
127+
render(<ConsoleModal />);
128+
const downloadButton = screen.getByText("Download logs");
129+
130+
// Act
131+
await userEvent.click(downloadButton);
132+
133+
// Assert
134+
expect(createElementSpy).toHaveBeenCalledWith("a");
135+
expect(appendChildSpy).toHaveBeenCalled();
136+
expect(clickSpy).toHaveBeenCalled();
137+
expect(removeChildSpy).toHaveBeenCalled();
138+
expect(revokeObjectURLSpy).toHaveBeenCalledWith("blob:url");
139+
140+
// Verify blob creation
141+
const blobCalls = createObjectURLSpy.mock.calls;
142+
expect(blobCalls.length).toBeGreaterThan(0);
143+
const blob = blobCalls[0][0] as Blob;
144+
expect(blob).toBeInstanceOf(Blob);
145+
expect(blob.type).toBe("text/plain");
146+
});
147+
148+
it("should generate filename with current date", async () => {
149+
// Arrange
150+
showConsoleValue = true;
151+
lammpsOutputValue = ["test log"];
152+
const mockDate = new Date("2024-01-15T12:00:00Z");
153+
vi.spyOn(global, "Date").mockImplementation(() => mockDate as unknown as Date);
154+
vi.spyOn(mockDate, "toISOString").mockReturnValue("2024-01-15T12:00:00.000Z");
155+
156+
render(<ConsoleModal />);
157+
const downloadButton = screen.getByText("Download logs");
158+
159+
// Act
160+
await userEvent.click(downloadButton);
161+
162+
// Assert
163+
const linkElement = createElementSpy.mock.results[0].value as HTMLAnchorElement;
164+
expect(linkElement.download).toBe("lammps-logs-2024-01-15.txt");
165+
});
166+
});
167+
168+
describe("copy logs", () => {
169+
it("should copy logs to clipboard when Copy logs button is clicked", async () => {
170+
// Arrange
171+
showConsoleValue = true;
172+
lammpsOutputValue = ["line 1", "line 2"];
173+
174+
render(<ConsoleModal />);
175+
const copyButton = screen.getByText("Copy logs");
176+
177+
// Act
178+
await userEvent.click(copyButton);
179+
180+
// Assert
181+
await waitFor(() => {
182+
expect(mockClipboard.writeText).toHaveBeenCalledWith("line 1\nline 2");
183+
});
184+
});
185+
186+
it("should use fallback method when clipboard API fails", async () => {
187+
// Arrange
188+
showConsoleValue = true;
189+
lammpsOutputValue = ["fallback test"];
190+
mockClipboard.writeText.mockRejectedValue(new Error("Clipboard API not available"));
191+
192+
// Mock textarea element for fallback
193+
const mockTextArea = {
194+
value: "",
195+
style: {} as CSSStyleDeclaration,
196+
select: vi.fn(),
197+
} as Partial<HTMLTextAreaElement> as HTMLTextAreaElement;
198+
createElementSpy.mockReturnValueOnce(mockTextArea);
199+
200+
render(<ConsoleModal />);
201+
const copyButton = screen.getByText("Copy logs");
202+
203+
// Act
204+
await userEvent.click(copyButton);
205+
206+
// Assert
207+
await waitFor(() => {
208+
expect(createElementSpy).toHaveBeenCalledWith("textarea");
209+
expect(mockTextArea.value).toBe("fallback test");
210+
expect(appendChildSpy).toHaveBeenCalled();
211+
expect(mockTextArea.select).toHaveBeenCalled();
212+
expect(execCommandSpy).toHaveBeenCalledWith("copy");
213+
expect(removeChildSpy).toHaveBeenCalled();
214+
});
215+
});
216+
});
217+
218+
describe("analyze in notebook", () => {
219+
it("should close modal and set preferred view to notebook when Analyze in notebook button is clicked", async () => {
220+
// Arrange
221+
showConsoleValue = true;
222+
lammpsOutputValue = [];
223+
224+
render(<ConsoleModal />);
225+
const analyzeButton = screen.getByText("Analyze in notebook");
226+
227+
// Act
228+
await userEvent.click(analyzeButton);
229+
230+
// Assert
231+
expect(mockSetShowConsole).toHaveBeenCalledWith(false);
232+
expect(mockSetPreferredView).toHaveBeenCalledTimes(2);
233+
expect(mockSetPreferredView).toHaveBeenNthCalledWith(1, undefined);
234+
expect(mockSetPreferredView).toHaveBeenNthCalledWith(2, "notebook");
235+
});
236+
});
237+
238+
describe("close", () => {
239+
it("should close modal when Close button is clicked", async () => {
240+
// Arrange
241+
showConsoleValue = true;
242+
lammpsOutputValue = [];
243+
244+
render(<ConsoleModal />);
245+
const closeButton = screen.getByText("Close");
246+
247+
// Act
248+
await userEvent.click(closeButton);
249+
250+
// Assert
251+
expect(mockSetShowConsole).toHaveBeenCalledWith(false);
252+
});
253+
254+
it("should close modal when onCancel is triggered", async () => {
255+
// Arrange
256+
showConsoleValue = true;
257+
lammpsOutputValue = [];
258+
259+
render(<ConsoleModal />);
260+
const modal = screen.getByRole("dialog");
261+
262+
// Act - simulate ESC key or backdrop click
263+
await userEvent.keyboard("{Escape}");
264+
265+
// Assert
266+
expect(mockSetShowConsole).toHaveBeenCalledWith(false);
267+
});
268+
});
269+
270+
describe("console key update", () => {
271+
it("should update console key when showConsole changes to true", async () => {
272+
// Arrange
273+
showConsoleValue = false;
274+
lammpsOutputValue = [];
275+
276+
const { rerender } = render(<ConsoleModal />);
277+
expect(screen.queryByTestId("console")).not.toBeInTheDocument();
278+
279+
// Act - change showConsole to true
280+
showConsoleValue = true;
281+
rerender(<ConsoleModal />);
282+
283+
// Assert
284+
await waitFor(() => {
285+
expect(screen.getByTestId("console")).toBeInTheDocument();
286+
});
287+
});
288+
});
289+
});

0 commit comments

Comments
 (0)