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,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {test} from '@mattermost/playwright-lib';
/**
* @objective Verify that Browse Channels modal search results prioritize channels whose DisplayName matches the search
* term over channels that only match on other fields.
*/
test(
'MM-67953 Browse Channels modal prioritizes DisplayName matches in search results',
{tag: ['@browse_channels']},
async ({pw}) => {
// # Initialize setup
const {team, user, adminClient} = await pw.initSetup();
// # Create channels whose DisplayName matches the search term
const displayMatchA = await adminClient.createChannel({
team_id: team.id,
name: 'ac-gamma-conversation-' + Date.now(),
display_name: 'Gamma: Conversation',
type: 'O',
});
const displayMatchB = 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
const purposeMatchA = await adminClient.createChannel({
team_id: team.id,
name: 'ac-alpha-channel-' + Date.now(),
display_name: 'Alpha Channel',
type: 'O',
purpose: 'alpha release of gamma',
});
const purposeMatchB = 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();
// # Open the Browse Channels modal
const dialog = await channelsPage.openBrowseChannelsModal();
await dialog.toBeVisible();
// # Search for the term that matches DisplayName on some channels and Purpose on others
await dialog.fillSearchInput('gamma');
await dialog.toBeDoneLoading();
await dialog.toHaveNResults(4);
// DisplayName matches come first, sorted alphabetically
await dialog.toHaveChannelAsNthResult(displayMatchA.name, 0);
await dialog.toHaveChannelAsNthResult(displayMatchB.name, 1);
// Purpose-only matches come after, sorted alphabetically by DisplayName
await dialog.toHaveChannelAsNthResult(purposeMatchA.name, 2);
await dialog.toHaveChannelAsNthResult(purposeMatchB.name, 3);
},
);

View File

@@ -0,0 +1,174 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Team} from '@mattermost/types/teams';
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify cross-team search still renders the team selector and returns results correctly
* when the Onyx (dark) theme is active.
*/
test('MM-T5806 cross-team search works under the Onyx (dark) theme', {tag: '@search'}, async ({pw}) => {
// # Create a user with two teams and post a message to search for
const {adminClient, team, user} = await pw.initSetup();
const secondTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(secondTeam.id, [user.id]);
const message = 'this is a test post containing the word pineapple';
const channel = await adminClient.getChannelByName(team.id, 'town-square');
await adminClient.createPost({channel_id: channel.id, message});
// # Log in and switch to the Onyx (dark) theme
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
const settingsModal = await channelsPage.openSettings();
const displaySettings = await settingsModal.openDisplayTab();
await displaySettings.selectPremadeTheme('Onyx');
await settingsModal.close();
// # Search for the message across teams
await channelsPage.searchFor('pineapple');
// * Verify the team selector and search results render correctly under the dark theme
await channelsPage.searchResultsPanel.teamSelector.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(message);
});
/**
* @objective Verify the team filter in the search team-selector menu shows no results for a
* non-matching query, and correctly narrows to a single team for a partial matching query.
*/
test('MM-T5805 team filter shows no results for a non-matching query', {tag: '@search'}, async ({pw}) => {
// # Create a user and enough teams (5 total) for the team-filter input to render
// (see team_selector.spec.ts: the filter only shows with more than 4 teams)
const {adminClient, team, user} = await pw.initSetup();
const extraTeams: Record<string, Team> = {};
for (const fruit of ['Apple', 'Banana', 'Peach', 'Grape']) {
const newTeam = await adminClient.createTeam(await pw.random.team(fruit.toLowerCase(), fruit, 'O', true));
await adminClient.addUsersToTeam(newTeam.id, [user.id]);
extraTeams[fruit] = newTeam;
}
// # Log in, open search, and open the team selector
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
const teamSelector = channelsPage.searchBox.teamSelector;
await teamSelector.open();
await expect(teamSelector.filterInput).toBeVisible();
// # Filter by a string that matches no team
await teamSelector.filterBy('Rhino');
// * Verify none of the fruit teams match, but the current team is still shown (current team is always visible)
for (const fruit of Object.keys(extraTeams)) {
await expect(teamSelector.teamOption(extraTeams[fruit].display_name)).not.toBeVisible();
}
await expect(teamSelector.teamOption(team.display_name)).toBeVisible();
// # Filter by a partial string that matches exactly one team
await teamSelector.filterBy('Banana');
// * Verify only the matching team (plus the always-visible current team) is shown
await expect(teamSelector.teamOption(extraTeams.Banana.display_name)).toBeVisible();
await expect(teamSelector.teamOption(team.display_name)).toBeVisible();
for (const fruit of ['Apple', 'Peach', 'Grape']) {
await expect(teamSelector.teamOption(extraTeams[fruit].display_name)).not.toBeVisible();
}
});
/**
* @objective Verify the search team-selector defaults to the active team (not "All Teams"), that a
* typed query persists across team-selector changes, and that the "Your teams" section header in
* the dropdown only appears while a specific team (not "All Teams") is selected.
*/
test(
'MM-T5801 team selector defaults to the active team, preserves the query, and hides "Your teams" for All Teams',
{tag: '@search'},
async ({pw}) => {
// # Create a user and 4 more teams (5 total) so the "Your teams" section renders
// (see team_selector.spec.ts: the filter/section only shows with more than 4 teams)
const {adminClient, team, user} = await pw.initSetup();
const teams = [team];
for (let i = 0; i < 4; i++) {
const newTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(newTeam.id, [user.id]);
teams.push(newTeam);
}
const secondTeam = teams[1];
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
const teamSelector = channelsPage.searchBox.teamSelector;
// * Verify the selector defaults to the currently active team, not "All Teams"
await expect(teamSelector.menuButton).toContainText(team.display_name);
// # Type a query without submitting, then switch the team selector to a different team
await channelsPage.searchBox.searchInput.fill('pineapple');
await teamSelector.selectTeam(secondTeam.display_name);
// * Verify the button label updated and the query text was preserved
await expect(teamSelector.menuButton).toContainText(secondTeam.display_name);
await expect(channelsPage.searchBox.searchInput).toHaveValue('pineapple');
// * Verify the "Your teams" section header is shown while a specific team is selected
await teamSelector.open();
await expect(teamSelector.yourTeamsHeader).toBeVisible();
// # Switch to "All Teams" (the menu is already open from the check above)
await teamSelector.teamOption('All Teams').click();
// * Verify the "Your teams" section header is hidden once "All Teams" is selected
await teamSelector.open();
await expect(teamSelector.yourTeamsHeader).not.toBeVisible();
},
);
/**
* @objective Verify the "in:" channel filter is cleared from the search query when the team
* selector is switched to "All Teams", since the filter only makes sense within a single team.
*
* @knownIssue Switching the team selector to "All Teams" does NOT clear the "in:<channel>" clause
* from the query text — it's left in place, and submitting silently re-scopes the search back to
* the channel's own team rather than actually searching "All Teams".
*/
test.fixme(
'MM-T5804 "in:" channel filter is cleared when switching search to All Teams',
{tag: '@search'},
async ({pw}) => {
// # Create a user with a second team so "All Teams" is a real, distinct selector choice
const {adminClient, team, user} = await pw.initSetup();
const secondTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(secondTeam.id, [user.id]);
const channel = await adminClient.getChannelByName(team.id, 'town-square');
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
// # Type "in:" and pick the channel from the autocomplete suggestion
const {searchInput} = channelsPage.searchBox;
await searchInput.fill('in:');
await searchInput.pressSequentially(channel.name);
await channelsPage.searchBox.container.getByText(channel.display_name, {exact: false}).first().click();
await expect(searchInput).toHaveValue(`in:${channel.name} `);
// # Switch the team selector to "All Teams"
await channelsPage.searchBox.teamSelector.selectAllTeams();
// * Verify the "in:" clause was cleared, since it's meaningless once scoped to All Teams
await expect(searchInput).not.toHaveValue(new RegExp(`in:${channel.name}`));
},
);

View File

@@ -0,0 +1,61 @@
// 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 Find channel search limits results to 50 when there are more channels matching the search query
*/
test(
'MM-T5424 Find channel search returns only 50 results when there are more than 50 channels with similar names',
{tag: '@smoke'},
async ({pw}) => {
const {adminClient, user, team} = await pw.initSetup();
const commonName = 'test_channel';
// # Create more than 50 channels with similar names
const channelsRes = [];
for (let i = 0; i < 100; i++) {
let suffix = i.toString();
if (i < 10) {
suffix = `0${i}`;
}
const channel = pw.random.channel({
teamId: team.id,
name: `${commonName}_${suffix}`,
displayName: `Test Channel ${suffix}`,
});
channelsRes.push(adminClient.createChannel(channel));
}
await Promise.all(channelsRes);
// # Log in a user in new browser context
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Click on "Find channel" and type "test_channel"
await channelsPage.sidebarLeft.findChannelButton.click();
await channelsPage.findChannelsModal.toBeVisible();
await channelsPage.findChannelsModal.input.fill(commonName);
const limitCount = 50;
// * Verify only 50 results for similar name are displayed
await expect(channelsPage.findChannelsModal.searchList).toHaveCount(limitCount);
// * Verify the first 50 channels are visible in the search results
for (let i = 0; i < limitCount; i++) {
let suffix = i.toString();
if (i < 10) {
suffix = `0${i}`;
}
await expect(channelsPage.findChannelsModal.container.getByTestId(`${commonName}_${suffix}`)).toBeVisible();
}
},
);

