Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/wojtekmaj/react-async-button. A button that handles Promises for your React app.
Something went wrong. Try again.
12 kB · 427 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428import { beforeEach, describe, expect, it, vi } from 'vitest';import React, { createRef } from 'react';import { act, render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';
import AsyncButton from './index.js';
vi.useFakeTimers();
const pendingConfig = { children: 'Loading…',};
const successConfig = { children: 'Success!',};
const errorConfig = { children: 'Try again',};
describe('<AsyncButton /> component', () => { const defaultProps = { children: 'Click me', pendingConfig, successConfig, errorConfig, } satisfies React.ComponentProps<typeof AsyncButton>;
let user: ReturnType<typeof userEvent.setup>; beforeEach(() => { // See https://github.com/testing-library/react-testing-library/issues/1195 (globalThis.jest as Record<string, unknown>) = { advanceTimersByTime: vi.advanceTimersByTime.bind(vi), };
user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi), }); });
it('renders button properly', () => { render(<AsyncButton {...defaultProps} />);
expect(screen.queryByRole('button')).toBeInTheDocument(); });
it('passes ref correctly', () => { const ref = createRef<HTMLButtonElement>();
render(<AsyncButton {...defaultProps} ref={ref} />);
const button = screen.getByRole('button');
expect(ref.current).toBe(button); });
it('calls onClick properly', async () => { const onClick = vi.fn();
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1); expect(onClick).toHaveBeenCalledWith(expect.any(Object)); });
it('changes button state to success on click if onClick is synchronous', async () => { const onClick = () => { // Intentionally empty };
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
const button2 = screen.getByRole('button'); expect(button2).toHaveTextContent('Success!'); });
it('changes button state to default after refresh timeout has passed', async () => { const onClick = () => { // Intentionally empty };
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
const button2 = screen.getByRole('button'); expect(button2).toHaveTextContent('Success!');
act(() => { vi.advanceTimersByTime(1000); });
const button3 = screen.getByRole('button'); expect(button3).toHaveTextContent('Success!');
act(() => { vi.advanceTimersByTime(1000); });
const button4 = screen.getByRole('button'); expect(button4).toHaveTextContent('Click me'); });
it('changes button state to pending on click if onClick is asynchronous', async () => { let resolve: () => void; const onClick = () => new Promise<void>((res) => { resolve = res; });
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
const button2 = screen.getByRole('button'); expect(button2).toHaveTextContent('Loading…');
await act(async () => { resolve(); }); });
it('changes button state to success after asynchronous onClick is resolved', async () => { let resolve: () => void; const onClick = () => new Promise<void>((res) => { resolve = res; });
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
const button2 = screen.getByRole('button'); expect(button2).toHaveTextContent('Loading…');
await act(async () => { resolve(); });
const button3 = screen.getByRole('button'); expect(button3).toHaveTextContent('Success!'); });
it('changes button state to default after refresh timeout has passed', async () => { let resolve: () => void; const onClick = () => new Promise<void>((res) => { resolve = res; });
render(<AsyncButton {...defaultProps} onClick={onClick} />);
const button = screen.getByRole('button');
await user.click(button);
const button2 = screen.getByRole('button'); expect(button2).toHaveTextContent('Loading…');
await act(async () => { resolve(); });
const button3 = screen.getByRole('button'); expect(button3).toHaveTextContent('Success!');
act(() => { vi.advanceTimersByTime(1000); });
const button4 = screen.getByRole('button'); expect(button4).toHaveTextContent('Success!');
act(() => { vi.advanceTimersByTime(1000); });
const button5 = screen.getByRole('button'); expect(button5).toHaveTextContent('Click me'); });
it('should allow button props to be passed by default', () => { // @ts-expect-no-error <AsyncButton {...defaultProps} type="submit" />; });
it('should allow button props to be passed given as="button"', () => { // @ts-expect-no-error <AsyncButton {...defaultProps} as="button" disabled />; });
it('should not allow link props to be passed given as="button"', () => { // @ts-expect-error-next-line <AsyncButton {...defaultProps} as="button" href="https://example.com" />;
// Sanity check // @ts-expect-error-next-line <button href="https://example.com"></button>; });
it('should allow link props to be passed given as="a"', () => { // @ts-expect-no-error <AsyncButton {...defaultProps} as="a" href="https://example.com" />; });
it('should not allow button props to be passed given as="a"', () => { // @ts-expect-error-next-line <AsyncButton {...defaultProps} as="a" disabled href="https://example.com" />;
// Sanity check // @ts-expect-error-next-line <a disabled href="https://example.com"> Click me </a>; });
it('should not allow button props to be passed given as={MyButton}', () => { function MyButton() { return <button type="submit"></button>; }
// @ts-expect-error-next-line <AsyncButton {...defaultProps} as={MyButton} type="submit" />;
// Sanity check function MyCustomComponent({ as, ...otherProps }: { as: React.ElementType }) { const Component = as || 'div'; return <Component {...otherProps} />; }
// @ts-expect-error-next-line <MyCustomComponent as={MyButton} type="submit" />; });
it('should not allow invalid values for as', () => { // @ts-expect-error-next-line <AsyncButton {...defaultProps} as={5} type="submit" />;
// Sanity check function MyCustomComponent({ as }: { as: React.ElementType }) { const Component = as || 'div'; return <Component />; }
// @ts-expect-error-next-line <MyCustomComponent as={5} />; });
it('should allow sync HTMLButtonElement event handlers to be passed by default', () => { function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} onClick={onClick} />; });
it('should allow async HTMLButtonElement event handlers to be passed by default', () => { async function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} onClick={onClick} />; });
it('should not allow HTMLAnchorElement event handlers to be passed by default', () => { function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-error-next-line <AsyncButton {...defaultProps} onClick={onClick} />; });
it('should allow sync HTMLButtonElement event handlers to be passed given as="button"', () => { function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as="button" onClick={onClick} />; });
it('should allow async HTMLButtonElement event handlers to be passed given as="button"', () => { async function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as="button" onClick={onClick} />; });
it('should not allow HTMLAnchorElement event handlers to be passed given as="button"', () => { function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-error-next-line <AsyncButton {...defaultProps} as="button" onClick={onClick} />;
// Sanity check // @ts-expect-error-next-line <button onClick={onClick}></button>; });
it('should allow sync HTMLAnchorElement event handlers to be passed given as="a"', () => { function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as="a" onClick={onClick} />; });
it('should allow async HTMLAnchorElement event handlers to be passed given as="a"', () => { async function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as="a" onClick={onClick} />; });
it('should not allow HTMLButtonElement event handlers to be passed given as="a"', () => { function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-error-next-line <AsyncButton {...defaultProps} as="a" onClick={onClick} />;
// Sanity check // @ts-expect-error-next-line <a href="https://example.com" onClick={onClick}> Click me </a>; });
it('should allow sync HTMLButtonElement event handlers to be passed given as={MyButton} that handles onClick', () => { function MyButton({ onClick, }: { onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; }) { return <button onClick={onClick} type="submit"></button>; }
function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as={MyButton} onClick={onClick} />; });
it('should allow async HTMLButtonElement event handlers to be passed given as={MyButton} that handles onClick', () => { function MyButton({ onClick, }: { onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; }) { return <button onClick={onClick} type="submit"></button>; }
async function onClick(event: React.MouseEvent<HTMLButtonElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as={MyButton} onClick={onClick} />; });
it('should allow HTMLAnchorElement event handlers to be passed given as={MyAnchor} that handles onClick', () => { function MyAnchor({ onClick, }: { onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void; }) { return ( <a href="https://example.com" onClick={onClick}> Click me </a> ); }
function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-no-error <AsyncButton {...defaultProps} as={MyAnchor} onClick={onClick} />; });
it('should not allow event handlers to be passed given as={MyButton} that does not handle onClick', () => { function MyAnchor() { return <a href="https://example.com">Click me</a>; }
function onClick(event: React.MouseEvent<HTMLAnchorElement>) { event.preventDefault(); }
// @ts-expect-error-next-line <AsyncButton {...defaultProps} as={MyAnchor} onClick={onClick} />;
// Sanity check // @ts-expect-error-next-line <MyAnchor onClick={onClick} />; });});