Add tests on top of ui/buttons, ui/links and ui/inputs (#342)
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
|
||||||
}
|
|
||||||
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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>,
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -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>,
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
};
|
||||||
29
front/src/modules/ui/components/links/PrimaryLink.tsx
Normal file
29
front/src/modules/ui/components/links/PrimaryLink.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user