MM-45255 Update web app to React 18 (#33858)

* Dependencies: Remove unused dependency on @mattermost/dynamic-virtualized-list

* Update Components package to React 18 and new version of RTL

* Upgrade React, React Redux, RTL, and associated libraries

I had to upgrade React Redux for the new version of React, and that
brought with it new versions of associated packges (Redux, Reselect,
Redux Thunk). A few other libraries needed to be updated or have their
explicit dependencies overridden for the new version of Redux as well.

To note for future dependency upgrades, redux-mock-store is deprecated,
and redux-batched-actions and redux-persist are inactive.

For RTL:
1. `@testing-library/react-hooks` has been rolled into
   `@testing-library/react`, and its interface has changed.
2. I had to make some changes to get TS to use the types for the new
   methods added to `expect`.

* Dependencies: Fix dom-accessibility-api patch from #33553

* Tests: Fix tests that use jest.spyOn with react-redux

* Functional: Remove usage of defaultProps on function components

* Tests: Remove usage of react-dom/test-utils

* Functional: Rename conflicting context prop on Apps components

* Tests: Always await on userEvent calls

* Functional: Patch react-overlays to fix pressing escape in unit tests

I did something similar in React Bootstrap a few weeks ago.

See https://github.com/mattermost/react-bootstrap/pull/5

* Tests: Prevent tests from fetching from real URLs

* Tests: Update snapshots changed by upgrading react-redux and styled-components

* Functional: Stop passing deprecated pure parameter to connect

* Tests: Change how we intercept console errors in tests to preserve stack traces

* Tests: Fix incorrect usage of act in Enzyme tests

These tests are a mix of:
1. Not calling act when performing something that will update the DOM (like clicking on a button or invoking a method) which either caused warnings or failed snapshots as changes weren't visible.
2. Calling act in weird ways (such as wrapping mount in an async act) which caused Enzyme to not function

Some of these changes just silence warnings, but most of them are required to make the test even run

* Tests: Fix incorrect usage of act in RTL tests

* Tests: Fix a few minor issues in tests

* Functional: Add note for why we're not using ReactDOM.createRoot

* Functional: Fix focus trap infinite recursion in test

* Types: Replace removed React.SFC

* Types: Fix type of functions in FormattedMessage values prop

* Functional: Fix DropdownInputHybrid placeholder

* Types: Patch type definitions of react-batched-actions

* Types: Fix mattermost-redux build failing due to type check in Redux Dev Tools

* Dependencies: Add type definitions for react-is

* Types: Update types around ReactNode and ReactElement

React's gotten more strict with these, so we need to be more careful with what
we return from a render method. In some of these places, we also misused some
types, so hopefully I've sorted those out.

* Types: Explicitly added types to all instances of useCallback

* Types/Tests: Update typing of Redux actions and hooks

useDispatch is sort of stricter now, but it doesn't seem to rely on the global type of `Dispatch` any more, so I ended up having to add an extra overload to make that work.

* Tests: Update new tests in useChannelSystemPolicies for new version of RTL

These were added on master after I updated RTL on the branch

* Tests: Update action used to test initial store state

* Functional: Remove remnants of code for hot reloading Redux store

* Types/Tests: Update typing around React Router

* Types/Functional: Update typing involving the FormattedMessage values prop

There's a couple functional changes to ensure that the value passed is either a valid string or Date

* Types: Misc fixes

* Functional: Don't pass unused props to ChannelHeader

* Functional: Ensure plugin setting text is rendered before passing to Setting component

The previous version might've allowed MessageDescriptors to be passed unformatted
into the Setting component (which would then be rendered in the DOM). As best as
I can tell, we never actually did that, so this shouldn't change anything in practice.

* Tests: Make tests for identifyElementRegion more reliable

* Tests: Update recent tests for new version of RTL

* E2E: Make editLastPostWithNewMessage more reliable

* Downgrade React to 18.2.0 and manually dedupe React versions

18.2.0 and 18.3.0 are nearly identical to one another, except 18.3.x includes
warnings when using any API that will be removed in React 19. I don't want to
flood the console and test logs with warnings for things we're not addressing
for the time being.

Also, they didn't export act from React itself until 18.3.1 for some reason
(despite the old import path printing a warning), so I needed to revert the
changes to its import path.

To get this all to work, for some reason, I had to manually delete all the
entries for `react` and `react-dom` from the lockfile to get NPM to use a
single version of React and ReactDOM everywhere. I did discover `npm dedupe`
in the process, but that didn't solve this problem where I was trying to
consistently downgrade everything.

* Revert changes to notice file build tool to speed up CI

* Add explicit version of types/scheduler

The version of `@types/react` that we use says it works with any version of
`@types/scheduler` which causes NPM to install a newer version of it which
is missing a file of types that it needs.

* Update new test to await on userEvent

* Fix Playwright test that relied on autogenerated class name

* Tests: Disable test for identifyElementRegion

* Functional: Change DynamicVirtualizedList ListItem to use useLayoutEffect

In a previous commit, I changed the RHS to use the monorepo
DynamicVirtualizedList instead of the old version that lived in its own repo.
That caused the RHS to no longer scroll to the bottom on initial mount or be
able to render additional pages (even though the posts are loaded). This seems
like it has to do with the improved size calculation that Zubair made because
that's the main difference in the monorepo version of that component.

For some reason I don't entirely understand, changing to useLayoutEffect seems
to fix that. I think that's because the old measurement code is written as a
class component, and useLayoutEffect fires at the same time as
componentDidMount/componentDidUpdate.

* Types: Revert some type changes to reduce test log output

* Functional: Fix places where useSelector returned new results when called with the same arguments

I wasn't planning on fixing this now, but I think the increased length of the warning logs in the tests are causing
the GitHub action for the unit tests to abort as it reaches around 10000 lines long.

* Tests: Fix place where mocked selector returned new results when called with the same arguments

Same reason as before, but this one only occurred because of a test's mocked selector. I replaced it with
a real one to get around that.

* Tests: Fail tests when selector returns new results when called with the same arguments

* Attempt to fix web app unit tests failing in CI

* Change CI tests to set workerIdleMemoryLimit instead of reducing maxWorkers

* Increase workerIdleMemoryLimit in CI

* Revert changes to test-ci command and revert changes to how unit test logs are collected

* Unrevert changes to test logging, re-add workerIdleMemoryLimit, and try disabling test coverage

* Actually disable coverage

* Fix flaky test

* Update a couple new tests to fit PR and remove an unnecessary act

* Replace bad mock in new unit test

* Fix types of new code

* Remove mock from new unit test
This commit is contained in:
Harrison Healey 2025-10-07 11:11:12 -04:00 committed by GitHub
parent ff3cc4e837
commit 535d93ee98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
344 changed files with 3259 additions and 4192 deletions

View file

@ -71,7 +71,7 @@ function editLastPostWithNewMessage(message: string) {
// # Update the post message and click Save
cy.get('#edit_textbox').clear().type(message)
cy.get('#create_post').findByText('Save').should('be.visible').click();
cy.get('#create_post').findByText('Save').scrollIntoView().click();
}
Cypress.Commands.add('editLastPostWithNewMessage', editLastPostWithNewMessage);

View file

@ -20,12 +20,12 @@ export default class MessagePriority {
// Priority menu that opens when clicking the icon
this.priorityMenu = container.locator('[role="menu"]');
// Standard priority option in the menu (id comes from webapp implementation)
this.standardPriorityOption = this.priorityMenu.locator('#menu-item-priority-standard');
// Priority dialog elements
this.priorityDialog = container.page().getByRole('menu');
this.dialogHeader = container.page().locator('h4.modal-title');
// Standard priority option in the menu
this.standardPriorityOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
}
async clickPriorityIcon() {
@ -67,10 +67,4 @@ export default class MessagePriority {
await expect(this.priorityDialog).toBeVisible();
await expect(this.dialogHeader).toHaveText('Message priority');
}
async verifyStandardOptionSelected() {
const standardOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
await expect(standardOption).toBeVisible();
await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
}
}

View file

@ -25,7 +25,7 @@ test(
// * Verify priority dialog appears with standard option selected
await channelsPage.messagePriority.verifyPriorityDialog();
await channelsPage.messagePriority.verifyStandardOptionSelected();
await channelsPage.messagePriority.verifyStandardPrioritySelected();
// # Close priority menu
await channelsPage.messagePriority.closePriorityMenu();

View file

@ -6,20 +6,21 @@
"version": "10.12.0",
"private": true,
"dependencies": {
"@floating-ui/react": "0.26.28",
"@floating-ui/react": "0.26.6",
"@giphy/js-fetch-api": "5.1.0",
"@giphy/react-components": "8.1.0",
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
"@mattermost/client": "10.12.0",
"@mattermost/desktop-api": "5.10.0-2",
"@mattermost/dynamic-virtualized-list": "github:mattermost/dynamic-virtualized-list#08dde0c34a12d0384740db27d55e398d139d7a51",
"@mattermost/types": "10.12.0",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",
"@mui/styled-engine-sc": "5.11.11",
"@redux-devtools/extension": "3.3.0",
"@tanstack/react-table": "8.10.7",
"@tippyjs/react": "4.2.6",
"@types/color-hash": "1.0.2",
"@types/react-is": "18.2.1",
"@types/turndown": "5.0.5",
"bootstrap": "3.4.1",
"buffer": "6.0.3",
@ -58,16 +59,16 @@
"pdfjs-dist": "4.4.168",
"process": "0.11.10",
"prop-types": "15.8.1",
"react": "17.0.2",
"react": "18.2.0",
"react-beautiful-dnd": "13.1.1",
"react-bootstrap": "github:mattermost/react-bootstrap#7d8660f06188a6433bb0f69b5816a97dc9ebe48c",
"react-bootstrap": "github:mattermost/react-bootstrap#05559f4c61c5a314783c390d2d82906ee8c7e558",
"react-color": "2.19.3",
"react-day-picker": "8.3.6",
"react-dom": "17.0.2",
"react-dom": "18.2.0",
"react-intl": "*",
"react-is": "17.0.2",
"react-is": "18.2.0",
"react-overlays": "0.9.3",
"react-redux": "7.2.4",
"react-redux": "9.2.0",
"react-router-dom": "5.3.4",
"react-select": "5.9.0",
"react-transition-group": "4.4.5",
@ -75,10 +76,10 @@
"react-window": "1.8.11",
"react-window-infinite-loader": "1.0.10",
"rebound": "0.1.0",
"redux": "4.2.0",
"redux": "5.0.1",
"redux-batched-actions": "0.5.0",
"redux-persist": "6.0.0",
"redux-thunk": "2.4.2",
"redux-thunk": "3.1.0",
"regenerator-runtime": "0.13.10",
"semver": "7.6.3",
"serialize-error": "11.0.3",
@ -94,16 +95,16 @@
"zen-observable": "0.10.0"
},
"devDependencies": {
"@cfaester/enzyme-adapter-react-18": "0.8.0",
"@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*",
"@mattermost/mmjstool": "github:mattermost/mattermost-utilities#7b63833d208d482ba4a1c12230bb3e68dd9c5e5e",
"@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "3.1.2",
"@testing-library/jest-dom": "5.16.4",
"@testing-library/react": "12.1.4",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "13.5.0",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.8.0",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/country-list": "2.1.0",
"@types/enzyme": "3.10.11",
"@types/jest": "28.1.8",
@ -112,33 +113,32 @@
"@types/luxon": "3.0.2",
"@types/mark.js": "8.11.6",
"@types/marked": "0.7.4",
"@types/react": "17.0.83",
"@types/react": "18.2.64",
"@types/react-beautiful-dnd": "13.1.2",
"@types/react-bootstrap": "0.32.35",
"@types/react-color": "3.0.6",
"@types/react-custom-scrollbars": "4.0.10",
"@types/react-dom": "17.0.25",
"@types/react-is": "17.0.2",
"@types/react-dom": "18.2.25",
"@types/react-overlays": "1.1.3",
"@types/react-redux": "7.1.31",
"@types/react-router-dom": "5.3.3",
"@types/react-transition-group": "4.4.5",
"@types/react-virtualized-auto-sizer": "1.0.1",
"@types/react-window": "1.8.5",
"@types/react-window-infinite-loader": "1.0.6",
"@types/redux-mock-store": "1.0.3",
"@types/redux-mock-store": "1.5.0",
"@types/regenerator-runtime": "0.13.8",
"@types/scheduler": "0.16.8",
"@types/semver": "7.5.8",
"@types/shallow-equals": "1.0.3",
"@types/styled-components": "5.1.32",
"@types/tinycolor2": "1.4.6",
"@types/zen-observable": "0.8.7",
"babel-plugin-styled-components": "2.1.4",
"copy-webpack-plugin": "11.0.0",
"emoji-datasource": "6.1.1",
"emoji-datasource-apple": "6.1.1",
"emoji-datasource-google": "6.1.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-17-updated": "1.0.2",
"enzyme-to-json": "3.6.2",
"eslint-plugin-no-only-tests": "3.1.0",
"external-remotes-plugin": "1.0.0",
@ -185,7 +185,7 @@
"test:watch": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --watch",
"test:updatesnapshot": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --updateSnapshot",
"test:debug": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --forceExit --detectOpenHandles --verbose",
"test-ci": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --ci --maxWorkers=100% --coverage",
"test-ci": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --ci --maxWorkers=100% --workerIdleMemoryLimit=4096MB",
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
"mmjstool": "mmjstool",

View file

@ -228,7 +228,7 @@ export function openAppsModal(form: AppForm, context: AppContext): AnyAction {
dialogType: AppsForm,
dialogProps: {
form,
context,
appContext: context,
},
});
}

View file

@ -159,7 +159,7 @@ describe('components/AboutBuildModal', () => {
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
});
test('should call onExited callback when the modal is hidden', () => {
test('should call onExited callback when the modal is hidden', async () => {
const onExited = jest.fn();
const state = {
entities: {
@ -185,7 +185,7 @@ describe('components/AboutBuildModal', () => {
state,
);
userEvent.click(screen.getByText('Close'));
await userEvent.click(screen.getByText('Close'));
expect(onExited).toHaveBeenCalledTimes(1);
});
@ -252,7 +252,7 @@ describe('components/AboutBuildModal', () => {
test('should handle API errors gracefully', async () => {
// Temporarily suppress console.error for this test
jest.spyOn(console, 'error').mockImplementation(() => {});
console.error = jest.fn();
// Mock the API call to throw an error
jest.spyOn(Client4, 'getLicenseLoadMetric').mockRejectedValue(new Error('API error'));

View file

@ -129,13 +129,16 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
};
handleOpenMarketplace = (): void => {
const openMarketplaceData = {
modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
dialogType: MarketplaceModal,
};
this.props.actions.openModal(openMarketplaceData);
this.closeDropdown();
// Wait for the menu to close to avoid clashing between the menu's focus trap and the modal's
requestAnimationFrame(() => {
const openMarketplaceData = {
modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
dialogType: MarketplaceModal,
};
this.props.actions.openModal(openMarketplaceData);
});
};
onClickAppBinding = async (binding: AppBinding) => {

View file

@ -8,7 +8,7 @@ import {Permissions} from 'mattermost-redux/constants';
import ActionsMenu from 'components/actions_menu';
import ModalController from 'components/modal_controller';
import {act, renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
function ActionsMenuTestWrapper(props: Omit<React.ComponentProps<typeof ActionsMenu>, 'isMenuOpen' | 'handleDropdownOpened'>) {
@ -72,7 +72,7 @@ describe('ActionsMenu', () => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Open the menu
screen.getByRole('button').click();
await userEvent.click(screen.getByRole('button'));
// The dialog should open up
await waitFor(() => {
@ -82,17 +82,17 @@ describe('ActionsMenu', () => {
});
// The focus starts on the dialog itself, so pressing tab should move it to the button
userEvent.tab();
await userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// The focus should be trapped, so hitting tab again shouldn't change the focus
userEvent.tab();
await userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// Pressing enter should open the marketplace modal and close the menu
userEvent.keyboard('{Enter}');
await userEvent.keyboard('{Enter}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();
@ -100,16 +100,14 @@ describe('ActionsMenu', () => {
});
// Pressing escape should close the marketplace modal
act(() => {
userEvent.keyboard('{Escape}');
});
await userEvent.type(screen.getByRole('dialog'), '{Escape}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'App Marketplace'})).not.toBeInTheDocument();
});
// Reopen the menu
screen.getByRole('button').click();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeVisible();
@ -117,7 +115,7 @@ describe('ActionsMenu', () => {
expect(screen.queryByRole('dialog')).toHaveAccessibleName('actions');
// Pressing escape should close the dialog
userEvent.keyboard('{Escape}');
await userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();

View file

@ -33,7 +33,7 @@ function TestResultsModal({
onExited,
actions,
}: Props): JSX.Element {
const dispatch = useDispatch<any>();
const dispatch = useDispatch();
const [term, setTerm] = useState<string>('');
const [users, setUsers] = useState<UserProfile[]>([]);
const [total, setTotal] = useState<number>(0);

View file

@ -30,7 +30,7 @@ const USERS_PER_PAGE = 10;
// - improve search
export const SyncedUserList = ({userIds, noResultsMessageId, noResultsDefaultMessage, actions}: SyncedUserListProps): JSX.Element => {
const dispatch = useDispatch<any>();
const dispatch = useDispatch();
const [users, setUsers] = useState<UserProfile[]>([]);
const [currentPage, setCurrentPage] = useState(0);

View file

@ -3,7 +3,6 @@
import {shallow} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import type {AccessControlPolicy} from '@mattermost/types/access_control';
@ -11,6 +10,8 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import type {Column} from 'components/admin_console/data_grid/data_grid';
import {act} from 'tests/react_testing_utils';
import PolicyList from './policies';
const mockHistoryPushInternal = jest.fn();

View file

@ -3,11 +3,11 @@
import {shallow} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import type {ChannelWithTeamData} from '@mattermost/types/channels';
import {useChannelAccessControlActions} from 'hooks/useChannelAccessControlActions';
import {act} from 'tests/react_testing_utils';
import PolicyDetails from './policy_details';

View file

@ -11,7 +11,7 @@ type Props = {
text: string | React.ReactNode;
};
export const MenuItemBlockableLinkImpl: React.SFC<Props> = (props: Props): JSX.Element => {
export const MenuItemBlockableLinkImpl = (props: Props): JSX.Element => {
const {to, text} = props;
return (
<BlockableLink to={to}>{text}</BlockableLink>

View file

@ -58,7 +58,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
const connector = connect(mapStateToProps, mapDispatchToProps, null, {pure: false});
const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;

View file

@ -4,6 +4,9 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -12,22 +15,14 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -42,6 +37,9 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -50,22 +48,14 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -80,6 +70,9 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -88,22 +81,14 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}

View file

@ -22,7 +22,7 @@ export const creditCardExpiredBanner = (setShowCreditCardBanner: (value: boolean
id='admin.billing.subscription.creditCardHasExpired.description'
defaultMessage='Please <link>update your payment information</link> to avoid any disruption.'
values={{
link: (text: string) => <BlockableLink to='/admin_console/billing/payment_info'>{text}</BlockableLink>,
link: (text) => <BlockableLink to='/admin_console/billing/payment_info'>{text}</BlockableLink>,
}}
/>
}
@ -46,7 +46,7 @@ export const paymentFailedBanner = () => {
id='billing.subscription.info.mostRecentPaymentFailed.description.mostRecentPaymentFailed'
defaultMessage='It looks your most recent payment failed because the credit card on your account has expired. Please <link>update your payment information</link> to avoid any disruption.'
values={{
link: (text: string) => <BlockableLink to='/admin_console/billing/payment_info'>{text}</BlockableLink>,
link: (text) => <BlockableLink to='/admin_console/billing/payment_info'>{text}</BlockableLink>,
}}
/>
}

View file

@ -4,6 +4,9 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -12,22 +15,14 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -42,6 +37,9 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -50,22 +48,14 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -80,6 +70,9 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -88,22 +81,14 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -118,6 +103,9 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -126,22 +114,14 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}

View file

@ -12,7 +12,7 @@ import type {UserProfile} from '@mattermost/types/users';
import {debounce} from 'mattermost-redux/actions/helpers';
import {getMissingProfilesByIds, searchProfiles} from 'mattermost-redux/actions/users';
import {getUsersByIDs} from 'mattermost-redux/selectors/entities/users';
import {makeGetUsersByIds} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
@ -75,7 +75,8 @@ export function UserSelector({id, isMulti, className, multiSelectOnChange, multi
}
}, [dispatch, initialValue, isMulti, multiSelectInitialValue, singleSelectInitialValue]);
const initialUsers = useSelector((state: GlobalState) => getUsersByIDs(state, initialValue || []));
const getUsersByIds = useMemo(makeGetUsersByIds, []);
const initialUsers = useSelector((state: GlobalState) => getUsersByIds(state, initialValue || []));
const selectInitialValue = initialUsers.
filter((userProfile) => Boolean(userProfile)).
map((userProfile: UserProfile) => ({

View file

@ -58,7 +58,7 @@ const CustomEnableDisableGuestAccountsSetting = ({
id='admin.guest_access.helpText'
defaultMessage='When true, external guest can be invited to channels within teams. Please see <a>Permissions Schemes</a> for which roles can invite guests.'
values={{
a: (chunks: string) => <Link to='/admin_console/user_management/permissions/system_scheme'>{chunks}</Link>,
a: (chunks) => <Link to='/admin_console/user_management/permissions/system_scheme'>{chunks}</Link>,
}}
/>
);

View file

@ -3,13 +3,12 @@
import {screen, fireEvent} from '@testing-library/react';
import React from 'react';
import {act} from 'react-dom/test-utils';
import type {UserPropertyField, UserPropertyFieldGroupID, UserPropertyFieldType} from '@mattermost/types/properties';
import {Client4} from 'mattermost-redux/client';
import {renderWithContext} from 'tests/react_testing_utils';
import {act, renderWithContext} from 'tests/react_testing_utils';
import CustomProfileAttributes from './custom_profile_attributes';

View file

@ -31,7 +31,7 @@ const AttributeHelpText = memo(({attributeKey, attributeName, attributeType}: At
defaultMessage='(Optional) The attribute in the AD/LDAP server used to populate the {name} of users in Mattermost. When set, users cannot edit their {name}, since it is synchronized with the LDAP server. When left blank, users can set their {name} in <strong>Account Menu > Account Settings > Profile</strong>.'
values={{
name: attributeName,
strong: (msg: string) => <strong>{msg}</strong>,
strong: (msg) => <strong>{msg}</strong>,
}}
/>
)}
@ -51,7 +51,7 @@ const AttributeHelpText = memo(({attributeKey, attributeName, attributeType}: At
defaultMessage='(Warning) This attribute will be converted to a TEXT attribute, if the field is set to synchronize.'
values={{
name: attributeName,
strong: (msg: string) => <strong>{msg}</strong>,
strong: (msg) => <strong>{msg}</strong>,
}}
/>
</div>
@ -134,7 +134,7 @@ const CustomProfileAttributes: React.FC<Props> = (props: Props): JSX.Element | n
id='admin.customProfileAttributes.subtitle'
defaultMessage='You can add or remove custom profile attributes by going to the <link>system properties page</link>.'
values={{
link: (msg: string) => (
link: (msg) => (
<Link
to='/admin_console/system_attributes/user_attributes'
>

View file

@ -205,7 +205,7 @@ export default class CustomTermsOfServiceSettings extends OLDAdminSettings<Props
<FormattedMessage
{...messages.enableTermsOfServiceHelp}
values={{
a: (chunks: string) => <Link to='/admin_console/site_config/customization'>{chunks}</Link>,
a: (chunks) => <Link to='/admin_console/site_config/customization'>{chunks}</Link>,
}}
/>
}

View file

@ -165,7 +165,7 @@ export default class GroupUsers extends React.PureComponent<Props, State> {
'AD/LDAP Connector is configured to sync and manage this group and its users. <a>Click here to view</a>'
}
values={{
a: (chunks: string) => (
a: (chunks) => (
<Link to='/admin_console/authentication/ldap'>
{chunks}
</Link>

View file

@ -256,7 +256,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.no_filters_added'}
defaultMessage={'Are you sure you want to apply these IP filter changes? There are currently no filters added, so <strong>all IP addresses will have access to the workspace.</strong>'}
values={{
strong: (content: string) => <strong>{content}</strong>,
strong: (content) => <strong>{content}</strong>,
}}
/>
);
@ -269,7 +269,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.turn_off_ip_filtering'}
defaultMessage={'Are you sure you want to turn off IP Filtering? <strong>All IP addresses will have access to the workspace.</strong>'}
values={{
strong: (content: string) => <strong>{content}</strong>,
strong: (content) => <strong>{content}</strong>,
}}
/>
);
@ -282,7 +282,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.apply_ip_filter_changes_are_you_sure'}
defaultMessage={'Are you sure you want to apply these IP Filter changes? <strong>Users with IP addresses outside of the IP ranges provided will no longer have access to the workspace.</strong>'}
values={{
strong: (content: string) => <strong>{content}</strong>,
strong: (content) => <strong>{content}</strong>,
}}
/>
);

View file

@ -21,7 +21,7 @@ const JobCancelButton = (props: Props): JSX.Element|null => {
const intl = useIntl();
let cancelButton = null;
const handleClick = useCallback((e) => {
const handleClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
onClick(job.id);
}, [onClick, job.id]);

View file

@ -61,9 +61,9 @@ const LDAPCustomSetting = (props: Props) => {
if (props.setting.showTitle) {
return (
<Setting
label={props.setting.label}
label={label}
inputId={props.setting.key}
helpText={props.setting.help_text}
helpText={helpText}
>
{componentInstance}
</Setting>

View file

@ -84,7 +84,7 @@ type State = {
[x: string]: unknown;
saveNeeded: false | 'both' | 'permissions' | 'config';
saving: boolean;
serverError: string | { message: string; id?: string } | null;
serverError: string | null;
confirmNeededId: string;
showConfirmId: string;
clientWarning: string | boolean;

View file

@ -4,6 +4,9 @@ exports[`components/admin_console/license_settings/modals/confirm_license_remova
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -12,22 +15,14 @@ exports[`components/admin_console/license_settings/modals/confirm_license_remova
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}

View file

@ -4,6 +4,9 @@ exports[`components/admin_console/license_settings/modals/upload_license_modal s
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -12,22 +15,14 @@ exports[`components/admin_console/license_settings/modals/upload_license_modal s
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
@ -47,6 +42,9 @@ exports[`components/admin_console/license_settings/modals/upload_license_modal s
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -55,22 +53,14 @@ exports[`components/admin_console/license_settings/modals/upload_license_modal s
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}

View file

@ -3,7 +3,6 @@
import {shallow} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import * as reactRedux from 'react-redux';
import {General} from 'mattermost-redux/constants';
@ -11,6 +10,7 @@ import {General} from 'mattermost-redux/constants';
import * as i18Selectors from 'selectors/i18n';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import UploadLicenseModal from './upload_license_modal';

View file

@ -3,10 +3,10 @@
import type {ReactWrapper} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import {Provider} from 'react-redux';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import RenewalLicenseCard from './renew_license_card';

View file

@ -39,7 +39,7 @@ const RenewLicenseCard: React.FC<RenewLicenseCardProps> = ({license, totalUsers,
id='admin.license.renewalCard.licenseExpiring'
defaultMessage='License expires in {days} days on {date, date, long}.'
values={{
date: endOfLicense,
date: endOfLicense.toDate(),
days: daysToEndLicense,
}}
/>
@ -51,7 +51,7 @@ const RenewLicenseCard: React.FC<RenewLicenseCardProps> = ({license, totalUsers,
id='admin.license.renewalCard.licenseExpired'
defaultMessage='License expired on {date, date, long}.'
values={{
date: endOfLicense,
date: endOfLicense.toDate(),
}}
/>
);

View file

@ -104,7 +104,7 @@ const TeamEditionRightPanel: React.FC<TeamEditionRightPanelProps> = ({
id='admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsInitial'
defaultMessage='By clicking <b>Upgrade</b>, I agree to the terms of the Mattermost '
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<a

View file

@ -46,7 +46,7 @@ export const EmbargoedEntityTrialError = () => {
id='admin.license.trial-request.embargoed'
defaultMessage='We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.'
values={{
link: (text: string) => (
link: (text) => (
<ExternalLink
location='trial_banner'
href={LicenseLinks.EMBARGOED_COUNTRIES}
@ -114,7 +114,7 @@ const TrialBanner = ({
defaultMessage: 'We were unable to process the request due to limitations for embargoed countries. <link>Learn more in our documentation</link>, or reach out to legal@mattermost.com for questions around export limitations.',
},
{
link: (text: string) => (
link: (text) => (
<ExternalLink
location='trial_banner'
href={LicenseLinks.EMBARGOED_COUNTRIES}

View file

@ -33,7 +33,7 @@ const TrialLicenseCard: React.FC<Props> = ({license}: Props) => {
id='admin.license.trialLicenseCard.expiringToday'
defaultMessage='Your free trial expires <b>Today at {time}</b>. Contact sales to purchase a license and continue using advanced features after the trial ends.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
time: moment(endDate).endOf('day').format('h:mm a ') + moment().tz(getBrowserTimezone()).format('z'),
}}
/>
@ -45,7 +45,7 @@ const TrialLicenseCard: React.FC<Props> = ({license}: Props) => {
id='admin.license.trialLicenseCard.expiringAfterFewDays'
defaultMessage='Your free trial will expire in <b>{daysCount} {daysCount, plural, one {day} other {days}}</b>. Contact sales to purchase a license and continue using advanced features.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
daysCount: daysToEndLicense,
}}
/>

View file

@ -4,13 +4,13 @@
import type {ReactWrapper} from 'enzyme';
import {mount, shallow} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import {IntlProvider} from 'react-intl';
import {General} from 'mattermost-redux/constants';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal';
import {act} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ManageTeamsDropdown from './manage_teams_dropdown';

View file

@ -330,7 +330,7 @@ export class MessageExportSettings extends OLDAdminSettings<BaseProps & WrappedC
<FormattedMessage
{...messages.exportFormat_description_details}
values={{
a: (chunks: string) => (
a: (chunks) => (
<Link to='/admin_console/environment/file_storage'>
{chunks}
</Link>

View file

@ -87,6 +87,9 @@ exports[`components/admin_console/permission_schemes_settings/permission_descrip
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
@ -95,22 +98,14 @@ exports[`components/admin_console/permission_schemes_settings/permission_descrip
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}

View file

@ -89,7 +89,7 @@ export default function EditPostTimeLimitModal(props: Props) {
id='editPost.timeLimitModal.description'
defaultMessage='Setting a time limit <b>applies to all users</b> who have the "Edit Post" permissions in any permission scheme.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<div className='pt-3'>

View file

@ -9,14 +9,13 @@ import type {Role} from '@mattermost/types/roles';
import WithTooltip from 'components/with_tooltip';
import type {AdditionalValues} from './permissions_tree/types';
import {rolesRolesStrings} from './strings/roles';
type Props = {
id: string;
inherited?: Partial<Role>;
selectRow: (id: string) => void;
additionalValues?: AdditionalValues | AdditionalValues['edit_post'];
additionalValues?: Record<string, unknown>;
description: string | JSX.Element;
}
@ -50,7 +49,7 @@ const PermissionDescription = ({
defaultMessage='Inherited from <link>{name}</link>.'
values={{
name: formattedName,
link: (text: string) => (
link: (text) => (
<a>{text}</a>
),
}}

View file

@ -143,7 +143,7 @@ export default class PermissionGroup extends React.PureComponent<Props, State> {
return true;
};
renderPermission = (permission: string, additionalValues: AdditionalValues) => {
renderPermission = (permission: string, additionalValues: Record<string, any>) => {
if (!this.isInScope(permission)) {
return null;
}

View file

@ -9,7 +9,6 @@ import type {Role} from '@mattermost/types/roles';
import PermissionCheckbox from './permission_checkbox';
import PermissionDescription from './permission_description';
import type {AdditionalValues} from './permissions_tree/types';
import {permissionRolesStrings} from './strings/permissions';
type Props = {
@ -21,7 +20,7 @@ type Props = {
selectRow: (id: string) => void;
value: string;
onChange: (id: string) => void;
additionalValues: AdditionalValues;
additionalValues: Record<string, any>;
}
const PermissionRow = ({

View file

@ -16,8 +16,4 @@ export type Group = {
isVisible?: (license?: ClientLicense) => boolean;
}
export type AdditionalValues = {
[edit_post: string]: {
editTimeLimitButton: JSX.Element;
};
}
export type AdditionalValues = Record<string, Record<string, any>>;

View file

@ -968,9 +968,9 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
if (setting.showTitle) {
return (
<Setting
label={setting.label}
label={label}
inputId={setting.key}
helpText={setting.help_text}
helpText={helpText}
>
{componentInstance}
</Setting>

View file

@ -38,7 +38,7 @@ function SecureConnectionDeleteModal({
id={'admin.secure_connections.confirm.delete.text'}
defaultMessage={'Are you sure you want to delete the secure connection <strong>{displayName}</strong>?'}
values={{
strong: (chunk: string) => <strong>{chunk}</strong>,
strong: (chunk) => <strong>{chunk}</strong>,
displayName,
}}
/>

View file

@ -19,8 +19,6 @@ import {Client4} from 'mattermost-redux/client';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getActiveTeamsList, getTeam} from 'mattermost-redux/selectors/entities/teams';
import type {ActionFuncAsync} from 'types/store';
export const useRemoteClusters = () => {
const [remoteClusters, setRemoteClusters] = useState<RemoteCluster[]>();
const [loadingState, setLoadingState] = useState<boolean | ClientError>(true);
@ -153,7 +151,7 @@ export const useSharedChannelRemoteRows = (remoteId: string, opts: {filter: 'hom
}
setLoadingState(true);
dispatch<ActionFuncAsync<IDMappedObjects<SharedChannelRemoteRow>>>(async (dispatch, getState) => {
dispatch(async (dispatch, getState) => {
const collected: IDMappedObjects<SharedChannelRemoteRow> = {};
const missing: SharedChannelRemote[] = [];

View file

@ -65,7 +65,7 @@ describe('components/admin_console/server_logs/Logs', () => {
test.each(['caller', 'msg', 'worker', 'job_id', 'whatever'])('should search input be performed on %s attribute',
async (searchString: string) => {
const searchInput = screen.getByTestId('searchInput');
userEvent.type(searchInput, searchString);
await userEvent.type(searchInput, searchString);
await waitFor(() => {
expect(screen.queryByText('msg 1')).toBeInTheDocument();
@ -77,7 +77,7 @@ describe('components/admin_console/server_logs/Logs', () => {
test.each(['level', 'timestamp'])('should search input not be performed on %s attribute',
async (searchString: string) => {
const searchInput = screen.getByTestId('searchInput');
userEvent.type(searchInput, searchString);
await userEvent.type(searchInput, searchString);
await waitFor(() => {
expect(screen.queryByText('msg 1')).not.toBeInTheDocument();

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act} from '@testing-library/react-hooks';
import {act} from '@testing-library/react';
import type {DeepPartial} from '@mattermost/types/utilities';
@ -106,13 +106,12 @@ describe('useOperation', () => {
await act(async () => {
await actionPromise;
const [, status3] = result.current;
expect(status3.loading).toBe(false);
expect(await actionPromise).toBe('test response value');
expect(status3.error).toBe(undefined);
});
const [, status3] = result.current;
expect(status3.loading).toBe(false);
expect(status3.error).toBe(undefined);
expect(await actionPromise!).toBe('test response value');
});
it('should run operation on command with error and loading phases: false -> true -> false', async () => {

View file

@ -75,10 +75,10 @@ describe('SystemProperties', () => {
expect(screen.getByRole('heading', {name: 'Configure user attributes'})).toBeInTheDocument();
expect(screen.queryByDisplayValue('test attribute 0')).toBeInTheDocument();
expect(screen.queryByDisplayValue('test attribute 1')).toBeInTheDocument();
expect(screen.queryByDisplayValue('test attribute 2')).toBeInTheDocument();
expect(screen.queryByDisplayValue('test attribute 3')).toBeInTheDocument();
expect(await screen.findByDisplayValue('test attribute 0')).toBeInTheDocument();
expect(await screen.findByDisplayValue('test attribute 1')).toBeInTheDocument();
expect(await screen.findByDisplayValue('test attribute 2')).toBeInTheDocument();
expect(await screen.findByDisplayValue('test attribute 3')).toBeInTheDocument();
});
});
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act} from '@testing-library/react-hooks';
import {act} from '@testing-library/react';
import type {UserPropertyField, UserPropertyFieldPatch} from '@mattermost/types/properties';
import type {DeepPartial} from '@mattermost/types/utilities';
@ -9,7 +9,7 @@ import type {DeepPartial} from '@mattermost/types/utilities';
import {Client4} from 'mattermost-redux/client';
import {generateId} from 'mattermost-redux/utils/helpers';
import {renderHookWithContext} from 'tests/react_testing_utils';
import {renderHookWithContext, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
@ -71,7 +71,7 @@ describe('useUserPropertyFields', () => {
getFields.mockResolvedValue([field0, field1, field2, field3]);
it('should return a collection', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -100,7 +100,7 @@ describe('useUserPropertyFields', () => {
});
it('should successfully handle edits', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -151,7 +151,7 @@ describe('useUserPropertyFields', () => {
it('should successfully handle reordering', async () => {
patchField.mockImplementation((id: string, patch: UserPropertyFieldPatch) => Promise.resolve({...baseField, ...patch, id, update_at: Date.now()} as UserPropertyField));
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -202,7 +202,7 @@ describe('useUserPropertyFields', () => {
});
it('should successfully handle deletes', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -251,7 +251,7 @@ describe('useUserPropertyFields', () => {
it('should successfully handle creates', async () => {
createField.mockImplementation((patch) => Promise.resolve({...baseField, ...patch, id: generateId()} as UserPropertyField));
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -300,7 +300,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate name uniqueness', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -330,7 +330,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate names already taken', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@ -370,7 +370,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate name required', async () => {
const {result, rerender, waitFor} = renderHookWithContext(() => {
const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());

View file

@ -212,7 +212,7 @@ export default class SystemRolePermissions extends React.PureComponent<Props, St
id='admin.permissions.roles.system_custom_group_admin.introduction'
defaultMessage='The built-in Custom Group Manager role can be used to delegate the administration of <a>Custom Groups</a> to users other than the System Admin.'
values={{
a: (chunks: string) => (
a: (chunks) => (
<ExternalLink
href='https://docs.mattermost.com/welcome/manage-custom-groups.html'
location='adminConsoleSystemRoles'
@ -234,7 +234,7 @@ export default class SystemRolePermissions extends React.PureComponent<Props, St
id='admin.permissions.roles.system_custom_group_admin.permissions_info'
defaultMessage='This role has permission to create, edit, and delete custom user groups by selecting <b>User groups</b> from the Products menu.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
</p>

View file

@ -49,9 +49,7 @@ exports[`SystemUserDetail should match default snapshot 1`] = `
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
@ -277,9 +275,7 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = `
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
@ -505,9 +501,7 @@ exports[`SystemUserDetail should not show manage user settings button when user
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
@ -733,9 +727,7 @@ exports[`SystemUserDetail should show manage user settings button as activated 1
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
@ -967,9 +959,7 @@ exports[`SystemUserDetail should show manage user settings button as disabled wh
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"
@ -1195,9 +1185,7 @@ exports[`SystemUserDetail should show the activate user button as disabled when
class="AdminUserCard__user-info"
>
<span>
</span>
<span
class="AdminUserCard__user-nickname"

View file

@ -13,7 +13,7 @@ import SystemUserDetail, {getUserAuthenticationTextField} from 'components/admin
import type {Params, Props} from 'components/admin_console/system_user_detail/system_user_detail';
import type {MockIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, waitFor, within} from 'tests/react_testing_utils';
import {renderWithContext, screen, waitFor, waitForElementToBeRemoved} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
@ -54,21 +54,16 @@ describe('SystemUserDetail', () => {
} as RouteComponentProps<Params>),
};
const waitForLoadingToFinish = async (container: HTMLElement) => {
const noUserBody = container.querySelector('.noUserBody');
const spinner = within(noUserBody as HTMLElement).getByTestId('loadingSpinner');
expect(spinner).toBeInTheDocument();
await waitFor(() => {
expect(container.querySelector('[data-testid="loadingSpinner"]')).not.toBeInTheDocument();
});
const waitForLoadingToFinish = async () => {
await waitForElementToBeRemoved(screen.queryAllByTitle('Loading Icon'));
await waitFor(() => expect(screen.queryByText('No teams found')).toBeInTheDocument());
};
test('should match default snapshot', async () => {
const props = defaultProps;
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@ -80,7 +75,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@ -92,7 +87,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@ -104,7 +99,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@ -118,7 +113,7 @@ describe('SystemUserDetail', () => {
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
const activateButton = container.querySelector('button[disabled]');
expect(activateButton).toHaveTextContent('Deactivate (Managed By LDAP)');
@ -133,7 +128,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext(<SystemUserDetail {...props}/>);
await waitForLoadingToFinish(container);
await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});

View file

@ -21,7 +21,7 @@ interface Props {
policyEnforcedToggleAvailable: boolean;
}
const SyncGroupsToggle: React.SFC<Props> = (props: Props): JSX.Element => {
const SyncGroupsToggle = (props: Props): JSX.Element => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
return (
<LineSwitch
@ -61,7 +61,7 @@ const SyncGroupsToggle: React.SFC<Props> = (props: Props): JSX.Element => {
);
};
const AllowAllToggle: React.SFC<Props> = (props: Props): JSX.Element | null => {
const AllowAllToggle = (props: Props): JSX.Element | null => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
if (isSynced) {
return null;
@ -113,7 +113,7 @@ const AllowAllToggle: React.SFC<Props> = (props: Props): JSX.Element | null => {
);
};
const PolicyEnforceToggle: React.SFC<Props> = (props: Props): JSX.Element | null => {
const PolicyEnforceToggle = (props: Props): JSX.Element | null => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced, policyEnforcedToggleAvailable} = props;
if (isSynced) {
return null;
@ -152,7 +152,7 @@ const PolicyEnforceToggle: React.SFC<Props> = (props: Props): JSX.Element | null
);
};
export const ChannelModes: React.SFC<Props> = (props: Props): JSX.Element => {
export const ChannelModes = (props: Props): JSX.Element => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, groupsSupported, policyEnforced, policyEnforcedToggleAvailable, abacSupported} = props;
return (
<AdminPanel

View file

@ -36,7 +36,7 @@ const SyncGroupsToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked
id='admin.team_settings.team_details.syncGroupMembersDescr'
defaultMessage='When enabled, adding and removing users from groups will add or remove them from this team. The only way of inviting members to this team is by adding the groups they belong to. <link>Learn More</link>'
values={{
link: (msg: string) => (
link: (msg) => (
<ExternalLink
href='https://www.mattermost.com/pl/default-ldap-group-constrained-team-channel.html'
location='team_modes'

View file

@ -146,7 +146,7 @@ export function TeamProfile({team, isArchived, onToggleArchive, isDisabled, save
id='admin.teamSettings.teamDetail.teamName'
defaultMessage='<b>Team Name</b>:'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<br/>
@ -157,7 +157,7 @@ export function TeamProfile({team, isArchived, onToggleArchive, isDisabled, save
id='admin.teamSettings.teamDetail.teamDescription'
defaultMessage='<b>Team Description</b>:'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<br/>

View file

@ -47,7 +47,7 @@ const UsersToBeRemovedModal = ({total, scope, scopeId, users, onExited}: Props)
defaultMessage='<b>{total, number} {total, plural, one {User} other {Users}}</b> To Be Removed'
values={{
total,
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
);

View file

@ -13,7 +13,7 @@ import type {FileUpload} from 'components/file_upload/file_upload';
import type Textbox from 'components/textbox/textbox';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {renderWithContext, userEvent, screen, act} from 'tests/react_testing_utils';
import {renderWithContext, userEvent, screen} from 'tests/react_testing_utils';
import Constants, {Locations, StoragePrefixes} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
@ -201,9 +201,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
);
const textbox = screen.getByTestId('post_textbox');
await act(async () => {
userEvent.type(textbox, 'something{esc}');
});
await userEvent.type(textbox, 'something{escape}');
expect(textbox).not.toHaveFocus();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
@ -230,17 +228,15 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
const textbox = screen.getByTestId('edit_textbox');
await act(async () => {
userEvent.type(textbox, 'something{esc}');
});
await userEvent.type(textbox, 'something{escape}', {advanceTimers: jest.advanceTimersByTime});
expect(textbox).not.toHaveFocus();
// save is called with a short delayed after pressing escape key
act(() => {
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT + 50);
});
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT + 50);
expect(mockedRemoveDraft).toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
jest.useRealTimers();
});
});
@ -269,14 +265,13 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(screen.getByPlaceholderText('Write to Test Channel')).toHaveValue('original draft');
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(screen.getByPlaceholderText('Write to Other Channel')).toHaveValue('a different draft');
});
@ -288,20 +283,16 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
initialState,
);
await act(async () => {
userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text');
});
await userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text');
expect(mockedUpdateDraft).not.toHaveBeenCalled();
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(mockedUpdateDraft).toHaveBeenCalled();
expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({
@ -330,14 +321,12 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(mockedUpdateDraft).not.toHaveBeenCalled();
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(mockedUpdateDraft).not.toHaveBeenCalled();
});
@ -360,20 +349,16 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
await act(async () => {
userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text');
});
await userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text');
expect(mockedUpdateDraft).not.toHaveBeenCalled();
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(mockedUpdateDraft).toHaveBeenCalled();
expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({
@ -400,21 +385,17 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
await act(async () => {
userEvent.clear(screen.getByPlaceholderText('Write to Test Channel'));
});
await userEvent.clear(screen.getByPlaceholderText('Write to Test Channel'));
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(mockedRemoveDraft).toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
@ -431,14 +412,12 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
await act(async () => {
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
});
rerender(
<AdvancedTextEditor
{...baseProps}
channelId={otherChannelId}
/>,
);
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();

View file

@ -51,7 +51,7 @@ export default function EditPostFooter(props: Props) {
defaultMessage='<strong>{key}ENTER</strong> to Save, <strong>ESC</strong> to Cancel'
values={{
key: sendOnCtrlEnter ? ctrlSendKey : '',
strong: (x: string) => <strong>{x}</strong>,
strong: (x) => <strong>{x}</strong>,
}}
/>
</div>

View file

@ -63,7 +63,7 @@ describe('FormattingBar', () => {
expect(screen.queryByLabelText('show hidden formatting options')).not.toBeInTheDocument();
});
test('MM-56705 should not submit form when clicking on hidden formatting button', () => {
test('MM-56705 should not submit form when clicking on hidden formatting button', async () => {
jest.spyOn(Hooks, 'useFormattingBarControls').mockReturnValue({wideMode: 'narrow', ...splitFormattingBarControls('narrow')});
const onSubmit = jest.fn();
@ -76,13 +76,13 @@ describe('FormattingBar', () => {
expect(screen.queryByLabelText('heading')).toBe(null);
userEvent.click(screen.getByLabelText('show hidden formatting options'));
await userEvent.click(screen.getByLabelText('show hidden formatting options'));
expect(screen.queryByLabelText('heading')).toBeVisible();
expect(onSubmit).not.toHaveBeenCalled();
});
test('should disable tooltip when hidden controls are shown', () => {
test('should disable tooltip when hidden controls are shown', async () => {
jest.spyOn(Hooks, 'useFormattingBarControls').mockReturnValue({wideMode: 'narrow', ...splitFormattingBarControls('narrow')});
const {container} = renderWithContext(
@ -92,7 +92,7 @@ describe('FormattingBar', () => {
const hiddenControlsButton = screen.getByLabelText('show hidden formatting options');
// Click to show hidden controls
userEvent.click(hiddenControlsButton);
await userEvent.click(hiddenControlsButton);
// Find the WithTooltip component and verify it has disabled prop
const tooltipWrapper = container.querySelector('.tooltipContainer');

View file

@ -12,7 +12,7 @@ import {DotsHorizontalIcon} from '@mattermost/compass-icons/components';
import WithTooltip from 'components/with_tooltip';
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
import type {ApplyMarkdownOptions, MarkdownMode} from 'utils/markdown/apply_markdown';
import FormattingIcon, {IconContainer} from './formatting_icon';
import {useFormattingBarControls} from './hooks';
@ -178,7 +178,7 @@ const FormattingBar = (props: FormattingBarProps): JSX.Element => {
* function signature as if we would define it directly in the props of
* the FormattingIcon component. This should improve render-performance
*/
const makeFormattingHandler = useCallback((mode) => () => {
const makeFormattingHandler = useCallback((mode: MarkdownMode) => () => {
// if the formatting is disabled just return without doing anything
if (disableControls) {
return;

View file

@ -85,7 +85,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
extraProps.trailingElements = teammateTimeDisplay;
}
const tomorrowClickHandler = useCallback((e) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
const tomorrowClickHandler = useCallback((e: React.UIEvent) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
const optionTomorrow = (
<Menu.Item
@ -105,7 +105,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
/>
);
const nextMondayClickHandler = useCallback((e) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
const nextMondayClickHandler = useCallback((e: React.UIEvent) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
const optionNextMonday = (
<Menu.Item

View file

@ -86,7 +86,7 @@ function RecentUsedCustomDate({handleOnSelect, userCurrentTimezone, nextMonday,
}
return {};
}, [recentlyUsedCustomDate]);
const handleRecentlyUsedCustomTime = useCallback((e) => handleOnSelect(e, recentlyUsedCustomDateVal.timestamp!), [handleOnSelect, recentlyUsedCustomDateVal.timestamp]);
const handleRecentlyUsedCustomTime = useCallback((e: React.UIEvent) => handleOnSelect(e, recentlyUsedCustomDateVal.timestamp!), [handleOnSelect, recentlyUsedCustomDateVal.timestamp]);
if (
!shouldShowRecentlyUsedCustomTime(now.toMillis(), recentlyUsedCustomDateVal, userCurrentTimezone, tomorrow9amTime, nextMonday)

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type React from 'react';
import {useCallback, useRef, useState} from 'react';
import {useCallback, useMemo, useRef, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import type {ServerError} from '@mattermost/types/errors';
@ -12,7 +12,7 @@ import {FileTypes} from 'mattermost-redux/action_types';
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
import {Permissions} from 'mattermost-redux/constants';
import {getChannel, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels';
import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files';
import {makeGetFileIdsForPost} from 'mattermost-redux/selectors/entities/files';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@ -79,6 +79,7 @@ const useSubmit = (
const dispatch = useDispatch();
const getFilesIdsForPost = useMemo(makeGetFileIdsForPost, []);
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId || ''));
const isDraftSubmitting = useRef(false);

View file

@ -1,13 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent} from '@testing-library/react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import SystemAnalytics from 'components/analytics/system_analytics';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
const StatTypes = Constants.StatTypes;
@ -92,7 +91,7 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
renderWithContext(<SystemAnalytics {...baseProps}/>, state, {useMockedStore: true});
const detailsElement = screen.getByText('Load Advanced Statistics');
fireEvent.click(detailsElement);
await userEvent.click(detailsElement);
await screen.findByTestId('totalPostsLineChart');
@ -237,12 +236,8 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
renderWithContext(<SystemAnalytics {...baseProps}/>, state, {useMockedStore: true});
await new Promise(process.nextTick);
const detailsElement = screen.getByText('Load Advanced Statistics');
fireEvent.click(detailsElement);
await screen.findByTestId('totalPostsLineChart');
await userEvent.click(detailsElement);
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();

View file

@ -251,8 +251,8 @@ const ConfigurationAnnouncementBar = (props: Props) => {
}
if (props.canViewSystemErrors && props.config?.SiteURL === '') {
const values: Record<string, ReactNode> = {
linkSite: (msg: string) => (
const values = {
linkSite: (msg: ReactNode[]) => (
<ExternalLink
href={props.siteURL}
location='configuration_announcement_bar'
@ -260,7 +260,7 @@ const ConfigurationAnnouncementBar = (props: Props) => {
{msg}
</ExternalLink>
),
linkConsole: (msg: string) => (
linkConsole: (msg: ReactNode[]) => (
<Link to='/admin_console/environment/web_server'>
{msg}
</Link>

View file

@ -42,7 +42,7 @@ const NoInternetConnection: React.FC<NoInternetConnectionProps> = (props: NoInte
id='announcement_bar.warn.contact_support_email'
defaultMessage='<a>Contact support</a>.'
values={{
a: (chunks: string) => (
a: (chunks) => (
<ExternalLink
href='mailto:support@mattermost.com'
location='announcement_bar'

View file

@ -3,7 +3,7 @@
import React from 'react';
import {renderWithContext, userEvent, screen, waitFor} from 'tests/react_testing_utils';
import {renderWithContext, userEvent, screen} from 'tests/react_testing_utils';
import * as utilsNotifications from 'utils/notifications';
import NotificationPermissionBar from './index';
@ -63,9 +63,7 @@ describe('NotificationPermissionBar', () => {
expect(screen.getByText('We need your permission to show notifications in the browser.')).toBeInTheDocument();
await waitFor(async () => {
userEvent.click(screen.getByText('Manage notification preferences'));
});
await userEvent.click(screen.getByText('Manage notification preferences'));
expect(utilsNotifications.requestNotificationPermission).toHaveBeenCalled();
expect(screen.queryByText('We need your permission to show browser notifications.')).not.toBeInTheDocument();

View file

@ -3,10 +3,10 @@
import type {ReactWrapper} from 'enzyme';
import React from 'react';
import {act} from 'react-dom/test-utils';
import {Provider} from 'react-redux';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import RenewalLink from './renewal_link';

View file

@ -182,7 +182,7 @@ describe('AppsFormComponent', () => {
const fields = [selectField];
const props = {
...baseProps,
context: {},
appsContext: {},
form: {
fields,
},
@ -1117,7 +1117,7 @@ describe('AppsFormComponent', () => {
});
describe('onHide and Modal Behavior', () => {
test('should call onHide prop when onHide is triggered', () => {
test('should call onHide prop when onHide is triggered', async () => {
const mockOnHide = jest.fn();
const props = {
@ -1128,7 +1128,7 @@ describe('AppsFormComponent', () => {
renderWithContext(<AppsForm {...props}/>);
const cancelButton = screen.getByRole('button', {name: /cancel/i});
userEvent.click(cancelButton);
await userEvent.click(cancelButton);
expect(mockOnHide).toHaveBeenCalled();
});
@ -1145,8 +1145,8 @@ describe('AppsFormComponent', () => {
}).not.toThrow();
const cancelButton = screen.getByRole('button', {name: /cancel/i});
expect(() => {
userEvent.click(cancelButton);
expect(async () => {
await userEvent.click(cancelButton);
}).not.toThrow();
});
@ -1168,8 +1168,8 @@ describe('AppsFormComponent', () => {
expect(cancelButton).toBeInTheDocument();
// Clicking cancel should handle submit_on_cancel logic
expect(() => {
userEvent.click(cancelButton);
expect(async () => {
await userEvent.click(cancelButton);
}).not.toThrow();
});
});

View file

@ -13,7 +13,7 @@ import {RawAppsFormContainer} from './apps_form_container';
describe('components/apps_form/AppsFormContainer', () => {
const emojiMap = new EmojiMap(new Map());
const context = {
const appContext = {
app_id: 'app',
channel_id: 'channel',
team_id: 'team',
@ -51,7 +51,7 @@ describe('components/apps_form/AppsFormContainer', () => {
path: '/form_url',
},
},
context,
appContext,
actions: {
doAppSubmit: jest.fn().mockResolvedValue({}),
doAppFetchForm: jest.fn(),

View file

@ -18,7 +18,7 @@ import AppsForm from './apps_form_component';
type Props = {
intl: IntlShape;
form?: AppForm;
context?: AppContext;
appContext?: AppContext;
timezone?: string;
onExited: () => void;
onHide?: () => void;
@ -59,11 +59,11 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
const errMsg = this.props.intl.formatMessage({id: 'apps.error.form.no_submit', defaultMessage: '`submit` is not defined'});
return {error: makeCallErrorResponse(makeErrorMsg(errMsg))};
}
if (!this.props.context) {
if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
const creq = createCallRequest(form.submit, this.props.context, {}, submission.values);
const creq = createCallRequest(form.submit, this.props.appContext, {}, submission.values);
const res = await this.props.actions.doAppSubmit(creq, this.props.intl) as DoAppCallResult<FormResponseData>;
if (res.error) {
return res;
@ -127,11 +127,11 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
defaultMessage: 'Called refresh on no refresh field.',
})))};
}
if (!this.props.context) {
if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
const creq = createCallRequest(form.source, this.props.context, {}, values);
const creq = createCallRequest(form.source, this.props.appContext, {}, values);
creq.selected_field = field.name;
const res = await this.props.actions.doAppFetchForm(creq, this.props.intl);
@ -180,11 +180,11 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
defaultMessage: '`lookup` is not defined.',
})))};
}
if (!this.props.context) {
if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
const creq = createCallRequest(field.lookup, this.props.context, {}, values);
const creq = createCallRequest(field.lookup, this.props.appContext, {}, values);
creq.selected_field = field.name;
creq.query = userInput;
@ -194,7 +194,7 @@ class AppsFormContainer extends React.PureComponent<Props, State> {
render() {
const {form} = this.state;
if (!form?.submit || !this.props.context) {
if (!form?.submit || !this.props.appContext) {
return null;
}

View file

@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {lazy, type ComponentType} from 'react';
import React, {lazy} from 'react';
import type {PluggableComponentType, PluggableProps} from 'plugins/pluggable/pluggable';
import type {PluginsState, ProductSubComponentNames} from 'types/store/plugins';
export function makeAsyncComponent<ComponentProps>(displayName: string, LazyComponent: React.ComponentType<ComponentProps>, fallback: React.ReactNode = null) {
const Component: ComponentType<ComponentProps> = (props) => (
const Component = (props: ComponentProps & React.JSX.IntrinsicAttributes) => (
<React.Suspense fallback={fallback}>
<LazyComponent {...props}/>
</React.Suspense>

View file

@ -23,7 +23,8 @@ import {openDirectChannelToUserId} from 'actions/channel_actions';
import {closeModal} from 'actions/views/modals';
import {isModalOpen} from 'selectors/views/modals';
import MemberList from 'components/channel_members_rhs/member_list';
import type {ListItem} from 'components/channel_members_rhs/member_list';
import MemberList, {ListItemType} from 'components/channel_members_rhs/member_list';
import {ModalIdentifiers} from 'utils/constants';
import {mapFeatureIdToTranslation} from 'utils/notify_admin_utils';
@ -44,17 +45,6 @@ export interface ChannelMember {
displayName: string;
}
enum ListItemType {
Member = 'member',
FirstSeparator = 'first-separator',
Separator = 'separator',
}
export interface ListItem {
type: ListItemType;
data: ChannelMember | JSX.Element;
}
const MembersContainer = styled.div`
flex: 1 1 auto;
padding: 0 4px 16px;

View file

@ -134,7 +134,7 @@ export default class Authorize extends React.PureComponent<Props, State> {
defaultMessage='Authorize <b>{appName}</b> to Connect to Your <b>Mattermost</b> User Account'
values={{
appName: app.name,
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
</div>
@ -145,7 +145,7 @@ export default class Authorize extends React.PureComponent<Props, State> {
defaultMessage='The app <b>{appName}</b> would like the ability to access and modify your basic information.'
values={{
appName: app.name,
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
</p>
@ -155,7 +155,7 @@ export default class Authorize extends React.PureComponent<Props, State> {
defaultMessage='Allow <b>{appName}</b> access?'
values={{
appName: app.name,
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
</h2>

View file

@ -28,7 +28,6 @@ type Props = {
inputClassName: string;
helpText?: React.ReactNode | string;
placeholder?: string;
footer?: Node;
disabled?: boolean;
toggleFocus?: ((focus: boolean) => void) | null;
listComponent: typeof SuggestionList | typeof ModalSuggestionList;
@ -110,7 +109,6 @@ export default class AutocompleteSelector extends React.PureComponent<Props, Sta
const {
providers,
placeholder,
footer,
label,
labelClassName,
helpText,
@ -178,7 +176,6 @@ export default class AutocompleteSelector extends React.PureComponent<Props, Sta
listPosition={listPosition}
/>
{helpTextContent}
{footer}
</div>
</div>
);

View file

@ -38,7 +38,7 @@ function BookmarkDeleteModal({
id={'channel_bookmarks.confirm.delete.text'}
defaultMessage={'Are you sure you want to delete the bookmark <strong>{displayName}</strong>?'}
values={{
strong: (chunk: string) => <strong>{chunk}</strong>,
strong: (chunk) => <strong>{chunk}</strong>,
displayName,
}}
/>

View file

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ComponentProps} from 'react';
import React, {useCallback, useRef} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import styled from 'styled-components';
@ -89,7 +88,7 @@ const CreateModalNameInput = ({
setEmoji('');
};
const handleInputChange: ComponentProps<typeof Input>['onChange'] = useCallback((e) => {
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.currentTarget.value);
}, []);

View file

@ -34,7 +34,7 @@ describe('components/channel_header/components/HeaderIconWrapper', () => {
expect(screen.getByLabelText('Recent mentions')).toBeVisible();
expect(screen.queryByText('Recent mentions')).not.toBeInTheDocument();
userEvent.hover(screen.getByLabelText('Recent mentions'));
await userEvent.hover(screen.getByLabelText('Recent mentions'));
await waitFor(() => {
expect(screen.queryByText('Recent mentions')).toBeInTheDocument();
@ -55,7 +55,7 @@ describe('components/channel_header/components/HeaderIconWrapper', () => {
expect(screen.queryByText('b')).not.toBeInTheDocument();
expect(screen.queryByText('c')).not.toBeInTheDocument();
userEvent.hover(screen.getByLabelText('Recent mentions'));
await userEvent.hover(screen.getByLabelText('Recent mentions'));
await waitFor(() => {
expect(screen.queryByText('Recent mentions')).toBeInTheDocument();

View file

@ -3,7 +3,6 @@
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
@ -127,4 +126,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default withRouter(connector(ChannelHeader));
export default connector(ChannelHeader);

View file

@ -1,10 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent, screen} from '@testing-library/react';
import {fireEvent, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import {act} from 'react-dom/test-utils';
import {GenericModal} from '@mattermost/components';
import type {Channel} from '@mattermost/types/channels';
@ -18,7 +17,7 @@ import ChannelInviteModal from 'components/channel_invite_modal/channel_invite_m
import type {Value} from 'components/multiselect/multiselect';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext} from 'tests/react_testing_utils';
import {act, renderWithContext} from 'tests/react_testing_utils';
type UserProfileValue = Value & UserProfile;
@ -291,41 +290,33 @@ describe('components/channel_invite_modal', () => {
membersInTeam: {'user-1': {user_id: 'user-1', team_id: channel.team_id, roles: '', delete_at: 0, scheme_admin: false, scheme_guest: false, scheme_user: true, mention_count: 0, mention_count_root: 0, msg_count: 0, msg_count_root: 0} as TeamMembership},
};
await act(async () => {
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
// Type the search term
await userEvent.type(input, 'user-1');
// Type the search term
await userEvent.type(input, 'user-1');
// Wait for the promise to resolve
await act(async () => {
// Wait for the dropdown option to appear
const option = await screen.findByText('user-1');
// Wait for the dropdown option to appear
const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
// Click the option
userEvent.click(option);
// Click the option
await userEvent.click(option);
// Confirm that the user is now displayed in the selected users
expect(screen.getByText('user-1')).toBeInTheDocument();
// Confirm that the user is now displayed in the selected users
expect(screen.getByText('user-1')).toBeInTheDocument();
// Find and click the save button
const saveButton = getByText('Add');
fireEvent.click(saveButton);
});
// Find and click the save button
const saveButton = getByText('Add');
await userEvent.click(saveButton);
// Wait for the promise to resolve
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
// Check that addUsersToChannel was called
// Check that addUsersToChannel was called
await waitFor(() => {
expect(addUsersToChannelMock).toHaveBeenCalled();
});
});
@ -347,41 +338,33 @@ describe('components/channel_invite_modal', () => {
membersInTeam: {'user-1': {user_id: 'user-1', team_id: channel.team_id, roles: '', delete_at: 0, scheme_admin: false, scheme_guest: false, scheme_user: true, mention_count: 0, mention_count_root: 0, msg_count: 0, msg_count_root: 0} as TeamMembership},
};
await act(async () => {
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
// Type the search term
await userEvent.type(input, 'user-1');
// Type the search term
await userEvent.type(input, 'user-1');
// Wait for the promise to resolve
await act(async () => {
// Wait for the dropdown option to appear
const option = await screen.findByText('user-1');
// Wait for the dropdown option to appear
const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
// Click the option
userEvent.click(option);
// Click the option
await userEvent.click(option);
// Confirm that the user is now displayed in the selected users
expect(screen.getByText('user-1')).toBeInTheDocument();
// Confirm that the user is now displayed in the selected users
expect(screen.getByText('user-1')).toBeInTheDocument();
// Find and click the save button
const saveButton = getByText('Add');
fireEvent.click(saveButton);
});
// Find and click the save button
const saveButton = getByText('Add');
await userEvent.click(saveButton);
// Wait for the promise to resolve
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
// Check that addUsersToChannel was called
// Check that addUsersToChannel was called
await waitFor(() => {
expect(addUsersToChannelMock).toHaveBeenCalled();
});
});
@ -399,33 +382,29 @@ describe('components/channel_invite_modal', () => {
};
await act(async () => {
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
const {getByText} = renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
// First, we need to simulate selecting a user
const input = screen.getByRole('combobox', {name: /search for people/i});
await userEvent.type(input, 'user-1');
await userEvent.type(input, 'user-1');
await act(async () => {
const option = await screen.findByText('user-1');
const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
userEvent.click(option);
await userEvent.click(option);
expect(screen.getByText('user-1')).toBeInTheDocument();
expect(screen.getByText('user-1')).toBeInTheDocument();
const saveButton = getByText('Add');
fireEvent.click(saveButton);
});
const saveButton = getByText('Add');
await userEvent.click(saveButton);
// Check that onAddCallback was called and addUsersToChannel was not
expect(onAddCallback).toHaveBeenCalled();
expect(props.actions.addUsersToChannel).not.toHaveBeenCalled();
});
// Check that onAddCallback was called and addUsersToChannel was not
expect(onAddCallback).toHaveBeenCalled();
expect(props.actions.addUsersToChannel).not.toHaveBeenCalled();
});
test('should trim the search term', async () => {
@ -441,25 +420,22 @@ describe('components/channel_invite_modal', () => {
},
};
await act(async () => {
renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
renderWithContext(
<ChannelInviteModal
{...props}
/>,
);
// Find the search input
const input = screen.getByRole('combobox', {name: /search for people/i});
// Find the search input
const input = screen.getByRole('combobox', {name: /search for people/i});
// Directly trigger the change event with a value that has spaces
// Directly trigger the change event with a value that has spaces
act(() => {
fireEvent.change(input, {target: {value: ' something '}});
});
// Wait for the search timeout plus some extra time
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
});
// Verify the search was called with the trimmed term
// Verify the search was called with the trimmed term
await waitFor(() => {
expect(searchProfilesMock).toHaveBeenCalledWith(
expect.stringContaining('something'),
expect.any(Object),

View file

@ -573,7 +573,7 @@ const ChannelInviteModalComponent = (props: Props) => {
id='channel_invite.no_options_message'
defaultMessage='No matches found - <InvitationModalLink>Invite them to the team</InvitationModalLink>'
values={{
InvitationModalLink: (chunks: string) => (
InvitationModalLink: (chunks) => (
<InviteModalLink
id='customNoOptionsMessageLink'
abacChannelPolicyEnforced={props.channel.policy_enforced}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect} from 'react';
import React, {useCallback, useEffect, useMemo} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
@ -41,9 +41,9 @@ const GroupOption = (props: Props) => {
addUserProfile,
} = props;
const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
const getProfilesByIdsAndUsernames = useMemo(makeGetProfilesByIdsAndUsernames, []);
const profiles = useSelector((state: GlobalState) => getProfilesByIdsAndUsernames(state, {allUserIds: group.member_ids || [], allUsernames: []}) as UserProfileValue[]);
const profiles = useSelector((state: GlobalState) => getProfilesByIdsAndUsernames(state, {allUserIds: group.member_ids}) as UserProfileValue[]);
const overflowNames = useSelector((state: GlobalState) => {
if (group?.member_ids) {
return group?.member_ids.map((userId) => displayNameGetter(state, true)(getUser(state, userId))).join(', ');

View file

@ -22,6 +22,8 @@ import TestHelper from 'packages/mattermost-redux/test/test_helper';
import mockStore from 'tests/test_store';
import {joinPrivateChannelPrompt} from 'utils/channel_utils';
import type {Match} from './channel_identifier_router';
jest.mock('actions/global_actions', () => ({
emitChannelClickEvent: jest.fn(),
}));
@ -137,7 +139,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToChannelByChannelId({params: {team: 'team1', identifier: 'channel_id3', path: '/'}, url: ''}, history as any) as any));
await testStore.dispatch((goToChannelByChannelId({params: {team: 'team1', identifier: 'channel_id3', path: '/'}, url: ''} as Match, history as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3', '');
expect(history.replace).toHaveBeenCalledWith('/team1/channels/achannel3');
});
@ -147,14 +149,14 @@ describe('Actions', () => {
test('switch to channel on different team with same name', async () => {
const testStore = await mockStore(initialState);
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team2', identifier: 'achannel', path: '/'}, url: ''}, {} as any) as any));
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team2', identifier: 'achannel', path: '/'}, url: ''} as Match, {} as any)));
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel2);
});
test('switch to public channel we have locally but need to join', async () => {
const testStore = await mockStore(initialState);
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: 'achannel3', path: '/'}, url: ''}, {} as any) as any));
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: 'achannel3', path: '/'}, url: ''} as Match, {} as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3', 'achannel3');
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel3);
});
@ -165,7 +167,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id3a', name: 'achannel3a', team_id: 'team_id1', type: 'O'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3a', 'achannel3a');
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel);
});
@ -187,7 +189,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id6', name: 'achannel6', team_id: 'team_id1', type: 'P'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(getChannelByNameAndTeamName).toHaveBeenCalledWith('team1', channel.name, true);
expect(getChannelMember).toHaveBeenCalledWith(channel.id, 'current_user_id');
expect(joinPrivateChannelPrompt).toHaveBeenCalled();
@ -227,7 +229,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id6', name: 'achannel6', team_id: 'team_id1', type: 'P'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(getChannelByNameAndTeamName).toHaveBeenCalledWith('team1', channel.name, true);
expect(getChannelMember).toHaveBeenCalledWith(channel.id, 'current_user_id');
expect(joinPrivateChannelPrompt).toHaveBeenCalled();
@ -240,7 +242,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team1', identifier: 'channel', path: '/'}, url: ''}, history as any, 'user_id2') as any));
await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team1', identifier: 'channel', path: '/'}, url: ''} as Match, history as any, 'user_id2') as any));
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@ -248,7 +250,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team2', identifier: 'channel', path: '/'}, url: ''}, history as any, 'user_id2') as any));
await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team2', identifier: 'channel', path: '/'}, url: ''} as Match, history as any, 'user_id2') as any));
expect(history.replace).toHaveBeenCalledWith('/team2/messages/@user2');
});
});
@ -258,7 +260,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team1', identifier: 'current_user_id__user_id2', path: '/'}, url: ''}, history as any) as any));
await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team1', identifier: 'current_user_id__user_id2', path: '/'}, url: ''} as Match, history as any)));
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@ -266,7 +268,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team2', identifier: 'current_user_id__user_id2', path: '/'}, url: ''}, history as any) as any));
await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team2', identifier: 'current_user_id__user_id2', path: '/'}, url: ''} as Match, history as any)));
expect(history.replace).toHaveBeenCalledWith('/team2/messages/@user2');
});
});
@ -276,7 +278,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user2@bladekick.com', path: '/'}, url: ''}, history as any) as any));
await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user2@bladekick.com', path: '/'}, url: ''} as Match, history as any)));
expect(getUserByEmail).not.toHaveBeenCalled();
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@ -285,7 +287,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user3@bladekick.com', path: '/'}, url: ''}, history as any) as any));
await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user3@bladekick.com', path: '/'}, url: ''} as Match, history as any)));
expect(getUserByEmail).toHaveBeenCalledWith('user3@bladekick.com');
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user3');
});

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {History} from 'history';
import React from 'react';
import {getHistory} from 'utils/browser_history';
@ -14,18 +15,20 @@ describe('components/channel_layout/CenterChannel', () => {
const baseProps = {
match: {
isExact: false,
params: {
identifier: 'identifier',
team: 'team',
path: '/path',
},
path: '/team/channel/identifier',
url: '/team/channel/identifier',
},
actions: {
onChannelByIdentifierEnter: jest.fn(),
},
history: [],
history: [] as unknown as History,
};
test('should call onChannelByIdentifierEnter on props change', () => {
@ -36,11 +39,13 @@ describe('components/channel_layout/CenterChannel', () => {
const props2 = {
match: {
isExact: false,
params: {
identifier: 'identifier2',
team: 'team2',
path: '/path2',
},
path: '/team2/channel/identifier2',
url: '/team2/channel/identifier2',
},
};
@ -62,12 +67,14 @@ describe('components/channel_layout/CenterChannel', () => {
const props = {
...baseProps,
match: {
isExact: false,
params: {
identifier: 'identifier',
team: 'team',
path: '/path',
postid: 'abcd',
},
path: '/team/channel/identifier/abcd',
url: '/team/channel/identifier/abcd',
},
};
@ -80,12 +87,14 @@ describe('components/channel_layout/CenterChannel', () => {
const props = {
...baseProps,
match: {
isExact: false,
params: {
identifier: 'identifier1',
team: 'team1',
path: '/path1',
postid: 'abcd',
},
path: '/team1/channel/identifier1/abcd',
url: '/team1/channel/identifier1/abcd',
},
};

View file

@ -1,31 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {History} from 'history';
import React from 'react';
import type {match} from 'react-router-dom';
import ChannelView from 'components/channel_view/index';
import {getHistory} from 'utils/browser_history';
import Constants from 'utils/constants';
export interface Match {
params: {
identifier: string;
team: string;
postid?: string;
path: string;
};
url: string;
}
export type Match = match<{
identifier: string;
team: string;
postid?: string;
path: string;
}>;
export type MatchAndHistory = Pick<Props, 'match' | 'history'>
type Props = {
match: Match;
actions: {
onChannelByIdentifierEnter: (props: MatchAndHistory) => any;
onChannelByIdentifierEnter: (props: MatchAndHistory) => void;
};
history: any;
history: History;
};
export default class ChannelIdentifierRouter extends React.PureComponent<Props> {

View file

@ -19,6 +19,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {Constants, ModalIdentifiers} from 'utils/constants';
import type {ModalData} from 'types/actions';
import type {MMAction} from 'types/store';
const ROWS_FROM_BOTTOM_TO_OPEN_UP = 2;
@ -39,7 +40,7 @@ export interface Props {
updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise<ActionResult>;
removeChannelMember: (channelId: string, userId: string) => Promise<ActionResult>;
getChannelMember: (channelId: string, userId: string) => void;
openModal: <P>(modalData: ModalData<P>) => void;
openModal: <P>(modalData: ModalData<P>) => MMAction;
};
}

View file

@ -71,7 +71,7 @@ export interface Props {
const ActionBar = ({className, channelType, membersCount, canManageMembers, editing, actions}: Props) => {
const showManageButton = channelType !== Constants.GM_CHANNEL && membersCount > 1;
const handleShortcut = useCallback((e) => {
const handleShortcut = useCallback((e: KeyboardEvent) => {
if (isKeyPressed(e, Constants.KeyCodes.ESCAPE) && editing) {
actions.stopEditing();
}

View file

@ -7,7 +7,7 @@ import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {renderWithContext, screen, waitFor, act} from 'tests/react_testing_utils';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import Member from './member';
import type {ChannelMember} from './member_list';
@ -197,13 +197,10 @@ describe('components/channel_members_rhs/Member', () => {
const icon = screen.getByTestId('SharedChannelIcon');
expect(icon).toBeInTheDocument();
await act(async () => {
userEvent.hover(icon);
jest.advanceTimersByTime(1000);
await userEvent.hover(icon, {advanceTimers: jest.advanceTimersByTime});
await waitFor(() => {
expect(screen.getByText('Shared with: Remote Organization')).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText('Shared with: Remote Organization')).toBeInTheDocument();
});
});
@ -229,13 +226,10 @@ describe('components/channel_members_rhs/Member', () => {
const icon = screen.getByTestId('SharedChannelIcon');
expect(icon).toBeInTheDocument();
await act(async () => {
userEvent.hover(icon);
jest.advanceTimersByTime(1000);
await userEvent.hover(icon, {advanceTimers: jest.advanceTimersByTime});
await waitFor(() => {
expect(screen.getByText('Shared with trusted organizations')).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText('Shared with trusted organizations')).toBeInTheDocument();
});
});

View file

@ -26,10 +26,10 @@ export enum ListItemType {
Separator = 'separator',
}
export interface ListItem {
export type ListItem = {
type: ListItemType;
data: ChannelMember | JSX.Element;
}
};
export interface Props {
channel: Channel;
members: ListItem[];
@ -120,7 +120,7 @@ const MemberList = ({
key={index}
style={style}
>
{members[index].data}
{members[index].data as JSX.Element}
</div>
);
default:

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, screen, waitFor} from '@testing-library/react';
import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@ -70,9 +70,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.queryByText('Mobile Notifications')).not.toBeInTheDocument();
expect(screen.getByText('Follow all threads in this channel')).toBeInTheDocument();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
@ -99,17 +97,13 @@ describe('ChannelNotificationsModal', () => {
);
const ignoreChannel = screen.getByTestId('ignoreMentions');
await act(async () => {
await userEvent.click(ignoreChannel);
});
await userEvent.click(ignoreChannel);
expect(ignoreChannel).toBeChecked();
// Verify the checkbox label is present
expect(screen.getByText('Ignore mentions for @channel, @here and @all')).toBeInTheDocument();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@ -146,20 +140,14 @@ describe('ChannelNotificationsModal', () => {
expect(nothingRadio).toBeInTheDocument();
// Test clicking through the options
await act(async () => {
await userEvent.click(allRadio);
});
await userEvent.click(allRadio);
expect(allRadio).toBeChecked();
await act(async () => {
await userEvent.click(mentionsRadio);
});
await userEvent.click(mentionsRadio);
expect(mentionsRadio).toBeChecked();
expect(allRadio).not.toBeChecked();
await act(async () => {
await userEvent.click(nothingRadio);
});
await userEvent.click(nothingRadio);
expect(nothingRadio).toBeChecked();
expect(mentionsRadio).not.toBeChecked();
@ -168,9 +156,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.getByText(/Mentions, direct messages, and keywords only/)).toBeInTheDocument();
expect(screen.getByText(/Nothing/)).toBeInTheDocument();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@ -194,14 +180,10 @@ describe('ChannelNotificationsModal', () => {
renderWithContext(<ChannelNotificationsModal {...baseProps}/>);
// Since the default value is checked, we will uncheck the checkbox
await act(async () => {
await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
});
await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.toBeChecked();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() => {
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@ -252,9 +234,7 @@ describe('ChannelNotificationsModal', () => {
// When "same as desktop" is checked, mobile-specific options should not be visible
expect(screen.queryByTestId('mobile-notify-me-radio-section')).not.toBeInTheDocument();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@ -300,26 +280,18 @@ describe('ChannelNotificationsModal', () => {
const noneRadio = screen.getByTestId('MobileNotification-none');
// Test clicking through the mobile notification options
await act(async () => {
await userEvent.click(allRadio);
});
await userEvent.click(allRadio);
expect(allRadio).toBeChecked();
await act(async () => {
await userEvent.click(mentionRadio);
});
await userEvent.click(mentionRadio);
expect(mentionRadio).toBeChecked();
expect(allRadio).not.toBeChecked();
await act(async () => {
await userEvent.click(noneRadio);
});
await userEvent.click(noneRadio);
expect(noneRadio).toBeChecked();
expect(mentionRadio).not.toBeChecked();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@ -356,9 +328,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.getByText('Mobile Notifications')).toBeInTheDocument();
expect(screen.getByText('Mute or ignore')).toBeInTheDocument();
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
});
await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(

View file

@ -246,7 +246,7 @@ export class ChannelSelectorModal extends React.PureComponent<Props, State> {
id='channelSelectorModal.title'
defaultMessage='Add Channels to <b>Channel Selection</b> List'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
</Modal.Title>

View file

@ -6,11 +6,10 @@ import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import TableEditor from 'components/admin_console/access_control/editors/table_editor/table_editor';
import SaveChangesPanel from 'components/widgets/modals/components/save_changes_panel';
import {useChannelAccessControlActions} from 'hooks/useChannelAccessControlActions';
import {useChannelSystemPolicies} from 'hooks/useChannelSystemPolicies';
import {renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils';
import {act, renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ChannelSettingsAccessRulesTab from './channel_settings_access_rules_tab';
@ -25,32 +24,9 @@ jest.mock('components/admin_console/access_control/editors/table_editor/table_ed
return jest.fn(() => React.createElement('div', {'data-testid': 'table-editor'}, 'TableEditor'));
});
// Mock SaveChangesPanel
jest.mock('components/widgets/modals/components/save_changes_panel', () => {
const React = require('react');
return jest.fn((props) => {
return React.createElement('div', {
'data-testid': 'save-changes-panel',
'data-state': props.state,
}, [
React.createElement('button', {
key: 'save',
'data-testid': 'SaveChangesPanel__save-btn',
onClick: props.handleSubmit,
}, 'Save'),
React.createElement('button', {
key: 'cancel',
'data-testid': 'SaveChangesPanel__cancel-btn',
onClick: props.handleCancel,
}, 'Reset'),
]);
});
});
const mockUseChannelAccessControlActions = useChannelAccessControlActions as jest.MockedFunction<typeof useChannelAccessControlActions>;
const mockUseChannelSystemPolicies = useChannelSystemPolicies as jest.MockedFunction<typeof useChannelSystemPolicies>;
const MockedTableEditor = TableEditor as jest.MockedFunction<typeof TableEditor>;
const MockedSaveChangesPanel = SaveChangesPanel as jest.MockedFunction<typeof SaveChangesPanel>;
describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () => {
const mockActions = {
@ -181,9 +157,9 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
},
});
// Suppress console methods for tests
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
// Console methods are suppressed in these tests. They'll be restored by setup_jest.ts after the test.
console.error = jest.fn();
console.warn = jest.fn();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(window, 'alert').mockImplementation(() => {});
});
@ -274,13 +250,15 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
expect(screen.getByText('Select user attributes and values as rules to restrict channel membership')).toBeInTheDocument();
});
test('should call useChannelAccessControlActions hook', () => {
test('should call useChannelAccessControlActions hook', async () => {
renderWithContext(
<ChannelSettingsAccessRulesTab {...baseProps}/>,
initialState,
);
expect(mockUseChannelAccessControlActions).toHaveBeenCalledTimes(2); // Once for the hook call and once for the mock return
await waitFor(() => {
expect(mockUseChannelAccessControlActions).toHaveBeenCalledTimes(2); // Once for the hook call and once for the mock return
});
});
test('should load user attributes on mount', async () => {
@ -363,7 +341,9 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
const onChangeCallback = MockedTableEditor.mock.calls[0][0].onChange;
// Simulate expression change
onChangeCallback('user.attributes.department == "Engineering"');
act(() => {
onChangeCallback('user.attributes.department == "Engineering"');
});
expect(setAreThereUnsavedChanges).toHaveBeenCalledWith(true);
});
@ -808,7 +788,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
expect(screen.getByTestId('table-editor')).toBeInTheDocument();
});
expect(screen.queryByTestId('save-changes-panel')).not.toBeInTheDocument();
expect(screen.queryByText('You have unsaved changes')).not.toBeInTheDocument();
});
test('should show SaveChangesPanel when expression changes', async () => {
@ -828,7 +808,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
onChangeCallback('user.attributes.department == "Engineering"');
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
});
@ -856,7 +836,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
});
@ -904,11 +884,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for SaveChangesPanel to appear
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Save button
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@ -985,11 +965,6 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(saveButton);
await userEvent.click(saveButton);
// Wait for async operations to complete
await waitFor(() => {
expect(mockActions.saveChannelPolicy).toHaveBeenCalled();
});
// Should only have been called once due to duplicate prevention
expect(mockActions.saveChannelPolicy).toHaveBeenCalledTimes(1);
});
@ -1015,16 +990,16 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for SaveChangesPanel to appear
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Reset button
const resetButton = screen.getByTestId('SaveChangesPanel__cancel-btn');
const resetButton = screen.getByText('Reset');
await userEvent.click(resetButton);
// Verify panel disappears
await waitFor(() => {
expect(screen.queryByTestId('save-changes-panel')).not.toBeInTheDocument();
expect(screen.queryByText('You have unsaved changes')).not.toBeInTheDocument();
});
// Verify checkbox is reset
@ -1050,9 +1025,8 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
onChangeCallback('invalid expression');
await waitFor(() => {
const panel = screen.getByTestId('save-changes-panel');
const panel = screen.getByText('Invalid expression format');
expect(panel).toBeInTheDocument();
expect(panel).toHaveAttribute('data-state', 'error');
});
});
@ -1086,53 +1060,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
const panel = screen.getByTestId('save-changes-panel');
expect(panel).toBeInTheDocument();
expect(panel).toHaveAttribute('data-state', 'error');
const panel = screen.getByText('You have unsaved changes');
expect(panel).toHaveClass('error');
});
});
test('should pass correct props to SaveChangesPanel', async () => {
renderWithContext(
<ChannelSettingsAccessRulesTab {...baseProps}/>,
initialState,
);
// Wait for initial loading to complete
await waitFor(() => {
expect(screen.getByTestId('table-editor')).toBeInTheDocument();
});
// Add an expression first to enable the checkbox
const onChangeCallback = MockedTableEditor.mock.calls[0][0].onChange;
onChangeCallback('user.attributes.department == "Engineering"');
await waitFor(() => {
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeDisabled();
});
// Toggle auto-sync to show panel
const checkbox = screen.getByRole('checkbox');
await userEvent.click(checkbox);
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
});
expect(MockedSaveChangesPanel).toHaveBeenCalledWith(
expect.objectContaining({
handleSubmit: expect.any(Function),
handleCancel: expect.any(Function),
handleClose: expect.any(Function),
tabChangeError: false,
state: undefined,
cancelButtonText: 'Reset',
}),
expect.anything(),
);
});
test('should update SaveChangesPanel state to saved after successful save', async () => {
// Ensure the searchUsers mock returns current user for validation to pass
mockActions.searchUsers.mockResolvedValue({
@ -1170,11 +1102,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Save button
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@ -1189,8 +1121,8 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for save to complete and panel to show saved state
await waitFor(() => {
const panel = screen.getByTestId('save-changes-panel');
expect(panel).toHaveAttribute('data-state', 'saved');
const panel = screen.getByText('Settings saved');
expect(panel).toBeVisible();
});
});
});
@ -1358,10 +1290,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@ -1433,10 +1365,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger self-exclusion validation
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@ -1488,10 +1420,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@ -1559,10 +1491,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear

View file

@ -75,7 +75,7 @@ function ChannelSettingsArchiveTab({
defaultMessage='Are you sure you wish to archive the <strong>{display_name}</strong> channel?'
values={{
display_name: channel.display_name,
strong: (chunks: string) => <strong>{chunks}</strong>,
strong: (chunks) => <strong>{chunks}</strong>,
}}
/>
</p>

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, screen} from '@testing-library/react';
import {screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@ -93,9 +93,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(toggle).not.toHaveClass('active');
// Click the toggle to enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner text and color inputs should be visible when banner is enabled
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
@ -130,9 +128,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument();
// Click the toggle to enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner settings should now be visible
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
@ -146,9 +142,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@ -164,28 +158,20 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
// Set banner color
const colorInput = screen.getByTestId('color-inputColorValue');
await act(async () => {
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#AA00AA');
});
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#AA00AA');
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the updated values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@ -202,11 +188,9 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...{...baseProps, channel: mockChannelWithBanner}}/>);
// Change the banner text
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'Changed banner text');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'Changed banner text');
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@ -215,9 +199,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Reset button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
// Form should be reset to original values
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toHaveValue('Test banner text');
@ -230,20 +212,14 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Leave banner text empty
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show error state
const errorMessage = screen.getByText(/Banner text is required/);
@ -255,19 +231,15 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Create a string that exceeds the allowed character limit
const longText = 'a'.repeat(1025);
// Enter long banner text
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, longText);
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, longText);
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@ -286,9 +258,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(previewButton).not.toHaveClass('active');
// Click the preview button
await act(async () => {
await userEvent.click(previewButton);
});
await userEvent.click(previewButton);
// Preview should now be active
expect(previewButton).toHaveClass('active');
@ -301,9 +271,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
// Click the toggle to disable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner settings should now be hidden
expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument();
@ -316,21 +284,15 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text but leave color empty
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show error state
const errorMessage = screen.getByText(/Banner color is required/);
@ -345,28 +307,20 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...baseProps}/>);
// Enable the banner
await act(async () => {
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
});
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'New banner text');
// Enter a valid hex color
await act(async () => {
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#ff0000');
});
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#ff0000');
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the correct color
expect(patchChannel).toHaveBeenCalledWith('channel1', expect.objectContaining({
@ -402,18 +356,14 @@ describe('ChannelSettingsConfigurationTab', () => {
);
// Enter a invalid hex color
await act(async () => {
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, 'not-a-color');
});
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, 'not-a-color');
// Do another action to trigger blur on this input so color is validated
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'Test text');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, 'Test text');
// if invalid, the color automatically returns to the original color
expect(screen.getByTestId('color-inputColorValue')).toHaveValue(originalColor);
@ -422,11 +372,8 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
// Modify the color to a valid one
await act(async () => {
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#123456');
});
await userEvent.clear(colorInput);
await userEvent.type(colorInput, '#123456');
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@ -442,23 +389,17 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext(<ChannelSettingsConfigurationTab {...{...baseProps, channel: mockChannelWithBanner}}/>);
// Add whitespace to the banner text
await act(async () => {
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, ' Banner text with whitespace ');
});
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
await userEvent.clear(textInput);
await userEvent.type(textInput, ' Banner text with whitespace ');
// Add whitespace to the banner color
await act(async () => {
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, ' #00FF00 ');
});
const colorInput = screen.getByTestId('color-inputColorValue');
await userEvent.clear(colorInput);
await userEvent.type(colorInput, ' #00FF00 ');
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@ -475,7 +416,6 @@ describe('ChannelSettingsConfigurationTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// The text input should now have the trimmed value
const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
expect(textInput).toHaveValue('Banner text with whitespace');
});
});

View file

@ -199,9 +199,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button in the SaveChangesPanel.
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the updated values (without type change).
// Note: URL should remain unchanged when editing existing channels
@ -242,9 +240,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@ -286,9 +282,7 @@ describe('ChannelSettingsInfoTab', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@ -315,9 +309,7 @@ describe('ChannelSettingsInfoTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Reset button.
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
// Form should be reset to original values.
expect(screen.getByRole('textbox', {name: 'Channel name'})).toHaveValue('Test Channel');
@ -344,9 +336,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button.
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show 'error' state.
const errorMessage = screen.getByText(/There are errors in the form above/);
@ -527,9 +517,7 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify the modal is shown
expect(screen.getByTestId('convert-confirm-modal')).toBeInTheDocument();
@ -548,14 +536,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click confirm button in modal
await act(async () => {
await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
});
await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
// Verify updateChannelPrivacy was called
expect(updateChannelPrivacy).toHaveBeenCalledWith('channel1', 'P');
@ -574,14 +558,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click cancel button in modal
await act(async () => {
await userEvent.click(screen.getByText(/Cancel/i));
});
await userEvent.click(screen.getByText(/Cancel/i));
// Verify updateChannelPrivacy was not called
expect(updateChannelPrivacy).not.toHaveBeenCalled();
@ -603,14 +583,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
await act(async () => {
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
});
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click confirm button in modal
await act(async () => {
await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
});
await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
// Verify error state is shown
expect(screen.getByText(/There are errors in the form above/)).toBeInTheDocument();

View file

@ -5,11 +5,10 @@ import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import type {DeepPartial} from '@mattermost/types/utilities';
import {General} from 'mattermost-redux/constants';
import {renderWithContext} from 'tests/react_testing_utils';
import type {GlobalState} from 'types/store';
import {TestHelper} from 'utils/test_helper';
import ChannelSettingsModal from './channel_settings_modal';
@ -18,35 +17,6 @@ let mockPrivateChannelPermission = true;
let mockPublicChannelPermission = true;
let mockManageChannelAccessRulesPermission = false;
// Variable to control group-constrained status in tests
let mockGroupConstrained = false;
// Mock the redux selectors
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
getChannel: jest.fn().mockImplementation((state, channelId) => {
// Return a mock channel based on the channelId
return {
id: channelId,
team_id: 'team1',
display_name: 'Test Channel',
name: channelId === 'default_channel' ? 'town-square' : 'test-channel',
purpose: 'Testing purpose',
header: 'Channel header',
type: mockChannelType, // Use a variable to control the channel type
create_at: 0,
update_at: 0,
delete_at: 0,
last_post_at: 0,
total_msg_count: 0,
extra_update_at: 0,
creator_id: 'creator1',
last_root_post_at: 0,
scheme_id: '',
group_constrained: mockGroupConstrained,
};
}),
}));
// Mock the channel banner selector
jest.mock('mattermost-redux/selectors/entities/channel_banner', () => ({
selectChannelBannerEnabled: jest.fn().mockImplementation((state) => {
@ -140,9 +110,6 @@ type TabType = {
display?: boolean;
};
// Variable to control the channel type in tests
let mockChannelType = 'O';
// Mock the settings sidebar
jest.mock('components/settings_sidebar', () => {
return function MockSettingsSidebar({tabs, activeTab, updateTab}: {tabs: TabType[]; activeTab: string; updateTab: (tab: string) => void}): JSX.Element {
@ -165,30 +132,58 @@ jest.mock('components/settings_sidebar', () => {
};
});
const baseProps = {
channelId: 'channel1',
isOpen: true,
onExited: jest.fn(),
focusOriginElement: 'button1',
};
describe('ChannelSettingsModal', () => {
const channelId = 'channel1';
const baseProps = {
channelId,
isOpen: true,
onExited: jest.fn(),
focusOriginElement: 'button1',
};
function makeTestState() {
return {
entities: {
channels: {
channels: {
[channelId]: TestHelper.getChannelMock({
id: channelId,
type: General.OPEN_CHANNEL,
purpose: 'Testing purpose',
header: 'Channel header',
group_constrained: false,
}),
},
},
general: {
license: {
SkuShortName: '',
},
},
},
};
}
beforeEach(() => {
jest.clearAllMocks();
mockChannelType = 'O'; // Default to public channel
mockPrivateChannelPermission = true;
mockPublicChannelPermission = true;
mockManageChannelAccessRulesPermission = false; // Default to no access rules permission
mockGroupConstrained = false; // Default to not group-constrained
});
it('should render the modal with correct header text', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
expect(screen.getByText('Channel Settings')).toBeInTheDocument();
});
it('should render Info tab by default', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the lazy-loaded components
await waitFor(() => {
@ -197,7 +192,9 @@ describe('ChannelSettingsModal', () => {
});
it('should switch tabs when clicked', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -216,11 +213,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for default channel', async () => {
renderWithContext(
<ChannelSettingsModal
{...{...baseProps, channelId: 'default_channel'}}
/>,
);
const testState = makeTestState();
testState.entities.channels.channels[channelId].name = 'town-square';
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -235,10 +232,11 @@ describe('ChannelSettingsModal', () => {
});
it('should show archive tab for public channel when user has permission', async () => {
mockChannelType = 'O';
mockPublicChannelPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -250,10 +248,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for public channel when user does not have permission', async () => {
mockChannelType = 'O';
mockPublicChannelPermission = false;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -265,10 +264,12 @@ describe('ChannelSettingsModal', () => {
});
it('should show archive tab for private channel when user has permission', async () => {
mockChannelType = 'P';
mockPrivateChannelPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -280,10 +281,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for private channel when user does not have permission', async () => {
mockChannelType = 'P';
mockPrivateChannelPermission = false;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -295,67 +298,44 @@ describe('ChannelSettingsModal', () => {
});
it('should not show configuration tab with no license', async () => {
const baseState: DeepPartial<GlobalState> = {
entities: {
general: {
license: {
SkuShortName: '',
},
},
},
};
renderWithContext(<ChannelSettingsModal {...baseProps}/>, baseState as GlobalState);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should not show configuration tab with professional license', async () => {
const baseState: DeepPartial<GlobalState> = {
entities: {
general: {
license: {
SkuShortName: 'professional',
},
},
},
};
renderWithContext(<ChannelSettingsModal {...baseProps}/>, baseState as GlobalState);
const testState = makeTestState();
testState.entities.general.license.SkuShortName = 'professional';
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should not show configuration tab with enterprise license', async () => {
const baseState: DeepPartial<GlobalState> = {
entities: {
general: {
license: {
SkuShortName: 'enterprise',
},
},
},
};
renderWithContext(<ChannelSettingsModal {...baseProps}/>, baseState as GlobalState);
const testState = makeTestState();
testState.entities.general.license.SkuShortName = 'enterprise';
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should show configuration tab when enterprise advanced license', async () => {
const baseState: DeepPartial<GlobalState> = {
entities: {
general: {
license: {
SkuShortName: 'advanced',
},
},
},
};
renderWithContext(<ChannelSettingsModal {...baseProps}/>, baseState as GlobalState);
const testState = makeTestState();
testState.entities.general.license.SkuShortName = 'advanced';
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
expect(screen.getByTestId('configuration-tab-button')).toBeInTheDocument();
});
describe('Access Control tab visibility', () => {
it('should show Access Control tab for private channel when user has permission', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -368,10 +348,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for private channel when user lacks permission', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = false;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -384,10 +366,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for public channel even with permission', async () => {
mockChannelType = 'O';
mockManageChannelAccessRulesPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -400,10 +383,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for public channel without permission', async () => {
mockChannelType = 'O';
mockManageChannelAccessRulesPermission = false;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -416,10 +400,12 @@ describe('ChannelSettingsModal', () => {
});
it('should be able to navigate to Access Control tab when visible', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -439,10 +425,12 @@ describe('ChannelSettingsModal', () => {
});
it('should show correct tab label as "Access Control"', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -455,14 +443,13 @@ describe('ChannelSettingsModal', () => {
});
it('should show Access Control tab for default channel if private and user has permission', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
renderWithContext(
<ChannelSettingsModal
{...{...baseProps, channelId: 'default_channel'}}
/>,
);
const testState = makeTestState();
testState.entities.channels.channels[channelId].name = 'town-square';
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -474,11 +461,13 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained private channel even with permission', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
mockGroupConstrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
testState.entities.channels.channels[channelId].group_constrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -491,11 +480,13 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained private channel without permission', async () => {
mockChannelType = 'P';
mockManageChannelAccessRulesPermission = false;
mockGroupConstrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
testState.entities.channels.channels[channelId].group_constrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -508,11 +499,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained public channel', async () => {
mockChannelType = 'O';
mockManageChannelAccessRulesPermission = true;
mockGroupConstrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
const testState = makeTestState();
testState.entities.channels.channels[channelId].group_constrained = true;
renderWithContext(<ChannelSettingsModal {...baseProps}/>, testState);
// Wait for the sidebar to load
await waitFor(() => {
@ -531,7 +523,7 @@ describe('ChannelSettingsModal', () => {
});
it('should close immediately when no unsaved changes exist', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
renderWithContext(<ChannelSettingsModal {...baseProps}/>, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@ -546,7 +538,7 @@ describe('ChannelSettingsModal', () => {
});
it('should prevent close on first attempt with unsaved changes', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
renderWithContext(<ChannelSettingsModal {...baseProps}/>, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@ -571,7 +563,7 @@ describe('ChannelSettingsModal', () => {
});
it('should allow close on second attempt (warn-once behavior)', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
renderWithContext(<ChannelSettingsModal {...baseProps}/>, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@ -598,7 +590,7 @@ describe('ChannelSettingsModal', () => {
});
it('should reset warning state when changes are saved', async () => {
renderWithContext(<ChannelSettingsModal {...baseProps}/>);
renderWithContext(<ChannelSettingsModal {...baseProps}/>, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();

View file

@ -9,30 +9,7 @@ exports[`components/channel_view Should match snapshot if channel is archived 1`
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader
canRestrictDirectMessage={false}
channelId="channelId"
channelIsArchived={true}
deactivatedChannel={false}
enableOnboardingFlow={true}
enableWebSocketEventScope={false}
fetchIsRestrictedDM={[MockFunction]}
goToLastViewedChannel={[MockFunction]}
history={Object {}}
isChannelBookmarksEnabled={false}
isCloud={false}
isFirstAdmin={false}
location={Object {}}
match={
Object {
"params": Object {},
"url": "/team/channel/channelId",
}
}
missingChannelRole={false}
restrictDirectMessage={false}
teamUrl="/team"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>
@ -79,30 +56,7 @@ exports[`components/channel_view Should match snapshot if channel is deactivated
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader
canRestrictDirectMessage={false}
channelId="channelId"
channelIsArchived={false}
deactivatedChannel={true}
enableOnboardingFlow={true}
enableWebSocketEventScope={false}
fetchIsRestrictedDM={[MockFunction]}
goToLastViewedChannel={[MockFunction]}
history={Object {}}
isChannelBookmarksEnabled={false}
isCloud={false}
isFirstAdmin={false}
location={Object {}}
match={
Object {
"params": Object {},
"url": "/team/channel/channelId",
}
}
missingChannelRole={false}
restrictDirectMessage={false}
teamUrl="/team"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>
@ -148,30 +102,7 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
id="centerChannelFileDropOverlay"
overlayType="center"
/>
<ChannelHeader
canRestrictDirectMessage={false}
channelId="channelId"
channelIsArchived={false}
deactivatedChannel={false}
enableOnboardingFlow={true}
enableWebSocketEventScope={false}
fetchIsRestrictedDM={[MockFunction]}
goToLastViewedChannel={[MockFunction]}
history={Object {}}
isChannelBookmarksEnabled={false}
isCloud={false}
isFirstAdmin={false}
location={Object {}}
match={
Object {
"params": Object {},
"url": "/team/channel/channelId",
}
}
missingChannelRole={false}
restrictDirectMessage={false}
teamUrl="/team"
/>
<ChannelHeader />
<ChannelBanner
channelId="channelId"
/>

View file

@ -122,7 +122,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
id='channelView.archivedChannelWithDeactivatedUser'
defaultMessage='You are viewing an archived channel with a <b>deactivated user</b>. New messages cannot be posted.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<button
@ -151,7 +151,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
id='channelView.archivedChannel'
defaultMessage='You are viewing an <b>archived channel</b>. New messages cannot be posted.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<button
@ -180,7 +180,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
id='channelView.noSharedTeam'
defaultMessage='You no longer have any teams in common with this user. New messages cannot be posted.'
values={{
b: (chunks: string) => <b>{chunks}</b>,
b: (chunks) => <b>{chunks}</b>,
}}
/>
<button
@ -221,7 +221,7 @@ export default class ChannelView extends React.PureComponent<Props, State> {
overlayType='center'
id={DropOverlayIdCenterChannel}
/>
<ChannelHeader {...this.props}/>
<ChannelHeader/>
{this.props.isChannelBookmarksEnabled && <ChannelBookmarks channelId={this.props.channelId}/>}
<ChannelBanner channelId={this.props.channelId}/>
<DeferredPostView

View file

@ -54,7 +54,7 @@ function CloudInvoicePreview(props: Props) {
<FormattedMessage
id='cloud.invoice_pdf_preview.download'
values={{
downloadLink: (msg: string) => (
downloadLink: (msg) => (
<ExternalLink
href={props.url || ''}
location='cloud_invoice_preview'

Some files were not shown because too many files have changed in this diff Show more