diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index f925e615d..be4d583f9 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -24,6 +24,9 @@ jobs: - name: Generate version file run: ./scripts/generate-version.sh + - name: Mock version for stable visual testing + run: cp .storybook/mocks/version.ts src/version.ts + - name: Build Storybook run: bun x storybook build --stats-json diff --git a/.storybook/main.ts b/.storybook/main.ts index db423e756..9ffcdc644 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -19,6 +19,8 @@ const config: StorybookConfig = { resolve: { alias: { "@": path.join(process.cwd(), "src"), + // Note: VERSION mocking for stable visual testing is handled by overwriting + // src/version.ts in the Chromatic CI workflow, not via alias here }, }, }); diff --git a/.storybook/mocks/version.ts b/.storybook/mocks/version.ts new file mode 100644 index 000000000..04e48ceb0 --- /dev/null +++ b/.storybook/mocks/version.ts @@ -0,0 +1,9 @@ +// Mock version for Storybook visual testing +// This ensures consistent snapshots in Chromatic +// Apple's classic demo time: 9:41 AM on January 24, 2024 +export const VERSION = { + git_commit: "abc1234", + git_describe: "v1.0.0", + buildTime: "2024-01-24T17:41:00Z", // 9:41 AM PST +}; + diff --git a/src/App.stories.tsx b/src/App.stories.tsx index 96a2b7c0b..0a44d052f 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -5,6 +5,9 @@ import type { ProjectConfig } from "./config"; import type { FrontendWorkspaceMetadata } from "./types/workspace"; import type { IPCApi } from "./types/ipc"; +// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST) +const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime(); + // Mock window.api for App component function setupMockAPI(options: { projects?: Map; @@ -374,7 +377,7 @@ export const ActiveWorkspaceWithChat: Story = { parts: [{ type: "text", text: "Add authentication to the user API endpoint" }], metadata: { historySequence: 1, - timestamp: Date.now() - 300000, + timestamp: STABLE_TIMESTAMP - 300000, }, }); @@ -402,7 +405,7 @@ export const ActiveWorkspaceWithChat: Story = { ], metadata: { historySequence: 2, - timestamp: Date.now() - 290000, + timestamp: STABLE_TIMESTAMP - 290000, model: "claude-sonnet-4-20250514", usage: { inputTokens: 1250, @@ -420,7 +423,7 @@ export const ActiveWorkspaceWithChat: Story = { parts: [{ type: "text", text: "Yes, add JWT token validation" }], metadata: { historySequence: 3, - timestamp: Date.now() - 280000, + timestamp: STABLE_TIMESTAMP - 280000, }, }); @@ -452,7 +455,7 @@ export const ActiveWorkspaceWithChat: Story = { ], metadata: { historySequence: 4, - timestamp: Date.now() - 270000, + timestamp: STABLE_TIMESTAMP - 270000, model: "claude-sonnet-4-20250514", usage: { inputTokens: 2100, @@ -470,7 +473,7 @@ export const ActiveWorkspaceWithChat: Story = { parts: [{ type: "text", text: "Can you run the tests to make sure it works?" }], metadata: { historySequence: 5, - timestamp: Date.now() - 240000, + timestamp: STABLE_TIMESTAMP - 240000, }, }); @@ -502,7 +505,7 @@ export const ActiveWorkspaceWithChat: Story = { ], metadata: { historySequence: 6, - timestamp: Date.now() - 230000, + timestamp: STABLE_TIMESTAMP - 230000, model: "claude-sonnet-4-20250514", usage: { inputTokens: 2800, @@ -525,7 +528,7 @@ export const ActiveWorkspaceWithChat: Story = { ], metadata: { historySequence: 7, - timestamp: Date.now() - 180000, + timestamp: STABLE_TIMESTAMP - 180000, }, }); @@ -562,7 +565,7 @@ export const ActiveWorkspaceWithChat: Story = { ], metadata: { historySequence: 8, - timestamp: Date.now() - 170000, + timestamp: STABLE_TIMESTAMP - 170000, model: "claude-sonnet-4-20250514", usage: { inputTokens: 3500, diff --git a/src/components/Messages/AssistantMessage.stories.tsx b/src/components/Messages/AssistantMessage.stories.tsx index c22db4446..0ffe8151f 100644 --- a/src/components/Messages/AssistantMessage.stories.tsx +++ b/src/components/Messages/AssistantMessage.stories.tsx @@ -3,6 +3,9 @@ import { AssistantMessage } from "./AssistantMessage"; import type { DisplayedMessage } from "@/types/message"; import { action } from "@storybook/addon-actions"; +// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST) +const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime(); + const clipboardWriteText = (data: string) => { action("copy-text")(data); return Promise.resolve(); @@ -52,7 +55,7 @@ const createAssistantMessage = ( isStreaming: false, isPartial: false, isCompacted: false, - timestamp: Date.now(), + timestamp: STABLE_TIMESTAMP, model: "anthropic:claude-sonnet-4-5", ...overrides, }); diff --git a/src/components/Messages/ReasoningMessage.stories.tsx b/src/components/Messages/ReasoningMessage.stories.tsx index b8e3d13e9..e92d8c97f 100644 --- a/src/components/Messages/ReasoningMessage.stories.tsx +++ b/src/components/Messages/ReasoningMessage.stories.tsx @@ -2,6 +2,9 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ReasoningMessage } from "./ReasoningMessage"; import type { DisplayedMessage } from "@/types/message"; +// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST) +const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime(); + const meta = { title: "Messages/ReasoningMessage", component: ReasoningMessage, @@ -34,7 +37,7 @@ const createReasoningMessage = ( historySequence: 1, isStreaming: false, isPartial: false, - timestamp: Date.now(), + timestamp: STABLE_TIMESTAMP, ...overrides, }); diff --git a/src/components/Messages/StreamErrorMessage.stories.tsx b/src/components/Messages/StreamErrorMessage.stories.tsx index ec86cb9ef..c26fd97e5 100644 --- a/src/components/Messages/StreamErrorMessage.stories.tsx +++ b/src/components/Messages/StreamErrorMessage.stories.tsx @@ -3,6 +3,9 @@ import { StreamErrorMessage } from "./StreamErrorMessage"; import type { DisplayedMessage } from "@/types/message"; import type { StreamErrorType } from "@/types/errors"; +// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST) +const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime(); + const meta = { title: "Messages/StreamErrorMessage", component: StreamErrorMessage, @@ -35,7 +38,7 @@ const createStreamErrorMessage = ( error, errorType, historySequence: 1, - timestamp: Date.now(), + timestamp: STABLE_TIMESTAMP, ...overrides, }); diff --git a/src/components/Messages/UserMessage.stories.tsx b/src/components/Messages/UserMessage.stories.tsx index a74b9dc01..f876d69bc 100644 --- a/src/components/Messages/UserMessage.stories.tsx +++ b/src/components/Messages/UserMessage.stories.tsx @@ -3,6 +3,9 @@ import { action } from "@storybook/addon-actions"; import { UserMessage } from "./UserMessage"; import type { DisplayedMessage } from "@/types/message"; +// Stable timestamp for visual testing (Apple demo time: Jan 24, 2024, 9:41 AM PST) +const STABLE_TIMESTAMP = new Date("2024-01-24T09:41:00-08:00").getTime(); + const clipboardWriteText = (data: string) => { action("copy-text")(data); return Promise.resolve(); @@ -42,7 +45,7 @@ const createUserMessage = ( historyId: "hist-1", content, historySequence: 1, - timestamp: Date.now(), + timestamp: STABLE_TIMESTAMP, ...overrides, }); diff --git a/src/components/TipsCarousel.stories.tsx b/src/components/TipsCarousel.stories.tsx index 148133b60..b68972a70 100644 --- a/src/components/TipsCarousel.stories.tsx +++ b/src/components/TipsCarousel.stories.tsx @@ -19,7 +19,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: () => , + render: () => , }; export const WithExplanation: Story = { @@ -28,7 +28,7 @@ export const WithExplanation: Story = {
Tips rotate automatically based on time. Hover to see the gradient effect:
- +
Tips change every hour to provide variety and convey UX information.
@@ -40,7 +40,7 @@ export const DebugControls: Story = { render: () => (
For debugging, you can use:
- +
window.setTip(0) // Show first tip
window.setTip(1) // Show second tip
@@ -65,7 +65,7 @@ export const InContext: Story = { justifyContent: "center", }} > - +
Mode: Plan diff --git a/src/components/TipsCarousel.tsx b/src/components/TipsCarousel.tsx index 8979c551a..647d18950 100644 --- a/src/components/TipsCarousel.tsx +++ b/src/components/TipsCarousel.tsx @@ -37,7 +37,12 @@ const TIPS = [ }, ]; -export const TipsCarousel: React.FC = () => { +interface TipsCarouselProps { + /** Override the automatic tip rotation (for testing/Storybook) */ + fixedTipIndex?: number; +} + +export const TipsCarousel: React.FC = ({ fixedTipIndex }) => { const [manualTipIndex, setManualTipIndex] = useState(null); // Calculate tip based on hours since epoch @@ -48,7 +53,7 @@ export const TipsCarousel: React.FC = () => { return hoursSinceEpoch % TIPS.length; }; - const currentTipIndex = manualTipIndex ?? calculateTipIndex(); + const currentTipIndex = fixedTipIndex ?? manualTipIndex ?? calculateTipIndex(); // Expose setTip to window for debugging useEffect(() => {