Skip to content

Commit

Permalink
Merge pull request #309 from chromaui/256-loader-for-snapshot-image
Browse files Browse the repository at this point in the history
Show spinner while snapshot image is loading
  • Loading branch information
ghengeveld committed May 28, 2024
2 parents 8a46d55 + 7b89f8d commit d95836b
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 40 deletions.
20 changes: 16 additions & 4 deletions src/components/SnapshotImage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const meta = {
args: {
componentName: "Shapes",
storyName: "Primary",
baselineImage: { imageUrl: "/A.png", imageWidth: 880 },
latestImage: { imageUrl: "/B.png", imageWidth: 880 },
baselineImage: { imageUrl: "/A.png", imageWidth: 880, imageHeight: 280 },
latestImage: { imageUrl: "/B.png", imageWidth: 880, imageHeight: 280 },
diffImage: { imageUrl: "/B-comparison.png", imageWidth: 880 },
focusImage: { imageUrl: "/B-focus.png", imageWidth: 880 },
comparisonResult: ComparisonResult.Changed,
Expand Down Expand Up @@ -51,7 +51,7 @@ export const BothVisible = {

export const Wider = {
args: {
latestImage: { imageUrl: "/shapes-wider.png", imageWidth: 768 },
latestImage: { imageUrl: "/shapes-wider.png", imageWidth: 768, imageHeight: 472 },
diffImage: { imageUrl: "/shapes-comparison.png", imageWidth: 768 },
focusImage: { imageUrl: "/shapes-focus.png", imageWidth: 768 },
diffVisible: true,
Expand All @@ -68,7 +68,7 @@ export const WiderConstrained = {

export const Taller = {
args: {
latestImage: { imageUrl: "/shapes-taller.png", imageWidth: 588 },
latestImage: { imageUrl: "/shapes-taller.png", imageWidth: 588, imageHeight: 684 },
diffImage: { imageUrl: "/shapes-comparison.png", imageWidth: 768 },
focusImage: { imageUrl: "/shapes-focus.png", imageWidth: 768 },
diffVisible: true,
Expand Down Expand Up @@ -98,3 +98,15 @@ export const Loading = {
},
},
} satisfies Story;

export const OverlayLoading = {
...BothVisible,
parameters: {
msw: {
handlers: [
http.get("/B-comparison.png", () => delay("infinite")),
http.get("/B-focus.png", () => delay("infinite")),
],
},
},
} satisfies Story;
123 changes: 93 additions & 30 deletions src/components/SnapshotImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { styled, useTheme } from "@storybook/theming";
import React, { ComponentProps } from "react";

import { CaptureImage, CaptureOverlayImage, ComparisonResult, Test } from "../gql/graphql";
import { Spinner } from "./design-system";
import { Stack } from "./Stack";
import { Text } from "./Text";

Expand All @@ -13,17 +14,9 @@ export const Container = styled.div<{ href?: string; target?: string }>(
background: "transparent",
overflow: "hidden",
margin: 2,
maxWidth: "calc(100% - 4px)",

img: {
maxWidth: "100%",
transition: "filter 0.1s ease-in-out",
},
"img[data-overlay]": {
position: "absolute",
opacity: 0.7,
pointerEvents: "none",
},
div: {
"& > div": {
display: "flex",
flexDirection: "column",
alignItems: "center",
Expand Down Expand Up @@ -64,17 +57,55 @@ export const Container = styled.div<{ href?: string; target?: string }>(
}
);

const StyledStack = styled(Stack)(({ theme }) => ({
margin: "30px 15px",
const ImageWrapper = styled.div<{ isVisible?: boolean }>(({ isVisible }) => ({
position: isVisible ? "static" : "absolute",
visibility: isVisible ? "visible" : "hidden",
maxWidth: "100%",
minHeight: 100,
}));

const Image = styled.img({
display: "block",
width: "100%",
height: "auto",
transition: "filter 0.1s ease-in-out, opacity 0.1s ease-in-out",

"&[data-overlay]": {
position: "absolute",
opacity: 0.7,
pointerEvents: "none",
transition: "opacity 0.1s ease-in-out",
},
});

const StyledStack = styled(Stack)({
margin: "30px 15px",
});

const getOverlayImageLoaded = ({
comparisonImageLoaded,
focusImageLoaded,
showDiff,
showFocus,
}: {
comparisonImageLoaded: boolean;
focusImageLoaded: boolean;
showDiff: boolean;
showFocus: boolean;
}) => {
if (showDiff && showFocus) return comparisonImageLoaded && focusImageLoaded;
if (showDiff) return comparisonImageLoaded;
if (showFocus) return focusImageLoaded;
return true;
};

interface SnapshotImageProps {
componentName?: NonNullable<NonNullable<Test["story"]>["component"]>["name"];
storyName?: NonNullable<Test["story"]>["name"];
testUrl: Test["webUrl"];
comparisonResult?: ComparisonResult;
latestImage?: Pick<CaptureImage, "imageUrl" | "imageWidth">;
baselineImage?: Pick<CaptureImage, "imageUrl" | "imageWidth">;
latestImage?: Pick<CaptureImage, "imageUrl" | "imageWidth" | "imageHeight">;
baselineImage?: Pick<CaptureImage, "imageUrl" | "imageWidth" | "imageHeight">;
baselineImageVisible?: boolean;
diffImage?: Pick<CaptureOverlayImage, "imageUrl" | "imageWidth">;
focusImage?: Pick<CaptureOverlayImage, "imageUrl" | "imageWidth">;
Expand Down Expand Up @@ -106,47 +137,79 @@ export const SnapshotImage = ({
const showDiff = hasDiff && diffVisible;
const showFocus = hasFocus && focusVisible;

const [latestImageLoaded, setLatestImageLoaded] = React.useState(false);
const [baselineImageLoaded, setBaselineImageLoaded] = React.useState(false);
const [comparisonImageLoaded, setComparisonImageLoaded] = React.useState(false);
const [focusImageLoaded, setFocusImageLoaded] = React.useState(false);
const snapshotImageLoaded = baselineImageVisible ? baselineImageLoaded : latestImageLoaded;
const overlayImageLoaded = getOverlayImageLoaded({
comparisonImageLoaded,
focusImageLoaded,
showDiff,
showFocus,
});

return (
<Container {...props} {...containerProps}>
{latestImage && (
<img
alt={`Latest snapshot for the '${storyName}' story of the '${componentName}' component`}
src={latestImage.imageUrl}
<ImageWrapper
isVisible={!baselineImage || !baselineImageVisible}
style={{
display: baselineImageVisible ? "none" : "block",
aspectRatio: `${latestImage.imageWidth} / ${latestImage.imageHeight}`,
width: latestImage.imageWidth,
}}
/>
>
{(!latestImageLoaded || !overlayImageLoaded) && <Spinner />}
<Image
alt={`Latest snapshot for the '${storyName}' story of the '${componentName}' component`}
src={latestImage.imageUrl}
style={{ opacity: latestImageLoaded ? 1 : 0 }}
onLoad={() => setLatestImageLoaded(true)}
/>
</ImageWrapper>
)}
{baselineImage && (
<img
alt={`Baseline snapshot for the '${storyName}' story of the '${componentName}' component`}
src={baselineImage.imageUrl}
<ImageWrapper
isVisible={baselineImageVisible}
style={{
display: baselineImageVisible ? "block" : "none",
aspectRatio: `${baselineImage.imageWidth} / ${baselineImage.imageHeight}`,
width: baselineImage.imageWidth,
}}
/>
>
{(!baselineImageLoaded || !overlayImageLoaded) && <Spinner />}
<Image
alt={`Baseline snapshot for the '${storyName}' story of the '${componentName}' component`}
src={baselineImage.imageUrl}
style={{ opacity: baselineImageLoaded ? 1 : 0 }}
onLoad={() => setBaselineImageLoaded(true)}
/>
</ImageWrapper>
)}
{hasDiff && (
<img
{hasDiff && snapshotImageLoaded && (
<Image
alt=""
data-overlay="diff"
src={diffImage.imageUrl}
style={{
width: diffImage.imageWidth,
maxWidth: `${(diffImage.imageWidth / latestImage.imageWidth) * 100}%`,
opacity: showDiff ? 0.7 : 0,
opacity: showDiff && comparisonImageLoaded ? 0.7 : 0,
}}
onLoad={() => setComparisonImageLoaded(true)}
/>
)}
{hasFocus && (
<img
{hasFocus && snapshotImageLoaded && (
<Image
alt=""
data-overlay="focus"
src={focusImage.imageUrl}
style={{
width: focusImage.imageWidth,
maxWidth: `${(focusImage.imageWidth / latestImage.imageWidth) * 100}%`,
opacity: showFocus ? 0.7 : 0,
opacity: showFocus && focusImageLoaded ? 0.7 : 0,
filter: showFocus ? "blur(2px)" : "none",
}}
onLoad={() => setFocusImageLoaded(true)}
/>
)}
{hasDiff && <ShareAltIcon />}
Expand Down
4 changes: 2 additions & 2 deletions src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const documents = {
"\n fragment SelectedBuildFields on Build {\n __typename\n id\n number\n branch\n commit\n committedAt\n uncommittedHash\n status\n ... on StartedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n ... on CompletedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n }\n": types.SelectedBuildFieldsFragmentDoc,
"\n fragment StatusTestFields on Test {\n id\n status\n result\n story {\n storyId\n }\n }\n": types.StatusTestFieldsFragmentDoc,
"\n fragment LastBuildOnBranchTestFields on Test {\n status\n result\n }\n": types.LastBuildOnBranchTestFieldsFragmentDoc,
"\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n": types.StoryTestFieldsFragmentDoc,
"\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n imageHeight\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n imageHeight\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n": types.StoryTestFieldsFragmentDoc,
"\n mutation ReviewTest($input: ReviewTestInput!) {\n reviewTest(input: $input) {\n updatedTests {\n id\n status\n }\n userErrors {\n ... on UserError {\n __typename\n message\n }\n ... on BuildSupersededError {\n build {\n id\n }\n }\n ... on TestUnreviewableError {\n test {\n id\n }\n }\n }\n }\n }\n": types.ReviewTestDocument,
};

Expand Down Expand Up @@ -79,7 +79,7 @@ export function graphql(source: "\n fragment LastBuildOnBranchTestFields on Tes
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n"): (typeof documents)["\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n"];
export function graphql(source: "\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n imageHeight\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n imageHeight\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n"): (typeof documents)["\n fragment StoryTestFields on Test {\n id\n status\n result\n webUrl\n comparisons {\n id\n result\n browser {\n id\n key\n name\n version\n }\n captureDiff {\n diffImage(signed: true) {\n imageUrl\n imageWidth\n }\n focusImage(signed: true) {\n imageUrl\n imageWidth\n }\n }\n headCapture {\n captureImage(signed: true) {\n backgroundColor\n imageUrl\n imageWidth\n imageHeight\n thumbnailUrl\n }\n captureError {\n kind\n ... on CaptureErrorInteractionFailure {\n error\n }\n ... on CaptureErrorJSError {\n error\n }\n ... on CaptureErrorFailedJS {\n error\n }\n }\n }\n baseCapture {\n captureImage(signed: true) {\n imageUrl\n imageWidth\n imageHeight\n }\n }\n }\n mode {\n name\n globals\n }\n story {\n storyId\n name\n component {\n name\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit d95836b

Please sign in to comment.