Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/storybook/src/vitest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ export const createArgosScreenshotCommand = (
);
const after = await before(ctx);

const options = applyFitToContent(screenshotOptions, fitToContent);
// The story renders inside an `<iframe data-vitest="true">` on the host page
// and we screenshot the iframe's `<body>`. Anything overflowing the iframe box
// is not painted, so the screenshot gets cut. `setViewportSize` grows the iframe
// *before* `argosCSS` (which injects `fitToContent`'s `zoom`) is applied, so it
// can't account for the final content size. Re-fit the iframe height here, after
// stabilization has injected `argosCSS`, so the whole content is painted.
const userBeforeScreenshot = options?.beforeScreenshot;
const optionsWithFit: ArgosScreenshotOptions = {
...options,
beforeScreenshot: async (api) => {
await userBeforeScreenshot?.(api);
await ctx.page.evaluate(() => {
const iframe = document.querySelector('iframe[data-vitest="true"]');

if (
!(iframe instanceof HTMLIFrameElement) ||
!iframe.contentDocument
) {
return;
}

const { body, documentElement } = iframe.contentDocument;
const contentHeight = Math.max(
body.scrollHeight,
body.offsetHeight,
documentElement.scrollHeight,
);

// Only grow, never shrink: the iframe must contain the full content so
// it's painted, but we don't want to collapse an intentionally sized viewport.
if (contentHeight > iframe.clientHeight) {
iframe.style.height = `${contentHeight}px`;
}
});
},
};

const attachments = await storybookArgosScreenshot(
frame,
{
Expand Down Expand Up @@ -111,7 +149,7 @@ export const createArgosScreenshotCommand = (
);
},
},
applyFitToContent(screenshotOptions, fitToContent),
optionsWithFit,
);
await after();
return attachments;
Expand Down
37 changes: 37 additions & 0 deletions packages/storybook/stories/TallContent.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TallContent } from "./TallContent";
import type { Meta, StoryObj } from "@storybook/react-vite";

/**
* Regression stories for screenshots being cut off when the content is taller
* than the viewport in the Vitest integration.
*
* The story renders inside an `<iframe data-vitest="true">` and Argos
* screenshots the iframe's `<body>`. If the iframe is not grown to fit the
* content, everything overflowing the iframe box is not painted and the
* screenshot is cut. This must work both with and without `fitToContent`.
*/
const meta = {
title: "Repro/TallContent",
component: TallContent,
parameters: {
layout: "fullscreen",
argos: { modes: null },
},
} satisfies Meta<typeof TallContent>;

export default meta;
type Story = StoryObj<typeof meta>;

export const WithoutFitToContent: Story = {
args: { count: 40 },
parameters: {
argos: { fitToContent: false, modes: null },
},
};

export const WithFitToContent: Story = {
args: { count: 40 },
parameters: {
argos: { fitToContent: true, modes: null },
},
};
35 changes: 35 additions & 0 deletions packages/storybook/stories/TallContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";

type TallContentProps = {
/**
* Number of rows to render. Enough rows make the content taller than the
* browser viewport, which is required to reproduce the screenshot clipping.
*/
count?: number;
};

/**
* A component that renders a tall list of rows, taller than the viewport.
* Used to reproduce screenshots being cut off in the Vitest integration.
*/
export const TallContent = ({ count = 40 }: TallContentProps) => {
return (
<div style={{ padding: 16, fontFamily: "sans-serif" }}>
{Array.from({ length: count }, (_, index) => (
<div
key={index}
style={{
padding: "12px 16px",
margin: "8px 0",
border: "1px solid #ccc",
borderRadius: 6,
background: index % 2 ? "#f5f5f5" : "#fff",
}}
>
Row {index + 1} — this is a tall content row to force the page to
overflow the viewport height
</div>
))}
</div>
);
};
Loading