View File

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
decomposeKorean,
expect,
koreanTestPhrase,
test,
typeHangulCharacterWithIme,
typeHangulWithIme,
} from '@mattermost/playwright-lib';
test('Find Channels modal handles Korean IME input correctly', async ({pw, browserName}) => {
test.skip(browserName !== 'chromium', 'The API used to test this is only available in Chrome');
const {adminClient, user, team} = await pw.initSetup();
// # Create a channel named after the test phrase
const fullMatchChannel = pw.random.channel({
teamId: team.id,
name: 'full-match-channel',
displayName: koreanTestPhrase,
});
await adminClient.createChannel(fullMatchChannel);
// # And create a channel matching part of the test phrase
const partialMatchChannel = pw.random.channel({
teamId: team.id,
name: 'partial-match-channel',
displayName: koreanTestPhrase.substring(0, 10),
});
await adminClient.createChannel(partialMatchChannel);
// # Log in and go to Channels
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the channel switcher
await channelsPage.sidebarLeft.findChannelButton.click();
await channelsPage.findChannelsModal.toBeVisible();
// # Focus the input
const input = channelsPage.findChannelsModal.input;
await input.focus();
const firstHalf = koreanTestPhrase.substring(0, 5);
const secondHalf = koreanTestPhrase.substring(5);
// # Type the first half of the test phrase
await typeHangulWithIme(page, firstHalf);
// * Verify that characters are correctly composed and weren't doubled up
await expect(input).toHaveValue(firstHalf);
// * Verify that both channels are visible
await expect(page.getByRole('option', {name: fullMatchChannel.display_name, exact: true})).toBeVisible();
await expect(page.getByRole('option', {name: partialMatchChannel.display_name, exact: true})).toBeVisible();
// # Type the second half of the test phrase
await typeHangulWithIme(page, secondHalf);
// * Verify that characters are correctly composed and weren't doubled up
await expect(input).toHaveValue(koreanTestPhrase);
// * Verify that the first channel is still visible but that the second is not
await expect(page.getByRole('option', {name: fullMatchChannel.display_name, exact: true})).toBeVisible();
await expect(page.getByRole('option', {name: partialMatchChannel.display_name, exact: true})).not.toBeAttached();
});
test('Find Channels autocomplete lists channel after each incomplete Korean character', async ({pw, browserName}) => {
test.skip(browserName !== 'chromium', 'The API used to test this is only available in Chrome');
// For future reference of anyone working on this, if Firefox ever supports an equivalent to CDP, this test will
// likely fail because it seems to handle input value differently when there incomplete IME characters that haven't
// been committed yet.
const {adminClient, user, team} = await pw.initSetup();
const randomPrefix = await pw.random.id(8);
// # Create a channel named after the test phrase
const channel = pw.random.channel({
teamId: team.id,
name: 'korean-autocomplete-channel',
displayName: randomPrefix + koreanTestPhrase,
});
await adminClient.createChannel(channel);
// # Log in and go to Channels
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the Find Channels modal
await channelsPage.sidebarLeft.findChannelButton.click();
await channelsPage.findChannelsModal.toBeVisible();
// # Focus the input
const input = channelsPage.findChannelsModal.input;
await input.focus();
await input.fill(randomPrefix);
const client = await page.context().newCDPSession(page);
// # Type the test phrase one character at a time, verifying autocomplete after each completed character
const decomposed = decomposeKorean(koreanTestPhrase);
for (let i = 0; i < decomposed.length; i++) {
// # Type the next character using IME composition
await typeHangulCharacterWithIme(client, decomposed[i], decomposed[i - 1]);
// * Verify the input contains the correctly composed text so far
await expect(input).toHaveValue(randomPrefix + koreanTestPhrase.substring(0, i + 1));
// * Verify that the channel appears in the autocomplete results
await expect(page.getByRole('option', {name: channel.display_name, exact: true})).toBeVisible();
}
await client.detach();
});

View File

@@ -0,0 +1,151 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test.describe('Mobile view RHS auto-focus', () => {
// Shrink the window's width to trigger mobile view
test.use({viewport: {width: 400, height: 1024}});
/**
* @objective Opening a thread shouldn't focus the search textbox, even briefly
*/
test(
'opens thread in mobile view without ever focusing the mobile search input',
{tag: '@search'},
async ({pw, isMobile}) => {
const {user} = await pw.initSetup();
// # Log in
const {channelsPage, page} = await pw.testBrowser.login(user);
// # Go to a channel
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Make a post
await channelsPage.centerView.postCreate.postMessage('Root message for mobile thread focus test');
// # Set up a listener to track if the search box ever gets focus
await page.evaluate(() => {
(window as any).sbrSearchBoxFocusCount = 0;
document.addEventListener('focusin', (e) => {
if (e.target && 'id' in e.target && e.target.id === 'sbrSearchBox') {
(window as any).sbrSearchBoxFocusCount += 1;
}
});
});
// # Open the thread
const lastPost = await channelsPage.getLastPost();
await lastPost.openAThread();
await channelsPage.sidebarRight.toBeVisible();
// * Verify that the textbox is automatically focused on non-mobile devices
if (!isMobile) {
await expect(channelsPage.sidebarRight.postCreate.input).toBeFocused();
}
// * Verify the mobile RHS search input never received focus
const searchFocusCount = await page.evaluate(() => (window as any).sbrSearchBoxFocusCount);
expect(searchFocusCount).toBe(0);
},
);
/**
* @objective Opening the search RHS auto-focuses the search input in the RHS
*/
test('opens search RHS in mobile view and focuses the mobile search input', {tag: '@search'}, async ({pw}) => {
const {user} = await pw.initSetup();
// # Log in as the test user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Click the mobile channel header search button to open the search RHS
await channelsPage.mobileNavbar.openSearch();
await channelsPage.sidebarRight.toBeVisible();
// * Verify the mobile RHS search input is focused
await expect(channelsPage.sidebarRight.mobileSearchInput).toBeFocused();
});
/**
* @objective Tapping the mobile menu backdrop should dismiss/close the modal
*/
test('closes mobile menu when clicking the backdrop layer', {tag: '@mobile'}, async ({pw}) => {
const {user} = await pw.initSetup();
// # Log in as the test user and navigate to channels page
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the mobile channel header menu
await channelsPage.centerView.header.openChannelMenu();
// # Define the mobile menu modal locator
const menuModal = page.locator('.modal-dialog.menuModal');
// * Verify the menu modal is visible after the click
await expect(menuModal).toBeVisible();
// # Wait for the modal's entrance animation to finish. Until it does, the menu is still
// # sliding down through the top-left corner, so the click below would land on a menu item
await expect(menuModal.locator('..')).toHaveCSS('opacity', '1');
// # Click the modal's backdrop layer outside the menu content
await page.mouse.click(5, 5);
// * Verify the mobile menu modal is now successfully dismissed/hidden
await expect(menuModal).toBeHidden();
});
/**
* @objective Verify that in narrow/mobile view, the channel header shows an icon-only Search
* button; clicking it opens the RHS search panel where a search can be performed, and closing
* the panel returns to the icon-only Search button.
*/
test(
'MM-T349 MM-T369 search icon in narrow view opens search, searches, and closes back to icon',
{tag: '@search'},
async ({pw}) => {
const {user} = await pw.initSetup();
// # Log in as the test user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page and post a message to search for
await channelsPage.goto();
await channelsPage.toBeVisible();
const message = `test message for narrow view search ${pw.random.id()}`;
await channelsPage.centerView.postCreate.postMessage(message);
// * Verify the icon-only Search button is shown in the narrow-width header
await expect(channelsPage.mobileNavbar.searchButton).toBeVisible();
// # Click it to open the RHS search panel
await channelsPage.mobileNavbar.openSearch();
await channelsPage.sidebarRight.toBeVisible();
const {mobileSearchInput} = channelsPage.sidebarRight;
await expect(mobileSearchInput).toBeFocused();
// # Perform a search
await mobileSearchInput.fill(message);
await mobileSearchInput.press('Enter');
// * Verify search results appear
await channelsPage.searchResultsPanel.toContainText(message);
// # Close the search panel
await channelsPage.sidebarRight.closeMobile();
// * Verify it collapses back to the icon-only Search button
await expect(channelsPage.sidebarRight.container).not.toBeVisible();
await expect(channelsPage.mobileNavbar.searchButton).toBeVisible();
},
);
});

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('MM-64155 search box clear button should not leave type badge after closing the search box', async ({pw}) => {
// # Set up test with a user
const {user} = await pw.initSetup();
// # Log in as the test user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
// # Type something in the search box
const searchText = 'abcdef';
const {searchInput} = channelsPage.searchBox;
await searchInput.pressSequentially(searchText);
// * Verify text was entered
await expect(searchInput).toHaveValue(searchText);
// # Click the clear button
await channelsPage.searchBox.clearIfPossible();
// * Verify the input is cleared
await expect(searchInput).toHaveValue('');
// # Close the search box by clicking outside
await channelsPage.page.click('body', {position: {x: 0, y: 0}});
// * Verify the search box is closed
await expect(channelsPage.searchBox.container).not.toBeVisible();
// * Verify there is no search type badge/chip in the search bar
// The search type badge is rendered when searchType is either 'messages' or 'files'
const searchTypeBadge = channelsPage.page.getByTestId('searchTypeBadge');
await expect(searchTypeBadge).not.toBeVisible();
});

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, koreanTestPhrase, test, typeHangulWithIme} from '@mattermost/playwright-lib';
test('Search box handles Korean IME correctly', async ({pw, browserName}) => {
test.skip(browserName !== 'chromium', 'The API used to test this is only available in Chrome');
const {userClient, user, team} = await pw.initSetup();
// # Create a channel named after the test phrase
const testChannel = pw.random.channel({
teamId: team.id,
name: 'korean-test-channel',
displayName: koreanTestPhrase,
});
await userClient.createChannel(testChannel);
// # Log in a user in new browser context
const {channelsPage, page} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
const {searchInput} = channelsPage.searchBox;
await searchInput.focus();
// # Type into the textbox
const searchText = 'in:' + koreanTestPhrase.substring(0, 3);
await typeHangulWithIme(page, searchText);
// * Verify that the text was typed correctly into the search box
await expect(searchInput).toHaveValue(searchText);
// * Verify that the channel is suggested
await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(testChannel.display_name);
});

