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
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:
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Locator, Page} from '@playwright/test';
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
const AUTOCOMPLETE_ROUTE = /\/api\/v4\/teams\/[^/]+\/channels\/autocomplete/;
|
||||
|
||||
function searchedName(url: string) {
|
||||
return new URL(url).searchParams.get('name') ?? '';
|
||||
}
|
||||
|
||||
async function typeMentionAndWaitForRequests(input: Locator, page: Page, message: string) {
|
||||
let name = '';
|
||||
|
||||
for (const [index, character] of [...message].entries()) {
|
||||
let request: Promise<unknown> | undefined;
|
||||
if (index > 0) {
|
||||
name += character;
|
||||
request = page.waitForRequest(
|
||||
(request) => AUTOCOMPLETE_ROUTE.test(request.url()) && searchedName(request.url()) === name,
|
||||
);
|
||||
}
|
||||
|
||||
await input.pressSequentially(character);
|
||||
await request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds every channel search until the test releases it, so that the autocomplete can be observed while a
|
||||
* search is still in flight.
|
||||
*/
|
||||
async function holdChannelSearch(page: Page) {
|
||||
let release!: () => void;
|
||||
const released = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
|
||||
await page.route(AUTOCOMPLETE_ROUTE, async (route) => {
|
||||
await released;
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
/**
|
||||
* @objective Verify that the ~channel autocomplete renders the channels it already knows about while a
|
||||
* search for more channels is still in flight, and preserves those results when the response adds a member.
|
||||
*/
|
||||
test(
|
||||
'channel mention autocomplete shows local channels while searching for more',
|
||||
{tag: ['@mentions']},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create a public channel that the user is a member of, so it comes from the local store
|
||||
const localChannel = await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-local-' + Date.now(),
|
||||
display_name: 'AC Z Local',
|
||||
type: 'O',
|
||||
});
|
||||
await adminClient.addToChannel(user.id, localChannel.id);
|
||||
|
||||
// # Create a private channel that the user is a member of. Private channels are not included in the
|
||||
// # initial local public-channel results, but the server response adds them to the same channel list.
|
||||
const privateChannel = await adminClient.createPrivateChannel(
|
||||
team.id,
|
||||
'AC A Private',
|
||||
'ac-private-' + Date.now(),
|
||||
);
|
||||
await adminClient.addToChannel(user.id, privateChannel.id);
|
||||
|
||||
// # Create a channel that the user is not a member of, so it can only come from the search
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-remote-' + Date.now(),
|
||||
display_name: 'AC Remote',
|
||||
type: 'O',
|
||||
});
|
||||
|
||||
// # Log in as regular user
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Hold the channel search open so the initial local results can be observed independently of the response
|
||||
const releaseSearch = await holdChannelSearch(page);
|
||||
|
||||
// # Visit town-square channel
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Type a channel mention matching both channels
|
||||
const postCreate = channelsPage.centerView.postCreate;
|
||||
await postCreate.typeMessage('~ac-');
|
||||
|
||||
// * Verify the channels the user is a member of are shown without waiting for the search
|
||||
const suggestionList = postCreate.suggestionList;
|
||||
const myChannels = suggestionList.getByRole('group', {name: 'My Channels'});
|
||||
await expect(myChannels.getByRole('option')).toContainText(['AC Z Local']);
|
||||
|
||||
// * Verify the group of channels being searched for shows that it is still loading
|
||||
const otherChannels = suggestionList.getByRole('group', {name: 'Other Channels'});
|
||||
await expect(otherChannels.getByTestId('loadingSpinner')).toBeVisible();
|
||||
|
||||
// # Release the response and wait for it to be delivered
|
||||
const searchResponse = page.waitForResponse(
|
||||
(response) => AUTOCOMPLETE_ROUTE.test(response.url()) && searchedName(response.url()) === 'ac-',
|
||||
);
|
||||
releaseSearch();
|
||||
await searchResponse;
|
||||
|
||||
// * Verify the searched channel is shown once the search finishes
|
||||
await expect(otherChannels.getByRole('option')).toContainText(['AC Remote']);
|
||||
|
||||
// * Verify the member channel added by the response is paired with its own term, rather than mutating the
|
||||
// * already-rendered local result. The order the two are rendered in is not part of what's under test.
|
||||
const myChannelOptions = myChannels.getByRole('option');
|
||||
await expect(myChannelOptions).toHaveCount(2);
|
||||
await expect(myChannelOptions.filter({hasText: 'AC A Private'})).toHaveCount(1);
|
||||
await expect(myChannelOptions.filter({hasText: 'AC Z Local'})).toHaveCount(1);
|
||||
await expect(otherChannels.getByTestId('loadingSpinner')).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify that the ~channel autocomplete keeps showing results for what has been typed when an
|
||||
* earlier, slower search finishes after a later one.
|
||||
*/
|
||||
test('channel mention autocomplete ignores a search that finishes out of order', {tag: ['@mentions']}, async ({pw}) => {
|
||||
// # Initialize setup
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create channels that the user is not a member of, so they can only come from the search
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-alpha-' + Date.now(),
|
||||
display_name: 'AC Alpha',
|
||||
type: 'O',
|
||||
});
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-beta-' + Date.now(),
|
||||
display_name: 'AC Beta',
|
||||
type: 'O',
|
||||
});
|
||||
|
||||
// # Log in as regular user
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Hold the shortest search while allowing the fully typed search to finish first
|
||||
const staleSearchName = 'ac-';
|
||||
let releaseStaleSearch!: () => void;
|
||||
const staleSearchReleased = new Promise<void>((resolve) => {
|
||||
releaseStaleSearch = resolve;
|
||||
});
|
||||
|
||||
await page.route(AUTOCOMPLETE_ROUTE, async (route) => {
|
||||
if (searchedName(route.request().url()) === staleSearchName) {
|
||||
await staleSearchReleased;
|
||||
}
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
// # Visit town-square channel
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Note when the delayed search for the shortest mention finishes
|
||||
const staleSearch = page.waitForResponse(
|
||||
(response) => AUTOCOMPLETE_ROUTE.test(response.url()) && searchedName(response.url()) === staleSearchName,
|
||||
);
|
||||
const currentSearch = page.waitForResponse(
|
||||
(response) => AUTOCOMPLETE_ROUTE.test(response.url()) && searchedName(response.url()) === 'ac-alpha',
|
||||
);
|
||||
|
||||
// # Type a channel mention, waiting for each request so the stale response can be held deterministically
|
||||
const postCreate = channelsPage.centerView.postCreate;
|
||||
await typeMentionAndWaitForRequests(postCreate.input, page, '~ac-alpha');
|
||||
|
||||
// # Ensure the current search response is the one that populated the list before releasing the stale response
|
||||
await currentSearch;
|
||||
|
||||
// * Verify only the channel matching what was typed is shown
|
||||
const otherChannels = postCreate.suggestionList.getByRole('group', {name: 'Other Channels'});
|
||||
await expect(otherChannels.getByRole('option')).toContainText(['AC Alpha']);
|
||||
await expect(otherChannels.getByRole('option')).toHaveCount(1);
|
||||
|
||||
// # Release the stale response and wait for it to finish last
|
||||
releaseStaleSearch();
|
||||
await staleSearch;
|
||||
|
||||
// * Verify its results are ignored, since they no longer match what was typed
|
||||
await expect(otherChannels.getByRole('option')).toHaveCount(1);
|
||||
await expect(otherChannels.getByRole('option')).toContainText(['AC Alpha']);
|
||||
|
||||
// * Verify the mention can still be completed, so the suggestion list is still interactive
|
||||
await otherChannels.getByRole('option').first().click();
|
||||
await expect(postCreate.input).toHaveValue(/~ac-alpha/);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that the "Other Channels" group in the ~channel autocomplete in the message input prioritizes
|
||||
* channels whose DisplayName matches the search term.
|
||||
*/
|
||||
test(
|
||||
'MM-67953 channel mention autocomplete prioritizes DisplayName matches in Other Channels',
|
||||
{tag: ['@mentions']},
|
||||
async ({pw}) => {
|
||||
// # Initialize setup
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create channels whose DisplayName matches the search term (user is NOT a member)
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-conversation-' + Date.now(),
|
||||
display_name: 'Gamma: Conversation',
|
||||
type: 'O',
|
||||
});
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-gamma-logs-' + Date.now(),
|
||||
display_name: 'Gamma: Logs',
|
||||
type: 'O',
|
||||
});
|
||||
|
||||
// # Create channels whose Purpose matches but DisplayName does NOT (user is NOT a member)
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-alpha-channel-' + Date.now(),
|
||||
display_name: 'Alpha Channel',
|
||||
type: 'O',
|
||||
purpose: 'alpha release of gamma',
|
||||
});
|
||||
await adminClient.createChannel({
|
||||
team_id: team.id,
|
||||
name: 'ac-beta-channel-' + Date.now(),
|
||||
display_name: 'Beta Channel',
|
||||
type: 'O',
|
||||
purpose: 'beta release of gamma',
|
||||
});
|
||||
|
||||
// # Log in as regular user
|
||||
const {channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit town-square channel
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Type a channel mention in the message input to trigger autocomplete
|
||||
await channelsPage.centerView.postCreate.typeMessage('~gamma');
|
||||
|
||||
// # Wait for the suggestion list to appear
|
||||
const suggestionList = channelsPage.centerView.postCreate.suggestionList;
|
||||
await expect(suggestionList).toBeVisible();
|
||||
|
||||
// # Get all suggestion items within the "Other Channels" group
|
||||
const otherChannelsGroup = suggestionList.getByRole('group', {name: 'Other Channels'});
|
||||
await expect(otherChannelsGroup).toBeVisible();
|
||||
|
||||
const suggestions = otherChannelsGroup.getByRole('option');
|
||||
|
||||
// * Verify at least 4 suggestions are shown in "Other Channels"
|
||||
await expect(suggestions).toHaveCount(4);
|
||||
|
||||
// * Verify DisplayName-matching channels appear first, sorted alphabetically
|
||||
await expect(suggestions.nth(0)).toContainText('Gamma: Conversation');
|
||||
await expect(suggestions.nth(1)).toContainText('Gamma: Logs');
|
||||
|
||||
// * Verify Purpose-only-matching channels appear after, sorted alphabetically by DisplayName
|
||||
await expect(suggestions.nth(2)).toContainText('Alpha Channel');
|
||||
await expect(suggestions.nth(3)).toContainText('Beta Channel');
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user