MM-66972 Upgrade to node 24 and main dependencies with babel, webpack and jest (#34760)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions

* chore: upgrade to node 24 and dependencies mainly with babel, webpack and jest

* fix components tests, make trial modal passed on all node 20-24

* fix cache for platform packages

* updated test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
sabril 2026-01-14 13:14:01 +08:00 committed by GitHub
parent 92339d03ab
commit dab04576a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 5777 additions and 10651 deletions

View file

@ -15,6 +15,9 @@ runs:
path: | path: |
webapp/node_modules webapp/node_modules
webapp/channels/node_modules webapp/channels/node_modules
webapp/platform/client/node_modules
webapp/platform/components/node_modules
webapp/platform/types/node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('webapp/package-lock.json') }} key: node-modules-${{ runner.os }}-${{ hashFiles('webapp/package-lock.json') }}
- name: ci/cache-platform-builds - name: ci/cache-platform-builds
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0

View file

@ -20,7 +20,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
cache: "npm" cache: "npm"

View file

@ -133,7 +133,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: ci/setup-node - name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}" if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node id: setup_node
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
@ -164,7 +164,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: ci/setup-node - name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}" if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node id: setup_node
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
@ -246,7 +246,7 @@ jobs:
ref: ${{ inputs.commit_sha }} ref: ${{ inputs.commit_sha }}
fetch-depth: 0 fetch-depth: 0
- name: ci/setup-node - name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node id: setup_node
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
@ -333,7 +333,7 @@ jobs:
ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: ci/setup-node - name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node id: setup_node
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
@ -412,7 +412,7 @@ jobs:
e2e-tests/${{ inputs.TEST }}/results/ e2e-tests/${{ inputs.TEST }}/results/
- name: ci/setup-node - name: ci/setup-node
if: "${{ inputs.enable_reporting }}" if: "${{ inputs.enable_reporting }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node id: setup_node
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"

View file

@ -282,6 +282,12 @@ jobs:
steps: steps:
- name: Checkout mattermost project - name: Checkout mattermost project
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: ci/setup-node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: "npm"
cache-dependency-path: "webapp/package-lock.json"
- name: Run setup-go-work - name: Run setup-go-work
run: make setup-go-work run: make setup-go-work
- name: Build - name: Build

2
.nvmrc
View file

@ -1 +1 @@
20.11 24.11

View file