View File

@@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('Search box suggestion must be case insensitive', async ({pw}) => {
const {user} = await pw.initSetup();
// # Log in a user in new browser context
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
const searchWord = 'off';
const searchOutput = 'In:off-topic';
const channelName = 'Off-Topic';
// Should work as expected when using lowercase
// # Type in lowercase "off" to search for the "Off-Topic" channel
const {searchInput} = channelsPage.searchBox;
await searchInput.pressSequentially(`In:${searchWord}`);
// * The suggestion should be visible
await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(channelName);
// # Press Enter to select the suggestion and another Enter to search
await searchInput.press('Enter');
await searchInput.press('Enter');
// * The search box should contain the selected suggestion
await expect(channelsPage.globalHeader.searchBox.getByText(searchOutput, {exact: true})).toBeVisible();
// Should work as expected when using uppercase
// # Open the search bar
await channelsPage.globalHeader.openSearch();
// # Clear its content
await channelsPage.searchBox.clearIfPossible();
// # Type in uppercase "OFF" to search for the "Off-Topic" channel
await searchInput.type(`In:${searchWord.toUpperCase()}`);
// * The suggestion should be visible
await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(channelName);
// # Press Enter to select the suggestion and another Enter to search
await searchInput.press('Enter');
await searchInput.press('Enter');
// * The search box should contain the selected suggestion
await expect(channelsPage.globalHeader.searchBox.getByText(searchOutput, {exact: true})).toBeVisible();
});
test('remove extra whitespace when selecting a user', async ({pw}) => {
// # Set up test with two users
const {user, adminUser: admin} = await pw.initSetup();
// # Log in as the test user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
// # Type "from:" followed by multiple spaces
const {searchInput} = channelsPage.searchBox;
await searchInput.pressSequentially(`from: ${admin.username}`);
// * The suggestion should be visible
await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchBox.selectedSuggestion).toHaveText('@' + admin.username);
// # Press enter to validate the selection
await searchInput.press('Enter');
// * Verify the search box shows "from:username" without extra spaces
const expectedText = `from:${admin.username} `;
await expect(searchInput).toHaveValue(expectedText);
});
/**
* @objective Verify that grouped search suggestions are trimmed to ten results and retain the selected channel's
* term/item pairing.
*/
test('limits grouped channel suggestions and preserves the selected channel', {tag: '@search'}, async ({pw}) => {
const {team, user, adminClient} = await pw.initSetup();
const prefix = `search-trim-${pw.random.id()}`;
const channels = [];
// The search endpoint returns up to 50 channels; the search UI trims the grouped results to 10.
for (let i = 0; i < 12; i++) {
const channel = await adminClient.createPublicChannel(team.id, `Search Trim ${i} ${prefix}`, `${prefix}-${i}`);
await adminClient.addToChannel(user.id, channel.id);
channels.push(channel);
}
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
const {searchInput, container} = channelsPage.searchBox;
await searchInput.fill(`In:${prefix}`);
// * Verify trimResults applies across the grouped channel results
const suggestions = container.getByRole('option');
await expect(suggestions).toHaveCount(10);
// # Select a result that survived trimming and verify the term/item pairing updates the query correctly
const renderedText = (await suggestions.allInnerTexts()).join('\n');
const selectedChannel = channels.find((channel) => renderedText.includes(channel.display_name));
if (!selectedChannel) {
throw new Error('Expected at least one created channel to remain in the trimmed suggestions');
}
await suggestions.filter({hasText: selectedChannel.display_name}).first().click();
await expect(searchInput).toHaveValue(new RegExp(`In:${selectedChannel.name}\\s`));
});

View File

