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: |
webapp/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') }}
- name: ci/cache-platform-builds
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0

View file

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

View file

@ -133,7 +133,7 @@ jobs:
fetch-depth: 0
- name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node
with:
node-version-file: ".nvmrc"
@ -164,7 +164,7 @@ jobs:
fetch-depth: 0
- name: ci/setup-node
if: "${{ inputs.run_preflight_checks }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node
with:
node-version-file: ".nvmrc"
@ -246,7 +246,7 @@ jobs:
ref: ${{ inputs.commit_sha }}
fetch-depth: 0
- name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node
with:
node-version-file: ".nvmrc"
@ -333,7 +333,7 @@ jobs:
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
- name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node
with:
node-version-file: ".nvmrc"
@ -412,7 +412,7 @@ jobs:
e2e-tests/${{ inputs.TEST }}/results/
- name: ci/setup-node
if: "${{ inputs.enable_reporting }}"
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
id: setup_node
with:
node-version-file: ".nvmrc"

View file

@ -282,6 +282,12 @@ jobs:
steps:
- name: Checkout mattermost project
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
run: make setup-go-work
- 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
echo '
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"
healthcheck:
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)
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
# shellcheck disable=SC2016
echo '
playwright:
image: mcr.microsoft.com/playwright:v1.57.0-noble
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.playwright"
environment:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,21 @@ import {fireEvent, render} from 'tests/react_testing_utils';
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', () => {
test('should attach scroll handler to the correct element', () => {
const onScroll = jest.fn();

View file

@ -136,7 +136,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
role="grid"
>
<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
style="height: 7740px; width: 100%;"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -318,10 +318,8 @@ describe('components/login/Login', () => {
const button = screen.getByRole('link', {name: 'Gitlab Icon GitLab 2'});
expect(button.style).toMatchObject({
color: 'rgb(0, 255, 0)',
borderColor: '#00ff00',
});
expect(button.style.color).toBe('rgb(0, 255, 0)');
expect(button.style.borderColor).toBe('rgb(0, 255, 0)');
});
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'});
expect(button.style).toMatchObject({
color: 'rgb(0, 255, 0)',
borderColor: '#00ff00',
});
expect(button.style.color).toBe('rgb(0, 255, 0)');
expect(button.style.borderColor).toBe('rgb(0, 255, 0)');
});
it('should redirect on login', async () => {

View file

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

View file

@ -173,7 +173,7 @@ exports[`InviteMembers component should match snapshot when it is cloud 1`] = `
id="react-select-2-input"
role="combobox"
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"
type="text"
value=""

View file

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

View file

@ -21,6 +21,7 @@ import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'matte
import {redirectUserToDefaultTeam, emitUserLoggedOutEvent} from 'actions/global_actions';
import {reloadPage} from 'utils/browser_utils';
import {ActionTypes, StoragePrefixes} from 'utils/constants';
import {doesCookieContainsMMUserId} from 'utils/utils';
@ -165,7 +166,7 @@ export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc<void>
// detected login from a different tab
function reloadOnFocus() {
location.reload();
reloadPage();
}
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 {renderWithContext, waitFor} from 'tests/react_testing_utils';
import * as BrowserUtils from 'utils/browser_utils';
import {StoragePrefixes} from 'utils/constants';
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
@ -26,6 +27,10 @@ jest.mock('utils/utils', () => ({
applyTheme: jest.fn(),
}));
jest.mock('utils/browser_utils', () => ({
reloadPage: jest.fn(),
}));
jest.mock('actions/global_actions', () => ({
redirectUserToDefaultTeam: jest.fn(),
}));
@ -93,11 +98,9 @@ describe('components/Root', () => {
};
let originalMatchMedia: (query: string) => MediaQueryList;
let originalReload: () => void;
beforeAll(() => {
originalMatchMedia = window.matchMedia;
originalReload = window.location.reload;
Object.defineProperty(window, 'matchMedia', {
writable: true,
@ -106,22 +109,17 @@ describe('components/Root', () => {
media: query,
})),
});
Object.defineProperty(window.location, 'reload', {
configurable: true,
writable: true,
});
window.location.reload = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
// Reset the reloadPage mock after each test
(BrowserUtils.reloadPage as jest.Mock).mockClear();
});
afterAll(() => {
window.matchMedia = originalMatchMedia;
window.location.reload = originalReload;
});
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'));
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
<ForwardRef
href="https://mattermost.com/security-updates/"
key=".1"
key="1/.1"
location="signup"
>
https://mattermost.com/security-updates/
@ -336,7 +336,7 @@ exports[`components/signup/Signup should match snapshot for all signup options e
Sign up at
<ForwardRef
href="https://mattermost.com/security-updates/"
key=".1"
key="1/.1"
location="signup"
>
https://mattermost.com/security-updates/

View file

@ -225,7 +225,7 @@ Object {
id="react-select-2-input"
role="combobox"
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"
type="text"
value=""
@ -312,7 +312,7 @@ Object {
id="react-select-3-input"
role="combobox"
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"
type="text"
value=""

View file

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

View file

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

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.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
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 TestHelper from 'packages/mattermost-redux/test/test_helper';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
describe('components/three_days_left_trial_modal/three_days_left_trial_modal', () => {
// required state to mount using the provider
const user = TestHelper.fakeUserWithId();
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(),
limitsOverpassed: false,
};
const store = mockStore(state);
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<ThreeDaysLeftTrialModal {...props}/>
</Provider>,
);
expect(wrapper.debug()).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
});
test('should match snapshot when limits are overpassed and show the limits panel', () => {
const wrapper = shallow(
<Provider store={store}>
<ThreeDaysLeftTrialModal
{...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>,
test('should render the modal with header, subtitle, feature cards, and view plans button', () => {
renderWithContext(
<ThreeDaysLeftTrialModal {...defaultProps}/>,
state,
);
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', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<ThreeDaysLeftTrialModal
{...props}
limitsOverpassed={true}
/>
</Provider>,
test('should show limits overpassed content when limitsOverpassed is true', () => {
renderWithContext(
<ThreeDaysLeftTrialModal
{...defaultProps}
limitsOverpassed={true}
/>,
state,
);
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 wrapper = mountWithIntl(
<Provider store={store}>
<ThreeDaysLeftTrialModal
{...props}
onExited={mockOnExited}
/>
</Provider>,
renderWithContext(
<ThreeDaysLeftTrialModal
{...defaultProps}
onExited={mockOnExited}
/>,
state,
);
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>
<li
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"
tabindex="-1"
>

View file

@ -1,4 +1,4 @@
@import "utils/_mixins";
@use "utils/mixins";
#userAccountMenu {
.userAccountMenu_nameMenuItem {
@ -21,14 +21,14 @@
font-size: 16px;
font-weight: 600;
line-height: 22px;
@include textEllipsis;
@include mixins.textEllipsis;
}
span.userAccountMenu_nameMenuItem_secondaryLabel {
max-width: 150px;
font-size: 14px;
line-height: 18px;
@include textEllipsis;
@include mixins.textEllipsis;
}
}
@ -61,7 +61,7 @@
.label-elements {
> span {
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],
"location": Object {
"hash": "",
"pathname": "undefinedundefined",
"pathname": "/",
"search": "",
"state": undefined,
},

View file

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

View file

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

View file

@ -76,7 +76,9 @@ describe('components/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', () => {

View file

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

View file

@ -33,7 +33,7 @@ exports[`TooltipContent have correct structure with title and emoji 1`] = `
aria-label=":smile:"
class="emoticon"
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
@ -61,7 +61,7 @@ exports[`TooltipContent have correct structure with title and large emoji 1`] =
aria-label=":smile:"
class="emoticon"
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
@ -121,7 +121,7 @@ exports[`TooltipContent have correct structure with title, emoji and hint 1`] =
aria-label=":smile:"
class="emoticon"
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

View file

@ -70,18 +70,15 @@ describe('selectors/i18n', () => {
});
describe('locale from query parameter', () => {
// Helper function to mock window.location.search with locale query parameter
const setWindowLocaleQueryParameter = (locale) => {
window.location.search = `?locale=${locale}`;
};
// Helper function to reset window.location.search
const resetWindowLocationSearch = () => {
window.location.search = '';
const url = new URL(window.location.href);
url.searchParams.set('locale', locale);
window.history.replaceState({}, '', url.toString());
};
afterEach(() => {
resetWindowLocationSearch();
// Reset the URL
window.history.replaceState({}, '', 'http://localhost:8065/');
});
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()});
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
// use node-fetch despite some mismatched parameters.
@ -116,6 +107,12 @@ afterEach(() => {
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);
}

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

View file

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

View file

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

View file

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

View file

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