Skip to content

Commit c23fe61

Browse files
committed
frontend:stories: add test for terminal.tsx file
1 parent 08c2fd1 commit c23fe61

20 files changed

+4404
-10
lines changed

frontend/src/components/common/Terminal.stories.tsx

Lines changed: 568 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import 'vitest-canvas-mock';
18+
import { act, render } from '@testing-library/react';
19+
import React from 'react';
20+
import { TestContext } from '../../test';
21+
import Terminal from './Terminal';
22+
23+
const encoder = new TextEncoder();
24+
25+
const Channel = {
26+
StdOut: 1,
27+
} as const;
28+
29+
function buildMessage(channel: number, text: string): ArrayBuffer {
30+
const encoded = encoder.encode(text);
31+
const buffer = new Uint8Array([channel, ...encoded]);
32+
return buffer.buffer;
33+
}
34+
35+
/** Minimal pod-shaped mock for Terminal (no Pod import to avoid k8s chain in isolation). */
36+
function createMockPod(exec: (c: string, onData: (d: ArrayBuffer) => void) => Promise<unknown>) {
37+
return {
38+
metadata: { name: 'mock-pod' },
39+
spec: {
40+
nodeSelector: { 'kubernetes.io/os': 'linux' },
41+
containers: [{ name: 'main' }],
42+
initContainers: [],
43+
ephemeralContainers: [],
44+
},
45+
exec,
46+
attach: exec,
47+
};
48+
}
49+
50+
describe('Terminal', () => {
51+
beforeEach(() => {
52+
vi.useFakeTimers();
53+
});
54+
55+
afterEach(() => {
56+
vi.useRealTimers();
57+
});
58+
59+
it('does not throw when stream emits data after unmount (send/onData after cleanup)', async () => {
60+
const streamReturn = {
61+
cancel: () => {},
62+
getSocket: () => ({ readyState: 1, send: () => {} } as unknown as WebSocket),
63+
};
64+
const pod = createMockPod(async (_container: string, onData: (data: ArrayBuffer) => void) => {
65+
await Promise.resolve();
66+
setTimeout(() => onData(buildMessage(Channel.StdOut, 'late data after unmount')), 5000);
67+
return streamReturn;
68+
});
69+
70+
const { unmount } = render(
71+
<TestContext>
72+
<Terminal item={pod as any} open onClose={() => {}} />
73+
</TestContext>
74+
);
75+
76+
await act(async () => {
77+
vi.advanceTimersByTime(100);
78+
});
79+
await act(() => new Promise(res => process.nextTick(res)));
80+
81+
unmount();
82+
83+
await act(() => {
84+
vi.advanceTimersByTime(5000);
85+
});
86+
87+
expect(true).toBe(true);
88+
});
89+
});

frontend/src/components/common/__snapshots__/Terminal.TerminalAttachEmptyFirstOutput.stories.storyshot

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.

frontend/src/components/common/__snapshots__/Terminal.TerminalConnectedAndReady.stories.storyshot

Lines changed: 330 additions & 0 deletions
Large diffs are not rendered by default.

frontend/src/components/common/__snapshots__/Terminal.TerminalConnectionFailed.stories.storyshot