@@ -0,0 +1,36 @@
// 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 a cleared search term stays cleared after the search results panel is closed.
*/
test('MM-T352 keeps a cleared search term empty after closing search results', {tag: '@search'}, async ({pw}) => {
// # Create a user with a searchable post and open the channel
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
const term = `London${pw.random.id()}`;
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message: term});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Search for the post, reopen search, and clear the query
await channelsPage.searchFor(term);
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
// * Verify the query is empty while the search results remain open
await expect(channelsPage.searchBox.searchInput).toHaveValue('');
await channelsPage.searchResultsPanel.toBeVisible();
// # Close both search surfaces, then open search again
await channelsPage.searchBox.searchBoxClose.click();
await channelsPage.sidebarRight.close();
await channelsPage.globalHeader.openSearch();
// * Verify the cleared term does not reappear
await expect(channelsPage.searchBox.searchInput).toHaveValue('');
});

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify the "on:" search date filter can be set by clicking a day in the calendar picker,
* and that the resulting date-filtered search returns the matching post.
*/
test('MM-T596 sets the search date with the calendar picker', {tag: '@search'}, async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'town-square');
// # Pin the user's timezone and clock so "today" in the picker is a fixed, known date
await adminClient.patchUser({
id: user.id,
timezone: {automaticTimezone: '', manualTimezone: 'UTC', useAutomaticTimezone: 'false'},
});
const todayTimestamp = Date.UTC(2020, 5, 15, 12, 0);
const todayFilter = toDateFilter(todayTimestamp);
const message = `Calendar Picker ${pw.random.id()}`;
await adminClient.createPost({
channel_id: channel.id,
user_id: user.id,
message,
create_at: todayTimestamp,
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await page.clock.setFixedTime(todayTimestamp);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Type "on:" to open the day picker, then click today's date
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill('on:');
await channelsPage.searchBox.getDayPickerDay(15).click();
// * Verify the search box auto-populates with the selected date in YYYY-MM-DD format
await expect(channelsPage.searchBox.searchInput).toHaveValue(`on:${todayFilter} `);
// # Complete the query with the message text and submit
await channelsPage.searchBox.searchInput.press('End');
await channelsPage.searchBox.searchInput.pressSequentially(message);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(channelsPage.searchResultsContainer).toBeVisible();
// * Verify the post created on that date is returned
await expect(channelsPage.searchResultItems).toHaveCount(1);
await expect(channelsPage.getSearchResultItem(message)).toBeVisible();
});
/**
* @objective Verify editing the "on:" search date by keyboard changes which posts match: the original
* date returns the post, and a later date does not.
*/
test('MM-T600 updates the search date with the keyboard', {tag: '@search'}, async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
// # Pin the user's timezone and post a message on a known date
await adminClient.patchUser({
id: user.id,
timezone: {automaticTimezone: '', manualTimezone: 'UTC', useAutomaticTimezone: 'false'},
});
const postTimestamp = Date.UTC(2020, 5, 15, 12, 0);
const postDateFilter = toDateFilter(postTimestamp);
const laterDateFilter = toDateFilter(postTimestamp + 7 * 24 * 60 * 60 * 1000);
const message = `Date ${pw.random.id()}`;
await adminClient.createPost({
channel_id: channel.id,
user_id: user.id,
message,
create_at: postTimestamp,
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Search for the message filtered to the date it was posted
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill(`on:${postDateFilter} ${message}`);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(channelsPage.searchResultsContainer).toBeVisible();
// * Verify the post is returned for its own date
await expect(channelsPage.searchResultItems).toHaveCount(1);
await expect(channelsPage.getSearchResultItem(message)).toBeVisible();
// # Reopen the search box and change the date filter to a week later, keeping the same message text
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill(`on:${laterDateFilter} ${message}`);
await channelsPage.searchBox.searchInput.press('Enter');
// * Verify the post is no longer returned for the later date
await expect(channelsPage.searchResultItems).toHaveCount(0);
});
/**
* @objective Verify that pressing Backspace after a completed date filter reopens the calendar day picker.
*/
test('MM-T3997 reopens the date picker when backspacing after a date filter', {tag: '@search'}, async ({pw}) => {
const {user, team} = await pw.initSetup();
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Open search and type a "before:" date filter to open the day picker
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill('before:');
await expect(channelsPage.searchBox.getDayPickerDay(15)).toBeVisible();
// # Select a day from the picker
await channelsPage.searchBox.getDayPickerDay(15).click();
// * Verify the date is added to the query and the picker closes
await expect(channelsPage.searchBox.searchInput).toHaveValue(/before:\d{4}-\d{2}-\d{2} /);
await expect(channelsPage.searchBox.getDayPickerDay(15)).not.toBeVisible();
// # Backspace into the completed date filter so the date becomes partial again
await channelsPage.searchBox.searchInput.click();
await page.keyboard.press('End');
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
// * Verify the search input now holds a partial date and the day picker reappears
await expect(channelsPage.searchBox.searchInput).toHaveValue(/before:\d{4}-\d{2}-1$/);
await expect(channelsPage.searchBox.getDayPickerDay(15)).toBeVisible();
});
function toDateFilter(timestamp: number): string {
return new Date(timestamp).toISOString().slice(0, 10);
}

View File

@@ -0,0 +1,142 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {duration, expect, test, type ChannelsPage} from '@mattermost/playwright-lib';
/**
* @objective Verify changing timezone changes which posts match an on: date filter.
*/
test('MM-T595 Changing timezone changes day search results appears', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
const identifier = `timezone-${pw.random.id()}`;
const targetMessage = `targetAM ${identifier}`;
const targetTimestamp = Date.UTC(2018, 9, 31, 23, 59);
// # Create a post close to a day boundary and search in UTC
await adminClient.patchUser({
id: user.id,
timezone: {automaticTimezone: '', manualTimezone: 'UTC', useAutomaticTimezone: 'false'},
});
await adminClient.createPost({
channel_id: channel.id,
user_id: user.id,
message: targetMessage,
create_at: targetTimestamp,
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const utcQuery = `on:2018-10-31 ${identifier}`;
await submitSearch(channelsPage, utcQuery);
// * Verify the result appears for the UTC date
await expectSearchResult(channelsPage, targetMessage, utcQuery);
// # Change timezone and run the same date-filtered search
await adminClient.patchUser({
id: user.id,
timezone: {automaticTimezone: '', manualTimezone: 'Europe/Brussels', useAutomaticTimezone: 'false'},
});
await channelsPage.page.reload();
await submitSearch(channelsPage, utcQuery);
// * Verify the post no longer matches the previous day in the new timezone
await expectNoSearchResult(channelsPage, targetMessage);
});
/**
* @objective Verify editing a date-filtered search query, via the interactive day picker, updates the search results.
*/
test('MM-T599 Edit date and search again', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
const targetMessage = `calendarUpdate-${pw.random.id()}`;
const targetTimestamp = Date.UTC(2019, 0, 15, 9, 30);
// # Create a dated post and pin the client's clock to that date, so "today" in the day picker is Jan 15, 2019
await adminClient.createPost({
channel_id: channel.id,
user_id: user.id,
message: targetMessage,
create_at: targetTimestamp,
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await page.clock.setFixedTime(targetTimestamp);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Type "on:" to open the day picker and click today's date
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill('on:');
await channelsPage.searchBox.getDayPickerDay(15).click();
// * Verify the search box auto-populates with the selected date
await expect(channelsPage.searchBox.searchInput).toHaveValue('on:2019-01-15 ');
// # Complete and submit the query
await channelsPage.searchBox.searchInput.press('End');
await channelsPage.searchBox.searchInput.pressSequentially(targetMessage);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(channelsPage.searchResultsContainer).toBeVisible();
// * Verify exactly one matching result for the original date
await expect(channelsPage.searchResultItems).toHaveCount(1);
await expect(channelsPage.getSearchResultItem(targetMessage)).toBeVisible();
// # Reopen the channel, then reopen the day picker by backspacing right after the date
await channelsPage.goto(team.name, channel.name);
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
const originalDateQuery = `on:2019-01-15 ${targetMessage}`;
await channelsPage.searchBox.searchInput.fill(originalDateQuery);
for (let i = 0; i < targetMessage.length + 1; i++) {
await channelsPage.searchBox.searchInput.press('ArrowLeft');
}
await channelsPage.searchBox.searchInput.press('Backspace');
// # Click the day after the pinned date
await channelsPage.searchBox.getDayPickerDay(16).click();
// * Verify the search box updates to the edited date, then submit
await expect(channelsPage.searchBox.searchInput).toHaveValue(`on:2019-01-16 ${targetMessage}`);
await channelsPage.searchBox.searchInput.press('Enter');
// * Verify the original post is not returned for the edited date
await expect(channelsPage.searchResultItems).toHaveCount(0);
});
async function submitSearch(channelsPage: ChannelsPage, query: string) {
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill(query);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(channelsPage.searchResultsContainer).toBeVisible();
}
async function expectSearchResult(
channelsPage: ChannelsPage,
text: string,
query?: string,
timeout = duration.half_min,
) {
const result = channelsPage.getSearchResultItem(text);
await expect(async () => {
if (await result.isVisible({timeout: duration.one_sec}).catch(() => false)) {
return;
}
if (query) {
await submitSearch(channelsPage, query);
} else {
await channelsPage.searchBox.searchInput.press('Enter');
}
await expect(result).toBeVisible({timeout: duration.one_sec * 5});
}).toPass({timeout});
}
async function expectNoSearchResult(channelsPage: ChannelsPage, text: string) {
await expect(channelsPage.getSearchResultItem(text)).toHaveCount(0);
}

View File

@@ -0,0 +1,44 @@
// 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 "from:" search modifier autocompletes a username and returns that user's posts.
*
* MM-T376 covers the same from:user autocomplete behavior and is covered by this test.
*/
test('MM-T376 MM-T377 completes a from: user search from the autocomplete', {tag: ['@search']}, async ({pw}) => {
// # Create the test user plus another user who authors a post
const {user, team, adminClient} = await pw.initSetup();
const [author] = await adminClient.createUsers(team.id, 1, 'author');
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
await adminClient.addToChannel(author.id, townSquare.id);
const token = `fromsearch${pw.random.id()}`;
const {client: authorClient} = await pw.makeClient(author);
await authorClient.createPost({channel_id: townSquare.id, message: token});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Open search and type the from: modifier
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
const {searchInput} = channelsPage.searchBox;
await searchInput.fill('from:');
// # Select the author from the autocomplete suggestions
await channelsPage.searchBox.selectSuggestionMatching(author.username);
// * Verify the username is placed into the query without the leading @
expect(await searchInput.inputValue()).toContain(`from:${author.username}`);
// # Submit the search
await searchInput.press('Enter');
// * Verify the author's post is returned in the results
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
});

View File

@@ -0,0 +1,99 @@
// 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 hashtag search is not case sensitive and returns all case variants of a hashtag.
*/
test('MM-T359 returns all case variants when searching a hashtag', {tag: ['@search']}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post the same hashtag word in three different letter cases
const suffix = pw.random.id();
const upper = `#CASE${suffix}`;
const mixed = `#Case${suffix}`;
const lower = `#case${suffix}`;
await channelsPage.postMessage(upper);
await channelsPage.postMessage(mixed);
await channelsPage.postMessage(lower);
// # Search for the lowercase hashtag
await channelsPage.searchFor(lower);
// * Verify all three case variants are returned (case-sensitive text checks)
await expect(channelsPage.searchResultsPanel.getResultItems()).toHaveCount(3);
await expect(channelsPage.searchResultsPanel.container).toContainText(upper);
await expect(channelsPage.searchResultsPanel.container).toContainText(mixed);
await expect(channelsPage.searchResultsPanel.container).toContainText(lower);
});
/**
* @objective Verify that clicking a hashtag in a Recent Mentions result runs a hashtag search.
*/
test('MM-T360 searches a hashtag from a recent mention', {tag: '@search'}, async ({pw}) => {
// # Create the test user plus a second user who will mention them
const {user, team, adminClient} = await pw.initSetup();
const [mentioner] = await adminClient.createUsers(team.id, 1, 'mentioner');
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
await adminClient.addToChannel(mentioner.id, townSquare.id);
// # The second user posts a message with a hashtag that mentions the test user
const hashtag = `#hello${pw.random.id()}`;
const {client: mentionerClient} = await pw.makeClient(mentioner);
await mentionerClient.createPost({channel_id: townSquare.id, message: `${hashtag} @${user.username}`});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Open Recent Mentions
await channelsPage.globalHeader.openRecentMentions();
// * Verify the mention with the hashtag is listed
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(hashtag);
// # Click the hashtag in the mention
await channelsPage.searchResultsPanel.container.getByRole('link', {name: hashtag}).first().click();
// * Verify the view navigated to hashtag search results that include the message
await expect(channelsPage.globalHeader.searchBox).toContainText(hashtag);
await channelsPage.searchResultsPanel.toContainText(hashtag);
});
/**
* @objective Verify that clicking a hashtag in a Saved messages result runs a hashtag search.
*/
test('MM-T361 searches a hashtag from a saved message', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user, and post + save a message with a hashtag
const {user, team, userClient} = await pw.initSetup();
const hashtag = `#hello${pw.random.id()}`;
const townSquare = await userClient.getChannelByName(team.id, 'town-square');
const post = await userClient.createPost({channel_id: townSquare.id, message: `${hashtag} World`});
await userClient.savePreferences(user.id, [
{user_id: user.id, category: 'flagged_post', name: post.id, value: 'true'},
]);
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Open Saved messages
await channelsPage.globalHeader.openSavedMessages();
// * Verify the saved message with the hashtag is listed
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(hashtag);
// # Click the hashtag in the saved message
await channelsPage.searchResultsPanel.container.getByRole('link', {name: hashtag}).first().click();
// * Verify the view navigated to hashtag search results that include the message
await expect(channelsPage.globalHeader.searchBox).toContainText(hashtag);
await channelsPage.searchResultsPanel.toContainText(hashtag);
});

View File

@@ -0,0 +1,140 @@
// 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 searching with trailing double quotes highlights only the searched terms, with no unexpected highlighting.
*/
test('MM-T354 highlights only the searched terms when searching with double quotes', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message that contains the two search words plus an unrelated unique word
const marker = `zzz${pw.random.id()}`;
await channelsPage.postMessage(`test tool ${marker}`);
// # Search using the two words followed by an empty double-quoted phrase
await channelsPage.searchFor('test tool ""');
// * Verify the message is returned
await channelsPage.searchResultsPanel.toContainText(marker);
// * Verify only the two searched words are highlighted (no unexpected highlighting)
const highlights = channelsPage.searchResultsPanel.getHighlightedTerms();
await expect(highlights).toHaveCount(2);
await expect(highlights.filter({hasText: 'test'})).toHaveCount(1);
await expect(highlights.filter({hasText: 'tool'})).toHaveCount(1);
await expect(highlights.filter({hasText: marker})).toHaveCount(0);
});
/**
* @objective Verify that the search result highlighting reflects the submitted query and does not change while the search input is edited before submitting again.
*/
test(
'MM-T371 keeps search result highlighting unchanged while editing the search input',
{tag: '@search'},
async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message that contains two distinct words
const suffix = pw.random.id();
const firstWord = `apple${suffix}`;
const secondWord = `banana${suffix}`;
await channelsPage.postMessage(`${firstWord} ${secondWord}`);
// # Search for the first word
await channelsPage.searchFor(firstWord);
// * Verify the result contains both words but highlights only the searched (first) word
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(secondWord);
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
// # Reopen the search box and replace the query with the second word without submitting
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
const {searchInput} = channelsPage.searchBox;
await expect(searchInput).toHaveValue(firstWord);
await searchInput.fill(secondWord);
// * Verify the search results still highlight the originally searched (first) word
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveText([firstWord]);
},
);
/**
* @objective Verify that search term highlighting appears in the search results panel but does not persist in the saved or pinned message panels.
*/
test(
'MM-T372 does not persist search highlighting in the saved or pinned message panels',
{tag: '@search'},
async ({pw}) => {
// # Create and log in as a test user, and open a channel
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.createPublicChannel(team.id, 'Highlight', 'highlight');
await adminClient.addToChannel(user.id, channel.id);
const token = `highlightterm${pw.random.id()}`;
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Post a message, then pin it to the channel and save it
await channelsPage.postMessage(`test message ${token}`);
const post = await channelsPage.getLastPost();
await post.hover();
await post.postMenu.openDotMenu();
await channelsPage.postDotMenu.pinToChannelMenuItem.click();
await post.hover();
await post.postMenu.saveButton.click();
// # Open the saved messages panel
await channelsPage.globalHeader.openSavedMessages();
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
// * Verify no search term is highlighted in the saved messages panel
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveCount(0);
// # Open the pinned messages panel
await channelsPage.centerView.header.openPinnedMessages();
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
// * Verify no search term is highlighted in the pinned messages panel
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveCount(0);
// # Search for the message so the term is highlighted
await channelsPage.searchFor(token);
await channelsPage.searchResultsPanel.toContainText(token);
// * Verify the search term is highlighted in the search results panel
await expect(channelsPage.searchResultsPanel.getHighlightedTerms().first()).toBeVisible();
// # Close the search results panel, then reopen the saved messages panel
await channelsPage.sidebarRight.close();
await channelsPage.globalHeader.openSavedMessages();
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
// * Verify the previously searched term is not highlighted in the saved messages panel
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveCount(0);
// # Reopen the pinned messages panel
await channelsPage.centerView.header.openPinnedMessages();
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
// * Verify the previously searched term is not highlighted in the pinned messages panel
await expect(channelsPage.searchResultsPanel.getHighlightedTerms()).toHaveCount(0);
},
);

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify the "in:" search autocomplete shows a renamed channel by both its new display name and
* its original handle, so it stays findable after a display-name change.
*/
test(
'MM-T1449 shows the channel display name and handle in the in: search autocomplete',
{tag: '@search'},
async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
// # Create a public channel whose handle differs from its display name, then add the user
const id = pw.random.id();
const handle = `original-${id}`;
const updatedName = `Updated ${id}`;
const channel = await adminClient.createPublicChannel(team.id, `Original ${id}`, handle);
await adminClient.addToChannel(user.id, channel.id);
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Rename the channel's display name via Channel Settings
const channelSettings = await channelsPage.openChannelSettings();
const infoSettings = await channelSettings.openInfoTab();
await infoSettings.updateName(updatedName);
await channelSettings.save();
await channelSettings.close();
await channelsPage.centerView.header.toHaveTitle(updatedName);
// # Open search and type an "in:" filter matching the channel's original handle
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
await channelsPage.searchBox.searchInput.fill('in:');
await channelsPage.searchBox.searchInput.pressSequentially(handle);
// * Verify the autocomplete shows the channel by its new display name and its original handle
const suggestion = channelsPage.searchBox.container.getByRole('option').filter({hasText: updatedName});
await expect(suggestion).toBeVisible();
await expect(suggestion).toContainText(updatedName);
await expect(suggestion).toContainText(`~${handle}`);
},
);

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify the "in:" search modifier autocompletes a direct-message user and returns matching DM posts.
*/
test('MM-T358 searches for a message in a direct message channel', {tag: '@search'}, async ({pw}) => {
// # Create a direct message and a uniquely searchable post
const {user, team, adminClient} = await pw.initSetup();
const [otherUser] = await adminClient.createUsers(team.id, 1, 'dm-user');
const dmChannel = await adminClient.createDirectChannel([user.id, otherUser.id]);
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
const message = `Hello${pw.random.id()}`;
await adminClient.createPost({channel_id: dmChannel.id, user_id: user.id, message});
await adminClient.createPost({channel_id: townSquare.id, user_id: user.id, message});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Type the in: modifier and select the other user from the autocomplete
await channelsPage.globalHeader.openSearch();
const {searchInput} = channelsPage.searchBox;
await searchInput.fill('in:');
await channelsPage.searchBox.selectSuggestionMatching(`@${otherUser.username}`);
// * Verify the direct-message user is placed into the query
await expect(searchInput).toHaveValue(`in:@${otherUser.username} `);
// # Add the message text and submit the search
await searchInput.pressSequentially(message);
await searchInput.press('Enter');
await channelsPage.searchResultsPanel.toBeVisible();
// * Verify the matching direct message is returned
await expect(channelsPage.searchResultsPanel.getResultByText(message)).toHaveCount(1);
});

