Add tests on top of ui/buttons, ui/links and ui/inputs (#342)

This commit is contained in:
Charles Bochet
2023-06-21 03:47:00 +02:00
committed by GitHub
parent e2eb40c1ea
commit e2d8c3a2ec
14 changed files with 284 additions and 61 deletions

View File

@ -2,11 +2,9 @@ import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
type OwnProps = { type OwnProps = {
label: string; children: React.ReactNode;
icon?: React.ReactNode;
fullWidth?: boolean; fullWidth?: boolean;
onClick?: () => void; } & React.ButtonHTMLAttributes<HTMLButtonElement>;
};
const StyledButton = styled.button<{ fullWidth: boolean }>` const StyledButton = styled.button<{ fullWidth: boolean }>`
align-items: center; align-items: center;
@ -24,29 +22,20 @@ const StyledButton = styled.button<{ fullWidth: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-weight: ${({ theme }) => theme.fontWeightBold}; font-weight: ${({ theme }) => theme.fontWeightBold};
gap: 8px; gap: ${({ theme }) => theme.spacing(2)};
justify-content: center; justify-content: center;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)}; padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')}; width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
`; `;
export function PrimaryButton({ export function PrimaryButton({
label, children,
icon,
fullWidth, fullWidth,
onClick, ...props
}: OwnProps): JSX.Element { }: OwnProps): JSX.Element {
return ( return (
<StyledButton <StyledButton fullWidth={fullWidth ?? false} {...props}>
fullWidth={fullWidth ?? false} {children}
onClick={() => {
if (onClick) {
onClick();
}
}}
>
{icon}
{label}
</StyledButton> </StyledButton>
); );
} }

View File

