Skip to content

Commit 2936548

Browse files
committed
feat: improve PortForward stories and mock data
- Refactor withElectronEnv decorator to remove duplicate logic already handled in beforeEach. - Update MSW mock endpoint to use wildcard pattern '*/api/v1/...' to support both clustered and non-clustered environments.
1 parent 3281ac6 commit 2936548

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 { Meta, StoryFn } from '@storybook/react';
18+
import { screen, within } from '@testing-library/react';
19+
import userEvent from '@testing-library/user-event';
20+
import { http, HttpResponse } from 'msw';
21+
import Pod from '../../../lib/k8s/pod';
22+
import { TestContext } from '../../../test';
23+
import PortForward from './PortForward';
24+
25+
/**
26+
* Decorator that wraps each story in TestContext.
27+
* Electron env simulation is handled by the beforeEach hook in Meta.
28+
*/
29+
function withElectronEnv(Story: StoryFn) {
30+
return (
31+
<TestContext>
32+
<Story />
33+
</TestContext>
34+
);
35+
}
36+
37+
const podJson = {
38+
apiVersion: 'v1',
39+
kind: 'Pod',
40+
metadata: {
41+
name: 'my-nginx-pod',
42+
namespace: 'default',
43+
uid: 'abc-123',
44+
creationTimestamp: '2025-01-01T00:00:00Z',
45+
},
46+
spec: {
47+
nodeName: 'worker-node-1',
48+
containers: [
49+
{
50+
name: 'nginx',
51+
image: 'nginx:latest',
52+
imagePullPolicy: 'Always',
53+
ports: [{ containerPort: 80, protocol: 'TCP' }],
54+
},
55+
],
56+
},
57+
status: {
58+
phase: 'Running',
59+
conditions: [],
60+
containerStatuses: [],
61+
startTime: null,
62+
},
63+
};
64+
65+
const mockPod = new Pod(podJson);
66+
mockPod.cluster = 'minikube';
67+
68+
const mockPortForwardList = http.get('*/clusters/*/portforward/list', () => HttpResponse.json([]));
69+
70+
const mockPodsEndpoint = http.get('*/api/v1/namespaces/default/pods', () =>
71+
HttpResponse.json({
72+
kind: 'PodList',
73+
apiVersion: 'v1',
74+
metadata: {},
75+
items: [mockPod.jsonData],
76+
})
77+
);
78+
79+
export default {
80+
title: 'Resource/PortForward',
81+
component: PortForward,
82+
decorators: [withElectronEnv],
83+
beforeEach: () => {
84+
// Simulate Electron environment so isElectron() returns true
85+
const hadProcess = 'process' in window;
86+
const originalProcess = (window as any).process;
87+
(window as any).process = { ...originalProcess, type: 'renderer' };
88+
89+
// Clear portforwards from localStorage so prior stories don't affect this one
90+
localStorage.removeItem('portforwards');
91+
92+
// Cleanup: restore window.process after the story is done
93+
return () => {
94+
if (hadProcess) {
95+
(window as any).process = originalProcess;
96+
} else {
97+
delete (window as any).process;
98+
}
99+
};
100+
},
101+
} as Meta;
102+
103+
// Story 1: Port Forward Form (initial state)
104+
const PortForwardFormTemplate: StoryFn = () => (
105+
<PortForward containerPort={80} resource={mockPod} />
106+
);
107+
108+
export const PortForwardForm = PortForwardFormTemplate.bind({});
109+
PortForwardForm.parameters = {
110+
msw: {
111+
handlers: {
112+
story: [mockPortForwardList, mockPodsEndpoint],
113+
},
114+
},
115+
};
116+
117+
// Story 2: Connection Pending Loading State
118+
const LoadingTemplate: StoryFn = () => <PortForward containerPort={80} resource={mockPod} />;
119+
120+
export const ConnectionPendingLoading = LoadingTemplate.bind({});
121+
ConnectionPendingLoading.play = async ({ canvasElement }: any) => {
122+
const canvas = within(canvasElement);
123+
const user = userEvent.setup();
124+
const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
125+
await user.click(forwardBtn);
126+
127+
const startBtn = await screen.findByRole('button', { name: /^Start$/i });
128+
await user.click(startBtn);
129+
};
130+
ConnectionPendingLoading.parameters = {
131+
storyshots: {
132+
disable: true,
133+
},
134+
msw: {
135+
handlers: {
136+
story: [
137+
mockPodsEndpoint,
138+
mockPortForwardList,
139+
// Make startPortForward hang to show loading state
140+
http.post('*/clusters/*/portforward', () => {
141+
return new Promise(() => {
142+
// Never resolves keeps the loading spinner visible
143+
});
144+
}),
145+
],
146+
},
147+
},
148+
};
149+
150+
// Story 3: Connection Established Success (Running state)
151+
const ConnectionEstablishedTemplate: StoryFn = () => (
152+
<PortForward containerPort={80} resource={mockPod} />
153+
);
154+
155+
export const ConnectionEstablishedSuccess = ConnectionEstablishedTemplate.bind({});
156+
ConnectionEstablishedSuccess.parameters = {
157+
msw: {
158+
handlers: {
159+
story: [
160+
mockPodsEndpoint,
161+
http.get('*/clusters/*/portforward/list', () =>
162+
HttpResponse.json([
163+
{
164+
id: 'pf-123',
165+
pod: 'my-nginx-pod',
166+
service: '',
167+
serviceNamespace: '',
168+
namespace: 'default',
169+
cluster: 'minikube',
170+
port: '8080',
171+
targetPort: '80',
172+
status: 'Running',
173+
},
174+
])
175+
),
176+
],
177+
},
178+
},
179+
};
180+
181+
// Story 4: Connection Failed Error State
182+
const ConnectionFailedTemplate: StoryFn = () => (
183+
<PortForward containerPort={80} resource={mockPod} />
184+
);
185+
186+
export const ConnectionFailedError = ConnectionFailedTemplate.bind({});
187+
ConnectionFailedError.play = async ({ canvasElement }: any) => {
188+
const canvas = within(canvasElement);
189+
const user = userEvent.setup();
190+
const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
191+
await user.click(forwardBtn);
192+
193+
const startBtn = await screen.findByRole('button', { name: /^Start$/i });
194+
await user.click(startBtn);
195+
};
196+
ConnectionFailedError.parameters = {
197+
storyshots: {
198+
disable: true,
199+
},
200+
msw: {
201+
handlers: {
202+
story: [
203+
mockPodsEndpoint,
204+
mockPortForwardList,
205+
http.post('*/clusters/*/portforward', () =>
206+
HttpResponse.json(
207+
{ message: 'Connection refused: unable to connect to pod my-nginx-pod on port 80' },
208+
{ status: 500 }
209+
)
210+
),
211+
],
212+
},
213+
},
214+
};
215+
216+
// Story 5: Port Already In Use Error
217+
const PortInUseTemplate: StoryFn = () => <PortForward containerPort={80} resource={mockPod} />;
218+
219+
export const PortAlreadyInUseError = PortInUseTemplate.bind({});
220+
PortAlreadyInUseError.play = async ({ canvasElement }: any) => {
221+
const canvas = within(canvasElement);
222+
const user = userEvent.setup();
223+
const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
224+
await user.click(forwardBtn);
225+
226+
const startBtn = await screen.findByRole('button', { name: /^Start$/i });
227+
await user.click(startBtn);
228+
};
229+
PortAlreadyInUseError.parameters = {
230+
storyshots: {
231+
disable: true,
232+
},
233+
msw: {
234+
handlers: {
235+
story: [
236+
mockPodsEndpoint,
237+
mockPortForwardList,
238+
http.post('*/clusters/*/portforward', () =>
239+
HttpResponse.json(
240+
{ message: 'Port 8080 is already in use. Please choose a different local port.' },
241+
{ status: 500 }
242+
)
243+
),
244+
],
245+
},
246+
},
247+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<body>
2+
<div>
3+
<div
4+
class="MuiBox-root css-0"
5+
>
6+
<a
7+
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineHover css-2ugbm1-MuiTypography-root-MuiLink-root"
8+
href="http://127.0.0.1:8080"
9+
target="_blank"
10+
>
11+
http://127.0.0.1:8080
12+
</a>
13+
<button
14+
aria-label="Stop port forward"
15+
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-colorPrimary MuiIconButton-sizeSmall css-18expok-MuiButtonBase-root-MuiIconButton-root"
16+
data-mui-internal-clone-element="true"
17+
tabindex="0"
18+
type="button"
19+
>
20+
<span
21+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
22+
/>
23+
</button>
24+
</div>
25+
</div>
26+
</body>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<body>
2+
<div>
3+
<div
4+
class="MuiBox-root css-0"
5+
>
6+
<button
7+
aria-label="Start port forward"
8+
class="MuiButtonBase-root MuiButton-root MuiButton-outlined MuiButton-outlinedPrimary MuiButton-sizeMedium MuiButton-outlinedSizeMedium MuiButton-colorPrimary MuiButton-disableElevation MuiButton-root MuiButton-outlined MuiButton-outlinedPrimary MuiButton-sizeMedium MuiButton-outlinedSizeMedium MuiButton-colorPrimary MuiButton-disableElevation css-1ftyvt9-MuiButtonBase-root-MuiButton-root"
9+
style="text-transform: none;"
10+
tabindex="0"
11+
type="button"
12+
>
13+
<p
14+
class="MuiTypography-root MuiTypography-body1 css-1ezega9-MuiTypography-root"
15+
>
16+
Forward port
17+
</p>
18+
<span
19+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
20+
/>
21+
</button>
22+
</div>
23+
</div>
24+
</body>

0 commit comments

Comments
 (0)