View File

@@ -0,0 +1,96 @@
// 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 a wildcard (*) is disregarded when it is not immediately preceded by text.
*/
test('MM-T351 disregards a wildcard that is not preceded by text', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post two messages, one with a longer variant of the search term
const token = `qwerty${pw.random.id()}`;
await channelsPage.postMessage(`${token}jkl`);
await channelsPage.postMessage(token);
// # Search using a trailing wildcard separated by a space
await channelsPage.searchFor(`${token} *`);
// * Verify the wildcard is disregarded and the exact-term message is returned
await channelsPage.searchResultsPanel.toContainText(token);
});
/**
* @objective Verify that the top navigation buttons remain clickable and open their panels while focus is in the search box.
*/
test('MM-T367 clicks top navigation buttons while the search box has focus', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// The open search box traps focus and marks the top-right header buttons aria-hidden, so match hidden too.
// (The channel-info button lives in the channel header, which the search popup overlays, so it is not
// reachable while the search box is open in the current UI and is intentionally not exercised here.)
const recentMentionsButton = page.getByRole('button', {name: 'Recent mentions', includeHidden: true});
const savedMessagesButton = page.getByRole('button', {name: 'Saved messages', includeHidden: true});
// # Put focus in the search box and type text, then click Recent mentions
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
await channelsPage.searchBox.searchInput.fill('some text');
await recentMentionsButton.click();
// * Verify the Recent Mentions panel opens
await channelsPage.searchResultsPanel.toHaveHeading('Recent Mentions');
// # Focus the search box again and click Saved messages
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
await channelsPage.searchBox.searchInput.fill('some text');
await savedMessagesButton.click();
// * Verify the Saved messages panel opens
await expect(
channelsPage.searchResultsPanel.container.getByRole('heading', {name: 'Saved messages'}).first(),
).toBeVisible();
});
/**
* @objective Verify that the search box shows help text when opened from the channel and again after opening a thread.
*/
test('MM-T370 shows search help text when the search box is opened', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message and open its thread
await channelsPage.postMessage(`help text ${pw.random.id()}`);
// # Open the search box
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
// * Verify the search help text is shown
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
// # Close the search box, then open a thread
await channelsPage.page.keyboard.press('Escape');
await expect(channelsPage.searchBox.container).not.toBeVisible();
const post = await channelsPage.getLastPost();
await post.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.toBeVisible();
// * Verify the search help text is shown again
await expect(channelsPage.page.getByText('Filter your search with:')).toBeVisible();
});

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify clearing a completed search displays search options without removing the existing results.
*/
test('MM-T353 displays search options after clearing the query', {tag: '@search'}, async ({pw}) => {
// # Create a user with a searchable post and open the channel
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'town-square');
const searchWord = `Hello${pw.random.id()}`;
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message: searchWord});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Search for the post, reopen search, and clear the query
await channelsPage.searchFor(searchWord);
await channelsPage.globalHeader.openSearch();
await expect(channelsPage.searchBox.searchInput).toHaveValue(searchWord);
await channelsPage.searchBox.clearIfPossible();
await expect(channelsPage.searchBox.searchInput).toHaveValue('');
// * Verify search options appear and the existing results remain visible
await expect(channelsPage.searchBox.searchHints).toBeVisible();
await expect(channelsPage.searchResultsPanel.getResultByText(searchWord)).toBeVisible();
// # Close and reopen the search box
await channelsPage.searchBox.searchBoxClose.click();
await channelsPage.globalHeader.openSearch();
// * Verify search options are still displayed
await expect(channelsPage.searchBox.searchHints).toBeVisible();
});