@ -2,10 +2,9 @@ import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
type OwnProps = { type OwnProps = {
label: string; children: React.ReactNode;
icon?: React.ReactNode;
fullWidth?: boolean; fullWidth?: boolean;
}; } & React.ButtonHTMLAttributes<HTMLButtonElement>;
const StyledButton = styled.button<{ fullWidth: boolean }>` const StyledButton = styled.button<{ fullWidth: boolean }>`
align-items: center; align-items: center;
@ -31,14 +30,13 @@ const StyledButton = styled.button<{ fullWidth: boolean }>`
`; `;
export function SecondaryButton({ export function SecondaryButton({
label, children,
icon,
fullWidth, fullWidth,
...props
}: OwnProps): JSX.Element { }: OwnProps): JSX.Element {
return ( return (
<StyledButton fullWidth={fullWidth ?? false}> <StyledButton fullWidth={fullWidth ?? false} {...props}>
{icon} {children}
{label}
</StyledButton> </StyledButton>
); );
} }

View File

@ -1,11 +0,0 @@
import styled from '@emotion/styled';
type OwnProps = {
text: string;
};
const StyledButton = styled.button``;
export function TertiaryButton({ text }: OwnProps): JSX.Element {
return <StyledButton>{text}</StyledButton>;
}

View File

@ -0,0 +1,33 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { IconButton } from '../IconButton';
const meta: Meta<typeof IconButton> = {
title: 'UI/Buttons/IconButton',
component: IconButton,
};
export default meta;
type Story = StoryObj<typeof IconButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<IconButton onClick={clickJestFn} icon={<IconArrowRight size={15} />} />,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};

View File

@ -0,0 +1,47 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconBrandGoogle } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { PrimaryButton } from '../PrimaryButton';
const meta: Meta<typeof PrimaryButton> = {
title: 'UI/Buttons/PrimaryButton',
component: PrimaryButton,
};
export default meta;
type Story = StoryObj<typeof PrimaryButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<PrimaryButton onClick={clickJestFn}>A Primary Button</PrimaryButton>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};
export const WithIcon: Story = {
render: getRenderWrapperForComponent(
<PrimaryButton>
<IconBrandGoogle size={16} stroke={4} />A Primary Button
</PrimaryButton>,
),
};
export const FullWidth: Story = {
render: getRenderWrapperForComponent(
<PrimaryButton fullWidth={true}>A Primary Button</PrimaryButton>,
),
};

View File

@ -0,0 +1,47 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconBrandGoogle } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { SecondaryButton } from '../SecondaryButton';
const meta: Meta<typeof SecondaryButton> = {
title: 'UI/Buttons/SecondaryButton',
component: SecondaryButton,
};
export default meta;
type Story = StoryObj<typeof SecondaryButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<SecondaryButton onClick={clickJestFn}>A Primary Button</SecondaryButton>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};
export const WithIcon: Story = {
render: getRenderWrapperForComponent(
<SecondaryButton>
<IconBrandGoogle size={16} stroke={4} />A Primary Button
</SecondaryButton>,
),
};
export const FullWidth: Story = {
render: getRenderWrapperForComponent(
<SecondaryButton fullWidth={true}>A Primary Button</SecondaryButton>,
),
};

View File

@ -4,7 +4,7 @@ import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import { textInputStyle } from '@/ui/layout/styles/themes'; import { textInputStyle } from '@/ui/layout/styles/themes';
import Link from '../../link/Link'; import { RawLink } from '../../links/RawLink';
import { EditableCell } from '../EditableCell'; import { EditableCell } from '../EditableCell';
type OwnProps = { type OwnProps = {
@ -50,7 +50,7 @@ export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
nonEditModeContent={ nonEditModeContent={
<div> <div>
{isValidPhoneNumber(inputValue) ? ( {isValidPhoneNumber(inputValue) ? (
<Link <RawLink
href={parsePhoneNumber(inputValue, 'FR')?.getURI()} href={parsePhoneNumber(inputValue, 'FR')?.getURI()}
onClick={(event: MouseEvent<HTMLElement>) => { onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation(); event.stopPropagation();
@ -58,9 +58,9 @@ export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
> >
{parsePhoneNumber(inputValue, 'FR')?.formatInternational() || {parsePhoneNumber(inputValue, 'FR')?.formatInternational() ||
inputValue} inputValue}
</Link> </RawLink>
) : ( ) : (
<Link href="#">{inputValue}</Link> <RawLink href="#">{inputValue}</RawLink>
)} )}
</div> </div>
} }

View File

@ -2,8 +2,9 @@ import { ChangeEvent, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
type OwnProps = { type OwnProps = {
initialValue: string; value: string;
onChange: (text: string) => void; onChange?: (text: string) => void;
placeholder?: string;
fullWidth?: boolean; fullWidth?: boolean;
}; };
@ -29,20 +30,23 @@ const StyledInput = styled.input<{ fullWidth: boolean }>`
`; `;
export function TextInput({ export function TextInput({
initialValue, value,
onChange, onChange,
placeholder,
fullWidth, fullWidth,
}: OwnProps): JSX.Element { }: OwnProps): JSX.Element {
const [value, setValue] = useState(initialValue); const [internalValue, setInternalValue] = useState(value);
return ( return (
<StyledInput <StyledInput
fullWidth={fullWidth ?? false} fullWidth={fullWidth ?? false}
value={value} value={internalValue}
placeholder="Email" placeholder={placeholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); setInternalValue(event.target.value);
onChange(event.target.value); if (onChange) {
onChange(event.target.value);
}
}} }}
/> />
); );

View File

