diff --git a/frontend/src/components/common/Resource/PortForward.stories.tsx b/frontend/src/components/common/Resource/PortForward.stories.tsx
new file mode 100644
index 00000000000..ac3f6872c51
--- /dev/null
+++ b/frontend/src/components/common/Resource/PortForward.stories.tsx
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Meta, StoryFn } from '@storybook/react';
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { http, HttpResponse } from 'msw';
+import Pod from '../../../lib/k8s/pod';
+import { TestContext } from '../../../test';
+import PortForward from './PortForward';
+
+/**
+ * Decorator that wraps each story in TestContext.
+ * Electron env simulation is handled by the beforeEach hook in Meta.
+ */
+function withElectronEnv(Story: StoryFn) {
+ return (
+
+
+
+ );
+}
+
+const podJson = {
+ apiVersion: 'v1',
+ kind: 'Pod',
+ metadata: {
+ name: 'my-nginx-pod',
+ namespace: 'default',
+ uid: 'abc-123',
+ creationTimestamp: '2025-01-01T00:00:00Z',
+ },
+ spec: {
+ nodeName: 'worker-node-1',
+ containers: [
+ {
+ name: 'nginx',
+ image: 'nginx:latest',
+ imagePullPolicy: 'Always',
+ ports: [{ containerPort: 80, protocol: 'TCP' }],
+ },
+ ],
+ },
+ status: {
+ phase: 'Running',
+ conditions: [],
+ containerStatuses: [],
+ startTime: null,
+ },
+};
+
+const mockPod = new Pod(podJson);
+mockPod.cluster = 'minikube';
+
+const mockPortForwardList = http.get('*/clusters/*/portforward/list', () => HttpResponse.json([]));
+
+const mockPodsEndpoint = http.get('*/api/v1/namespaces/default/pods', () =>
+ HttpResponse.json({
+ kind: 'PodList',
+ apiVersion: 'v1',
+ metadata: {},
+ items: [mockPod.jsonData],
+ })
+);
+
+export default {
+ title: 'Resource/PortForward',
+ component: PortForward,
+ decorators: [withElectronEnv],
+ beforeEach: () => {
+ // Simulate Electron environment so isElectron() returns true
+ const hadProcess = 'process' in window;
+ const originalProcess = (window as any).process;
+ (window as any).process = { ...originalProcess, type: 'renderer' };
+
+ // Clear portforwards from localStorage so prior stories don't affect this one
+ localStorage.removeItem('portforwards');
+
+ // Cleanup: restore window.process after the story is done
+ return () => {
+ if (hadProcess) {
+ (window as any).process = originalProcess;
+ } else {
+ delete (window as any).process;
+ }
+ };
+ },
+} as Meta;
+
+// Story 1: Port Forward Form (initial state)
+const PortForwardFormTemplate: StoryFn = () => (
+
+);
+
+export const PortForwardForm = PortForwardFormTemplate.bind({});
+PortForwardForm.parameters = {
+ msw: {
+ handlers: {
+ story: [mockPortForwardList, mockPodsEndpoint],
+ },
+ },
+};
+
+// Story 2: Connection Pending Loading State
+const LoadingTemplate: StoryFn = () => ;
+
+export const ConnectionPendingLoading = LoadingTemplate.bind({});
+ConnectionPendingLoading.play = async ({ canvasElement }: any) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+ const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
+ await user.click(forwardBtn);
+
+ const startBtn = await screen.findByRole('button', { name: /^Start$/i });
+ await user.click(startBtn);
+};
+ConnectionPendingLoading.parameters = {
+ storyshots: {
+ disable: true,
+ },
+ msw: {
+ handlers: {
+ story: [
+ mockPodsEndpoint,
+ mockPortForwardList,
+ // Make startPortForward hang to show loading state
+ http.post('*/clusters/*/portforward', () => {
+ return new Promise(() => {
+ // Never resolves keeps the loading spinner visible
+ });
+ }),
+ ],
+ },
+ },
+};
+
+// Story 3: Connection Established Success (Running state)
+const ConnectionEstablishedTemplate: StoryFn = () => (
+
+);
+
+export const ConnectionEstablishedSuccess = ConnectionEstablishedTemplate.bind({});
+ConnectionEstablishedSuccess.parameters = {
+ msw: {
+ handlers: {
+ story: [
+ mockPodsEndpoint,
+ http.get('*/clusters/*/portforward/list', () =>
+ HttpResponse.json([
+ {
+ id: 'pf-123',
+ pod: 'my-nginx-pod',
+ service: '',
+ serviceNamespace: '',
+ namespace: 'default',
+ cluster: 'minikube',
+ port: '8080',
+ targetPort: '80',
+ status: 'Running',
+ },
+ ])
+ ),
+ ],
+ },
+ },
+};
+
+// Story 4: Connection Failed Error State
+const ConnectionFailedTemplate: StoryFn = () => (
+
+);
+
+export const ConnectionFailedError = ConnectionFailedTemplate.bind({});
+ConnectionFailedError.play = async ({ canvasElement }: any) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+ const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
+ await user.click(forwardBtn);
+
+ const startBtn = await screen.findByRole('button', { name: /^Start$/i });
+ await user.click(startBtn);
+};
+ConnectionFailedError.parameters = {
+ storyshots: {
+ disable: true,
+ },
+ msw: {
+ handlers: {
+ story: [
+ mockPodsEndpoint,
+ mockPortForwardList,
+ http.post('*/clusters/*/portforward', () =>
+ HttpResponse.json(
+ { message: 'Connection refused: unable to connect to pod my-nginx-pod on port 80' },
+ { status: 500 }
+ )
+ ),
+ ],
+ },
+ },
+};
+
+// Story 5: Port Already In Use Error
+const PortInUseTemplate: StoryFn = () => ;
+
+export const PortAlreadyInUseError = PortInUseTemplate.bind({});
+PortAlreadyInUseError.play = async ({ canvasElement }: any) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+ const forwardBtn = await canvas.findByRole('button', { name: /Start port forward/i });
+ await user.click(forwardBtn);
+
+ const startBtn = await screen.findByRole('button', { name: /^Start$/i });
+ await user.click(startBtn);
+};
+PortAlreadyInUseError.parameters = {
+ storyshots: {
+ disable: true,
+ },
+ msw: {
+ handlers: {
+ story: [
+ mockPodsEndpoint,
+ mockPortForwardList,
+ http.post('*/clusters/*/portforward', () =>
+ HttpResponse.json(
+ { message: 'Port 8080 is already in use. Please choose a different local port.' },
+ { status: 500 }
+ )
+ ),
+ ],
+ },
+ },
+};
diff --git a/frontend/src/components/common/Resource/__snapshots__/PortForward.ConnectionEstablishedSuccess.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/PortForward.ConnectionEstablishedSuccess.stories.storyshot
new file mode 100644
index 00000000000..527a8e4cea9
--- /dev/null
+++ b/frontend/src/components/common/Resource/__snapshots__/PortForward.ConnectionEstablishedSuccess.stories.storyshot
@@ -0,0 +1,26 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/common/Resource/__snapshots__/PortForward.PortForwardForm.stories.storyshot b/frontend/src/components/common/Resource/__snapshots__/PortForward.PortForwardForm.stories.storyshot
new file mode 100644
index 00000000000..34c9c9a305a
--- /dev/null
+++ b/frontend/src/components/common/Resource/__snapshots__/PortForward.PortForwardForm.stories.storyshot
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Forward port
+
+
+
+
+
+
\ No newline at end of file