View File

@@ -0,0 +1,240 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('MM-65630-1 Search results should show popout button that opens results in a new window', async ({pw}) => {
const {adminClient, user, team} = await pw.initSetup();
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Search Popout Channel',
name: 'search-popout-channel',
}),
);
await adminClient.addToChannel(user.id, channel.id);
const uniqueText = `popout-search-test-${pw.random.id()}`;
await adminClient.createPost({
channel_id: channel.id,
message: uniqueText,
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const page = channelsPage.page;
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.searchInput.fill(uniqueText);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(page.locator('#searchContainer')).toBeVisible();
await expect(page.locator('#searchContainer').getByText(uniqueText)).toBeVisible();
const popoutButton = page.locator('#searchContainer .PopoutButton');
await expect(popoutButton).toBeVisible();
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
await popoutPage.waitForLoadState('domcontentloaded');
const popoutUrl = popoutPage.url();
expect(popoutUrl).toContain('/_popout/rhs/');
expect(popoutUrl).toContain('/search');
expect(popoutUrl).toContain(`q=${encodeURIComponent(uniqueText)}`);
expect(popoutUrl).toContain('mode=search');
await expect(popoutPage.locator('#searchContainer')).toBeVisible({timeout: 10000});
await expect(popoutPage.locator('#searchContainer').getByText(uniqueText)).toBeVisible({timeout: 10000});
await popoutPage.close();
});
test('MM-65630-2 Recent mentions popout should open with the right results', async ({pw}) => {
const {adminClient, user, team} = await pw.initSetup();
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Mentions Popout Channel',
name: 'mentions-popout-channel',
}),
);
await adminClient.addToChannel(user.id, channel.id);
const mentionText = `hey @${user.username} check this mention-${pw.random.id()}`;
await adminClient.createPost({
channel_id: channel.id,
message: mentionText,
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const page = channelsPage.page;
await channelsPage.globalHeader.openRecentMentions();
await expect(page.locator('#searchContainer')).toBeVisible();
await expect(page.locator('#searchContainer').getByRole('heading', {name: 'Recent Mentions'})).toBeVisible();
await expect(page.locator('#searchContainer').getByText(mentionText)).toBeVisible();
const popoutButton = page.locator('#searchContainer .PopoutButton');
await expect(popoutButton).toBeVisible();
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
await popoutPage.waitForLoadState('domcontentloaded');
const popoutUrl = popoutPage.url();
expect(popoutUrl).toContain('/_popout/rhs/');
expect(popoutUrl).toContain('/search');
expect(popoutUrl).toContain('mode=mention');
await expect(popoutPage.locator('#searchContainer')).toBeVisible({timeout: 10000});
await expect(popoutPage.locator('#searchContainer').getByText(mentionText)).toBeVisible({timeout: 10000});
await popoutPage.close();
});
test('MM-65630-3 Saved messages popout should open with the right results', async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Saved Popout Channel',
name: 'saved-popout-channel',
}),
);
await adminClient.addToChannel(user.id, channel.id);
const savedText = `saved-message-test-${pw.random.id()}`;
const post = await adminClient.createPost({
channel_id: channel.id,
message: savedText,
});
await userClient.savePreferences(user.id, [
{
user_id: user.id,
category: 'flagged_post',
name: post.id,
value: 'true',
},
]);
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const page = channelsPage.page;
await channelsPage.globalHeader.savedMessagesButton.click();
await expect(page.locator('#searchContainer')).toBeVisible();
await expect(page.locator('#searchContainer').getByRole('heading', {name: 'Saved messages'})).toBeVisible();
await expect(page.locator('#searchContainer').getByText(savedText)).toBeVisible();
const popoutButton = page.locator('#searchContainer .PopoutButton');
await expect(popoutButton).toBeVisible();
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
await popoutPage.waitForLoadState('domcontentloaded');
const popoutUrl = popoutPage.url();
expect(popoutUrl).toContain('/_popout/rhs/');
expect(popoutUrl).toContain('/search');
expect(popoutUrl).toContain('mode=flag');
await expect(popoutPage.locator('#searchContainer')).toBeVisible({timeout: 10000});
await expect(popoutPage.locator('#searchContainer').getByText(savedText)).toBeVisible({timeout: 10000});
await popoutPage.close();
});
test('MM-65630-4 Search popout should not show popout button in the popout window itself', async ({pw}) => {
const {adminClient, user, team} = await pw.initSetup();
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Popout No Button Channel',
name: 'popout-no-button-channel',
}),
);
await adminClient.addToChannel(user.id, channel.id);
const uniqueText = `no-button-test-${pw.random.id()}`;
await adminClient.createPost({
channel_id: channel.id,
message: uniqueText,
});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const page = channelsPage.page;
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.searchInput.fill(uniqueText);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(page.locator('#searchContainer')).toBeVisible();
const [popoutPage] = await Promise.all([
page.waitForEvent('popup'),
page.locator('#searchContainer .PopoutButton').click(),
]);
await popoutPage.waitForLoadState('domcontentloaded');
await expect(popoutPage.locator('#searchContainer')).toBeVisible({timeout: 10000});
await expect(popoutPage.locator('.PopoutButton')).not.toBeVisible();
await expect(popoutPage.locator('#searchResultsCloseButton')).not.toBeVisible();
await popoutPage.close();
});
test('MM-65630-5 Search popout should preserve search type (files) in the URL', async ({pw}) => {
const {adminClient, user, team} = await pw.initSetup();
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Files Search Channel',
name: 'files-search-channel',
}),
);
await adminClient.addToChannel(user.id, channel.id);
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const page = channelsPage.page;
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.searchInput.fill('test');
await channelsPage.searchBox.searchInput.press('Enter');
await expect(page.locator('#searchContainer')).toBeVisible();
const filesTab = page.locator('#searchContainer').getByRole('tab', {name: /Files/});
await filesTab.click();
const popoutButton = page.locator('#searchContainer .PopoutButton');
await expect(popoutButton).toBeVisible();
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
await popoutPage.waitForLoadState('domcontentloaded');
const popoutUrl = popoutPage.url();
expect(popoutUrl).toContain('type=files');
await popoutPage.close();
});

View File