Lines changed: 335 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<body>
2+
<div
3+
aria-hidden="true"
4+
/>
5+
<div
6+
class="MuiDialog-root MuiModal-root css-zw3mfo-MuiModal-root-MuiDialog-root"
7+
role="presentation"
8+
>
9+
<div
10+
aria-hidden="true"
11+
class="MuiBackdrop-root MuiModal-backdrop css-yiavyu-MuiBackdrop-root-MuiDialog-backdrop"
12+
style="opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;"
13+
/>
14+
<div
15+
data-testid="sentinelStart"
16+
tabindex="0"
17+
/>
18+
<div
19+
class="MuiDialog-container MuiDialog-scrollPaper css-hz1bth-MuiDialog-container"
20+
role="presentation"
21+
style="opacity: 1; webkit-transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;"
22+
tabindex="-1"
23+
>
24+
<div
25+
aria-labelledby=":mock-test-id:"
26+
class="MuiPaper-root MuiPaper-outlined MuiPaper-rounded MuiDialog-paper MuiDialog-paperScrollPaper MuiDialog-paperWidthLg MuiDialog-paperFullWidth css-kl791l-MuiPaper-root-MuiDialog-paper"
27+
role="dialog"
28+
>
29+
<h2
30+
class="MuiTypography-root MuiTypography-h6 MuiDialogTitle-root css-8yphvn-MuiTypography-root-MuiDialogTitle-root"
31+
id=":mock-test-id:"
32+
style="display: flex;"
33+
>
34+
<div
35+
class="MuiGrid-root MuiGrid-container css-9cyib4-MuiGrid-root"
36+
>
37+
<div
38+
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
39+
>
40+
<h1
41+
class="MuiTypography-root MuiTypography-h1 css-1kazmbo-MuiTypography-root"
42+
style="font-size: 1.25rem; font-weight: 500; line-height: 1.6;"
43+
>
44+
Terminal: mock-pod
45+
</h1>
46+
</div>
47+
<div
48+
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
49+
>
50+
<div
51+
class="MuiBox-root css-0"
52+
>
53+
<button
54+
aria-label="Toggle fullscreen"
55+
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
56+
data-mui-internal-clone-element="true"
57+
tabindex="0"
58+
type="button"
59+
>
60+
<span
61+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
62+
/>
63+
</button>
64+
<button
65+
aria-label="Close"
66+
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
67+
data-mui-internal-clone-element="true"
68+
tabindex="0"
69+
type="button"
70+
>
71+
<span
72+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
73+
/>
74+
</button>
75+
</div>
76+
</div>
77+
</div>
78+
</h2>
79+
<div
80+
class="MuiDialogContent-root css-1ydrgiv-MuiDialogContent-root"
81+
>
82+
<div
83+
class="MuiBox-root css-0"
84+
>
85+
<div
86+
class="MuiFormControl-root css-1rb4qd1-MuiFormControl-root"
87+
>
88+
<label
89+
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-standard MuiFormLabel-colorPrimary MuiFormLabel-filled MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-sizeMedium MuiInputLabel-standard css-1xqhkuv-MuiFormLabel-root-MuiInputLabel-root"
90+
data-shrink="true"
91+
id="container-name-chooser-label"
92+
>
93+
Container
94+
</label>
95+
<div
96+
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-colorPrimary MuiInputBase-formControl css-m7fptp-MuiInputBase-root-MuiInput-root-MuiSelect-root"
97+
>
98+
<div
99+
aria-controls=":mock-test-id:"
100+
aria-expanded="false"
101+
aria-haspopup="listbox"
102+
aria-labelledby="container-name-chooser-label container-name-chooser"
103+
class="MuiSelect-select MuiSelect-standard MuiInputBase-input MuiInput-input css-9t3t1q-MuiSelect-select-MuiInputBase-input-MuiInput-input"
104+
id="container-name-chooser"
105+
role="combobox"
106+
tabindex="0"
107+
>
108+
main
109+
</div>
110+
<input
111+
aria-hidden="true"
112+
aria-invalid="false"
113+
class="MuiSelect-nativeInput css-yf8vq0-MuiSelect-nativeInput"
114+
tabindex="-1"
115+
value="main"
116+
/>
117+
<svg
118+
aria-hidden="true"
119+
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiSelect-icon MuiSelect-iconStandard css-pqjvzy-MuiSvgIcon-root-MuiSelect-icon"
120+
data-testid="ArrowDropDownIcon"
121+
focusable="false"
122+
viewBox="0 0 24 24"
123+
>
124+
<path
125+
d="M7 10l5 5 5-5z"
126+
/>
127+
</svg>
128+
</div>
129+
</div>
130+
</div>
131+
<div
132+
class="MuiBox-root css-ffkpbp"
133+
>
134+
<div
135+
id="xterm-container"
136+
style="flex: 1; display: flex; flex-direction: column-reverse;"
137+
/>
138+
</div>
139+
</div>
140+
</div>
141+
</div>
142+
<div
143+
data-testid="sentinelEnd"
144+
tabindex="0"
145+
/>
146+
</div>
147+
</body>

0 commit comments

Comments
 (0)