Skip to content

Commit 31a8455

Browse files
authored
🤖 ci: add Storybook coverage for RightSidebar tabs (#1127)
_Generated with `mux`_ Follow-up to #1125 - adds Storybook coverage for the RightSidebar tab styling changes. ## Changes - **ORPC mock enhancements**: - Add `MockSessionUsage` type matching `SessionUsageFileSchema` - Add `sessionUsage` option to mock client factory - Wire up `getSessionUsage` to return mock data per workspace - **Story helper updates**: - Extend `setupSimpleChatStory` to accept `sessionUsage` option - **New stories** (`App/RightSidebar`): - `CostsTabWithCost`: Shows tab with \bash.56 session cost - `CostsTabSmallCost`: Shows \$<0.01 for tiny costs - `CostsTabLargeCost`: Shows \2.34 for larger sessions - `CostsTabNoCost`: Empty state (no cost in tab label) - `ReviewTabSelected`: Review panel visible - `SwitchToReviewTab`: Interaction test for tab switching
1 parent 4d7dff4 commit 31a8455

File tree

4 files changed

+184
-5
lines changed

4 files changed

+184
-5
lines changed

.storybook/mocks/orpc.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,34 @@ import type { ChatStats } from "@/common/types/chatStats";
1111
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";
1212
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";
1313

14+
/** Session usage data structure matching SessionUsageFileSchema */
15+
export interface MockSessionUsage {
16+
byModel: Record<
17+
string,
18+
{
19+
input: { tokens: number; cost_usd?: number };
20+
cached: { tokens: number; cost_usd?: number };
21+
cacheCreate: { tokens: number; cost_usd?: number };
22+
output: { tokens: number; cost_usd?: number };
23+
reasoning: { tokens: number; cost_usd?: number };
24+
model?: string;
25+
}
26+
>;
27+
lastRequest?: {
28+
model: string;
29+
usage: {
30+
input: { tokens: number; cost_usd?: number };
31+
cached: { tokens: number; cost_usd?: number };
32+
cacheCreate: { tokens: number; cost_usd?: number };
33+
output: { tokens: number; cost_usd?: number };
34+
reasoning: { tokens: number; cost_usd?: number };
35+
model?: string;
36+
};
37+
timestamp: number;
38+
};
39+
version: 1;
40+
}
41+
1442
export interface MockORPCClientOptions {
1543
projects?: Map<string, ProjectConfig>;
1644
workspaces?: FrontendWorkspaceMetadata[];
@@ -40,6 +68,8 @@ export interface MockORPCClientOptions {
4068
exitCode?: number;
4169
}>
4270
>;
71+
/** Session usage data per workspace (for Costs tab) */
72+
sessionUsage?: Map<string, MockSessionUsage>;
4373
}
4474

4575
/**
@@ -69,6 +99,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
6999
providersList = [],
70100
onProjectRemove,
71101
backgroundProcesses = new Map(),
102+
sessionUsage = new Map(),
72103
} = options;
73104

74105
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));
@@ -207,7 +238,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
207238
terminate: async () => ({ success: true, data: undefined }),
208239
sendToBackground: async () => ({ success: true, data: undefined }),
209240
},
210-
getSessionUsage: async () => undefined,
241+
getSessionUsage: async (input: { workspaceId: string }) => sessionUsage.get(input.workspaceId),
211242
},
212243
window: {
213244
setTitle: async () => undefined,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* RightSidebar tab stories - testing dynamic tab data display
3+
*
4+
* Uses wide viewport (1600px) to ensure RightSidebar tabs are visible.
5+
*/
6+
7+
import { appMeta, AppWithMocks, type AppStory } from "./meta.js";
8+
import { setupSimpleChatStory } from "./storyHelpers";
9+
import { createUserMessage, createAssistantMessage } from "./mockFactory";
10+
import { within, userEvent, waitFor } from "@storybook/test";
11+
import { RIGHT_SIDEBAR_TAB_KEY } from "@/common/constants/storage";
12+
import type { ComponentType } from "react";
13+
import type { MockSessionUsage } from "../../../.storybook/mocks/orpc";
14+
15+
export default {
16+
...appMeta,
17+
title: "App/RightSidebar",
18+
decorators: [
19+
(Story: ComponentType) => (
20+
<div style={{ width: 1600, height: "100dvh" }}>
21+
<Story />
22+
</div>
23+
),
24+
],
25+
parameters: {
26+
...appMeta.parameters,
27+
chromatic: {
28+
modes: {
29+
dark: { theme: "dark", viewport: 1600 },
30+
light: { theme: "light", viewport: 1600 },
31+
},
32+
},
33+
},
34+
};
35+
36+
/**
37+
* Helper to create session usage data with costs
38+
*/
39+
function createSessionUsage(cost: number): MockSessionUsage {
40+
const inputCost = cost * 0.6;
41+
const outputCost = cost * 0.2;
42+
const cachedCost = cost * 0.1;
43+
const reasoningCost = cost * 0.1;
44+
45+
return {
46+
byModel: {
47+
"claude-sonnet-4-20250514": {
48+
input: { tokens: 10000, cost_usd: inputCost },
49+
cached: { tokens: 5000, cost_usd: cachedCost },
50+
cacheCreate: { tokens: 0, cost_usd: 0 },
51+
output: { tokens: 2000, cost_usd: outputCost },
52+
reasoning: { tokens: 1000, cost_usd: reasoningCost },
53+
model: "claude-sonnet-4-20250514",
54+
},
55+
},
56+
version: 1,
57+
};
58+
}
59+
60+
/**
61+
* Costs tab with session cost displayed in tab label ($0.56)
62+
*/
63+
export const CostsTab: AppStory = {
64+
render: () => (
65+
<AppWithMocks
66+
setup={() => {
67+
localStorage.setItem(RIGHT_SIDEBAR_TAB_KEY, JSON.stringify("costs"));
68+
69+
return setupSimpleChatStory({
70+
workspaceId: "ws-costs",
71+
workspaceName: "feature/api",
72+
projectName: "my-app",
73+
messages: [
74+
createUserMessage("msg-1", "Help me build an API", { historySequence: 1 }),
75+
createAssistantMessage("msg-2", "I'll help you build a REST API.", {
76+
historySequence: 2,
77+
}),
78+
],
79+
sessionUsage: createSessionUsage(0.56),
80+
});
81+
}}
82+
/>
83+
),
84+
play: async ({ canvasElement }) => {
85+
const canvas = within(canvasElement);
86+
87+
// Session usage is fetched async via WorkspaceStore; wait to avoid snapshot races.
88+
await waitFor(
89+
() => {
90+
canvas.getByRole("tab", { name: /costs.*\$0\.56/i });
91+
},
92+
{ timeout: 5000 }
93+
);
94+
},
95+
};
96+
97+
/**
98+
* Review tab selected - click switches from Costs to Review tab
99+
*/
100+
export const ReviewTab: AppStory = {
101+
render: () => (
102+
<AppWithMocks
103+
setup={() => {
104+
localStorage.setItem(RIGHT_SIDEBAR_TAB_KEY, JSON.stringify("costs"));
105+
106+
return setupSimpleChatStory({
107+
workspaceId: "ws-review",
108+
workspaceName: "feature/review",
109+
projectName: "my-app",
110+
messages: [
111+
createUserMessage("msg-1", "Add a new component", { historySequence: 1 }),
112+
createAssistantMessage("msg-2", "I've added the component.", { historySequence: 2 }),
113+
],
114+
sessionUsage: createSessionUsage(0.42),
115+
});
116+
}}
117+
/>
118+
),
119+
play: async ({ canvasElement }) => {
120+
const canvas = within(canvasElement);
121+
122+
// Wait for session usage to land (avoid theme/mode snapshots diverging on timing).
123+
await waitFor(
124+
() => {
125+
canvas.getByRole("tab", { name: /costs.*\$0\.42/i });
126+
},
127+
{ timeout: 5000 }
128+
);
129+
130+
const reviewTab = canvas.getByRole("tab", { name: /^review/i });
131+
await userEvent.click(reviewTab);
132+
133+
await waitFor(() => {
134+
canvas.getByRole("tab", { name: /^review/i, selected: true });
135+
});
136+
},
137+
};

src/browser/stories/storyHelpers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
createGitStatusOutput,
2727
type GitStatusFixture,
2828
} from "./mockFactory";
29-
import { createMockORPCClient } from "../../../.storybook/mocks/orpc";
29+
import { createMockORPCClient, type MockSessionUsage } from "../../../.storybook/mocks/orpc";
3030

