import { 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(' component', () => { const defaultProps = { children: 'Click me', pendingConfig, successConfig, errorConfig, } satisfies React.ComponentProps; let user: ReturnType; beforeEach(() => { // See https://github.com/testing-library/react-testing-library/issues/1195 (globalThis.jest as Record) = { advanceTimersByTime: vi.advanceTimersByTime.bind(vi), }; user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi), }); }); it('renders button properly', () => { render(); expect(screen.queryByRole('button')).toBeInTheDocument(); }); it('passes ref correctly', () => { const ref = createRef(); render(); const button = screen.getByRole('button'); expect(ref.current).toBe(button); }); it('calls onClick properly', async () => { const onClick = vi.fn(); render(); 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(); 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(); 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((res) => { resolve = res; }); render(); 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((res) => { resolve = res; }); render(); 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((res) => { resolve = res; }); render(); 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 ; }); it('should allow button props to be passed given as="button"', () => { // @ts-expect-no-error ; }); it('should not allow link props to be passed given as="button"', () => { // @ts-expect-error-next-line ; // Sanity check // @ts-expect-error-next-line ; }); it('should allow link props to be passed given as="a"', () => { // @ts-expect-no-error ; }); it('should not allow button props to be passed given as="a"', () => { // @ts-expect-error-next-line ; // Sanity check // @ts-expect-error-next-line Click me ; }); it('should not allow button props to be passed given as={MyButton}', () => { function MyButton() { return ; } // @ts-expect-error-next-line ; // Sanity check function MyCustomComponent({ as, ...otherProps }: { as: React.ElementType }) { const Component = as || 'div'; return ; } // @ts-expect-error-next-line ; }); it('should not allow invalid values for as', () => { // @ts-expect-error-next-line ; // Sanity check function MyCustomComponent({ as }: { as: React.ElementType }) { const Component = as || 'div'; return ; } // @ts-expect-error-next-line ; }); it('should allow sync HTMLButtonElement event handlers to be passed by default', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should allow async HTMLButtonElement event handlers to be passed by default', () => { async function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should not allow HTMLAnchorElement event handlers to be passed by default', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-error-next-line ; }); it('should allow sync HTMLButtonElement event handlers to be passed given as="button"', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should allow async HTMLButtonElement event handlers to be passed given as="button"', () => { async function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should not allow HTMLAnchorElement event handlers to be passed given as="button"', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-error-next-line ; // Sanity check // @ts-expect-error-next-line ; }); it('should allow sync HTMLAnchorElement event handlers to be passed given as="a"', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should allow async HTMLAnchorElement event handlers to be passed given as="a"', () => { async function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should not allow HTMLButtonElement event handlers to be passed given as="a"', () => { function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-error-next-line ; // Sanity check // @ts-expect-error-next-line Click me ; }); it('should allow sync HTMLButtonElement event handlers to be passed given as={MyButton} that handles onClick', () => { function MyButton({ onClick, }: { onClick: (event: React.MouseEvent) => void; }) { return ; } function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should allow async HTMLButtonElement event handlers to be passed given as={MyButton} that handles onClick', () => { function MyButton({ onClick, }: { onClick: (event: React.MouseEvent) => void; }) { return ; } async function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should allow HTMLAnchorElement event handlers to be passed given as={MyAnchor} that handles onClick', () => { function MyAnchor({ onClick, }: { onClick: (event: React.MouseEvent) => void; }) { return ( Click me ); } function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-no-error ; }); it('should not allow event handlers to be passed given as={MyButton} that does not handle onClick', () => { function MyAnchor() { return Click me; } function onClick(event: React.MouseEvent) { event.preventDefault(); } // @ts-expect-error-next-line ; // Sanity check // @ts-expect-error-next-line ; }); });