diff --git a/assets/visual-test-illustration.mp4 b/assets/visual-test-illustration.mp4 new file mode 100644 index 00000000..2dd98b40 Binary files /dev/null and b/assets/visual-test-illustration.mp4 differ diff --git a/package.json b/package.json index a0c201a2..e799096e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "main": "dist/index.js", "files": [ + "assets/**/*", "dist/**/*", "README.md", "*.js", diff --git a/src/Introduction.mdx b/src/Introduction.mdx deleted file mode 100644 index 83a0b0da..00000000 --- a/src/Introduction.mdx +++ /dev/null @@ -1,10 +0,0 @@ -import { Meta } from '@storybook/addon-docs/blocks'; -import { VisualTestsIcon } from './components/icons/VisualTestsIcon'; - - - - - -# Welcome to the Visual Tests addon for Storybook - -Catch bugs in UI appearance automatically. Compare image snapshots to detect visual changes. diff --git a/src/Panel.tsx b/src/Panel.tsx index a78353bf..f929909e 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -15,7 +15,7 @@ import { TELEMETRY, } from './constants'; import { Authentication } from './screens/Authentication/Authentication'; -import { GitNotFound } from './screens/Errors/GitNotFound'; +import { GitError } from './screens/Errors/GitError'; import { LinkedProject } from './screens/LinkProject/LinkedProject'; import { LinkingProjectFailed } from './screens/LinkProject/LinkingProjectFailed'; import { LinkProject } from './screens/LinkProject/LinkProject'; @@ -149,7 +149,7 @@ export const Panel = ({ active, api }: PanelProps) => { } if (gitInfoError || !gitInfo) { - return withProviders(); + return withProviders(); } // Momentarily wait on addonState (should be very fast) diff --git a/src/TestProviderRender.tsx b/src/TestProviderRender.tsx index 33138d07..c5397519 100644 --- a/src/TestProviderRender.tsx +++ b/src/TestProviderRender.tsx @@ -103,11 +103,11 @@ export const TestProviderRender = () => { }); let warning: string | undefined; - if (!projectId) warning = 'Select a project'; - if (!isLoggedIn) warning = 'Login required'; - if (gitInfoError) warning = 'Git synchronization problem'; + if (isOffline) warning = 'Not available offline'; if (hasConfigProblem) warning = 'Configuration problem'; - if (isOffline) warning = 'Not available while offline'; + if (gitInfoError) warning = 'Git synchronization problem'; + if (!isLoggedIn) warning = 'Login required'; + if (!projectId) warning = 'Set up visual tests'; const isRunnable = !warning && testProviderState !== 'test-provider-state:crashed'; diff --git a/src/components/Box.tsx b/src/components/Box.tsx index 52680579..ddc62f00 100644 --- a/src/components/Box.tsx +++ b/src/components/Box.tsx @@ -17,9 +17,3 @@ export const Box = styled.div<{ warning?: boolean }>( ({ theme, warning }) => warning && { background: theme.base === 'dark' ? '#342e1a' : theme.background.warning } ); - -export const BoxList = styled.ul({ - margin: 0, - padding: 4, - listStylePosition: 'inside', -}); diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index ca794b81..dfdfc3c0 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -99,6 +99,7 @@ interface ScreenProps { footer?: ReactNode; ignoreConfig?: boolean; ignoreSuggestions?: boolean; + interstitial?: boolean; } const Container = styled.div({ @@ -107,14 +108,16 @@ const Container = styled.div({ height: '100%', }); -const Content = styled.div<{ hidden?: boolean }>(({ hidden, theme }) => ({ - background: theme.background.app, - display: hidden ? 'none' : 'flex', - flexDirection: 'column', - flexGrow: 1, - height: '100%', - overflowY: 'auto', -})); +const Content = styled.div<{ hidden?: boolean; interstitial?: boolean }>( + ({ hidden, interstitial, theme }) => ({ + background: interstitial ? theme.background.content : theme.background.app, + display: hidden ? 'none' : 'flex', + flexDirection: 'column', + flexGrow: 1, + height: '100%', + overflowY: 'auto', + }) +); export const Footer = styled.div(({ theme }) => ({ background: theme.background.bar, @@ -140,6 +143,7 @@ export const Screen = ({ ), ignoreConfig = false, ignoreSuggestions = !footer, + interstitial = false, }: ScreenProps) => { const { configVisible } = useControlsState(); const { toggleConfig } = useControlsDispatch(); @@ -164,7 +168,9 @@ export const Screen = ({ ignoreConfig={ignoreConfig} ignoreSuggestions={ignoreSuggestions} /> - + diff --git a/src/components/icons/CheckIcon.tsx b/src/components/icons/CheckIcon.tsx new file mode 100644 index 00000000..a12bf559 --- /dev/null +++ b/src/components/icons/CheckIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const CheckIcon = (props: any) => ( + + + +); diff --git a/src/components/icons/ChevronRightIcon.tsx b/src/components/icons/ChevronRightIcon.tsx new file mode 100644 index 00000000..888b5356 --- /dev/null +++ b/src/components/icons/ChevronRightIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const ChevronRightIcon = (props: any) => ( + + + +); diff --git a/src/components/icons/ChromaticIcon.tsx b/src/components/icons/ChromaticIcon.tsx new file mode 100644 index 00000000..778b2bb0 --- /dev/null +++ b/src/components/icons/ChromaticIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const ChromaticIcon = (props: any) => ( + + + + + + +); diff --git a/src/components/icons/LinkIcon.tsx b/src/components/icons/LinkIcon.tsx deleted file mode 100644 index 088c4d69..00000000 --- a/src/components/icons/LinkIcon.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; - -export const LinkIcon = (props: any) => ( - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/src/components/icons/VisualTestsIcon.tsx b/src/components/icons/VisualTestsIcon.tsx deleted file mode 100644 index e53be791..00000000 --- a/src/components/icons/VisualTestsIcon.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; - -export const VisualTestsIcon = (props: any) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/src/preset.ts b/src/preset.ts index 7f8c1596..04d52635 100644 --- a/src/preset.ts +++ b/src/preset.ts @@ -1,6 +1,6 @@ import { watch } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import { normalize, relative } from 'node:path'; +import { dirname, join, normalize, relative } from 'node:path'; import { type Configuration, getConfiguration, getGitInfo, type GitInfo } from 'chromatic/node'; import type { Channel } from 'storybook/internal/channels'; @@ -270,6 +270,13 @@ async function serverChannel(channel: Channel, options: Options & { configFile?: const config = { managerEntries, experimental_serverChannel: serverChannel, + staticDirs: async (inputDirs: string[]) => [ + ...inputDirs, + { + from: join(dirname(require.resolve('@chromatic-com/storybook/package.json')), 'assets'), + to: 'addon-visual-tests-assets', + }, + ], env: async ( env: Record, { configType }: { configType: 'DEVELOPMENT' | 'PRODUCTION' } diff --git a/src/screens/Authentication/AuthHeader.tsx b/src/screens/Authentication/AuthHeader.tsx index cdd4fee1..b7cf8ee1 100644 --- a/src/screens/Authentication/AuthHeader.tsx +++ b/src/screens/Authentication/AuthHeader.tsx @@ -34,13 +34,10 @@ export const AuthHeader = ({ onBack }: AuthHeaderProps) => ( trigger="hover" tooltip={} > - - + + + + diff --git a/src/screens/Authentication/Authentication.stories.tsx b/src/screens/Authentication/Authentication.stories.tsx index 221f7c72..2cdf6773 100644 --- a/src/screens/Authentication/Authentication.stories.tsx +++ b/src/screens/Authentication/Authentication.stories.tsx @@ -71,7 +71,7 @@ export const SignIn = { ), play: playAll(async ({ canvasElement }) => { const button = await findByRole(canvasElement, 'button', { - name: 'Enable', + name: /Get started/, }); await userEvent.click(button); }), diff --git a/src/screens/Authentication/SetSubdomain.tsx b/src/screens/Authentication/SetSubdomain.tsx index c0f7bf38..65b5317b 100644 --- a/src/screens/Authentication/SetSubdomain.tsx +++ b/src/screens/Authentication/SetSubdomain.tsx @@ -4,13 +4,24 @@ import { styled } from 'storybook/theming'; import { Container } from '../../components/Container'; import { Heading } from '../../components/Heading'; -import { LinkIcon } from '../../components/icons/LinkIcon'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; +import { ChromaticIcon } from '../../components/icons/ChromaticIcon'; import { Screen } from '../../components/Screen'; +import { Stack as BaseStack } from '../../components/Stack'; import { SuffixInput } from '../../components/SuffixInput'; import { Text } from '../../components/Text'; import { AuthHeader } from './AuthHeader'; +const Stack = styled(BaseStack)({ + alignSelf: 'stretch', +}); + +const Icon = styled(ChromaticIcon)({ + width: 40, + height: 40, + filter: 'drop-shadow(0 2px 5px rgba(0, 0, 0, 0.1))', + marginBottom: 10, +}); + const Form = styled.form({ position: 'relative', display: 'flex', @@ -58,29 +69,30 @@ export const SetSubdomain = ({ onBack, onSignIn }: SetSubdomainProps) => { -
- - -
- Sign in with SSO - Enter your team's Chromatic URL. -
- - - Continue - - + +
+ + Sign in with SSO + Enter your team's Chromatic URL. +
+
+ + + Continue + + +
); diff --git a/src/screens/Authentication/SignIn.tsx b/src/screens/Authentication/SignIn.tsx index d7a09910..e3a2fc6d 100644 --- a/src/screens/Authentication/SignIn.tsx +++ b/src/screens/Authentication/SignIn.tsx @@ -1,11 +1,11 @@ import React from 'react'; +import { styled } from 'storybook/theming'; import { Button } from '../../components/Button'; import { ButtonStack } from '../../components/ButtonStack'; import { Container } from '../../components/Container'; import { Heading } from '../../components/Heading'; -import { LinkIcon } from '../../components/icons/LinkIcon'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; +import { ChromaticIcon } from '../../components/icons/ChromaticIcon'; import { Screen } from '../../components/Screen'; import { Stack } from '../../components/Stack'; import { Text } from '../../components/Text'; @@ -17,14 +17,20 @@ interface SignInProps { onSignInWithSSO: () => void; } +const Icon = styled(ChromaticIcon)({ + width: 40, + height: 40, + filter: 'drop-shadow(0 2px 5px rgba(0, 0, 0, 0.1))', + marginBottom: 10, +}); + export const SignIn = ({ onBack, onSignIn, onSignInWithSSO }: SignInProps) => ( - +
- - + Sign in to begin visual testing Pinpoint bugs instantly by connecting with cloud browsers that run visual tests in diff --git a/src/screens/Authentication/Welcome.tsx b/src/screens/Authentication/Welcome.tsx index b6db5224..87d94c9a 100644 --- a/src/screens/Authentication/Welcome.tsx +++ b/src/screens/Authentication/Welcome.tsx @@ -1,44 +1,153 @@ +import { ChevronDownIcon } from '@storybook/icons'; import React from 'react'; +import { styled } from 'storybook/theming'; import { Button } from '../../components/Button'; -import { ButtonStack } from '../../components/ButtonStack'; -import { Container } from '../../components/Container'; -import { Heading } from '../../components/Heading'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; +import { Container as BaseContainer } from '../../components/Container'; +import { Link } from '../../components/design-system'; +import { Heading as BaseHeading } from '../../components/Heading'; +import { ChromeIcon } from '../../components/icons/ChromeIcon'; +import { FirefoxIcon } from '../../components/icons/FirefoxIcon'; +import { SafariIcon } from '../../components/icons/SafariIcon'; import { Screen } from '../../components/Screen'; import { Stack } from '../../components/Stack'; import { Text } from '../../components/Text'; -import { AuthHeader } from './AuthHeader'; interface WelcomeProps { onNext: () => void; onUninstall: () => void; } +const Heading = styled(BaseHeading)(({ theme }) => ({ + fontSize: theme.typography.size.s3, + '@container (min-width: 800px)': { + fontSize: theme.typography.size.m1, + }, +})); + +const Container = styled(BaseContainer)({ + alignItems: 'flex-start', + justifyContent: 'flex-start', + padding: '30px 30px 0 30px', + gap: 30, + '@container (min-width: 800px)': { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row-reverse', + padding: '20px 40px', + gap: 40, + }, +}); + +const Footer = styled.div({ + display: 'flex', + gap: 8, + alignItems: 'center', + justifyContent: 'center', + height: 40, +}); + +const ButtonRow = styled.div({ + display: 'flex', + gap: 8, +}); + +const CTA = styled.div({ + display: 'flex', + flexDirection: 'column', + gap: 8, +}); + +const VideoContainer = styled.div(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + maxWidth: 400, + overflow: 'hidden', + backgroundColor: 'white', + outline: `1px solid ${theme.appBorderColor}`, + borderRadius: 8, + video: { + margin: 4, + width: 'calc(100% - 8px)', + aspectRatio: '400/220', + }, +})); + +const Info = styled.div({ + display: 'flex', + flexDirection: 'row', + gap: 8, + justifyContent: 'space-between', + alignItems: 'center', + borderTop: `1px solid rgba(38, 85, 115, 0.15)`, + padding: '8px 15px 8px 10px', + color: '#5C6870', + fontSize: '13px', + pointerEvents: 'none', + '& > div': { + display: 'flex', + gap: 8, + }, + span: { + display: 'inline-flex', + alignItems: 'center', + gap: 5, + padding: '0 5px', + }, +}); + export const Welcome = ({ onNext, onUninstall }: WelcomeProps) => { return ( - - + - +
- - Visual tests - - Catch bugs in UI appearance automatically. Compare image snapshots to detect visual - changes. + Visual tests in Storybook + + Pinpoint visual bugs across browsers, viewports, and themes using Chromatic.
- - - - + + + + + + + No credit card required + +
+ + + + Testing 97/248 stories... +
+ + Light mode + + + + + +
+
+
+
+ Not interested? + onUninstall()}>Uninstall this addon +
); }; diff --git a/src/screens/Errors/GitError.stories.tsx b/src/screens/Errors/GitError.stories.tsx new file mode 100644 index 00000000..a36b805b --- /dev/null +++ b/src/screens/Errors/GitError.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { GitError } from './GitError'; + +const meta = { + component: GitError, + args: { + gitInfoError: new Error('Chromatic only works from inside a Git repository.'), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const NoCommit: Story = { + args: { + gitInfoError: new Error('Chromatic requires your Git repository to have at least one commit.'), + }, +}; diff --git a/src/screens/Errors/GitError.tsx b/src/screens/Errors/GitError.tsx new file mode 100644 index 00000000..2792ed07 --- /dev/null +++ b/src/screens/Errors/GitError.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { keyframes, styled } from 'storybook/theming'; + +import { Container } from '../../components/Container'; +import { Link } from '../../components/design-system'; +import { Heading } from '../../components/Heading'; +import { CheckIcon } from '../../components/icons/CheckIcon'; +import { ChevronRightIcon } from '../../components/icons/ChevronRightIcon'; +import { Screen } from '../../components/Screen'; +import { Stack } from '../../components/Stack'; +import { Text } from '../../components/Text'; +import { useTelemetry } from '../../utils/TelemetryContext'; + +const horizontalShake = keyframes` + 0% { + transform: translate(0, 0); + } + 4.41177% { + transform: translate(3px, 0); + } + 8.82353% { + transform: translate(0, 0); + } + 13.23529% { + transform: translate(3px, 0); + } + 17.64706% { + transform: translate(0, 0); + } + 22.05882% { + transform: translate(3px, 0); + } + 26.47059% { + transform: translate(0, 0); + } + 100% { + transform: translate(0, 0); + } +`; + +const Check = styled(CheckIcon)(({ theme }) => ({ + background: + theme.base === 'dark' + ? `color-mix(in srgb, ${theme.color.positive}, transparent 50%)` + : theme.color.positive, + color: 'white', + width: 20, + height: 20, + padding: 4, + borderRadius: '50%', +})); + +const Step = styled(ChevronRightIcon)(({ theme }) => ({ + background: theme.background.hoverable, + color: theme.color.secondary, + width: 20, + height: 20, + padding: 4, + borderRadius: '50%', + animation: `${horizontalShake} 3.72s ease infinite`, + transformOrigin: '50% 50%', +})); + +export const CheckList = styled.ul({ + textAlign: 'left', + listStyleType: 'none', + margin: 0, + padding: 0, + li: { + display: 'flex', + gap: 10, + padding: 10, + lineHeight: '20px', + }, +}); + +const Instructions = styled.div(({ theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: 8, + pre: { + margin: 0, + padding: '10px 12px', + fontSize: '12px', + background: theme.background.content, + border: `1px solid ${theme.appBorderColor}`, + borderRadius: 4, + }, +})); + +export const GitError = ({ gitInfoError }: { gitInfoError?: Error }) => { + const hasGitRepo = gitInfoError?.message.includes('one commit'); + useTelemetry('Errors', hasGitRepo ? 'GitError' : 'GitNotFound'); + return ( + + + +
+ Set up a Git repository + + Chromatic requires Git to associate test results with commits and branches. Run these + steps to get started: + +
+ +
  • + {hasGitRepo ? : } + + Initialize a Git repository + {!hasGitRepo &&
    git init
    } +
    +
  • +
  • + + + Stage all files +
    git add .
    +
    +
  • +
  • + + + Commit the changes +
    git commit -m "Initial commit"
    +
    +
  • +
    + + Visual tests requirements + +
    +
    +
    + ); +}; diff --git a/src/screens/Errors/GitNotFound.stories.tsx b/src/screens/Errors/GitNotFound.stories.tsx deleted file mode 100644 index 715376e3..00000000 --- a/src/screens/Errors/GitNotFound.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import { GitNotFound } from './GitNotFound'; - -const meta = { - component: GitNotFound, - args: { - gitInfoError: new Error('Git info not found'), - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/src/screens/Errors/GitNotFound.tsx b/src/screens/Errors/GitNotFound.tsx deleted file mode 100644 index 26333d69..00000000 --- a/src/screens/Errors/GitNotFound.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; - -import { Box, BoxList } from '../../components/Box'; -import { Code } from '../../components/Code'; -import { Container } from '../../components/Container'; -import { Link } from '../../components/design-system'; -import { Heading } from '../../components/Heading'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; -import { Screen } from '../../components/Screen'; -import { Stack } from '../../components/Stack'; -import { Text } from '../../components/Text'; -import { useTelemetry } from '../../utils/TelemetryContext'; -import { useUninstallAddon } from '../Uninstalled/UninstallContext'; - -export const GitNotFound = () => { - useTelemetry('Errors', 'GitNotFound'); - const { uninstallAddon } = useUninstallAddon(); - return ( - - - -
    - - Setup a Git repository - - Chromatic requires Git to associate test results with commits and branches. - -
    - - - Run these steps to get started: - -
  • - git init -
  • -
  • - git add . -
  • -
  • - git commit -m "Initial commit" -
  • -
    -
    -
    - - Visual tests requirements - - - uninstallAddon()}> - Uninstall - -
    -
    -
    - ); -}; diff --git a/src/screens/NoDevServer/NoDevServer.tsx b/src/screens/NoDevServer/NoDevServer.tsx index 0891831c..36530e9b 100644 --- a/src/screens/NoDevServer/NoDevServer.tsx +++ b/src/screens/NoDevServer/NoDevServer.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Code } from '../../components/Code'; import { Container } from '../../components/Container'; import { Heading } from '../../components/Heading'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; import { Screen } from '../../components/Screen'; import { Stack } from '../../components/Stack'; import { Text } from '../../components/Text'; @@ -14,7 +13,6 @@ export const NoDevServer = () => {
    - Visual tests Visual tests only runs locally. To test this Storybook, clone it to your machine and diff --git a/src/screens/Onboarding/InitialBuild.tsx b/src/screens/Onboarding/InitialBuild.tsx index 3da35abb..e2230674 100644 --- a/src/screens/Onboarding/InitialBuild.tsx +++ b/src/screens/Onboarding/InitialBuild.tsx @@ -1,11 +1,10 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { BuildProgressInline } from '../../components/BuildProgressBarInline'; import { Button } from '../../components/Button'; import { ButtonStack } from '../../components/ButtonStack'; import { Container } from '../../components/Container'; import { Heading } from '../../components/Heading'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; import { Screen } from '../../components/Screen'; import { Stack } from '../../components/Stack'; import { Text } from '../../components/Text'; @@ -14,7 +13,6 @@ import { useTelemetry } from '../../utils/TelemetryContext'; const Intro = () => (
    - Get started with visual testing Take an image snapshot of your stories to save their "last known good state" as test diff --git a/src/screens/Uninstalled/Uninstalled.tsx b/src/screens/Uninstalled/Uninstalled.tsx index 5a1aee30..9030ed17 100644 --- a/src/screens/Uninstalled/Uninstalled.tsx +++ b/src/screens/Uninstalled/Uninstalled.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Container } from '../../components/Container'; import { Heading } from '../../components/Heading'; -import { VisualTestsIcon } from '../../components/icons/VisualTestsIcon'; import { Screen } from '../../components/Screen'; import { Stack } from '../../components/Stack'; import { Text } from '../../components/Text'; @@ -15,7 +14,6 @@ export const Uninstalled = () => {
    - Uninstall complete Visual tests will vanish the next time you restart your Storybook.