3131
// ═══════════════════════════════════════════════════════════════════════════════
3232
// WORKSPACE SELECTION
@@ -155,6 +155,8 @@ export interface SimpleChatSetupOptions {
155155
gitStatus?: GitStatusFixture;
156156
providersConfig?: ProvidersConfigMap;
157157
backgroundProcesses?: BackgroundProcessFixture[];
158+
/** Session usage data for Costs tab */
159+
sessionUsage?: MockSessionUsage;
158160
}
159161

160162
/**
@@ -184,6 +186,11 @@ export function setupSimpleChatStory(opts: SimpleChatSetupOptions): APIClient {
184186
? new Map([[workspaceId, opts.backgroundProcesses]])
185187
: undefined;
186188

189+
// Set up session usage map
190+
const sessionUsageMap = opts.sessionUsage
191+
? new Map([[workspaceId, opts.sessionUsage]])
192+
: undefined;
193+
187194
// Return ORPC client
188195
return createMockORPCClient({
189196
projects: groupWorkspacesByProject(workspaces),
@@ -192,6 +199,7 @@ export function setupSimpleChatStory(opts: SimpleChatSetupOptions): APIClient {
192199
executeBash: createGitStatusExecutor(gitStatus),
193200
providersConfig: opts.providersConfig,
194201
backgroundProcesses: bgProcesses,
202+
sessionUsage: sessionUsageMap,
195203
});
196204
}
197205

src/browser/styles/globals.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,8 @@ body {
10031003
/* Root container */
10041004
html,
10051005
body,
1006-
#root {
1006+
#root,
1007+
#storybook-root {
10071008
height: 100dvh;
10081009
overflow: hidden;
10091010
}
@@ -1079,7 +1080,8 @@ body,
10791080
/* Ensure the app uses the full viewport height accounting for browser chrome */
10801081
html,
10811082
body,
1082-
#root {
1083+
#root,
1084+
#storybook-root {
10831085
/* Use dvh (dynamic viewport height) on supported browsers - this accounts for
10841086
mobile browser chrome and keyboard accessory bars */
10851087
min-height: 100dvh;
@@ -1090,7 +1092,8 @@ body,
10901092
/* Handle safe areas for notched devices and keyboard accessory bars */
10911093
@supports (padding: env(safe-area-inset-top)) {
10921094
/* Apply padding to account for iOS safe areas (notch at top, home indicator at bottom) */
1093-
#root {
1095+
#root,
1096+
#storybook-root {
10941097
padding-top: env(safe-area-inset-top, 0);
10951098
padding-bottom: env(safe-area-inset-bottom, 0);
10961099
padding-left: env(safe-area-inset-left, 0);

0 commit comments

Comments
 (0)