mattermost-mobile-upstream/app/components/markdown/inline_action_button/inline_action_button.test.tsx
Daniel Espino García 84ea188366
Mattermost Blocks (#9764)
* Mattermost Blocks

* Move block renderer to its own components folder

* Sync translation layer

* UX fixes

* Refactor button to use the button component

* Code review

* Fix CI

* Add animations to collapse blocks

* Add unit tests

* Add e2e tests and related fixes

* Address coderabbit comments

* Address UX comments

* Address feedback

* Address coderabbit comments

* Fix snapshots

* Sync with webapp translation

* Add node limit to button markdown node

* Address feedback

* i18n-extract

* Fix test

* Move image utils to the utils folder

* Remove unneeded style

* Revert build script change

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-06-29 18:42:16 +02:00

263 lines
8.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, fireEvent} from '@testing-library/react-native';
import React from 'react';
import {handleGotoLocation} from '@actions/remote/command';
import * as integrationActions from '@actions/remote/integrations';
import * as serverContext from '@context/server';
import {renderWithIntlAndTheme} from '@test/intl-test-helper';
import InlineActionButton from './inline_action_button';
jest.mock('@actions/remote/command');
jest.mock('@actions/remote/integrations');
jest.mock('@context/server', () => ({
...jest.requireActual('@context/server'),
useServerUrl: jest.fn(),
}));
const SERVER_URL = 'https://server.com';
const POST_ID = 'post1';
const VALID_HREF = 'mmaction://MxPlan?row=12';
beforeEach(() => {
jest.mocked(serverContext.useServerUrl).mockReturnValue(SERVER_URL);
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
describe('InlineActionButton', () => {
it('should render button with label for valid href', () => {
const {getByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
expect(getByTestId('inline_action_button')).toBeTruthy();
expect(getByText('Click me')).toBeTruthy();
});
it('should fall back to children when scheme is wrong', () => {
const {queryByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href='https://example.com'
postId={POST_ID}
baseTextStyle={null}
>
{'children-text'}
</InlineActionButton>,
);
expect(queryByTestId('inline_action_button')).toBeNull();
expect(getByText('children-text')).toBeTruthy();
});
it('should fall back when action id has invalid chars', () => {
const {queryByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href='mmaction://Mx-Plan'
postId={POST_ID}
baseTextStyle={null}
>
{'children-text'}
</InlineActionButton>,
);
expect(queryByTestId('inline_action_button')).toBeNull();
expect(getByText('children-text')).toBeTruthy();
});
it('should fall back when params exceed 2048 bytes', () => {
const {queryByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href={'mmaction://x?p=' + 'a'.repeat(2049)}
postId={POST_ID}
baseTextStyle={null}
>
{'children-text'}
</InlineActionButton>,
);
expect(queryByTestId('inline_action_button')).toBeNull();
expect(getByText('children-text')).toBeTruthy();
});
it('should fall back when postId is empty', () => {
const {queryByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href='mmaction://x?p=1'
postId=''
baseTextStyle={null}
>
{'children-text'}
</InlineActionButton>,
);
expect(queryByTestId('inline_action_button')).toBeNull();
expect(getByText('children-text')).toBeTruthy();
});
it('should dispatch postActionWithCookie with parsed query on press', async () => {
jest.mocked(integrationActions.postActionWithCookie).mockResolvedValue({data: {}});
const {getByTestId} = renderWithIntlAndTheme(
<InlineActionButton
href='mmaction://MxPlan?row=12&col=A'
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
fireEvent.press(getByTestId('inline_action_button'));
await act(async () => {
await new Promise((resolve) => process.nextTick(resolve));
});
expect(integrationActions.postActionWithCookie).toHaveBeenCalledWith(
SERVER_URL,
POST_ID,
'MxPlan',
'',
'',
{row: '12', col: 'A'},
'mm_block',
);
});
it('should not dispatch twice on rapid double press', async () => {
jest.mocked(integrationActions.postActionWithCookie).mockReturnValue(new Promise(() => {}));
const {getByTestId} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
const button = getByTestId('inline_action_button');
fireEvent.press(button);
fireEvent.press(button);
expect(integrationActions.postActionWithCookie).toHaveBeenCalledTimes(1);
});
it('should render error label when dispatch resolves with error', async () => {
jest.mocked(integrationActions.postActionWithCookie).mockResolvedValue({error: new Error('boom')});
const {getByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
fireEvent.press(getByTestId('inline_action_button'));
await act(async () => {
await new Promise((resolve) => process.nextTick(resolve));
});
expect(getByText('Action failed to execute.')).toBeTruthy();
});
it('should call handleGotoLocation when dispatch returns goto_location', async () => {
const gotoLocation = 'https://server.com/team/channels/town-square';
jest.mocked(integrationActions.postActionWithCookie).mockResolvedValue({
data: {goto_location: gotoLocation},
});
const {getByTestId} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
fireEvent.press(getByTestId('inline_action_button'));
await act(async () => {
await new Promise((resolve) => process.nextTick(resolve));
});
expect(handleGotoLocation).toHaveBeenCalledWith(
SERVER_URL,
expect.anything(),
gotoLocation,
);
});
it('should render timeout error when dispatch hangs past 15s', async () => {
jest.useFakeTimers({doNotFake: ['nextTick']});
jest.mocked(integrationActions.postActionWithCookie).mockReturnValue(new Promise(() => {}));
const {getByTestId, getByText} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
fireEvent.press(getByTestId('inline_action_button'));
await act(async () => {
jest.advanceTimersByTime(15000);
await new Promise((resolve) => process.nextTick(resolve));
});
expect(getByText('Action timed out. Try again.')).toBeTruthy();
});
it('should not setState after unmount', async () => {
jest.useFakeTimers({doNotFake: ['nextTick']});
jest.mocked(integrationActions.postActionWithCookie).mockReturnValue(new Promise(() => {}));
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const {getByTestId, unmount} = renderWithIntlAndTheme(
<InlineActionButton
href={VALID_HREF}
postId={POST_ID}
baseTextStyle={null}
>
{'Click me'}
</InlineActionButton>,
);
fireEvent.press(getByTestId('inline_action_button'));
unmount();
await act(async () => {
jest.advanceTimersByTime(15000);
await new Promise((resolve) => process.nextTick(resolve));
});
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});