@@ -0,0 +1,113 @@
// 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 a full username containing '-' or '_' is shown as a mention link in the mentioned user's search results.
*/
test('MM-T348 shows a full special-character username as a link in search results', {tag: '@search'}, async ({pw}) => {
// # Create a user whose username contains '-' and '_', plus a user who mentions them
const {user: mentioningUser, team, adminClient, userClient} = await pw.initSetup();
const [specialUser] = await adminClient.createUsers(team.id, 1, 'test-user_');
const townSquare = await adminClient.getChannelByName(team.id, 'town-square');
await adminClient.addToChannel(specialUser.id, townSquare.id);
// # The first user posts a message mentioning the special-character username
const token = `mention${pw.random.id()}`;
await userClient.createPost({
channel_id: townSquare.id,
message: `@${specialUser.username} ${token}`,
user_id: mentioningUser.id,
});
// # Log in as the mentioned user and open Recent Mentions
const {channelsPage} = await pw.testBrowser.login(specialUser);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openRecentMentions();
// * Verify the mention shows the full username as a clickable mention (rendered as a button)
await channelsPage.searchResultsPanel.toBeVisible();
const result = channelsPage.searchResultsPanel.getResultByText(token);
await expect(result.getByRole('button', {name: `@${specialUser.username}`})).toBeVisible();
});
/**
* @objective Verify that jumping from a search result opens a post that is not already displayed in the center channel.
*/
test('MM-T380 jumps from a search result to a post in another channel', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user, and create a channel with many posts
const {user, team, adminClient} = await pw.initSetup();
const linkChannel = await adminClient.createPublicChannel(team.id, 'Link Test', 'link-test');
await adminClient.addToChannel(user.id, linkChannel.id);
// # Post a target message surrounded by many other messages so it is not initially loaded elsewhere
const token = `asparagus${pw.random.id()}`;
for (let i = 0; i < 4; i++) {
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post before ${i}`});
}
await adminClient.createPost({channel_id: linkChannel.id, message: token});
for (let i = 0; i < 8; i++) {
await adminClient.createPost({channel_id: linkChannel.id, message: `RF Random Post after ${i}`});
}
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'off-topic');
await channelsPage.toBeVisible();
// # Search for the target message from a different channel
await channelsPage.searchFor(token);
await channelsPage.searchResultsPanel.toContainText(token);
// # Jump to the post from the search results
await channelsPage.searchResultsPanel.jumpToResultWithText(token);
// * Verify navigation to the target channel and that the target post is displayed in the center
await expect.poll(() => page.url(), {timeout: pw.duration.ten_sec}).toContain(`/channels/${linkChannel.name}`);
await expect(channelsPage.centerView.container.getByText(token, {exact: true})).toBeVisible();
});
/**
* @objective Verify that deleting a parent post from the search results removes both the post and its reply.
*/
test('MM-T381 deletes a parent post from the search results', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user, and open a channel
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.createPublicChannel(team.id, 'Delete Parent', 'delete-parent');
await adminClient.addToChannel(user.id, channel.id);
const parentToken = `parentpost${pw.random.id()}`;
const replyToken = `replypost${pw.random.id()}`;
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Post a message and reply to it, then close the thread
await channelsPage.postMessage(parentToken);
const post = await channelsPage.getLastPost();
await post.reply();
await channelsPage.sidebarRight.toBeVisible();
await channelsPage.sidebarRight.postMessage(replyToken);
await channelsPage.sidebarRight.close();
// * Verify both the parent post and its reply are searchable before deletion
await channelsPage.searchFor(replyToken);
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(1);
await channelsPage.searchFor(parentToken);
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(1);
// # Delete the parent post from the search result
await channelsPage.searchResultsPanel.openResultDotMenu(parentToken);
await channelsPage.postDotMenu.deleteMenuItem.click();
await channelsPage.deletePostModal.toBeVisible();
await channelsPage.deletePostModal.confirm();
// * Verify the deleted parent post is removed from the open search results in real time
await expect(channelsPage.searchResultsPanel.getResultByText(parentToken)).toHaveCount(0);
// * Verify the reply was also removed (searching for it returns no results)
await channelsPage.searchFor(replyToken);
await expect(channelsPage.searchResultsPanel.getResultByText(replyToken)).toHaveCount(0);
});

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {duration, expect, test, type ChannelsPage} from '@mattermost/playwright-lib';
/**
* @objective Verify message search displays matching results in the right-hand side, rendering markdown
* content correctly, and that jumping to the conversation and clearing the query keep results intact.
*/
test('MM-T350 Searching displays results in the RHS', async ({pw}) => {
const {adminClient, team, user} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
const identifier = pw.random.id();
const message = [
`Basic word search: Hello world! #hello ${identifier}`,
'',
'##### Hello',
'',
'```',
'Hello',
'```',
].join('\n');
// # Create and search for a message with markdown content: a heading, a code block, and a hashtag
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
const query = `hello ${identifier}`;
await submitSearch(channelsPage, query);
// * Verify the matching result appears in the RHS with each markdown element rendered distinctly
await expectSearchResult(channelsPage, 'Hello world', query);
const result = channelsPage.getSearchResultItem('Hello world');
await expect(result.locator('h5.markdown__heading')).toContainText('Hello');
await expect(result.locator('.post-code.post-code--wrap code')).toContainText('Hello');
await expect(result).toContainText('#hello');
// # Jump to the conversation from the search result
await result.getByRole('link', {name: 'Jump'}).click();
// # Reopen search and clear the query text
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
// * Verify the search results remain visible after clearing the query text
await expect(channelsPage.searchResultsContainer).toBeVisible();
});
/**
* @objective Verify that a new search replaces the previous results instead of combining them.
*/
test('MM-T355 replaces old search results with new results', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user
const {user, team} = await pw.initSetup();
const {channelsPage} = await pw.testBrowser.login(user);
const helloToken = `hello${pw.random.id()}`;
const writingToken = `writing${pw.random.id()}`;
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// # Post a message in Town Square and search for it
await channelsPage.postMessage(`${helloToken} there everyone`);
await channelsPage.searchFor(helloToken);
// * Verify the first message is shown in the results
await channelsPage.searchResultsPanel.toContainText(helloToken);
// # Post a different message in another channel and run a new search for it
await channelsPage.sidebarLeft.goToItem('off-topic');
await channelsPage.postMessage(`${writingToken} to you here`);
await channelsPage.searchFor(writingToken);
// * Verify the new results are shown and the old results are gone
await channelsPage.searchResultsPanel.toContainText(writingToken);
await expect(channelsPage.searchResultsPanel.getResultByText(helloToken)).toHaveCount(0);
});
/**
* @objective Verify that scrolling the center channel while the search results panel is open does not change the results.
*/
test('MM-T382 keeps search results unchanged while scrolling the center channel', {tag: '@search'}, async ({pw}) => {
// # Create and log in as a test user, and open a channel with many matching posts
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.createPublicChannel(team.id, 'Search Scroll', 'search-scroll');
await adminClient.addToChannel(user.id, channel.id);
const token = `searchfun${pw.random.id()}`;
const messageCount = 30;
for (let i = 0; i < messageCount; i++) {
await adminClient.createPost({channel_id: channel.id, message: `${token} ${i}`});
}
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Search for the matching posts
await channelsPage.searchFor(token);
await channelsPage.searchResultsPanel.toBeVisible();
const resultCountBefore = await channelsPage.searchResultsPanel.getResultItems().count();
expect(resultCountBefore).toBeGreaterThan(0);
// # Scroll the center channel up to load older messages
const box = await channelsPage.centerView.container.boundingBox();
expect(box).not.toBeNull();
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2);
for (let i = 0; i < 6; i++) {
await page.mouse.wheel(0, -1500);
await pw.wait(pw.duration.half_sec);
}
// * Verify the search results panel is still open and its results are unchanged
await channelsPage.searchResultsPanel.toBeVisible();
await channelsPage.searchResultsPanel.toContainText(token);
expect(await channelsPage.searchResultsPanel.getResultItems().count()).toBe(resultCountBefore);
});
async function submitSearch(channelsPage: ChannelsPage, query: string) {
await channelsPage.globalHeader.openSearch();
await channelsPage.searchBox.clearIfPossible();
await channelsPage.searchBox.searchInput.fill(query);
await channelsPage.searchBox.searchInput.press('Enter');
await expect(channelsPage.searchResultsContainer).toBeVisible();
}
async function expectSearchResult(
channelsPage: ChannelsPage,
text: string,
query?: string,
timeout = duration.half_min,
) {
const result = channelsPage.getSearchResultItem(text);
await expect(async () => {
if (await result.isVisible({timeout: duration.one_sec}).catch(() => false)) {
return;
}
if (query) {
await submitSearch(channelsPage, query);
} else {
await channelsPage.searchBox.searchInput.press('Enter');
}
await expect(result).toBeVisible({timeout: duration.one_sec * 5});
}).toPass({timeout});
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify a user can reply to a post from search results and see the reply in the opened thread.
*/
test('MM-T373 replies to a post from search results', {tag: '@search'}, async ({pw}) => {
// # Create a uniquely searchable post and open its channel
const {user, team, adminClient} = await pw.initSetup();
const channel = await adminClient.getChannelByName(team.id, 'off-topic');
const message = `asparagus${pw.random.id()}`;
await adminClient.createPost({channel_id: channel.id, user_id: user.id, message});
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channel.name);
await channelsPage.toBeVisible();
// # Search for the post and open its reply thread from the result
await channelsPage.searchFor(message);
await expect(channelsPage.searchResultsPanel.getResultByText(message)).toBeVisible();
await channelsPage.searchResultsPanel.replyToResultWithText(message);
await channelsPage.sidebarRight.toBeVisible();
// # Post a reply in the opened thread
const comment = `Replying to asparagus ${pw.random.id()}`;
await channelsPage.sidebarRight.postMessage(comment);
// * Verify the original post and new reply remain visible in the thread
await (await channelsPage.sidebarRight.getFirstPost()).toContainText(message);
await (await channelsPage.sidebarRight.getLastPost()).toContainText(comment);
});

