/* eslint-disable no-console */ import { HappyEmoji } from '@transferwise/icons'; import { ThemeProvider } from '@wise/components-theming'; import React from 'react'; import { Sentiment, Size, Theme, Variant } from '../common'; import { render, cleanup, screen, userEvent as user, fireEvent, mockMatchMedia, } from '../test-utils'; import Alert, { AlertAction, AlertArrowPosition, AlertType } from './Alert'; jest.mock('react', () => { const originReact = jest.requireActual('react'); const mUseReference = jest.fn(); return { ...originReact, useRef: mUseReference, }; }); mockMatchMedia(); describe('Alert', () => { let component: HTMLElement; let container: HTMLElement; let alert: HTMLElement; let closeButton: HTMLElement; let action: AlertAction; const userEvent = user.setup({ advanceTimers: jest.advanceTimersByTimeAsync, }); const classForType = (type: AlertType) => `alert-${type}`; const message = 'Your card is on its way.'; const origWarn = console.warn; let mockedWarn: jest.Mock; beforeAll(() => { mockedWarn = jest.fn(); console.warn = mockedWarn; }); afterEach(() => { jest.clearAllMocks(); }); afterAll(() => { console.warn = origWarn; }); describe('defaults', () => { beforeEach(() => { container = render( , ).container; component = screen.getByTestId('alert'); }); it('the message is rendered', () => { expect(screen.getByText(message)).toBeInTheDocument(); }); it('will be of type neutral', () => { expect(component).toHaveClass(classForType(Sentiment.NEUTRAL)); expect(screen.getByTestId('info-icon')).toBeInTheDocument(); }); it('is not dismissible', () => { expect(container.querySelector('button')).not.toBeInTheDocument(); }); it('has no arrow', () => { expect(component).not.toHaveClass('arrow'); }); }); describe('deprecated props', () => { it('renders arrows but logs a warning', () => { render(); component = screen.getByTestId('alert'); expect(component).toHaveClass('arrow'); expect(component).toHaveClass('arrow-bottom'); expect(mockedWarn).toHaveBeenCalledWith( expect.stringContaining( "Alert component doesn't support 'arrow' anymore, use 'InlinePrompt' instead.", ), ); }); it('renders children but logs a warning', () => { render({message}); expect(screen.getByText(message)).toBeInTheDocument(); expect(mockedWarn).toHaveBeenCalledWith( expect.stringContaining( "Alert component doesn't support 'children' anymore, use 'message' instead.", ), ); }); it('dismissible is ignored and a warning is logged', () => { ({ container } = render()); expect(container.querySelector('button')).not.toBeInTheDocument(); expect(mockedWarn).toHaveBeenCalledWith( expect.stringContaining( "Alert component doesn't support 'dismissible' anymore, use 'onDismiss' instead.", ), ); }); it('size is ignored and a warning is logged', () => { const { container: small } = render(); const { container: large } = render(); expect(small.innerHTML).toStrictEqual(large.innerHTML); expect(mockedWarn).toHaveBeenCalledWith( expect.stringContaining( "Alert component doesn't support 'size' anymore, please remove that prop.", ), ); }); it('maps type SUCCESS to type POSITIVE and logs a warning', () => { render(); const success = screen.getByTestId('alert'); expect(success).toHaveClass(classForType(Sentiment.POSITIVE)); expect(screen.getByTestId('check-icon')).toBeInTheDocument(); expect(mockedWarn).toHaveBeenCalledWith( "Alert component has deprecated 'success' value for the 'type' prop. Please use 'positive' instead.", ); }); it('maps type INFO to type NEUTRAL and logs a warning', () => { render(); const info = screen.getByTestId('alert'); expect(info).toHaveClass(classForType(Sentiment.NEUTRAL)); expect(screen.getByTestId('info-icon')).toBeInTheDocument(); expect(mockedWarn).toHaveBeenCalledWith( "Alert component has deprecated 'info' value for the 'type' prop. Please use 'neutral' instead.", ); }); }); it('maps type ERROR to type NEGATIVE and logs a warning', () => { render(); const error = screen.getByTestId('alert'); expect(error).toHaveClass(classForType(Sentiment.NEGATIVE)); expect(screen.getByTestId('cross-icon')).toBeInTheDocument(); expect(mockedWarn).toHaveBeenCalledWith( expect.stringContaining( "Alert component has deprecated 'error' value for the 'type' prop. Please use 'negative' instead.", ), ); }); describe('action', () => { it('sets text and href', () => { action = { href: 'fluffykittens.com', text: 'Learn more', }; render(); const element = screen.getByText(action.text?.toString() ?? ''); expect(element).toHaveAttribute('href', action.href); expect(element).not.toHaveAttribute('aria-label'); expect(element).not.toHaveAttribute('target'); }); it('sets text and onClick', async () => { action = { onClick: jest.fn(), text: 'Learn more', }; render(); await userEvent.click(screen.getByText('Learn more')); expect(action.onClick).toHaveBeenCalled(); }); it('adds additional attributes', () => { action = { 'aria-label': 'Learn more about fluffy kittens', href: 'fluffykittens.com', text: 'Learn more', target: '_blank', }; render(); const element = screen.getByText(action.text?.toString() ?? ''); expect(element).toHaveAttribute('aria-label', action['aria-label']); expect(element).toHaveAttribute('target', action.target); }); }); describe('markdown support', () => { const input = 'That is one **bold cat**'; const output = 'That is one bold cat'; it('converts message to markdown', () => { render(); expect(screen.getByTestId('alert')).toContainHTML(output); }); it('does not convert children to markdown', () => { render({input}); expect(screen.getByText(input)).toBeInTheDocument(); expect(screen.getByTestId('alert')).not.toContainHTML(output); }); }); describe('className', () => { it('applies provided classes', () => { render(); expect(screen.getByTestId('alert')).toHaveClass('cats'); }); }); describe('custom icon', () => { it('uses any provided icon in preference to the default', () => { const icon = ; render(); component = screen.getByTestId('alert'); expect(screen.getByTestId('happy-emoji-icon')).toBeInTheDocument(); }); }); describe('StatusIcon label override', () => { it('should accept the accessible name override for the icon', () => { const customIconLabel = 'Custom icon label'; render( , ); expect(screen.getByLabelText(customIconLabel)).toBeInTheDocument(); }); }); describe('onDismiss', () => { it('renders the close button if onDismiss is provided', async () => { render(); const button = await screen.findByRole('button', { name: 'Close' }); expect(button).toBeEnabled(); }); it('calls onDismiss when the close button is clicked', async () => { const onDismiss = jest.fn(); render(); const button = await screen.findByRole('button', { name: 'Close' }); await userEvent.click(button); expect(onDismiss).toHaveBeenCalledTimes(1); }); }); describe('types', () => { const getComponentWithType = (type: AlertType) => { render(); return screen.getByTestId('alert'); }; it('renders neutral', () => { component = getComponentWithType(Sentiment.NEUTRAL); expect(component).toHaveClass(classForType(Sentiment.NEUTRAL)); expect(screen.getByTestId('info-icon')).toBeInTheDocument(); }); it('renders positive', () => { component = getComponentWithType(Sentiment.POSITIVE); expect(component).toHaveClass(classForType(Sentiment.POSITIVE)); expect(screen.getByTestId('check-icon')).toBeInTheDocument(); }); it('renders negative', () => { component = getComponentWithType(Sentiment.NEGATIVE); expect(component).toHaveClass(classForType(Sentiment.NEGATIVE)); expect(screen.getByTestId('cross-icon')).toBeInTheDocument(); }); it('renders warning', () => { component = getComponentWithType(Sentiment.WARNING); expect(component).toHaveClass(classForType(Sentiment.WARNING)); expect(screen.getByTestId('alert-icon')).toBeInTheDocument(); }); it('renders error alerts with aria-role alert', () => { component = getComponentWithType(Sentiment.NEGATIVE); expect(screen.getByRole('alert')).toBeInTheDocument(); }); it('renders neutral alerts with aria-role status', () => { component = getComponentWithType(Sentiment.NEUTRAL); expect(screen.getByRole('status')).toBeInTheDocument(); }); }); describe('on touch devices', () => { const { location } = window; beforeAll(() => { jest.spyOn(window, 'open').mockImplementation(); // @ts-expect-error value gets set right after its deletion delete window.location; // @ts-expect-error TypeScript doesn't allow assigning a plain object to window.location window.location = { ...window.location, assign: jest.fn(), }; }); afterAll(() => { // @ts-expect-error TypeScript doesn't allow assigning a plain object to window.location window.location = location; }); describe('when target is not blank', () => { beforeEach(() => { action = { 'aria-label': 'Learn more about fluffy kittens', href: '/test', text: 'Learn more', }; render(); alert = screen.getByTestId('alert'); closeButton = screen.getByLabelText('Close'); jest.spyOn(React, 'useRef').mockReturnValue({ current: closeButton, }); }); it('loads action on tap', () => { fireEvent.touchStart(alert); expect(window.location.assign).not.toHaveBeenCalled(); fireEvent.touchEnd(alert); expect(window.location.assign).toHaveBeenCalledWith(action.href); }); it('doesn`t redirect on touch move', () => { fireEvent.touchStart(alert); expect(window.location.assign).not.toHaveBeenCalled(); fireEvent.touchMove(alert); expect(window.location.assign).not.toHaveBeenCalled(); fireEvent.touchEnd(alert); expect(window.location.assign).not.toHaveBeenCalled(); }); }); describe('when target is blank', () => { beforeEach(() => { action = { 'aria-label': 'Learn more about fluffy kittens', href: '/test', text: 'Learn more', target: '_blank', }; render(); alert = screen.getByTestId('alert'); closeButton = screen.getByLabelText('Close'); jest.spyOn(React, 'useRef').mockReturnValue({ current: closeButton, }); }); afterEach(() => { cleanup(); }); it('opens a new window on tap', () => { fireEvent.touchStart(alert); expect(window.open).not.toHaveBeenCalled(); fireEvent.touchEnd(alert); expect(window.open).toHaveBeenCalledWith(action.href); }); }); describe('when action is not provided', () => { beforeEach(() => { render(); alert = screen.getByTestId('alert'); closeButton = screen.getByLabelText('Close'); jest.spyOn(React, 'useRef').mockReturnValue({ current: closeButton, }); }); afterEach(() => { cleanup(); }); it('opens a new window on tap', () => { fireEvent.touchStart(alert); expect(window.open).not.toHaveBeenCalled(); fireEvent.touchEnd(alert); expect(window.open).not.toHaveBeenCalled(); }); }); it('when close button is clicked redirection is not triggered', () => { jest.spyOn(React, 'useRef').mockReturnValue({ current: closeButton, }); fireEvent.touchStart(closeButton); expect(window.open).not.toHaveBeenCalled(); fireEvent.touchEnd(closeButton); expect(window.open).not.toHaveBeenCalled(); }); }); describe('with personal theme', () => { it('renders StatusIcon for Personal theme', () => { render( , ); expect(screen.getByTestId('status-icon')).toBeInTheDocument(); }); }); describe('`active` prop backward compatibility', () => { it('should render wrapper and alert if `active` is unset', () => { render(); expect(screen.getByRole('status')).toBeInTheDocument(); expect(screen.getByText(message)).toBeInTheDocument(); }); }); it('should disable all focusable elements inside until announced', () => { action = { href: 'fluffykittens.com', text: 'Learn more', }; const { container } = render(); expect(screen.getByText(action.text as string)).toHaveAttribute('tabIndex', '-1'); expect(container.querySelector('button')).toHaveAttribute('tabIndex', '-1'); }); });