@ -0,0 +1,52 @@
import { expect } from '@storybook/jest';
import { jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { TextInput } from '../TextInput';
const meta: Meta<typeof TextInput> = {
title: 'UI/Inputs/TextInput',
component: TextInput,
};
export default meta;
type Story = StoryObj<typeof TextInput>;
const changeJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<TextInput value="A good value " onChange={changeJestFn} />,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(changeJestFn).toHaveBeenCalledTimes(0);
const input = canvas.getByRole('textbox');
await userEvent.type(input, 'cou');
expect(changeJestFn).toHaveBeenNthCalledWith(1, 'A good value c');
expect(changeJestFn).toHaveBeenNthCalledWith(2, 'A good value co');
expect(changeJestFn).toHaveBeenNthCalledWith(3, 'A good value cou');
},
};
export const Placeholder: Story = {
render: getRenderWrapperForComponent(
<TextInput value="" onChange={changeJestFn} placeholder="Placeholder" />,
),
};
export const FullWidth: Story = {
render: getRenderWrapperForComponent(
<TextInput
value="A good value"
onChange={changeJestFn}
placeholder="Placeholder"
fullWidth
/>,
),
};

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Link as ReactLink } from 'react-router-dom';
import styled from '@emotion/styled';
type OwnProps = {
children: React.ReactNode;
href: string;
onClick?: () => void;
fullWidth?: boolean;
};
const StyledClickable = styled.div`
display: flex;
a {
color: ${({ theme }) => theme.text40};
font-size: ${({ theme }) => theme.fontSizeSmall};
text-decoration: none;
}
`;
export function PrimaryLink({ href, children, onClick }: OwnProps) {
return (
<StyledClickable>
<ReactLink onClick={onClick} to={href}>
{children}
</ReactLink>
</StyledClickable>
);
}

View File

@ -17,7 +17,7 @@ const StyledClickable = styled.div`
} }
`; `;
function Link({ href, children, onClick }: OwnProps) { export function RawLink({ href, children, onClick }: OwnProps) {
return ( return (
<StyledClickable> <StyledClickable>
<ReactLink onClick={onClick} to={href}> <ReactLink onClick={onClick} to={href}>
@ -26,5 +26,3 @@ function Link({ href, children, onClick }: OwnProps) {
</StyledClickable> </StyledClickable>
); );
} }
export default Link;

View File

@ -0,0 +1,37 @@
import { MemoryRouter } from 'react-router-dom';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { PrimaryLink } from '../PrimaryLink';
const meta: Meta<typeof PrimaryLink> = {
title: 'UI/Links/PrimaryLink',
component: PrimaryLink,
};
export default meta;
type Story = StoryObj<typeof PrimaryLink>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<MemoryRouter>
<PrimaryLink href="/test" onClick={clickJestFn}>
A primary link
</PrimaryLink>
</MemoryRouter>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const link = canvas.getByRole('link');
await userEvent.click(link);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};

View File

@ -45,19 +45,17 @@ export function Index() {
<Logo /> <Logo />
<Title title="Welcome to Twenty" /> <Title title="Welcome to Twenty" />
<StyledContentContainer> <StyledContentContainer>
<PrimaryButton <PrimaryButton fullWidth={true} onClick={onGoogleLoginClick}>
fullWidth={true} <IconBrandGoogle size={theme.iconSizeSmall} stroke={4} />
label="Continue With Google" Continue With Google
icon={<IconBrandGoogle size={theme.iconSizeSmall} stroke={4} />} </PrimaryButton>
onClick={onGoogleLoginClick}
/>
<HorizontalSeparator /> <HorizontalSeparator />
<TextInput <TextInput
initialValue="" initialValue=""
onChange={(value) => console.log(value)} onChange={(value) => console.log(value)}
fullWidth={true} fullWidth={true}
/> />
<SecondaryButton label="Continue" fullWidth={true} /> <SecondaryButton fullWidth={true}>Continue</SecondaryButton>
</StyledContentContainer> </StyledContentContainer>
<FooterNote> <FooterNote>
By using Twenty, you agree to the Terms of Service and Data Processing By using Twenty, you agree to the Terms of Service and Data Processing

View File

@ -2,12 +2,14 @@ import styled from '@emotion/styled';
const StyledLayout = styled.div` const StyledLayout = styled.div`
background: ${(props) => props.theme.primaryBackground}; background: ${(props) => props.theme.primaryBackground};
border: 1px solid ${(props) => props.theme.lightBorder};
border-radius: 5px; border-radius: 5px;
box-shadow: 0px 0px 2px;
display: flex;
display: flex;
flex-direction: row; flex-direction: row;
height: fit-content; height: fit-content;
min-width: 300px;
padding: 20px; padding: 20px;
width: fit-content; width: fit-content;
`; `;