snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Locator} from '@playwright/test';
import {expect, test} from '@mattermost/playwright-lib';
function backgroundColor(item: Locator): Promise<string> {
return item.evaluate((el) => window.getComputedStyle(el).backgroundColor);
}
/**
* @objective Verify that hovering different channel types (public channel and direct message) in the
* sidebar highlights them by changing their background.
*/
test(
'MM-T842 highlights public channel and direct message items on hover',
{tag: '@channels_sidebar'},
async ({pw}) => {
const {adminClient, adminUser, team, user} = await pw.initSetup();
// # Create a public channel the user is a member of but is not currently viewing
const publicChannel = await adminClient.createPublicChannel(team.id, `Highlight ${pw.random.id()}`);
await adminClient.addToChannel(user.id, publicChannel.id);
// # Create a direct message and post to it so it appears in the sidebar
const dmChannel = await adminClient.createDirectChannel([adminUser.id, user.id]);
await adminClient.createPost({channel_id: dmChannel.id, user_id: adminUser.id, message: 'hi there'});
// # Log in and view Town Square so the other items are not the active channel
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// * Verify the public channel item background changes on hover
const publicItem = channelsPage.sidebarLeft.container.getByRole('link', {name: publicChannel.display_name});
await expect(publicItem).toBeVisible();
const publicBgBefore = await backgroundColor(publicItem);
await publicItem.hover();
await expect.poll(() => backgroundColor(publicItem)).not.toBe(publicBgBefore);
// * Verify the direct message item background changes on hover
const dmItem = channelsPage.sidebarLeft.container.getByRole('link', {name: adminUser.username});
await expect(dmItem).toBeVisible();
const dmBgBefore = await backgroundColor(dmItem);
await dmItem.hover();
await expect.poll(() => backgroundColor(dmItem)).not.toBe(dmBgBefore);
},
);

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PlaywrightExtended} from '@mattermost/playwright-lib';
import {expect, test} from '@mattermost/playwright-lib';
// GET /api/v4/users/me/channels and GET /api/v4/users/me/teams/{team_id}/channels, the two requests
// that populate the sidebar's channels. Anchored so the sibling /channels/members and
// /channels/categories routes are left alone.
const CHANNEL_LIST_PATH = /^\/api\/v4\/users\/me\/(channels|teams\/[^/]+\/channels)$/;
// GET /api/v4/users/{user_id}/channel_members, the request that populates the user's channel
// memberships. The DM half of loadProfilesForSidebar reads getMyChannels, which filters the channels
// through these, so it needs them as much as it needs the channels themselves.
const CHANNEL_MEMBERS_PATH = /^\/api\/v4\/users\/[^/]+\/channel_members$/;
// GET /api/v4/users, the paged "load some profiles" request.
const USER_PAGE_PATH = /^\/api\/v4\/users$/;
const REQUEST_DELAY_MS = 4000;
type Scenario = Awaited<ReturnType<typeof setupSidebarProfiles>>;
/**
* Sets up a favorited DM and a GM whose members appear nowhere else in the loaded state, then logs
* in without navigating anywhere yet.
*/
async function setupSidebarProfiles(pw: PlaywrightExtended) {
const {adminClient, user, userClient, team} = await pw.initSetup();
// # Create the teammates for the DM and the GM. They are deliberately left out of the
// channel the test lands on, so that nothing else pulls their profiles into the store.
const [dmUser, gmUser1, gmUser2] = await adminClient.createUsers(team.id, 3, 'sidebar-profile');
const dmChannel = await userClient.createDirectChannel([user.id, dmUser.id]);
const gmChannel = await userClient.createGroupChannel([user.id, gmUser1.id, gmUser2.id]);
// # Land on a channel containing only the test user, so the current channel's member
// profiles can't mask a missing DM or GM profile.
const landingChannel = await adminClient.createPublicChannel(team.id, 'Sidebar Profiles');
await adminClient.addToChannel(user.id, landingChannel.id);
// # Make both conversations visible in the sidebar
await userClient.savePreferences(user.id, [
{user_id: user.id, category: 'direct_channel_show', name: dmUser.id, value: 'true'},
{user_id: user.id, category: 'group_channel_show', name: gmChannel.id, value: 'true'},
]);
// # Move the DM into Favorites. A DM whose teammate profile is missing is filtered out of
// the Direct Messages category, so only another category surfaces the blank row.
const {categories} = await userClient.getChannelCategories(user.id, team.id);
const favorites = categories.find((category) => category.type === 'favorites');
const directMessages = categories.find((category) => category.type === 'direct_messages');
if (!favorites || !directMessages) {
throw new Error('Expected the default Favorites and Direct Messages categories to exist');
}
await userClient.updateChannelCategories(user.id, team.id, [
{...favorites, channel_ids: [dmChannel.id]},
{
...directMessages,
channel_ids: directMessages.channel_ids.filter((channelId) => channelId !== dmChannel.id),
},
]);
const {channelsPage, page} = await pw.testBrowser.login(user);
// # Empty the first page of users, which on a server this small would otherwise return
// every teammate and load the DM profile by accident.
await page.route(
(url) => USER_PAGE_PATH.test(url.pathname),
(route) => route.fulfill({status: 200, contentType: 'application/json', body: '[]'}),
);
return {channelsPage, page, team, landingChannel, dmUser, dmChannel, gmChannel};
}
/**
* Delays the requests matching the given path so the sidebar categories always win the race.
*/
async function delayRequests({page}: Scenario, path: RegExp) {
await page.route(
(url) => path.test(url.pathname),
async (route) => {
await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY_MS));
await route.continue();
},
);
}
/**
* Loads the landing channel. Nothing in the sidebar is clicked afterwards, because navigating to a
* channel reloads these profiles and would hide the bug.
*/
async function gotoLandingChannel({channelsPage, team, landingChannel}: Scenario) {
await channelsPage.goto(team.name, landingChannel.name);
await channelsPage.toBeVisible();
}
/**
* @objective Verify that direct and group message channels in the sidebar render the teammate's
* name and the member count when the channel list arrives after the sidebar categories.
*
* @precondition
* Two conditions that occur naturally on a busy account are forced here so the test is
* deterministic on a small server:
*
* 1. The channel list requests are delayed so the sidebar categories always resolve first. That
* ordering is what leaves the DM and GM profiles unloaded, and it happens on its own once an
* account has enough channels for the channel list to be the slower of the two.
* 2. The first page of GET /api/v4/users is emptied. On a server with a handful of users that one
* request happens to return every teammate and hides the missing DM profile; on a real server
* the teammates are nowhere near the first page.
*/
test(
'renders DM and GM sidebar rows when the channel list arrives after the categories',
{tag: '@sidebar_left'},
async ({pw}) => {
const scenario = await setupSidebarProfiles(pw);
// # Delay the channel list so the sidebar categories always win the race
await delayRequests(scenario, CHANNEL_LIST_PATH);
await gotoLandingChannel(scenario);
const {sidebarLeft} = scenario.channelsPage;
// * Verify the group message shows how many other members it has instead of 0
await expect(sidebarLeft.memberCountBadge(scenario.gmChannel.name)).toHaveText('2');
// * Verify the favorited DM shows the teammate's username instead of rendering an empty row
await expect(sidebarLeft.item(scenario.dmChannel.name)).toContainText(scenario.dmUser.username);
},
);
/**
* @objective Verify that direct and group message channels in the sidebar render the teammate's
* name and the member count when the channel memberships arrive after the sidebar categories.
*
* @precondition
* Same as the channel list ordering above, except the memberships are the slow request. This is the
* separate half of the dependency: the channels can all be present and the DM profiles still go
* unloaded, because getMyChannels filters the channels through the memberships.
*/
test(
'renders DM and GM sidebar rows when the channel memberships arrive after the categories',
{tag: '@sidebar_left'},
async ({pw}) => {
const scenario = await setupSidebarProfiles(pw);
// # Delay the channel memberships so the sidebar categories always win the race
await delayRequests(scenario, CHANNEL_MEMBERS_PATH);
await gotoLandingChannel(scenario);
const {sidebarLeft} = scenario.channelsPage;
// * Verify the group message shows how many other members it has instead of 0
await expect(sidebarLeft.memberCountBadge(scenario.gmChannel.name)).toHaveText('2');
// * Verify the favorited DM shows the teammate's username instead of rendering an empty row
await expect(sidebarLeft.item(scenario.dmChannel.name)).toContainText(scenario.dmUser.username);
},
);