View File

@@ -0,0 +1,266 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
test('team selector should not be visible if user belongs to only one team', async ({pw}) => {
// # Create a user with only one team
const {adminClient, user, team} = await pw.initSetup();
// # Create a channel in the team
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Test Channel',
name: 'test-channel',
}),
);
// # Post a message in the channel
const message = 'test message for search';
await adminClient.createPost({
channel_id: channel.id,
message,
});
// # Log in as the user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
// * Verify that the team selector is not visible in the search box
await channelsPage.searchBox.toBeVisible();
await channelsPage.searchBox.teamSelector.notToBeVisible();
// # Now search for the message to see search results
await channelsPage.searchBox.search(message);
await channelsPage.searchResultsPanel.toBeVisible();
// * Verify the team selector is not visible in search results panel
await channelsPage.searchResultsPanel.teamSelector.notToBeVisible();
});
test('team selector should be visible if user belongs to multiple teams', async ({pw}) => {
// # Create a user and admin client
const {adminClient, user, team} = await pw.initSetup();
// # Create a second team and add the user to it
const secondTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(secondTeam.id, [user.id]);
// # Create a channel in the first team
const channel = await adminClient.createChannel(
pw.random.channel({
teamId: team.id,
displayName: 'Test Channel',
name: 'test-channel-multi',
}),
);
// # Post a message in the channel
const message = 'test message for multiple teams search';
await adminClient.createPost({
channel_id: channel.id,
message,
});
// # Log in as the user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
// * Verify that the team selector is visible in the search box
await channelsPage.searchBox.toBeVisible();
const composerTeamSelector = channelsPage.searchBox.teamSelector;
await composerTeamSelector.toBeVisible();
// # Click on the team selector button
await composerTeamSelector.open();
// * Verify that both teams are visible in the menu
await expect(composerTeamSelector.teamOption(team.display_name)).toBeVisible();
await expect(composerTeamSelector.teamOption(secondTeam.display_name)).toBeVisible();
await expect(composerTeamSelector.teamOption('All Teams')).toBeVisible();
// # Now search for the message to see search results
await composerTeamSelector.close();
await channelsPage.searchBox.search(message);
await channelsPage.searchResultsPanel.toBeVisible();
// * Verify the team selector is visible in search results panel
const resultsTeamSelector = channelsPage.searchResultsPanel.teamSelector;
await resultsTeamSelector.toBeVisible();
// # Click on the team selector in results panel
await resultsTeamSelector.open();
// * Verify that both teams are visible in the results panel team selector
await expect(resultsTeamSelector.teamOption(team.display_name)).toBeVisible();
await expect(resultsTeamSelector.teamOption(secondTeam.display_name)).toBeVisible();
await expect(resultsTeamSelector.teamOption('All Teams')).toBeVisible();
});
test('team selector should show filter input with more than 4 teams', async ({pw}) => {
// # Create a user and admin client
const {adminClient, user, team} = await pw.initSetup();
// # Create 4 more teams (for a total of 5) and add the user to them
const teams = [team];
for (let i = 0; i < 4; i++) {
const newTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(newTeam.id, [user.id]);
teams.push(newTeam);
}
// # Log in as the user
const {channelsPage} = await pw.testBrowser.login(user);
// # Visit a default channel page
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
// # Open the search UI
await channelsPage.globalHeader.openSearch();
// # Verify team selector is visible and click on it
const teamSelector = channelsPage.searchBox.teamSelector;
await teamSelector.toBeVisible();
await teamSelector.open();
// # Verify the team filter input is visible with 5 teams
await expect(teamSelector.filterInput).toBeVisible();
// # Verify all teams are visible initially
for (const t of teams) {
await expect(teamSelector.teamOption(t.display_name)).toBeVisible();
}
// # Type filter text to match only one team
await teamSelector.filterBy(teams[2].display_name);
// # Verify only the matching team is visible (and the currently selected team)
await expect(teamSelector.teamOption(teams[0].display_name)).toBeVisible(); // Current team always visible
await expect(teamSelector.teamOption(teams[2].display_name)).toBeVisible(); // Filtered team
// # Verify other teams are hidden
for (let i = 1; i < teams.length; i++) {
if (i !== 2) {
// Skip the filtered team we're expecting to see
await expect(teamSelector.teamOption(teams[i].display_name)).not.toBeVisible();
}
}
// # Clear the filter
await teamSelector.filterBy('');
// # Verify all teams are visible again
for (const t of teams) {
await expect(teamSelector.teamOption(t.display_name)).toBeVisible();
}
});
test('MM-T5802 team filter list updates when a team is left', async ({pw}) => {
// # Create a user and 4 more teams (5 total) so the team-filter input renders
const {adminClient, team, user} = await pw.initSetup();
const teams = [team];
for (let i = 0; i < 4; i++) {
const newTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(newTeam.id, [user.id]);
teams.push(newTeam);
}
// # Log in as the user and open the team selector
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
const teamSelector = channelsPage.searchBox.teamSelector;
await teamSelector.open();
// * Verify all 5 teams are listed initially
for (const t of teams) {
await expect(teamSelector.teamOption(t.display_name)).toBeVisible();
}
// # Close the menu, then switch to and leave one of the extra teams
await teamSelector.close();
const teamToLeave = teams[teams.length - 1];
await channelsPage.leaveTeam(teamToLeave.name);
// # Reopen search and the team selector
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
await teamSelector.open();
// * Verify the left team no longer appears in the filter list
await expect(teamSelector.teamOption(teamToLeave.display_name)).not.toBeVisible();
// * Verify the remaining teams are still listed
for (const t of teams.slice(0, -1)) {
await expect(teamSelector.teamOption(t.display_name)).toBeVisible();
}
});
test('MM-T5803 search results narrow to the selected team and update after leaving that team', async ({pw}) => {
// # Create a user with two teams, each containing a distinct message
const {adminClient, team, user} = await pw.initSetup();
const secondTeam = await adminClient.createTeam(await pw.random.team('team', 'Team', 'O', true));
await adminClient.addUsersToTeam(secondTeam.id, [user.id]);
const messageInFirstTeam = 'pineapple message in the first team';
const messageInSecondTeam = 'pineapple message in the second team';
const firstChannel = await adminClient.getChannelByName(team.id, 'town-square');
const secondChannel = await adminClient.getChannelByName(secondTeam.id, 'town-square');
await adminClient.createPost({channel_id: firstChannel.id, message: messageInFirstTeam});
await adminClient.createPost({channel_id: secondChannel.id, message: messageInSecondTeam});
// # Log in, open search, and explicitly select "All Teams"
// (the selector defaults to the currently active team, not "All Teams", on a fresh open)
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name);
await channelsPage.toBeVisible();
await channelsPage.globalHeader.openSearch();
const composerTeamSelector = channelsPage.searchBox.teamSelector;
await composerTeamSelector.selectAllTeams();
await channelsPage.searchBox.search('pineapple');
// * Verify both teams' messages show up
await channelsPage.searchResultsPanel.toContainText(messageInFirstTeam);
await channelsPage.searchResultsPanel.toContainText(messageInSecondTeam);
// # Reopen the search composer and narrow its team selector to the second team specifically
// (changing the team via the results panel's own selector updates state but does not refresh
// the displayed results — the search must be resubmitted via the composer, as below)
await channelsPage.globalHeader.openSearch();
await composerTeamSelector.selectTeam(secondTeam.display_name);
// # Re-submit the search now that the composer is scoped to the second team
await channelsPage.searchBox.searchInput.click();
await channelsPage.searchBox.searchInput.press('Enter');
// * Verify only the second team's message shows
await expect(channelsPage.searchResultsPanel.container).toContainText(messageInSecondTeam);
await expect(channelsPage.searchResultsPanel.container).not.toContainText(messageInFirstTeam);
// # Leave the second team and re-run the same search
await channelsPage.leaveTeam(secondTeam.name);
await channelsPage.toBeVisible();
await channelsPage.searchFor('pineapple');
// * Verify only the first team's message shows now that the second team was left
await expect(channelsPage.searchResultsPanel.container).toContainText(messageInFirstTeam);
await expect(channelsPage.searchResultsPanel.container).not.toContainText(messageInSecondTeam);
});