@ -260,7 +260,7 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
# shellcheck disable=SC2016 # shellcheck disable=SC2016
echo ' echo '
webhook-interactions: webhook-interactions:
image: mattermostdevelopment/mirrored-node:${NODE_VERSION_REQUIRED} image: node:${NODE_VERSION_REQUIRED}
command: sh -c "npm install --global --legacy-peer-deps && exec node webhook_serve.js" command: sh -c "npm install --global --legacy-peer-deps && exec node webhook_serve.js"
healthcheck: healthcheck:
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"] test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"]
@ -275,11 +275,21 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
fi) fi)
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then $(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
# shellcheck disable=SC2016
echo ' echo '
playwright: playwright:
image: mcr.microsoft.com/playwright:v1.57.0-noble image: mcr.microsoft.com/playwright:v1.57.0-noble
entrypoint: ["/bin/bash", "-c"] entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"] command:
- |
# Install Node.js based on .nvmrc
NODE_VERSION=$$(cat /mattermost/.nvmrc)
echo "Installing Node.js $${NODE_VERSION}..."
curl -fsSL https://deb.nodesource.com/setup_$${NODE_VERSION%%.*}.x | bash -
apt-get install -y nodejs
echo "Node.js version: $$(node --version)"
# Wait for termination signal
until [ -f /var/run/mm_terminate ]; do sleep 5; done
env_file: env_file:
- "./.env.playwright" - "./.env.playwright"
environment: environment:

View file

@ -5981,9 +5981,9 @@
} }
}, },
"node_modules/systeminformation": { "node_modules/systeminformation": {
"version": "5.30.1", "version": "5.30.2",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.1.tgz", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.2.tgz",
"integrity": "sha512-5zK8Sqqn71b0AoYKnj8nurrugOVogo4hBxAeQR9N0lbC5V+Fkw1hRBRWLaKxBmuvX8v4xH3cxifOJjlhQQW1lQ==", "integrity": "sha512-Rrt5oFTWluUVuPlbtn3o9ja+nvjdF3Um4DG0KxqfYvpzcx7Q9plZBTjJiJy9mAouua4+OI7IUGBaG9Zyt9NgxA==",
"license": "MIT", "license": "MIT",
"os": [ "os": [
"darwin", "darwin",

View file

@ -1,2 +1 @@
save-exact=true save-exact=true
engine-strict=true

View file

@ -23,9 +23,9 @@ jobs:
- name: Check out web app - name: Check out web app
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version: 16.10.0 node-version-file: ".nvmrc"
- name: Download and install Cypress - name: Download and install Cypress
uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16 uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16

View file

@ -149,8 +149,8 @@
"imagemin-mozjpeg": "9.0.0", "imagemin-mozjpeg": "9.0.0",
"jest": "30.1.3", "jest": "30.1.3",
"jest-canvas-mock": "2.5.0", "jest-canvas-mock": "2.5.0",
"jest-cli": "29.7.0", "jest-cli": "30.1.3",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "30.1.0",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-watch-typeahead": "3.0.1", "jest-watch-typeahead": "3.0.1",
"nock": "13.2.8", "nock": "13.2.8",

View file

@ -3,7 +3,16 @@
import {jest} from '@jest/globals'; import {jest} from '@jest/globals';
const monacoMock = { const monacoMock: {
editor: {
create: jest.Mock;
defineTheme: jest.Mock;
setTheme: jest.Mock;
};
languages: {
registerCompletionItemProvider: jest.Mock;
};
} = {
editor: { editor: {
create: jest.fn(), create: jest.fn(),
defineTheme: jest.fn(), defineTheme: jest.fn(),

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import type {ReactNode} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
@ -108,7 +107,7 @@ const TrialBanner = ({
case TrialLoadStatus.Failed: case TrialLoadStatus.Failed:
return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'}); return formatMessage({id: 'start_trial.modal.failed', defaultMessage: 'Failed'});
case TrialLoadStatus.Embargoed: case TrialLoadStatus.Embargoed:
return formatMessage<ReactNode>( return formatMessage(
{ {
id: 'admin.license.trial-request.embargoed', 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.', 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.',

View file

@ -69,7 +69,7 @@ exports[`components/admin_console/permission_schemes_settings/permission_descrip
<span> <span>
Inherited from Inherited from
<a <a
key=".$.1" key="1/.1"
> >
All Members All Members
</a> </a>

View file

@ -1,4 +1,4 @@
@import 'utils/mixins'; @use 'utils/mixins';
.channel-invite { .channel-invite {
&__wrapper { &__wrapper {

View file

@ -121,7 +121,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
className="AlertBanner__footerMessage" className="AlertBanner__footerMessage"
> >
<FormattedList <FormattedList
key=".0" key="0/.0"
value={ value={
Array [ Array [
<Memo(Connect(Component)) <Memo(Connect(Component))
@ -299,7 +299,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
You can add You can add
<FormattedList <FormattedList
key=".1" key="1/.1"
value={ value={
Array [ Array [
<Memo(Connect(Component)) <Memo(Connect(Component))
@ -343,7 +343,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
</FormattedList> </FormattedList>
to this channel once they are members of the to this channel once they are members of the
<strong <strong
key=".3" key="3/.3"
> >
Team Name Display Team Name Display
</strong> </strong>
@ -549,7 +549,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
className="AlertBanner__footerMessage" className="AlertBanner__footerMessage"
> >
<Connect(Component) <Connect(Component)
key=".$user-0" key="0/.$user-0"
mentionName="user-0" mentionName="user-0"
> >
<Memo(AtMention) <Memo(AtMention)
@ -563,7 +563,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
</Connect(Component)> </Connect(Component)>
and and
<WithTooltip <WithTooltip
key=".2" key="2/.2"
title="@user-1, @user-2, @user-3, @user-4, @user-5, @user-6, @user-7, @user-8, @user-9, @user-10" title="@user-1, @user-2, @user-3, @user-4, @user-5, @user-6, @user-7, @user-8, @user-9, @user-10"
> >
<span <span
@ -799,7 +799,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
> >
You can add You can add
<Connect(Component) <Connect(Component)
key=".$user-0" key="1/.$user-0"
mentionName="user-0" mentionName="user-0"
> >
<Memo(AtMention) <Memo(AtMention)
@ -813,7 +813,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
</Connect(Component)> </Connect(Component)>
and and
<WithTooltip <WithTooltip
key=".3" key="3/.3"
title="@user-1, @user-2, @user-3, @user-4, @user-5, @user-6, @user-7, @user-8, @user-9, @user-10" title="@user-1, @user-2, @user-3, @user-4, @user-5, @user-6, @user-7, @user-8, @user-9, @user-10"
> >
<span <span
@ -843,7 +843,7 @@ exports[`components/channel_invite_modal/team_warning_banner should match snapsh
</WithTooltip> </WithTooltip>
to this channel once they are members of the to this channel once they are members of the
<strong <strong
key=".5" key="5/.5"
> >
Team Name Display Team Name Display
</strong> </strong>

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
@import "utils/_animations"; @use "utils/animations";
.ChannelSettingsModal__configurationTab { .ChannelSettingsModal__configurationTab {
display: flex; display: flex;
@ -32,7 +32,7 @@
} }
.channel_banner_section_body { .channel_banner_section_body {
@include fade-in; @include animations.fade-in;
display: flex; display: flex;
width: 100%; width: 100%;

View file

@ -7,6 +7,21 @@ import {fireEvent, render} from 'tests/react_testing_utils';
import Scrollbars from './scrollbars'; import Scrollbars from './scrollbars';
const originalGetComputedStyle = window.getComputedStyle;
beforeAll(() => {
window.getComputedStyle = (elt: Element, pseudoElt?: string | null) => {
if (pseudoElt) {
// Return an empty CSSStyleDeclaration-like object for pseudo elements
return {} as CSSStyleDeclaration;
}
return originalGetComputedStyle(elt);
};
});
afterAll(() => {
window.getComputedStyle = originalGetComputedStyle;
});
describe('Scrollbars', () => { describe('Scrollbars', () => {
test('should attach scroll handler to the correct element', () => { test('should attach scroll handler to the correct element', () => {
const onScroll = jest.fn(); const onScroll = jest.fn();

View file

@ -136,7 +136,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
role="grid" role="grid"
> >
<div <div
style="position: relative; height: 100px; width: 100px; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 100px; width: 100px; overflow: auto; -webkit-overflow-scrolling: touch; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 7740px; width: 100%;" style="height: 7740px; width: 100%;"

View file

@ -136,7 +136,7 @@ exports[`FileAttachment should match snapshot, after change from file to image 1
> >
<div <div
class="post-image normal" class="post-image normal"
style="background-image: url(thumbnail_id); background-size: cover;" style="background-image: url(\\"thumbnail_id\\"); background-size: cover;"
/> />
</a> </a>
<div <div

View file

@ -15,7 +15,7 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -478,7 +478,7 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>
@ -590,7 +590,7 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -1073,7 +1073,7 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>

View file

@ -15,7 +15,7 @@ exports[`components/integrations/AddOutgoingOAuthConnection should match snapsho
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -467,7 +467,7 @@ exports[`components/integrations/AddOutgoingOAuthConnection should match snapsho
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>

View file

@ -15,7 +15,7 @@ exports[`components/integrations/EditOutgoingOAuthConnection should match snapsh
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -643,7 +643,7 @@ https://myothersite.com/api/v2"
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>
@ -756,7 +756,7 @@ exports[`components/integrations/EditOutgoingOAuthConnection should match snapsh
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -1385,7 +1385,7 @@ https://myothersite.com/api/v2"
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>
@ -1498,7 +1498,7 @@ exports[`components/integrations/EditOutgoingOAuthConnection should match snapsh
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -2126,7 +2126,7 @@ https://myothersite.com/api/v2"
Get help with Get help with
<a <a
href="https://mattermost.com/pl/outgoing-oauth-connections" href="https://mattermost.com/pl/outgoing-oauth-connections"
key=".$.1" key="1/.1"
> >
configuring outgoing OAuth connections configuring outgoing OAuth connections
</a> </a>

View file

@ -15,7 +15,7 @@ exports[`components/integrations/InstalledOutgoingOAuthConnections should match
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -190,7 +190,7 @@ exports[`components/integrations/InstalledOutgoingOAuthConnections should match
Create Create
<ForwardRef <ForwardRef
href="https://mattermost.com/pl/setup-oauth-2.0" href="https://mattermost.com/pl/setup-oauth-2.0"
key=".$.1" key="1/.1"
location="installed_outgoing_oauth_connections" location="installed_outgoing_oauth_connections"
> >
<a <a

View file

@ -15,7 +15,7 @@ exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudie
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -109,7 +109,7 @@ exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudie
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -203,7 +203,7 @@ exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudie
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -297,7 +297,7 @@ exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudie
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },
@ -407,7 +407,7 @@ exports[`components/integrations/outgoing_oauth_connections/OAuthConnectionAudie
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },

View file

@ -318,10 +318,8 @@ describe('components/login/Login', () => {
const button = screen.getByRole('link', {name: 'Gitlab Icon GitLab 2'}); const button = screen.getByRole('link', {name: 'Gitlab Icon GitLab 2'});
expect(button.style).toMatchObject({ expect(button.style.color).toBe('rgb(0, 255, 0)');
color: 'rgb(0, 255, 0)', expect(button.style.borderColor).toBe('rgb(0, 255, 0)');
borderColor: '#00ff00',
});
}); });
it('should focus username field when there is an error', async () => { it('should focus username field when there is an error', async () => {
@ -374,10 +372,8 @@ describe('components/login/Login', () => {
const button = screen.getByRole('link', {name: 'OpenID Icon OpenID 2'}); const button = screen.getByRole('link', {name: 'OpenID Icon OpenID 2'});
expect(button.style).toMatchObject({ expect(button.style.color).toBe('rgb(0, 255, 0)');
color: 'rgb(0, 255, 0)', expect(button.style.borderColor).toBe('rgb(0, 255, 0)');
borderColor: '#00ff00',
});
}); });
it('should redirect on login', async () => { it('should redirect on login', async () => {

View file

@ -92,7 +92,7 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
<span> <span>
Error connecting to the marketplace server. Please check your settings in the Error connecting to the marketplace server. Please check your settings in the
<Link <Link
key=".1" key="1/.1"
to="/admin_console/plugins/plugin_management" to="/admin_console/plugins/plugin_management"
> >
System Console System Console
@ -204,7 +204,7 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
<span> <span>
Error connecting to the marketplace server. Please check your settings in the Error connecting to the marketplace server. Please check your settings in the
<Link <Link
key=".1" key="1/.1"
to="/admin_console/plugins/plugin_management" to="/admin_console/plugins/plugin_management"
> >
System Console System Console
@ -501,7 +501,7 @@ exports[`components/marketplace/ should render with error banner 1`] = `
<span> <span>
Error connecting to the marketplace server. Please check your settings in the Error connecting to the marketplace server. Please check your settings in the
<Link <Link
key=".1" key="1/.1"
to="/admin_console/plugins/plugin_management" to="/admin_console/plugins/plugin_management"
> >
System Console System Console

View file

@ -173,7 +173,7 @@ exports[`InviteMembers component should match snapshot when it is cloud 1`] = `
id="react-select-2-input" id="react-select-2-input"
role="combobox" role="combobox"
spellcheck="false" spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;" style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0" tabindex="0"
type="text" type="text"
value="" value=""

View file

@ -39,8 +39,10 @@ describe('SelectPropertyRenderer', () => {
const element = screen.getByTestId('select-property'); const element = screen.getByTestId('select-property');
expect(element).toBeInTheDocument(); expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('option1'); expect(element).toHaveTextContent('option1');
// Component applies inline styles via style prop
expect(element).toHaveStyle({ expect(element).toHaveStyle({
backgroundColor: 'rgba(var(--button-bg-rgb), 0.08)', backgroundColor: 'var(--sidebar-text-active-border)',
color: '#FFF', color: '#FFF',
}); });
}); });

View file

@ -21,6 +21,7 @@ import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'matte
import {redirectUserToDefaultTeam, emitUserLoggedOutEvent} from 'actions/global_actions'; import {redirectUserToDefaultTeam, emitUserLoggedOutEvent} from 'actions/global_actions';
import {reloadPage} from 'utils/browser_utils';
import {ActionTypes, StoragePrefixes} from 'utils/constants'; import {ActionTypes, StoragePrefixes} from 'utils/constants';
import {doesCookieContainsMMUserId} from 'utils/utils'; import {doesCookieContainsMMUserId} from 'utils/utils';
@ -165,7 +166,7 @@ export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc<void>
// detected login from a different tab // detected login from a different tab
function reloadOnFocus() { function reloadOnFocus() {
location.reload(); reloadPage();
} }
window.addEventListener('focus', reloadOnFocus); window.addEventListener('focus', reloadOnFocus);
} }

View file

@ -9,6 +9,7 @@ import * as GlobalActions from 'actions/global_actions';
import testConfigureStore from 'packages/mattermost-redux/test/test_store'; import testConfigureStore from 'packages/mattermost-redux/test/test_store';
import {renderWithContext, waitFor} from 'tests/react_testing_utils'; import {renderWithContext, waitFor} from 'tests/react_testing_utils';
import * as BrowserUtils from 'utils/browser_utils';
import {StoragePrefixes} from 'utils/constants'; import {StoragePrefixes} from 'utils/constants';
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions'; import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
@ -26,6 +27,10 @@ jest.mock('utils/utils', () => ({
applyTheme: jest.fn(), applyTheme: jest.fn(),
})); }));
jest.mock('utils/browser_utils', () => ({
reloadPage: jest.fn(),
}));
jest.mock('actions/global_actions', () => ({ jest.mock('actions/global_actions', () => ({
redirectUserToDefaultTeam: jest.fn(), redirectUserToDefaultTeam: jest.fn(),
})); }));
@ -93,11 +98,9 @@ describe('components/Root', () => {
}; };
let originalMatchMedia: (query: string) => MediaQueryList; let originalMatchMedia: (query: string) => MediaQueryList;
let originalReload: () => void;
beforeAll(() => { beforeAll(() => {
originalMatchMedia = window.matchMedia; originalMatchMedia = window.matchMedia;
originalReload = window.location.reload;
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
@ -106,22 +109,17 @@ describe('components/Root', () => {
media: query, media: query,
})), })),
}); });
Object.defineProperty(window.location, 'reload', {
configurable: true,
writable: true,
});
window.location.reload = jest.fn();
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
// Reset the reloadPage mock after each test
(BrowserUtils.reloadPage as jest.Mock).mockClear();
}); });
afterAll(() => { afterAll(() => {
window.matchMedia = originalMatchMedia; window.matchMedia = originalMatchMedia;
window.location.reload = originalReload;
}); });
test('should load config and license on mount and redirect to sign-up page', async () => { test('should load config and license on mount and redirect to sign-up page', async () => {
@ -228,7 +226,7 @@ describe('components/Root', () => {
window.dispatchEvent(new Event('focus')); window.dispatchEvent(new Event('focus'));
await waitFor(() => { await waitFor(() => {
expect(window.location.reload).toHaveBeenCalledTimes(1); expect(BrowserUtils.reloadPage).toHaveBeenCalledTimes(1);
}); });
}); });

View file

@ -170,7 +170,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
Sign up at Sign up at
<ForwardRef <ForwardRef
href="https://mattermost.com/security-updates/" href="https://mattermost.com/security-updates/"
key=".1" key="1/.1"
location="signup" location="signup"
> >
https://mattermost.com/security-updates/ https://mattermost.com/security-updates/
@ -336,7 +336,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
Sign up at Sign up at
<ForwardRef <ForwardRef
href="https://mattermost.com/security-updates/" href="https://mattermost.com/security-updates/"
key=".1" key="1/.1"
location="signup" location="signup"
> >
https://mattermost.com/security-updates/ https://mattermost.com/security-updates/

View file

@ -225,7 +225,7 @@ Object {
id="react-select-2-input" id="react-select-2-input"
role="combobox" role="combobox"
spellcheck="false" spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;" style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0" tabindex="0"
type="text" type="text"
value="" value=""
@ -312,7 +312,7 @@ Object {
id="react-select-3-input" id="react-select-3-input"
role="combobox" role="combobox"
spellcheck="false" spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;" style="color: inherit; background: 0px; opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0" tabindex="0"
type="text" type="text"
value="" value=""

View file

@ -507,7 +507,7 @@ exports[`components/threading/channel_threads/thread_footer should report total
<span> <span>
Last reply Last reply
<Memo(SemanticTime) <Memo(SemanticTime)
key=".$.1" key="1/.1"
value={2019-04-01T23:31:44.000Z} value={2019-04-01T23:31:44.000Z}
> >
<time <time
@ -1021,7 +1021,7 @@ exports[`components/threading/channel_threads/thread_footer should show unread i
<span> <span>
Last reply Last reply
<Memo(SemanticTime) <Memo(SemanticTime)
key=".$.1" key="1/.1"
value={2019-04-01T23:31:44.000Z} value={2019-04-01T23:31:44.000Z}
> >
<time <time

View file

@ -4,7 +4,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import React, {memo, useCallback, useEffect, useState} from 'react'; import React, {memo, useCallback, useEffect, useState} from 'react';
import type {ReactNode} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {useSelector, useDispatch, shallowEqual} from 'react-redux'; import {useSelector, useDispatch, shallowEqual} from 'react-redux';
import {Link, useRouteMatch} from 'react-router-dom'; import {Link, useRouteMatch} from 'react-router-dom';
@ -204,7 +203,7 @@ const GlobalThreads = () => {
id: 'globalThreads.threadPane.unselectedTitle', id: 'globalThreads.threadPane.unselectedTitle',
defaultMessage: '{numUnread, plural, =0 {Looks like youre all caught up} other {Catch up on your threads}}', defaultMessage: '{numUnread, plural, =0 {Looks like youre all caught up} other {Catch up on your threads}}',
}, {numUnread})} }, {numUnread})}
subtitle={formatMessage<ReactNode>({ subtitle={formatMessage({
id: 'globalThreads.threadPane.unreadMessageLink', id: 'globalThreads.threadPane.unreadMessageLink',
defaultMessage: 'You have {numUnread, plural, =0 {no unread threads} =1 {<link>{numUnread} thread</link>} other {<link>{numUnread} threads</link>}} {numUnread, plural, =0 {} other {with unread messages}}', defaultMessage: 'You have {numUnread, plural, =0 {no unread threads} =1 {<link>{numUnread} thread</link>} other {<link>{numUnread} threads</link>}} {numUnread, plural, =0 {} other {with unread messages}}',
}, { }, {

View file

@ -1,13 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/three_days_left_trial_modal/three_days_left_trial_modal should match snapshot 1`] = `
"<ContextProvider value={{...}}>
<ThreeDaysLeftTrialModal onExited={[Function: mockConstructor] { _isMockFunction: true, getMockImplementation: [Function (anonymous)], mock: Object [Object: null prototype] { calls: [], contexts: [], instances: [], invocationCallOrder: [], results: [] }, mockClear: [Function (anonymous)], mockReset: [Function (anonymous)], mockRestore: [Function (anonymous)], mockReturnValueOnce: [Function (anonymous)], mockResolvedValueOnce: [Function (anonymous)], mockRejectedValueOnce: [Function (anonymous)], mockReturnValue: [Function (anonymous)], mockResolvedValue: [Function (anonymous)], mockRejectedValue: [Function (anonymous)], mockImplementationOnce: [Function (anonymous)], withImplementation: [Function: bound withImplementation], mockImplementation: [Function (anonymous)], mockReturnThis: [Function (anonymous)], mockName: [Function (anonymous)], getMockName: [Function (anonymous)] }} limitsOverpassed={false} />
</ContextProvider>"
`;
exports[`components/three_days_left_trial_modal/three_days_left_trial_modal should match snapshot when limits are overpassed and show the limits panel 1`] = `
"<ContextProvider value={{...}}>
<ThreeDaysLeftTrialModal onExited={[Function: mockConstructor] { _isMockFunction: true, getMockImplementation: [Function (anonymous)], mock: Object [Object: null prototype] { calls: [], contexts: [], instances: [], invocationCallOrder: [], results: [] }, mockClear: [Function (anonymous)], mockReset: [Function (anonymous)], mockRestore: [Function (anonymous)], mockReturnValueOnce: [Function (anonymous)], mockResolvedValueOnce: [Function (anonymous)], mockRejectedValueOnce: [Function (anonymous)], mockReturnValue: [Function (anonymous)], mockResolvedValue: [Function (anonymous)], mockRejectedValue: [Function (anonymous)], mockImplementationOnce: [Function (anonymous)], withImplementation: [Function: bound withImplementation], mockImplementation: [Function (anonymous)], mockReturnThis: [Function (anonymous)], mockName: [Function (anonymous)], getMockName: [Function (anonymous)] }} limitsOverpassed={true} />
</ContextProvider>"
`;

View file

@ -1,20 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import {Provider} from 'react-redux';
import {GenericModal} from '@mattermost/components';
import ThreeDaysLeftTrialModal from 'components/three_days_left_trial_modal/three_days_left_trial_modal'; import ThreeDaysLeftTrialModal from 'components/three_days_left_trial_modal/three_days_left_trial_modal';
import TestHelper from 'packages/mattermost-redux/test/test_helper'; import TestHelper from 'packages/mattermost-redux/test/test_helper';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
describe('components/three_days_left_trial_modal/three_days_left_trial_modal', () => { describe('components/three_days_left_trial_modal/three_days_left_trial_modal', () => {
// required state to mount using the provider
const user = TestHelper.fakeUserWithId(); const user = TestHelper.fakeUserWithId();
const profiles = { const profiles = {
@ -73,70 +67,90 @@ describe('components/three_days_left_trial_modal/three_days_left_trial_modal', (
}, },
}; };
const props = { const defaultProps = {
onExited: jest.fn(), onExited: jest.fn(),
limitsOverpassed: false, limitsOverpassed: false,
}; };
const store = mockStore(state); beforeEach(() => {
jest.clearAllMocks();
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<ThreeDaysLeftTrialModal {...props}/>
</Provider>,
);
expect(wrapper.debug()).toMatchSnapshot();
}); });
test('should match snapshot when limits are overpassed and show the limits panel', () => { test('should render the modal with header, subtitle, feature cards, and view plans button', () => {
const wrapper = shallow( renderWithContext(
<Provider store={store}> <ThreeDaysLeftTrialModal {...defaultProps}/>,
<ThreeDaysLeftTrialModal state,
{...props}
limitsOverpassed={true}
/>
</Provider>,
);
expect(wrapper.debug()).toMatchSnapshot();
});
test('should show the three days left modal with the three cards', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<ThreeDaysLeftTrialModal {...props}/>
</Provider>,
); );
expect(wrapper.find('ThreeDaysLeftTrialModal ThreeDaysLeftTrialCard')).toHaveLength(3); // Header and subtitle
expect(screen.getByText('Your trial ends soon')).toBeInTheDocument();
expect(screen.getByText('There is still time to explore what our paid plans can help you accomplish.')).toBeInTheDocument();
// Three feature cards
expect(screen.getByText('Use SSO (with OpenID, SAML, Google, O365)')).toBeInTheDocument();
expect(screen.getByText('Synchronize your Active Directory/LDAP groups')).toBeInTheDocument();
expect(screen.getByText('Provide controlled access to the System Console')).toBeInTheDocument();
// View plans button
expect(screen.getByRole('button', {name: 'View plan options'})).toBeInTheDocument();
}); });
test('should show the workspace limits panel when limits are overpassed', () => { test('should show limits overpassed content when limitsOverpassed is true', () => {
const wrapper = mountWithIntl( renderWithContext(
<Provider store={store}> <ThreeDaysLeftTrialModal
<ThreeDaysLeftTrialModal {...defaultProps}
{...props} limitsOverpassed={true}
limitsOverpassed={true} />,
/> state,
</Provider>,
); );
expect(wrapper.find('ThreeDaysLeftTrialModal WorkspaceLimitsPanel')).toHaveLength(1);
// Different header and subtitle
expect(screen.getByText('Upgrade before the trial ends')).toBeInTheDocument();
expect(screen.getByText('There are 3 days left on your trial. Upgrade to our Professional or Enterprise plan to avoid exceeding your data limits on the Free plan.')).toBeInTheDocument();
// Shows limits panel instead of feature cards
expect(screen.getByText('Limits')).toBeInTheDocument();
expect(screen.queryByText('Use SSO (with OpenID, SAML, Google, O365)')).not.toBeInTheDocument();
}); });
test('should call on exited', () => { test('should call onExited when modal is closed', async () => {
const mockOnExited = jest.fn(); const mockOnExited = jest.fn();
const wrapper = mountWithIntl( renderWithContext(
<Provider store={store}> <ThreeDaysLeftTrialModal
<ThreeDaysLeftTrialModal {...defaultProps}
{...props} onExited={mockOnExited}
onExited={mockOnExited} />,
/> state,
</Provider>,
); );
wrapper.find(GenericModal).props().onExited?.(); const closeButton = screen.getByLabelText('Close');
await userEvent.click(closeButton);
expect(mockOnExited).toHaveBeenCalled(); await waitFor(() => {
expect(mockOnExited).toHaveBeenCalledTimes(1);
});
});
test('should not render when modal is not open', () => {
const closedState = {
...state,
views: {
modals: {
modalState: {
three_days_left_trial_modal: {
open: false,
},
},
},
},
};
renderWithContext(
<ThreeDaysLeftTrialModal {...defaultProps}/>,
closedState,
);
expect(screen.queryByText('Your trial ends soon')).not.toBeInTheDocument();
}); });
}); });

View file

@ -4,7 +4,7 @@ exports[`UserAccountNameMenuItem should not break if no props are passed 1`] = `
<div> <div>
<li <li
aria-haspopup="true" aria-haspopup="true"
class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR koIPww userAccountMenu_nameMenuItem" class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe userAccountMenu_nameMenuItem"
role="menuitem" role="menuitem"
tabindex="-1" tabindex="-1"
> >

View file

@ -1,4 +1,4 @@
@import "utils/_mixins"; @use "utils/mixins";
#userAccountMenu { #userAccountMenu {
.userAccountMenu_nameMenuItem { .userAccountMenu_nameMenuItem {
@ -21,14 +21,14 @@
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
line-height: 22px; line-height: 22px;
@include textEllipsis; @include mixins.textEllipsis;
} }
span.userAccountMenu_nameMenuItem_secondaryLabel { span.userAccountMenu_nameMenuItem_secondaryLabel {
max-width: 150px; max-width: 150px;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;
@include textEllipsis; @include mixins.textEllipsis;
} }
} }
@ -61,7 +61,7 @@
.label-elements { .label-elements {
> span { > span {
max-width: 196px; max-width: 196px;
@include textEllipsis; @include mixins.textEllipsis;
} }
} }
} }

View file

@ -27,7 +27,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },

View file

@ -27,7 +27,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"listen": [Function], "listen": [Function],
"location": Object { "location": Object {
"hash": "", "hash": "",
"pathname": "undefinedundefined", "pathname": "/",
"search": "", "search": "",
"state": undefined, "state": undefined,
}, },

View file

@ -1,4 +1,4 @@
@import 'utils/mixins'; @use 'utils/mixins';
.AdvancedTextbox { .AdvancedTextbox {
position: relative; position: relative;

View file

@ -76,7 +76,9 @@ describe('components/Menu', () => {
); );
const menu = screen.getByRole('menu'); const menu = screen.getByRole('menu');
expect(menu).toHaveStyle({maxHeight: '200px', backgroundColor: 'red'});
expect(menu.style.maxHeight).toBe('200px');
expect(menu.style.backgroundColor).toBe('red');
}); });
test('should apply custom className to menu list', () => { test('should apply custom className to menu list', () => {

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react'; import React from 'react';
import type {ReactNode} from 'react';
import {defineMessage, useIntl} from 'react-intl'; import {defineMessage, useIntl} from 'react-intl';
import type {LimitSummary} from 'components/common/hooks/useGetHighestThresholdCloudLimit'; import type {LimitSummary} from 'components/common/hooks/useGetHighestThresholdCloudLimit';
@ -151,7 +150,7 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser
id: 'workspace_limits.menu_limit.messages', id: 'workspace_limits.menu_limit.messages',
defaultMessage: 'Total messages', defaultMessage: 'Total messages',
}), }),
description: intl.formatMessage<ReactNode>( description: intl.formatMessage(
description, description,
values, values,
), ),
@ -196,7 +195,7 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser
id: 'workspace_limits.menu_limit.file_storage', id: 'workspace_limits.menu_limit.file_storage',
defaultMessage: 'File storage limit', defaultMessage: 'File storage limit',
}), }),
description: intl.formatMessage<ReactNode>( description: intl.formatMessage(
description, description,
values, values,
), ),

View file

@ -33,7 +33,7 @@ exports[`TooltipContent have correct structure with title and emoji 1`] = `
aria-label=":smile:" aria-label=":smile:"
class="emoticon" class="emoticon"
data-emoticon="smile" data-emoticon="smile"
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;" style="background-image: url(\\"/static/emoji/1f604.png\\"); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;"
/> />
</span> </span>
<span <span
@ -61,7 +61,7 @@ exports[`TooltipContent have correct structure with title and large emoji 1`] =
aria-label=":smile:" aria-label=":smile:"
class="emoticon" class="emoticon"
data-emoticon="smile" data-emoticon="smile"
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 48px; width: 48px; max-height: 48px; max-width: 48px; min-height: 48px; min-width: 48px; overflow: hidden;" style="background-image: url(\\"/static/emoji/1f604.png\\"); background-size: contain; height: 48px; width: 48px; max-height: 48px; max-width: 48px; min-height: 48px; min-width: 48px; overflow: hidden;"
/> />
</span> </span>
<span <span
@ -121,7 +121,7 @@ exports[`TooltipContent have correct structure with title, emoji and hint 1`] =
aria-label=":smile:" aria-label=":smile:"
class="emoticon" class="emoticon"
data-emoticon="smile" data-emoticon="smile"
style="background-image: url(/static/emoji/1f604.png); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;" style="background-image: url(\\"/static/emoji/1f604.png\\"); background-size: contain; height: 16px; width: 16px; max-height: 16px; max-width: 16px; min-height: 16px; min-width: 16px; overflow: hidden;"
/> />
</span> </span>
<span <span

View file

@ -70,18 +70,15 @@ describe('selectors/i18n', () => {
}); });
describe('locale from query parameter', () => { describe('locale from query parameter', () => {
// Helper function to mock window.location.search with locale query parameter
const setWindowLocaleQueryParameter = (locale) => { const setWindowLocaleQueryParameter = (locale) => {
window.location.search = `?locale=${locale}`; const url = new URL(window.location.href);
}; url.searchParams.set('locale', locale);
window.history.replaceState({}, '', url.toString());
// Helper function to reset window.location.search
const resetWindowLocationSearch = () => {
window.location.search = '';
}; };
afterEach(() => { afterEach(() => {
resetWindowLocationSearch(); // Reset the URL
window.history.replaceState({}, '', 'http://localhost:8065/');
}); });
test('returns locale from query parameter if provided and not logged in', () => { test('returns locale from query parameter if provided and not logged in', () => {

View file

@ -26,15 +26,6 @@ module.exports = async () => {
configure({adapter: new Adapter()}); configure({adapter: new Adapter()});
global.window = Object.create(window); global.window = Object.create(window);
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost:8065',
origin: 'http://localhost:8065',
port: '8065',
protocol: 'http:',
search: '',
},
});
// The current version of jsdom that's used by jest-environment-jsdom 29 doesn't support fetch, so we have to // The current version of jsdom that's used by jest-environment-jsdom 29 doesn't support fetch, so we have to
// use node-fetch despite some mismatched parameters. // use node-fetch despite some mismatched parameters.
@ -116,6 +107,12 @@ afterEach(() => {
continue; continue;
} }
// jsdom doesn't implement navigation, but this is expected behavior in tests
const errorStr = call[0] instanceof Error ? call[0].message : String(call[0]);
if (errorStr.includes('Not implemented:')) {
continue;
}
errors.push(call); errors.push(call);
} }

View file

@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* Wrapper for window.location.reload to make it mockable in tests.
*/
export function reloadPage(): void {
window.location.reload();
}

15991
webapp/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@
"name": "@mattermost/webapp", "name": "@mattermost/webapp",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18.10.0", "node": "^20 || ^22 || ^24",
"npm": ">=9.0.0 <12.0.0" "npm": "^10 || ^11"
}, },
"scripts": { "scripts": {
"postinstall": "patch-package && npm run build --workspace=platform/types --workspace=platform/client --workspace=platform/components", "postinstall": "patch-package && npm run build --workspace=platform/types --workspace=platform/client --workspace=platform/components",
@ -23,15 +23,16 @@
}, },
"dependencies": { "dependencies": {
"@mattermost/compass-icons": "0.1.52", "@mattermost/compass-icons": "0.1.52",
"react-intl": "7.1.14",
"typescript": "5.6.3" "typescript": "5.6.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.0", "@babel/core": "7.28.5",
"@babel/preset-env": "7.21.5", "@babel/preset-env": "7.28.5",
"@babel/preset-react": "7.18.6", "@babel/preset-react": "7.28.5",
"@babel/preset-typescript": "7.21.5", "@babel/preset-typescript": "7.28.5",
"@formatjs/cli": "6.7.4", "@formatjs/cli": "6.7.4",
"@types/node": "20.19.18", "@types/node": "24.10.4",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
"babel-plugin-formatjs": "10.5.1", "babel-plugin-formatjs": "10.5.1",
"babel-plugin-typescript-to-proptypes": "2.1.0", "babel-plugin-typescript-to-proptypes": "2.1.0",
@ -52,9 +53,9 @@
"strip-ansi": "7.1.0", "strip-ansi": "7.1.0",
"style-loader": "4.0.0", "style-loader": "4.0.0",
"typescript-eslint-language-service": "5.0.5", "typescript-eslint-language-service": "5.0.5",
"webpack": "5.95.0", "webpack": "5.103.0",
"webpack-cli": "5.1.4", "webpack-cli": "6.0.1",
"webpack-dev-server": "5.1.0" "webpack-dev-server": "5.2.2"
}, },
"overrides": { "overrides": {
"@deanwhillier/jest-matchmedia-mock": { "@deanwhillier/jest-matchmedia-mock": {

View file

@ -5,6 +5,7 @@
"target": "es2022", "target": "es2022",
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,

View file

@ -5,6 +5,7 @@
"target": "es2022", "target": "es2022",
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,

View file

@ -17,6 +17,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-runtime": "^7.17.0",
"jest-environment-jsdom": "30.1.0",
"@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-node-resolve": "^13.1.3",

View file

@ -5,6 +5,7 @@
"target": "es6", "target": "es6",
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,