Skip to content

Commit 09ad0de

Browse files
committed
feat: add tests for CallstackTable component
- Introduced unit tests for the CallstackTable component to ensure proper functionality and user interactions. - Implemented mock functions for scrolling behavior and state management, enhancing test coverage for frame index changes. - Added tests for rendering placeholder messages when the callstack is not ready, improving reliability of the component's UI. - This update enhances the overall test suite by validating critical behaviors of the CallstackTable, ensuring a robust user experience.
1 parent d091c98 commit 09ad0de

2 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { ThemeProvider } from "@mui/material/styles";
2+
import { configureStore } from "@reduxjs/toolkit";
3+
import { act, render, screen } from "@testing-library/react";
4+
import React, { useLayoutEffect } from "react";
5+
import { Provider as ReduxProvider } from "react-redux";
6+
import { beforeEach, describe, expect, it, vi } from "vitest";
7+
8+
import { ArgumentType } from "#/entities/argument/model/argumentObject";
9+
import type { CallFrame } from "#/features/callstack/model/callstackSlice";
10+
import {
11+
callstackSlice,
12+
type CallstackState,
13+
} from "#/features/callstack/model/callstackSlice";
14+
import { CallstackTable } from "#/features/callstack/ui/CallstackTable";
15+
import { StateThemeProvider } from "#/shared/ui/providers/StateThemeProvider";
16+
import type { RootState } from "#/store/makeStore";
17+
import { rootReducer } from "#/store/rootReducer";
18+
import { theme } from "#/themes";
19+
20+
const mockScrollToIndex = vi.fn();
21+
22+
vi.mock("react-virtuoso", () => ({
23+
TableVirtuoso: React.forwardRef<
24+
{ scrollToIndex: typeof mockScrollToIndex },
25+
Record<string, unknown>
26+
>(function MockTableVirtuoso(_, ref) {
27+
useLayoutEffect(() => {
28+
if (ref && typeof ref === "object" && "current" in ref) {
29+
const mutableRef = ref as {
30+
current: { scrollToIndex: typeof mockScrollToIndex };
31+
};
32+
mutableRef.current = {
33+
scrollToIndex: mockScrollToIndex,
34+
};
35+
}
36+
}, [ref]);
37+
return <div data-testid="table-virtuoso" />;
38+
}),
39+
}));
40+
41+
vi.mock("#/shared/hooks", async (importOriginal) => {
42+
const { mockUseI18nContext } = await import("#/shared/testUtils");
43+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
44+
const actual = await importOriginal<typeof import("#/shared/hooks")>();
45+
return {
46+
...actual,
47+
useI18nContext: mockUseI18nContext,
48+
};
49+
});
50+
51+
const createMockFrame = (id: string, timestamp: number): CallFrame => ({
52+
id,
53+
timestamp,
54+
name: "setVal",
55+
treeName: "tree",
56+
nodeId: "n1",
57+
structureType: "treeNode",
58+
argType: ArgumentType.BINARY_TREE,
59+
args: { value: 1 },
60+
});
61+
62+
const createStoreWithCallstack = (callstack: Partial<CallstackState>) => {
63+
return configureStore({
64+
reducer: rootReducer,
65+
preloadedState: {
66+
callstack: {
67+
isReady: false,
68+
isPlaying: false,
69+
result: null,
70+
runtime: null,
71+
startTimestamp: null,
72+
error: null,
73+
frames: { ids: [], entities: {} },
74+
frameIndex: -1,
75+
...callstack,
76+
},
77+
} as Partial<RootState>,
78+
});
79+
};
80+
81+
const renderWithProviders = (callstack: Partial<CallstackState>) => {
82+
const store = createStoreWithCallstack(callstack);
83+
return {
84+
store,
85+
...render(
86+
<ReduxProvider store={store}>
87+
<StateThemeProvider>
88+
<ThemeProvider theme={theme}>
89+
<CallstackTable />
90+
</ThemeProvider>
91+
</StateThemeProvider>
92+
</ReduxProvider>,
93+
),
94+
};
95+
};
96+
97+
describe("CallstackTable", () => {
98+
describe("scroll active row into view", () => {
99+
const frames = [
100+
createMockFrame("f1", 100),
101+
createMockFrame("f2", 200),
102+
createMockFrame("f3", 300),
103+
];
104+
105+
beforeEach(() => {
106+
mockScrollToIndex.mockClear();
107+
});
108+
109+
it("calls scrollToIndex when frameIndex changes to valid index", () => {
110+
const { store } = renderWithProviders({
111+
isReady: true,
112+
frames: {
113+
ids: frames.map((f) => f.id),
114+
entities: Object.fromEntries(frames.map((f) => [f.id, f])),
115+
},
116+
frameIndex: 0,
117+
startTimestamp: 100,
118+
});
119+
120+
expect(screen.getByTestId("table-virtuoso")).toBeInTheDocument();
121+
122+
expect(mockScrollToIndex).toHaveBeenCalledWith({
123+
index: 0,
124+
align: "center",
125+
behavior: "smooth",
126+
});
127+
128+
mockScrollToIndex.mockClear();
129+
130+
act(() => {
131+
store.dispatch(callstackSlice.actions.setFrameIndex(2));
132+
});
133+
134+
expect(mockScrollToIndex).toHaveBeenCalledWith({
135+
index: 2,
136+
align: "center",
137+
behavior: "smooth",
138+
});
139+
});
140+
141+
it("does not call scrollToIndex when frameIndex is negative", () => {
142+
renderWithProviders({
143+
isReady: true,
144+
frames: {
145+
ids: frames.map((f) => f.id),
146+
entities: Object.fromEntries(frames.map((f) => [f.id, f])),
147+
},
148+
frameIndex: -1,
149+
startTimestamp: 100,
150+
});
151+
152+
expect(mockScrollToIndex).not.toHaveBeenCalled();
153+
});
154+
155+
it("does not call scrollToIndex when frameIndex is out of bounds", () => {
156+
renderWithProviders({
157+
isReady: true,
158+
frames: {
159+
ids: frames.map((f) => f.id),
160+
entities: Object.fromEntries(frames.map((f) => [f.id, f])),
161+
},
162+
frameIndex: 10,
163+
startTimestamp: 100,
164+
});
165+
166+
expect(mockScrollToIndex).not.toHaveBeenCalled();
167+
});
168+
});
169+
170+
describe("when not ready", () => {
171+
it("renders placeholder message", () => {
172+
renderWithProviders({
173+
isReady: false,
174+
frames: { ids: [], entities: {} },
175+
frameIndex: -1,
176+
});
177+
178+
expect(
179+
screen.getByText(
180+
/Here you will see a table of runtime actions once the code is executed/,
181+
),
182+
).toBeInTheDocument();
183+
});
184+
});
185+
});

src/shared/testUtils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { fallbackProxy } from "#/context/I18nContext";
2+
3+
/**
4+
* Returns a mock implementation of useI18nContext that uses fallbackProxy for LL.
5+
* Use in vi.mock("#/shared/hooks", ...) to avoid wrapping components in I18nProvider.
6+
*
7+
* Must be imported dynamically inside the mock factory (vi.mock is hoisted):
8+
*
9+
* @example
10+
* vi.mock("#/shared/hooks", async (importOriginal) => {
11+
* const { mockUseI18nContext } = await import("#/shared/testUtils");
12+
* const actual = await importOriginal<typeof import("#/shared/hooks")>();
13+
* return {
14+
* ...actual,
15+
* useI18nContext: mockUseI18nContext,
16+
* };
17+
* });
18+
*/
19+
export const mockUseI18nContext = () => ({ LL: fallbackProxy });

0 commit comments

Comments
 (0)