Skip to content
93 changes: 93 additions & 0 deletions code/addons/review-changes/src/ReviewChangesPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { expect, fn, userEvent, within } from 'storybook/test';

import preview from '../../../.storybook/preview.tsx';
import { EVENTS } from './constants.ts';
import type { ReviewState } from './review-state.ts';
import { ReviewChangesPage, type ReviewChangesPageProps } from './ReviewChangesPage.tsx';

let registeredEventMap: Record<string, ((payload: ReviewState) => void) | undefined> = {};
const emitMock = fn();

const useChannelMock: NonNullable<ReviewChangesPageProps['useChannelHook']> = (eventMap) => {
registeredEventMap = eventMap as typeof registeredEventMap;
return emitMock as ReturnType<NonNullable<ReviewChangesPageProps['useChannelHook']>>;
};

const reviewState: ReviewState = {
title: 'Manager settings polish',
description: 'Updated settings views and spacing.',
branchName: 'feat/review-changes-page',
collections: [
{
title: 'Settings',
rationale: 'Primary settings surfaces changed.',
storyIds: [
'manager-settings-checklist--default',
'manager-settings-guidepage--default',
'manager-settings-aboutscreen--default',
],
},
],
};

const applyReviewState = () => {
const applyEventHandler = registeredEventMap[EVENTS.APPLY_REVIEW_STATE];
expect(applyEventHandler).toBeTruthy();
applyEventHandler?.(reviewState);
};

const meta = preview.meta({
component: ReviewChangesPage,
parameters: { layout: 'fullscreen' },
args: {
useChannelHook: useChannelMock,
locationSearch: '',
},
beforeEach: () => {
registeredEventMap = {};
emitMock.mockReset();
},
});

export const Collections = meta.story({
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByText(/Waiting for the agent/i)).toBeInTheDocument();
await expect(emitMock).toHaveBeenCalledWith(EVENTS.REQUEST_REVIEW_STATE);

applyReviewState();

await expect(await canvas.findByText('Manager settings polish')).toBeInTheDocument();
await expect(await canvas.findByRole('tab', { name: 'Collections' })).toBeInTheDocument();
},
});

export const Components = meta.story({
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(emitMock).toHaveBeenCalledWith(EVENTS.REQUEST_REVIEW_STATE);

applyReviewState();

const componentsTab = await canvas.findByRole('tab', { name: 'Components' });
await userEvent.click(componentsTab);

await expect(await canvas.findByText('Components view coming soon.')).toBeInTheDocument();
},
});

export const Details = meta.story({
args: {
locationSearch: '?collection=0&story=1',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(emitMock).toHaveBeenCalledWith(EVENTS.REQUEST_REVIEW_STATE);

applyReviewState();

await expect(await canvas.findByText('Settings')).toBeInTheDocument();
await expect(await canvas.findByRole('button', { name: '2/3' })).toBeInTheDocument();
await expect(await canvas.findByText('Latest on feat/review-changes-page')).toBeInTheDocument();
},
});
73 changes: 73 additions & 0 deletions code/addons/review-changes/src/ReviewChangesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { type FC, useEffect, useState } from 'react';

import { useChannel } from 'storybook/manager-api';

import { REVIEW_CHANGES_URL } from './constants.ts';
import { EVENTS } from './constants.ts';
import type { ReviewState } from './review-state.ts';
import {
buildReviewChangesDetailsHref,
parseReviewChangesDetailsLocation,
} from './review-navigation.ts';
import { DetailsScreen } from './screens/DetailsScreen.tsx';
import { SummaryScreen } from './screens/SummaryScreen.tsx';

export interface ReviewChangesPageProps {
/** Test hook for stories; defaults to manager-api useChannel. */
useChannelHook?: typeof useChannel;
/** Test hook for stories; defaults to window.location.search. */
locationSearch?: string;
}

// Container — wires the channel + manager api. The agent pushes a review via
// the MCP addon; we cache nothing here, just reflect the latest pushed state.
export const ReviewChangesPage: FC<ReviewChangesPageProps> = ({
useChannelHook = useChannel,
locationSearch,
}) => {
const [state, setState] = useState<ReviewState | null>(null);

const emit = useChannelHook({
[EVENTS.APPLY_REVIEW_STATE]: (next: ReviewState) => setState(next),
});

// Late/refreshed tab: ask the server to replay the cached overlay.
useEffect(() => {
emit(EVENTS.REQUEST_REVIEW_STATE);
}, [emit]);

const detailsLocation = parseReviewChangesDetailsLocation(
locationSearch ?? window.location.search
);
const collection = state?.collections[detailsLocation?.collectionIndex ?? -1];
const totalStories = collection?.storyIds.length ?? 0;
const hasDetailsState = !!collection && totalStories > 0 && detailsLocation !== null;

if (hasDetailsState) {
const normalizedStoryIndex = detailsLocation.storyIndex % totalStories;
const previousStoryIndex = (normalizedStoryIndex - 1 + totalStories) % totalStories;
const nextStoryIndex = (normalizedStoryIndex + 1) % totalStories;
const currentStoryId = collection.storyIds[normalizedStoryIndex];

return (
<DetailsScreen
collectionTitle={collection.title}
storyId={currentStoryId}
storyIndex={normalizedStoryIndex}
totalStories={totalStories}
backHref={REVIEW_CHANGES_URL}
previousHref={buildReviewChangesDetailsHref({
collectionIndex: detailsLocation.collectionIndex,
storyIndex: previousStoryIndex,
})}
nextHref={buildReviewChangesDetailsHref({
collectionIndex: detailsLocation.collectionIndex,
storyIndex: nextStoryIndex,
})}
branchName={state?.branchName}
/>
);
}

return <SummaryScreen state={state} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, within } from 'storybook/test';

import preview from '../../../../.storybook/preview.tsx';
import { CollectionGrid } from './CollectionGrid.tsx';

const demoStoryIds = [
'button-component--base',
'button-component--variants',
'button-component--sizes',
'manager-main--default',
'manager-sidebar-sidebar--simple',
'manager-settings-aboutscreen--default',
'components-tabs-tabsview--basic',
'bench--es-build-analyzer',
];

const meta = preview.meta({
component: CollectionGrid,
parameters: { layout: 'fullscreen' },
args: {
storyIds: demoStoryIds,
},
});

export const Default = meta.story({});

export const ManyStoriesAutoFit = meta.story({
globals: { viewport: { value: 'mobile1' } },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByRole('button', { name: /Review all/i })).toBeInTheDocument();

const cells = Array.from(
canvasElement.querySelectorAll('[data-testid="review-collection-grid-cell"]')
) as HTMLElement[];
const rows = new Set(cells.map((cell) => Math.round(cell.getBoundingClientRect().top)));
await expect(rows.size).toBeGreaterThan(1);
},
});

export const FewStoriesStretch = meta.story({
args: {
storyIds: ['manager-main--default', 'manager-settings-aboutscreen--default'],
},
globals: { viewport: { value: 'desktop' } },
play: async ({ canvasElement }) => {
const cells = canvasElement.querySelectorAll('[data-testid="review-collection-grid-cell"]');
await expect(cells.length).toBe(2);
},
});

export const HeightIsCapped = meta.story({
args: {
storyIds: ['manager-main--default'],
},
globals: { viewport: { value: 'desktop' } },
play: async ({ canvasElement }) => {
const cell = canvasElement.querySelector('[data-testid="review-collection-grid-cell"]');
await expect(cell).toBeTruthy();
await expect((cell as HTMLElement).getBoundingClientRect().height).toBeLessThanOrEqual(400);
},
});
Loading
Loading