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,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiOpenProfileModal`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Open the profile settings modal
* @param {string} section - such as `'General'`, `'Security'`, `'Notifications'`, `'Display'`, `'Sidebar'` and `'Advanced'`
* @return the "#accountSettingsModal"
*
* @example
* cy.uiOpenProfileModal('Profile Settings').within(() => {
* // Do something here
* });
*/
uiOpenProfileModal(section: string): Chainable<JQuery<HTMLElement>>;
/**
* Close the profile settings modal given that the modal itself is opened.
*
* @example
* cy.uiCloseAccountSettingsModal();
*/
uiCloseAccountSettingsModal(): Chainable;
/**
* Navigate to profile settings and verify the user's first, last name
* @param {String} firstname - expected user firstname
* @param {String} lastname - expected user lastname
*/
verifyAccountNameSettings(firstname: string, lastname: string): Chainable;
/**
* Navigate to account display settings and change collapsed reply threads setting
* @param {String} setting - ON or OFF
*/
uiChangeCRTDisplaySetting(setting: string): Chainable;
/**
* Navigate to account display settings and change message display setting
* @param {String} setting - COMPACT or STANDARD
*/
uiChangeMessageDisplaySetting(setting: string): Chainable;
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiOpenProfileModal', (section = '') => {
// # Open profile settings modal
cy.uiOpenUserMenu('Profile');
const profileSettingsModal = () => cy.findByRole('dialog', {name: 'Profile'}).should('be.visible');
if (!section) {
return profileSettingsModal();
}
// # Click on a particular section
cy.findByRoleExtended('tab', {name: section}).should('be.visible').click();
return profileSettingsModal();
});
Cypress.Commands.add('verifyAccountNameSettings', (firstname, lastname) => {
// # Go to Profile
cy.uiOpenProfileModal('Profile Settings');
// * Check name value
cy.get('#nameDesc').should('have.text', `${firstname} ${lastname}`);
cy.uiClose();
});
Cypress.Commands.add('uiChangeGenericDisplaySetting', (setting, option) => {
cy.uiOpenSettingsModal('Display');
cy.get(setting).scrollIntoView();
cy.get(setting).click();
cy.get('.section-max').scrollIntoView();
cy.get(option).check().should('be.checked');
cy.uiSaveAndClose();
});
/*
* Change the message display setting
* @param {String} setting - as 'STANDARD' or 'COMPACT'
*/
Cypress.Commands.add('uiChangeMessageDisplaySetting', (setting = 'STANDARD') => {
const SETTINGS = {STANDARD: '#message_displayFormatA', COMPACT: '#message_displayFormatB'};
cy.uiChangeGenericDisplaySetting('#message_displayTitle', SETTINGS[setting]);
});
/*
* Change the collapsed reply threads display setting
* @param {String} setting - as 'OFF' or 'ON'
*/
Cypress.Commands.add('uiChangeCRTDisplaySetting', (setting = 'OFF') => {
const SETTINGS = {
ON: '#collapsed_reply_threadsFormatA',
OFF: '#collapsed_reply_threadsFormatB',
};
cy.uiChangeGenericDisplaySetting('#collapsed_reply_threadsTitle', SETTINGS[setting]);
});

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiCloseAnnouncementBar`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Close the announcement bar if shown in the UI
*
* @example
* cy.uiCloseAnnouncementBar();
*/
uiCloseAnnouncementBar(): Chainable;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiCloseAnnouncementBar', () => {
cy.document().then((doc) => {
const announcementBar = doc.getElementsByClassName('announcement-bar')[0];
if (announcementBar) {
cy.get('.announcement-bar__close').click();
}
});
});

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiCreateEmptyBoard`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Create a board on a given menu item.
*
* @param {string} item - one of the template menu options, ex. 'Empty board'
*/
uiCreateBoard(item: string): Chainable;
/**
* Create an empty board.
* @example
* cy.uiCreateEmptyBoard();
*/
uiCreateEmptyBoard(): Chainable;
/**
* Create a board with the given title
*
* @param {string} title - title of the new board
*/
uiCreateNewBoard: (title?: string) => Chainable;
/**
* Create a new group with the given name
*
* @param {string} name - name of the new group
*/
uiAddNewGroup: (name?: string) => Chainable;
/**
* Create a card with the given title
*
* @param {string} title - title of the new card
* @param {string} columnIndex - the column index to create the card
*/
uiAddNewCard: (title?: string, columnIndex?: number) => Chainable;
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import timeouts from '../../fixtures/timeouts';
Cypress.Commands.add('uiCreateBoard', (item) => {
cy.log(`Create new board: ${item}`);
cy.uiAddBoard('Create new board');
cy.contains(item).click();
cy.contains('Use this template').click({force: true}).wait(timeouts.ONE_SEC);
});
Cypress.Commands.add('uiCreateEmptyBoard', () => {
cy.log('Create new empty board');
cy.contains('Create an empty board').click({force: true}).wait(timeouts.ONE_SEC);
});
Cypress.Commands.add('uiAddBoard', (item) => {
cy.get('.add-board-icon').should('be.visible').click();
cy.get('.menu-contents').should('be.visible');
if (item) {
cy.findByRole('button', {name: item}).click();
}
});
Cypress.Commands.add('uiCreateNewBoard', (title) => {
cy.log('**Create new empty board**');
cy.uiCreateEmptyBoard();
cy.findByPlaceholderText('Untitled board').should('be.visible');
cy.wait(timeouts.QUARTER_SEC);
if (title) {
cy.log('**Rename board**');
cy.findByPlaceholderText('Untitled board').type(`${title}{enter}`);
cy.findByRole('textbox', {name: title}).should('exist');
}
cy.wait(timeouts.HALF_SEC);
});
Cypress.Commands.add('uiAddNewGroup', (name) => {
cy.log('**Add a new group**');
cy.findByRole('button', {name: '+ Add a group'}).click();
cy.findByRole('textbox', {name: 'New group'}).should('exist');
if (name) {
cy.log('**Rename group**');
cy.findByRole('textbox', {name: 'New group'}).type(`{selectall}${name}{enter}`);
cy.findByRole('textbox', {name}).should('exist');
}
cy.wait(timeouts.HALF_SEC);
});
Cypress.Commands.add('uiAddNewCard', (title, columnIndex) => {
cy.log('**Add a new card**');
cy.findByRole('button', {name: '+ New'}).eq(columnIndex || 0).click();
cy.findByRole('dialog').should('exist');
if (title) {
cy.log('**Change card title**');
cy.findByPlaceholderText('Untitled').type(title);
}
});

View File

@@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiCreateChannel`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Create a new channel in the current team.
* @param {string} options.prefix - Prefix for the name of the channel, it will be added a random string ot it.
* @param {boolean} options.isPrivate - is the channel private or public (default)?
* @param {string} options.purpose - Channel's purpose
* @param {string} options.header - Channel's header
* @param {boolean} options.isNewSidebar) - the new sidebar has a different ui flow, set this setting to true to use that. Defaults to false.
* @param {string} options.createBoard) - Board template to create
*
* @example
* cy.uiCreateChannel({prefix: 'private-channel-', isPrivate: true, purpose: 'my private channel', header: 'my private header', isNewSidebar: false});
*/
uiCreateChannel(options: Record<string, unknown>): Chainable;
/**
* Add users to the current channel.
* @param {string[]} usernameList - list of userids to add to the channel
*
* @example
* cy.uiAddUsersToCurrentChannel(['user1', 'user2']);
*/
uiAddUsersToCurrentChannel(usernameList: string[]);
/**
* Invite users to the current channel.
* @param {string[]} usernameList - list of userids to be invited to the channel
*
* @example
* cy.uiInviteUsersToCurrentChannel(['user1', 'user2']);
*/
uiInviteUsersToCurrentChannel(usernameList: string[]);
/**
* Archive the current channel.
*
* @example
* cy.uiArchiveChannel();
*/
uiArchiveChannel();
/**
* Unarchive the current channel.
*
* @example
* cy.uiUnarchiveChannel();
*/
uiUnarchiveChannel();
/**
* Leave the current channel.
* @param {boolean} isPrivate - is the channel private or public (default)?
*
* @example
* cy.uiLeaveChannel(true);
*/
uiLeaveChannel(isPrivate?: boolean);
}
}

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getRandomId} from '../../utils';
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiCreateChannel', ({
prefix = 'channel-',
isPrivate = false,
purpose = '',
name = '',
createBoard = '',
}) => {
cy.uiBrowseOrCreateChannel('Create new channel');
cy.get('#new-channel-modal').should('be.visible');
if (isPrivate) {
cy.get('#public-private-selector-button-P').click().wait(TIMEOUTS.HALF_SEC);
} else {
cy.get('#public-private-selector-button-O').click().wait(TIMEOUTS.HALF_SEC);
}
const channelName = name || `${prefix}${getRandomId()}`;
cy.get('#input_new-channel-modal-name').should('be.visible').clear().type(channelName);
if (purpose) {
cy.get('#new-channel-modal-purpose').clear().type(purpose);
}
if (createBoard) {
cy.get('#add-board-to-channel').should('be.visible');
cy.findByTestId('add-board-to-channel-check').then((el) => {
if (el && !el.hasClass('checked')) {
el.click();
cy.get('.templates-selector').find('input').click({force: true});
cy.findByText(createBoard).scrollIntoView().should('be.visible').click({force: true});
}
});
}
cy.findByText('Create channel').click();
cy.get('#new-channel-modal').should('not.exist');
cy.get('#channelIntro').should('be.visible');
return cy.wrap({name: channelName});
});
Cypress.Commands.add('uiAddUsersToCurrentChannel', (usernameList) => {
if (usernameList.length) {
cy.get('#channelHeaderTitle').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
cy.get('#addUsersToChannelModal').should('be.visible');
usernameList.forEach((username) => {
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
});
cy.get('#saveItems').click();
cy.get('#addUsersToChannelModal').should('not.exist');
}
});
Cypress.Commands.add('uiInviteUsersToCurrentChannel', (usernameList) => {
if (usernameList.length) {
cy.get('#channelHeaderTitle').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
cy.get('#addUsersToChannelModal').should('be.visible');
usernameList.forEach((username) => {
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
});
cy.get('#saveItems').click();
cy.get('#addUsersToChannelModal').should('not.exist');
}
});
Cypress.Commands.add('uiArchiveChannel', () => {
cy.get('#channelHeaderTitle').click();
cy.get('#channelArchiveChannel').click();
return cy.get('#deleteChannelModalDeleteButton').click();
});
Cypress.Commands.add('uiUnarchiveChannel', () => {
cy.get('#channelHeaderTitle').should('be.visible').click();
cy.get('#channelUnarchiveChannel').should('be.visible').click();
return cy.get('#unarchiveChannelModalDeleteButton').should('be.visible').click();
});
Cypress.Commands.add('uiLeaveChannel', (isPrivate = false) => {
cy.get('#channelHeaderTitle').click();
if (isPrivate) {
cy.get('#channelLeaveChannel').click();
return cy.get('#confirmModalButton').click();
}
return cy.get('#channelLeaveChannel').click();
});
Cypress.Commands.add('goToDm', (username) => {
cy.uiAddDirectMessage().click({force: true});
// # Start typing part of a username that matches previously created users
cy.get('#selectItems input').typeWithForce(username);
cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC);
cy.findByRole('combobox', {name: 'Search for people'}).
typeWithForce(username).
wait(TIMEOUTS.ONE_SEC).
typeWithForce('{enter}');
// # Save the selected item
return cy.get('#saveItems').click().wait(TIMEOUTS.HALF_SEC);
});

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiGetChannelFavoriteButton`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get channel header button.
*
* @example
* cy.uiGetChannelHeaderButton().click();
*/
uiGetChannelHeaderButton(): Chainable;
/**
* Get favorite button from channel header.
*
* @example
* cy.uiGetChannelFavoriteButton().click();
*/
uiGetChannelFavoriteButton(): Chainable;
/**
* Get mute button from channel header.
*
* @example
* cy.uiGetMuteButton().click();
*/
uiGetMuteButton(): Chainable;
/**
* Get member button from channel header.
*
* @example
* cy.uiGetChannelMemberButton().click();
*/
uiGetChannelMemberButton(): Chainable;
/**
* Get pin button from channel header.
*
* @example
* cy.uiGetChannelPinButton().click();
*/
uiGetChannelPinButton(): Chainable;
/**
* Get files button from channel header.
*
* @example
* cy.uiGetChannelFileButton().click();
*/
uiGetChannelFileButton(): Chainable;
/**
* Get channel menu
*
* @example
* cy.uiGetChannelMenu();
*/
uiGetChannelMenu(): Chainable;
/**
* Open channel menu
* @param {string} [menu] - such as `'View Info'`, `'Notification Preferences'`, `'Team Settings'` and other items in the main menu.
* @return the channel menu
*
* @example
* cy.uiOpenChannelMenu();
*/
uiOpenChannelMenu(menu?: string): Chainable;
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Buttons
Cypress.Commands.add('uiGetChannelHeaderButton', () => {
return cy.get('#channelHeaderDropdownButton').should('be.visible');
});
Cypress.Commands.add('uiGetChannelFavoriteButton', () => {
return cy.get('#toggleFavorite').should('be.visible');
});
Cypress.Commands.add('uiGetMuteButton', () => {
return cy.get('#toggleMute').should('be.visible');
});
Cypress.Commands.add('uiGetChannelMemberButton', () => {
return cy.get('#member_rhs').should('be.visible');
});
Cypress.Commands.add('uiGetChannelPinButton', () => {
return cy.get('#channelHeaderPinButton').should('be.visible');
});
Cypress.Commands.add('uiGetChannelFileButton', () => {
return cy.get('#channelHeaderFilesButton').should('be.visible');
});
// Menus
Cypress.Commands.add('uiGetChannelMenu', (options = {exist: true}) => {
if (options.exist) {
return cy.get('#channelHeaderDropdownMenu').
should('be.visible');
}
return cy.get('#channelHeaderDropdownMenu').should('not.exist');
});
Cypress.Commands.add('uiOpenChannelMenu', (item = '') => {
// # Click on channel header button
cy.uiGetChannelHeaderButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetChannelMenu();
}
// # Click on a particular item
return cy.uiGetChannelMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
});

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getRandomId} from '../../utils';
import type {ChainableT} from '@/types';
/**
* Create a new category
*
* @param [categoryName] category's name
*
* @example
* cy.uiCreateSidebarCategory();
*/
function uiCreateSidebarCategory(categoryName: string = `category-${getRandomId()}`): ChainableT<any> {
// # Click on the sidebar menu dropdown and select Create Category
cy.uiBrowseOrCreateChannel('Create new category');
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
// # Fill in the category name and click 'Create'
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
invoke('val').should('equal', categoryName);
cy.findByRole('button', {name: 'Create'}).should('be.enabled').click();
});
// * Wait for the category to appear in the sidebar
cy.contains('.SidebarChannelGroup', categoryName, {matchCase: false});
return cy.wrap({displayName: categoryName});
}
Cypress.Commands.add('uiCreateSidebarCategory', uiCreateSidebarCategory);
/**
* Move a channel to a category.
* Open the channel menu, select Move to, and click either New Category or on the category.
*
* @param channelName channel's name
* @param categoryName category's name
* @param [newCategory=false] create a new category to move into
* @param [isChannelId=false] whether channelName is a channel ID
*
* @example
* cy.uiMoveChannelToCategory('Town Square', 'Favorites');
*/
function uiMoveChannelToCategory(channelName: string, categoryName: string, newCategory = false, isChannelId = false): ChainableT<any> {
// # Open the channel menu, select Move to
cy.uiGetChannelSidebarMenu(channelName, isChannelId).within(() => {
cy.findByText('Move to...').should('be.visible').trigger('mouseover');
});
// # Select the move to category
cy.findAllByRole('menu', {name: 'Move to submenu'}).should('be.visible').within(() => {
if (newCategory) {
cy.findByText('New Category').should('be.visible').click({force: true});
} else {
cy.findByText(categoryName).should('be.visible').click({force: true});
}
});
if (newCategory) {
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
// # Fill in the category name and click 'Create'
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
invoke('val').should('equal', categoryName);
cy.findByRole('button', {name: 'Create'}).should('be.enabled').click();
});
}
return cy.wrap({displayName: categoryName});
}
Cypress.Commands.add('uiMoveChannelToCategory', uiMoveChannelToCategory);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
uiCreateSidebarCategory: typeof uiCreateSidebarCategory;
uiMoveChannelToCategory: typeof uiMoveChannelToCategory;
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Iframe element in Stripe
*
* @example
* cy.getIframeBody();
*/
uiGetPaymentCardInput(): Chainable;
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiGetPaymentCardInput', () => {
return cy.
get('.__PrivateStripeElement > iframe').
its('0.contentDocument.body').should('not.be.empty').
then(cy.wrap);
});

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiSave`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Click 'Save' button
*
* @example
* cy.uiSave();
*/
uiSave(): Chainable;
/**
* Click 'Cancel' button
*
* @example
* cy.uiCancel();
*/
uiCancel(): Chainable;
/**
* Click 'Close' button
*
* @example
* cy.uiClose();
*/
uiClose(): Chainable;
/**
* Click Save then Close buttons
*
* @example
* cy.uiSaveAndClose();
*/
uiSaveAndClose(): Chainable;
/**
* Get a button by its text using "cy.findByRole"
*
* @param {String} label - Button text
*
* @example
* cy.uiGetButton('Save');
*/
uiGetButton(label: string): Chainable;
/**
* Get save button
*
* @example
* cy.uiSaveButton();
*/
uiSaveButton(): Chainable;
/**
* Get cancel button
*
* @example
* cy.uiCancelButton();
*/
uiCancelButton(): Chainable;
/**
* Get close button
*
* @example
* cy.uiCloseButton();
*/
uiCloseButton(): Chainable;
/**
* Get a radio button by its text using "cy.findByRole"
*
* @example
* cy.uiGetRadioButton('Custom Theme');
*/
uiGetRadioButton(): Chainable;
/**
* Get a heading by its text using "cy.findByRole"
*
* @param {string} headingText - Heading text
*
* @example
* cy.uiGetHeading('General Settings');
*/
uiGetHeading(headingText: string): Chainable;
/**
* Get a textbox by its text using "cy.findByRole"
*
* @param {string} text - Textbox label
*
* @example
* cy.uiGetTextbox('Nickname');
*/
uiGetTextbox(text: string): Chainable;
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiSave', () => {
return cy.findByRole('button', {name: 'Save'}).scrollIntoView().click();
});
Cypress.Commands.add('uiCancel', () => {
return cy.findByRole('button', {name: 'Cancel'}).click();
});
Cypress.Commands.add('uiClose', () => {
return cy.findAllByRole('button', {name: 'Close'}).eq(0).click();
});
Cypress.Commands.add('uiSaveAndClose', () => {
cy.uiSave();
cy.uiClose();
});
Cypress.Commands.add('uiGetButton', (name) => {
return cy.findByRole('button', {name});
});
Cypress.Commands.add('uiSaveButton', () => {
return cy.uiGetButton('Save');
});
Cypress.Commands.add('uiCancelButton', () => {
return cy.uiGetButton('Cancel');
});
Cypress.Commands.add('uiCloseButton', () => {
return cy.uiGetButton('Close');
});
Cypress.Commands.add('uiGetRadioButton', (name) => {
return cy.findByRole('radio', {name}).should('be.visible');
});
Cypress.Commands.add('uiGetHeading', (name) => {
return cy.findByRole('heading', {name}).should('be.visible');
});
Cypress.Commands.add('uiGetTextbox', (name) => {
return cy.findByRole('textbox', {name}).should('be.visible');
});
Cypress.Commands.add('uiCloseOnboardingTaskList', () => {
cy.get('[data-cy=onboarding-task-list-action-button]').then(($btn) => {
if ($btn.find('i.icon-close').length) {
$btn.trigger('click');
}
});
});

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Select compliance export format
* @param {string} exportFormat - compliance export format
*
* @example
* const EXPORTFORMAT = "Actiance XML";
* cy.uiEnableComplianceExport(Compliance Export Format);
*/
uiEnableComplianceExport(exportFormat: string): Chainable;
/**
* Go to Compliance Page
*/
uiGoToCompliancePage(): Chainable;
/**
* Click Run Export Compliance and wait for Success status
*/
uiExportCompliance(): Chainable;
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiEnableComplianceExport', (exportFormat = 'csv') => {
// * Verify that the page is loaded
cy.findByText('Enable Compliance Export:').should('be.visible');
cy.findByText('Compliance Export Time:').should('be.visible');
cy.findByText('Export Format:').should('be.visible');
// # Enable compliance export
cy.findByRole('radio', {name: /false/i}).click();
cy.findByRole('radio', {name: /true/i}).click();
// # Change export format
cy.findByTestId('exportFormatdropdown').should('be.visible').select(exportFormat);
// # Save settings
cy.uiSaveConfig({confirm: true});
});
Cypress.Commands.add('uiGoToCompliancePage', () => {
cy.visit('/admin_console/compliance/export');
cy.get('.admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Compliance Export');
});
Cypress.Commands.add('uiExportCompliance', () => {
// # Click the export job button
cy.findByRole('button', {name: /run compliance export job now/i}).click();
// # Small wait to ensure new row is add
cy.wait(TIMEOUTS.THREE_SEC);
// # Get the first row
cy.get('.job-table__table').find('tbody > tr').eq(0).as('firstRow');
// # Get the first table header
cy.get('.job-table__table').find('thead > tr').as('firstheader');
// # Wait until export is finished
cy.waitUntil(() => {
return cy.get('@firstRow').find('td:eq(0)').then((el) => {
return el[0].innerText.trim() === 'Success';
});
},
{
timeout: TIMEOUTS.FIVE_MIN,
interval: TIMEOUTS.ONE_SEC,
errorMsg: 'Compliance export did not finish in time',
});
});

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Go to Data Retention page
*/
uiGoToDataRetentionPage(): Chainable;
/**
* Click create policy button
*/
uiClickCreatePolicy(): Chainable;
/**
* Fill out custom policy form fields
* @param {string} name - policy name
* @param {string} durationDropdown - duration dropdown value (days, years, forever)
* @param {string?} durationText - duration text
*/
uiFillOutCustomPolicyFields(name: string, durationDropdown: string, durationText?: string): Chainable;
/**
* Search and add teams to custom policy
* @param {string[]} teamNames - array of team names
*/
uiAddTeamsToCustomPolicy(teamNames: string[]): Chainable;
/**
* Search and add channels to custom policy
* @param {string[]} channelNames - array of channel names
*/
uiAddChannelsToCustomPolicy(channelNames: string[]): Chainable;
/**
* Add teams to a custom policy
* @param {number} numberOfTeams - number of teams to add to the policy
*/
uiAddRandomTeamToCustomPolicy(numberOfTeams?: number): Chainable;
/**
* Add channels to a custom policy
* @param {number} numberOfTeams - number of teams to add to the policy
*/
uiAddRandomChannelToCustomPolicy(numberOfChannels?: number): Chainable;
/**
* Verify custom policy UI information
* @param {string} policyId - Custom Policy ID
* @param {string} description - The name of the policy
* @param {string} duration - How long messages last in the policy
* @param {string} appliedTo - Teams and channels the policy apples to
*/
uiVerifyCustomPolicyRow(policyId: string, description: string, duration: string, appliedTo: string): Chainable;
/**
* Click edit custom policy
* @param {string} policyId - Custom Policy ID
*/
uiClickEditCustomPolicyRow(policyId: string): Chainable;
/**
* Verify custom create policy response
* @param body - Response body
* @param {number} teamCount - Number of teams the policy applies to
* @param {number} channelCount - Number of channels the policy applies to
* @param {number} duration - How long messages last in the policy
* @param {string} displayName - The name of the policy
*/
uiVerifyPolicyResponse(body, teamCount: number, channelCount: number, duration: number, displayName: string): Chainable;
}
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiGoToDataRetentionPage', () => {
cy.visit('/admin_console/compliance/data_retention_settings');
cy.get('.DataRetentionSettings .admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Data Retention Policies');
});
Cypress.Commands.add('uiClickCreatePolicy', () => {
cy.uiGetButton('Add policy').click();
cy.get('.DataRetentionSettings .admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Custom Retention Policy');
});
Cypress.Commands.add('uiFillOutCustomPolicyFields', (name, durationDropdown, durationText = '') => {
// # Type policy name
cy.uiGetTextbox('Policy name').clear().type(name);
// # Add message retention values
cy.get('.CustomPolicy__fields #DropdownInput_message_retention').should('be.visible').click();
cy.get(`.message_retention__menu .message_retention__option span.option_${durationDropdown}`).should('be.visible').click();
if (durationText) {
cy.get('.CustomPolicy__fields input#message_retention_input').clear().type(durationText);
}
});
Cypress.Commands.add('uiAddTeamsToCustomPolicy', (teamNames) => {
cy.uiGetButton('Add teams').click();
teamNames.forEach((teamName) => {
cy.findByRole('combobox', {name: 'Search and add teams'}).typeWithForce(teamName);
cy.get('.team-info-block').then((el) => {
el.click();
});
});
cy.uiGetButton('Add').click();
});
Cypress.Commands.add('uiAddChannelsToCustomPolicy', (channelNames) => {
cy.uiGetButton('Add channels').click();
channelNames.forEach((channelName) => {
cy.findByRole('combobox', {name: 'Search and add channels'}).typeWithForce(channelName);
cy.wait(TIMEOUTS.ONE_SEC);
cy.get('.channel-info-block').then((el) => {
el.click();
});
});
cy.uiGetButton('Add').click();
});
Cypress.Commands.add('uiAddRandomTeamToCustomPolicy', (numberOfTeams = 1) => {
cy.uiGetButton('Add teams').click();
for (let i = 0; i < numberOfTeams; i++) {
cy.get('.team-info-block').first().then((el) => {
el.click();
});
}
cy.uiGetButton('Add').click();
});
Cypress.Commands.add('uiAddRandomChannelToCustomPolicy', (numberOfChannels = 1) => {
cy.uiGetButton('Add channels').click();
for (let i = 0; i < numberOfChannels; i++) {
cy.get('.channel-info-block').first().then((el) => {
el.click();
});
}
cy.uiGetButton('Add').click();
});
Cypress.Commands.add('uiVerifyCustomPolicyRow', (policyId, description, duration, appliedTo) => {
// * Assert row has correct description
cy.get(`#customDescription-${policyId}`).should('include.text', description);
// * Assert row has correct duration
cy.get(`#customDuration-${policyId}`).should('include.text', duration);
// * Assert row has correct team/channel counts
cy.get(`#customAppliedTo-${policyId}`).should('include.text', appliedTo);
});
Cypress.Commands.add('uiClickEditCustomPolicyRow', (policyId) => {
cy.get(`#customWrapper-${policyId}`).trigger('mouseover').click();
cy.findByRole('button', {name: /edit/i}).should('be.visible').click();
});
Cypress.Commands.add('uiVerifyPolicyResponse', (body, teamCount, channelCount, duration, displayName) => {
// * Assert response body exists
assert.isNotNull(body);
// * Assert response body contains an ID
assert.isNotNull(body.id);
// * Assert response body team_count matches supplied value
expect(body.team_count).to.equal(teamCount);
// * Assert response body channel_count matches supplied value
expect(body.channel_count).to.equal(channelCount);
// * Assert response body duration matches supplied value
expect(body.post_duration).to.equal(duration);
// * Assert response body display_name matches supplied value
expect(body.display_name).to.equal(displayName);
});

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '@/types';
Cypress.Commands.add('uiGetEmojiPicker', (): ChainableT<JQuery> => {
return cy.get('#emojiPicker').should('be.visible');
});
Cypress.Commands.add('uiOpenEmojiPicker', (): ChainableT<JQuery> => {
cy.get('body').then(($body) => {
if ($body.find('#emojiPicker:visible').length) {
cy.get('body').type('{esc}');
}
});
cy.get('#emojiPicker').should('not.exist');
cy.findByRole('button', {name: 'select an emoji'}).should('be.visible').click();
return cy.get('#emojiPicker').should('be.visible');
});
Cypress.Commands.add('uiOpenCustomEmoji', () => {
cy.uiOpenEmojiPicker();
cy.findByText('Custom Emoji').should('be.visible').click();
cy.url().should('include', '/emoji');
cy.get('.backstage-header').should('be.visible').and('contain', 'Custom Emoji');
});
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Open custom emoji
*
* @example
* cy.uiOpenCustomEmoji();
*/
uiGetEmojiPicker(): Chainable;
/**
* Open custom emoji
*
* @example
* cy.uiOpenCustomEmoji();
*/
uiOpenCustomEmoji(): Chainable;
/**
* Open emoji picker
*
* @example
* cy.uiOpenEmojiPicker();
*/
uiOpenEmojiPicker(): Chainable;
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of the Testing Library commands
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Extends `findByRole` by matching case to `name` as insensitive but sensitive to `text` value
* @param {string} role - button, input, textbox, etc.
* @param {Object} option - text value of the target element
*
* @example
* cy.findByRoleExtended('button', {name: 'Advanced'}).should('be.visible').click();
*/
findByRoleExtended(role: string, option: {name: string}): Chainable;
}
}

View File

@@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('findByRoleExtended', (role, {name}) => {
const re = RegExp(name, 'i');
return cy.findByRole(role, {name: re}).should('have.text', name);
});

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiOpenFilePreviewModal`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get file thumbnail from a post
*
* @param {string} filename
*
* @example
* cy.uiGetFileThumbnail('image.png');
*/
uiGetFileThumbnail(filename: string): Chainable;
/**
* Get file upload preview located below post textbox
*
* @example
* cy.uiGetFileUploadPreview();
*/
uiGetFileUploadPreview(): Chainable;
/**
* Wait for file upload preview located below post textbox
*
* @example
* cy.uiGetFileUploadPreview();
*/
uiGetFileUploadPreview(): Chainable;
/**
* Get file preview modal
*
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
*
* @example
* cy.uiGetFilePreviewModal();
*/
uiGetFilePreviewModal(option: Record<string, boolean>): Chainable;
/**
* Get Public Link
*
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
*
* @example
* cy.uiGetPublicLink();
*/
uiGetPublicLink(option: Record<string, boolean>): Chainable;
/**
* Open file preview modal
*
* @param {string} filename
*
* @example
* cy.uiOpenFilePreviewModal('image.png');
*/
uiOpenFilePreviewModal(filename: string): Chainable;
/**
* Close file preview modal
*
* @example
* cy.uiCloseFilePreviewModal();
*/
uiCloseFilePreviewModal(): Chainable;
/**
* Get main content of file preview modal
*
* @example
* cy.uiGetContentFilePreviewModal();
*/
uiGetContentFilePreviewModal(): Chainable;
/**
* Get download link button from file preview modal
*
* @example
* cy.uiGetDownloadLinkFilePreviewModal();
*/
uiGetDownloadLinkFilePreviewModal(): Chainable;
/**
* Get download button from file preview modal
*
* @example
* cy.uiGetDownloadFilePreviewModal();
*/
uiGetDownloadFilePreviewModal(): Chainable;
/**
* Get arrow left button from file preview modal
*
* @example
* cy.uiGetArrowLeftFilePreviewModal();
*/
uiGetArrowLeftFilePreviewModal(): Chainable;
/**
* Get arrow right button from file preview modal
*
* @example
* cy.uiGetArrowRightFilePreviewModal();
*/
uiGetArrowRightFilePreviewModal(): Chainable;
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiGetFileThumbnail', (filename) => {
// Gallery tiles preserve original filename casing on data-file-name,
// while the legacy thumbnail aria-label is always lowercased
// (see file_preview.tsx). Match each accordingly.
const tileMatches = Cypress.$('[data-testid="media-gallery-tile"]').filter((_, el) => {
return el.getAttribute('data-file-name')?.toLowerCase() === filename.toLowerCase();
});
if (tileMatches.length) {
return cy.wrap(tileMatches);
}
return cy.findByLabelText(`file thumbnail ${filename.toLowerCase()}`);
});
Cypress.Commands.add('uiGetFileUploadPreview', () => {
return cy.get('.file-preview__container');
});
Cypress.Commands.add('uiWaitForFileUploadPreview', () => {
cy.waitUntil(() => cy.uiGetFileUploadPreview().then((el) => {
return el.find('.post-image__thumbnail').length > 0;
}));
});
Cypress.Commands.add('uiGetFilePreviewModal', (options = {exist: true}) => {
if (options.exist) {
return cy.get('.file-preview-modal').should('be.visible');
}
return cy.get('.file-preview-modal').should('not.exist');
});
Cypress.Commands.add('uiGetPublicLink', (options = {exist: true}) => {
if (options.exist) {
return cy.get('.icon-link-variant').should('be.visible');
}
return cy.get('.icon-link-variant').should('not.exist');
});
Cypress.Commands.add('uiGetHeaderFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.file-preview-modal-header').should('be.visible');
});
Cypress.Commands.add('uiOpenFilePreviewModal', (filename) => {
if (filename) {
cy.uiGetFileThumbnail(filename).click();
return;
}
if (Cypress.$('[data-testid="media-gallery-tile"]').length) {
cy.get('[data-testid="media-gallery-tile"]').first().click();
} else {
cy.findByTestId('fileAttachmentList').children().first().click();
}
});
Cypress.Commands.add('uiCloseFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.icon-close').click();
});
Cypress.Commands.add('uiGetContentFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.file-preview-modal__content');
});
Cypress.Commands.add('uiGetDownloadLinkFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.icon-link-variant');
});
Cypress.Commands.add('uiGetDownloadFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.icon-download-outline');
});
Cypress.Commands.add('uiGetArrowLeftFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.icon-chevron-left');
});
Cypress.Commands.add('uiGetArrowRightFilePreviewModal', () => {
return cy.uiGetFilePreviewModal().find('.icon-chevron-right');
});

View File

@@ -0,0 +1,189 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiGetProductMenuButton`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get product switch button
*
* @example
* cy.uiGetProductMenuButton().click();
*/
uiGetProductMenuButton(): Chainable;
/**
* Get product switch menu
*
* @example
* cy.uiGetProductMenu().click();
*/
uiGetProductMenu(): Chainable;
/**
* Open product switch menu
*
* @param {string} item - menu item ex. System Console, Integrations, etc.
*
* @example
* cy.uiOpenProductMenu().click();
*/
uiOpenProductMenu(item: string = ''): Chainable;
/**
* Get set status button
*
* @example
* cy.uiGetSetStatusButton().click();
*/
uiGetSetStatusButton(): Chainable;
/**
* Get profile header
*
* @example
* cy.uiGetProfileHeader();
*/
uiGetProfileHeader(): Chainable;
/**
* Get status menu container
*
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
* @example
* cy.uiGetStatusMenuContainer({exist: false});
*/
uiGetStatusMenuContainer(option?: Record<string, boolean>): Chainable;
/**
* Get user menu
*
* @example
* cy.uiGetStatusMenu();
*/
uiGetStatusMenu(): Chainable;
/**
* Open help menu
*
* @param {string} item - menu item ex. Ask the community, Help resources, etc.
*
* @example
* cy.uiOpenHelpMenu();
*/
uiOpenHelpMenu(item: string): Chainable;
/**
* Get help button
*
* @example
* cy.uiGetHelpButton();
*/
uiGetHelpButton(): Chainable;
/**
* Get help menu
*
* @example
* cy.uiGetHelpMenu();
*/
uiGetHelpMenu(): Chainable;
/**
* Open user menu
*
* @param {string} [item] - menu item ex. Profile, Logout, etc.
*
* @example
* cy.uiOpenUserMenu();
*/
uiOpenUserMenu(item?: string): Chainable;
/**
* Get search form container
*
* @example
* cy.uiGetSearchContainer();
*/
uiGetSearchContainer(): Chainable;
/**
* Get search box
*
* @example
* cy.uiGetSearchBox();
*/
uiGetSearchBox(): Chainable;
/**
* Get at-mention button
*
* @example
* cy.uiGetRecentMentionButton();
*/
uiGetRecentMentionButton(): Chainable;
/**
* Get saved posts button
*
* @example
* cy.uiGetSavedPostButton();
*/
uiGetSavedPostButton(): Chainable;
/**
* Get settings button
*
* @example
* cy.uiGetSettingsButton();
*/
uiGetSettingsButton(): Chainable;
/**
* Get settings modal
*
* @example
* cy.uiGetSettingsModal();
*/
uiGetSettingsModal(): Chainable;
/**
* Get channel info button
*
* @example
* cy.uiGetChannelInfoButton();
*/
uiGetChannelInfoButton(): Chainable;
/**
* Open settings modal
*
* @param {string} section - ex. Display, Sidebar, etc.
*
* @example
* cy.uiOpenSettingsModal();
*/
uiOpenSettingsModal(section: string): Chainable;
/**
* User log out via user menu
*
* @example
* cy.uiLogout();
*/
uiLogout(): Chainable;
}
}

View File

@@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiGetProductMenuButton', () => {
return cy.findByRole('button', {name: 'Product switch menu'}).should('be.visible');
});
Cypress.Commands.add('uiGetProductMenu', () => {
return cy.get('.product-switcher-menu').should('be.visible');
});
Cypress.Commands.add('uiOpenProductMenu', (item = '') => {
// # Click on product switch button
cy.uiGetProductMenuButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetProductMenu();
}
// # Click on a particular item
return cy.uiGetProductMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
});
Cypress.Commands.add('uiGetSetStatusButton', () => {
// # Get set status button
return cy.get('#userAccountMenuButton').should('be.visible');
});
Cypress.Commands.add('uiGetProfileHeader', () => {
return cy.uiGetSetStatusButton().parent();
});
Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => {
if (options.exist) {
return cy.get('#userAccountMenu').should('exist').and('be.visible');
}
return cy.get('#userAccountMenu').should('not.exist');
});
Cypress.Commands.add('uiGetStatusMenu', (options = {visible: true}) => {
if (options.visible) {
return cy.get('#userAccountMenu').should('exist').and('be.visible');
}
return cy.get('#userAccountMenu').should('not.exist');
});
Cypress.Commands.add('uiOpenHelpMenu', (item = '') => {
// # Click on help status button
cy.uiGetHelpButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetHelpMenu();
}
// # Click on a particular item
return cy.uiGetHelpMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
});
Cypress.Commands.add('uiGetHelpButton', () => {
return cy.findByRole('button', {name: 'Help'}).should('be.visible');
});
Cypress.Commands.add('uiGetHelpMenu', (options = {visible: true}) => {
const dropdown = () => cy.get('#helpMenuPortal').find('.dropdown-menu');
if (options.visible) {
return dropdown().should('be.visible');
}
return dropdown().should('not.be.visible');
});
Cypress.Commands.add('uiOpenUserMenu', (item = '') => {
// # Click on user status button
cy.uiGetSetStatusButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetStatusMenu();
}
// # Click on a particular item
return cy.uiGetStatusMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
});
Cypress.Commands.add('uiGetSearchContainer', () => {
return cy.get('#searchFormContainer').should('be.visible');
});
Cypress.Commands.add('uiGetSearchBox', () => {
return cy.findByRole('searchbox').should('be.visible');
});
Cypress.Commands.add('uiGetRecentMentionButton', () => {
return cy.findByRole('button', {name: 'Recent mentions'}).should('be.visible');
});
Cypress.Commands.add('uiGetSavedPostButton', () => {
return cy.findByRole('button', {name: 'Saved messages'}).should('be.visible');
});
Cypress.Commands.add('uiGetSettingsButton', () => {
return cy.findByRole('button', {name: 'Settings'}).should('be.visible');
});
Cypress.Commands.add('uiGetChannelInfoButton', () => {
return cy.findByRole('button', {name: 'View Info'}).should('be.visible');
});
Cypress.Commands.add('uiGetSettingsModal', () => {
// # Get settings modal
return cy.findByRole('dialog', {name: 'Settings'});
});
Cypress.Commands.add('uiOpenSettingsModal', (section = '') => {
// # Open settings modal
cy.uiGetSettingsButton().click();
if (!section) {
return cy.uiGetSettingsModal();
}
// # Click on a particular section
cy.findByRoleExtended('tab', {name: section}).should('be.visible').click();
return cy.uiGetSettingsModal();
});
Cypress.Commands.add('uiLogout', () => {
// # Click logout via user menu
cy.uiOpenUserMenu('Log out');
cy.url().should('include', '/login');
cy.get('.login-body-message').should('be.visible');
cy.get('.login-body-card').should('be.visible');
});

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import './account_settings_modal';
import './announcement_bar';
import './boards';
import './channel';
import './channel_header';
import './channel_sidebar';
import './cloud_billing';
import './common';
import './compliance_export';
import './data_retention';
import './extend_testing_library';
import './global_header';
import './emoji';
import './file_preview';
import './login';
import './menu';
import './mfa';
import './modal';
import './post';
import './post_dropdown_menu';
import './search';
import './sidebar_left';
import './sidebar_right';
import './suggestion_list';
import './system';
import './team';
import './tooltip';

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Login vi UI at login page
*
* @param {UserProfile} user - user with username and password
*
* @example
* cy.uiLogin(user);
*/
uiLogin(user: UserProfile): Chainable;
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiLogin', (user = {}) => {
cy.url().should('include', '/login');
// # Type email and password, then Sign in
cy.get('#input_loginId').should('be.visible').type(user.email);
cy.get('#input_password-input').should('be.visible').type(user.password);
cy.get('#saveSetting').should('not.be.disabled').click();
});

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiOpenSystemConsoleMainMenu`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Open main menu at system console
* @param {string} item - such as `'Switch to [Team Name]'`, `'Administrator's Guide'`, `'Troubleshooting Forum'`, `'Commercial Support'`, `'About Mattermost'` and `'Log Out'`.
* @return the main menu
*
* @example
* cy.uiOpenSystemConsoleMainMenu();
*/
uiOpenSystemConsoleMainMenu(): Chainable;
/**
* Close main menu at system console
*
* @example
* cy.uiCloseSystemConsoleMainMenu();
*/
uiCloseSystemConsoleMainMenu(): Chainable;
/**
* Get main menu at system console
*
* @example
* cy.uiGetSystemConsoleMainMenu();
*/
uiGetSystemConsoleMainMenu(): Chainable;
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const SYSTEM_CONSOLE_MAIN_MENU = 'Menu Icon';
function openMenu(name, item) {
const menu = () => cy.findByRole('button', {name}).should('be.visible');
// # Open the menu
menu().should('be.visible').click();
if (!item) {
return menu();
}
// # Click on a particular item
return cy.findByRole('menu').findByText(item).scrollIntoView().should('be.visible').click();
}
function getMenu(name) {
return cy.findByRole('button', {name}).should('be.visible');
}
Cypress.Commands.add('uiOpenSystemConsoleMainMenu', (item = '') => {
return openMenu(SYSTEM_CONSOLE_MAIN_MENU, item);
});
Cypress.Commands.add('uiCloseSystemConsoleMainMenu', () => {
return cy.uiGetSystemConsoleMainMenu().click();
});
Cypress.Commands.add('uiGetSystemConsoleMainMenu', () => {
return getMenu(SYSTEM_CONSOLE_MAIN_MENU);
});
Cypress.Commands.add('uiOpenDndStatusSubMenuAndClick30Mins', () => {
cy.uiOpenUserMenu().findByText('Do not disturb').should('be.visible').trigger('mouseover');
cy.findByText('30 mins').should('be.visible').click({force: true});
});

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiGetMFASecret`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get MFA secret of a given user
* @param {string} userId - ID of user
*
* @returns {string} `secret` - MFA secret
*
* @example
* const headerLabel = 'What\'s New';
* cy.uiGetMFASecret('user-id').then((secret) => {
* // do something with the secret
* });
*/
uiGetMFASecret(userId: string): Chainable<string>;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import authenticator from 'authenticator';
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiGetMFASecret', (userId) => {
return cy.url().then((url) => {
if (url.includes('mfa/setup')) {
// # Complete MFA setup if we are on token setup page /mfa/setup
return cy.get('#mfa').wait(TIMEOUTS.HALF_SEC).find('.col-sm-12').then((p) => {
const secretp = p.text();
const secret = secretp.split(' ')[1];
const token = authenticator.generateToken(secret);
cy.findByPlaceholderText('MFA Code').type(token);
cy.findByText('Save').click();
cy.wait(TIMEOUTS.HALF_SEC);
cy.findByText('Okay').click();
return cy.wrap(secret);
});
}
// # If the user already has MFA enabled, reset the secret.
return cy.apiGenerateMfaSecret(userId).then((res) => {
return cy.wrap(res.code.secret);
});
});
});

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiCloseModal`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Close modal with header label
* @param {string} headerLabel - the header label
*
* @example
* const headerLabel = 'What\'s New';
* cy.uiCloseModal(headerLabel);
*/
uiCloseModal(headerLabel: string): Chainable;
}
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiCloseModal', (headerLabel) => {
// # Close modal with modal label
cy.get('#genericModalLabel', {timeout: TIMEOUTS.HALF_MIN}).should('have.text', headerLabel).parents().find('.modal-dialog').findByLabelText('Close').click();
});

View File

@@ -0,0 +1,325 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '../../types';
function uiGetPostTextBox(option = {exist: true}): ChainableT<JQuery> {
if (option.exist) {
return cy.get('#post_textbox').should('be.visible');
}
return cy.get('#post_textbox').should('not.exist');
}
Cypress.Commands.add('uiGetPostTextBox', uiGetPostTextBox);
function uiGetReplyTextBox(option = {exist: true}): ChainableT<JQuery> {
if (option.exist) {
return cy.get('#reply_textbox').should('be.visible');
}
return cy.get('#reply_textbox').should('not.exist');
}
Cypress.Commands.add('uiGetReplyTextBox', uiGetReplyTextBox);
function uiGetPostProfileImage(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__img').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostProfileImage', uiGetPostProfileImage);
function uiGetPostHeader(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__header').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostHeader', uiGetPostHeader);
function uiGetPostBody(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__body').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostBody', uiGetPostBody);
function uiGetPostThreadFooter(postId: string): ChainableT<JQuery> {
return getPost(postId).find('.ThreadFooter');
}
Cypress.Commands.add('uiGetPostThreadFooter', uiGetPostThreadFooter);
function uiGetPostEmbedContainer(postId: string): ChainableT<JQuery> {
return cy.uiGetPostBody(postId).
find('.file-preview__button').
should('be.visible');
}
Cypress.Commands.add('uiGetPostEmbedContainer', uiGetPostEmbedContainer);
function getCenterPostById(postId: string): ChainableT<JQuery> {
// Prefer postView rows: virtualized lists may detach #post_<id> from a bare cy.get() query.
return cy.findAllByTestId('postView', {timeout: 20000}).
filter(`#post_${postId}`).
should('have.length.at.least', 1).
first().
scrollIntoView().
should('be.visible');
}
function getPost(postId: string): ChainableT<JQuery> {
if (postId) {
return getCenterPostById(postId);
}
return cy.getLastPost();
}
Cypress.Commands.add('getPost', getPost);
function getPostMmBlocks(postId: string): ChainableT<JQuery> {
return getCenterPostById(postId).find('.mm-blocks').should('exist');
}
Cypress.Commands.add('getPostMmBlocks', getPostMmBlocks);
function getRhsPostMmBlocks(postId: string): ChainableT<JQuery> {
return cy.findAllByTestId('rhsPostView', {timeout: 20000}).
filter(`#rhsPost_${postId}`).
should('have.length.at.least', 1).
first().
find('.mm-blocks').
should('exist');
}
Cypress.Commands.add('getRhsPostMmBlocks', getRhsPostMmBlocks);
function editLastPostWithNewMessage(message: string) {
cy.uiGetPostTextBox().type('{uparrow}');
// * Edit Post Input should appear
cy.get('#edit_textbox').should('be.visible');
// # Update the post message and click Save
cy.get('#edit_textbox').clear().type(message);
cy.get('#create_post').findByText('Save').scrollIntoView().click();
}
Cypress.Commands.add('editLastPostWithNewMessage', editLastPostWithNewMessage);
export function verifySavedPost(postId: string, message: string) {
// * Check that the center save icon has been updated correctly
cy.get(`#post_${postId}`).trigger('mouseover', {force: true});
cy.get(`#CENTER_flagIcon_${postId}`).
should('have.class', 'post-menu__item').
and('have.attr', 'aria-label', 'remove from saved');
// # Open the post-dotmenu
cy.clickPostDotMenu(postId, 'CENTER');
// * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Remove from Saved').scrollIntoView().should('be.visible');
cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => {
// * Check that the post is highlighted
cy.get(`#post_${postId}`).should('have.class', 'post--pinned-or-flagged');
// * Check that the post pre-header is visible
cy.get('div.post-pre-header').should('be.visible');
// * Check that the post pre-header has the saved icon
cy.get('span.icon--post-pre-header').
should('be.visible').
within(() => {
cy.get('svg').should('have.attr', 'aria-label', 'Saved Icon');
});
// * Check that the post pre-header has the saved post link
cy.get('div.post-pre-header__text-container').
should('be.visible').
and('have.text', 'Saved').
within(() => {
cy.get('a').as('savedLink').should('be.visible');
});
});
// * Check that the saved posts list is not open in RHS before clicking the link in the post pre-header
cy.get('#searchContainer').should('not.exist');
// # Click the link
cy.get('@savedLink').click();
// * Check that the saved posts list is open in RHS
cy.get('#searchContainer').should('be.visible').within(() => {
cy.get('.sidebar--right__title').
should('be.visible').
and('have.text', 'Saved messages');
// * Check that the post pre-header is not shown for the saved message in RHS
cy.get(`#searchResult_${postId}`).within(() => {
cy.get(`#rhsPostMessageText_${postId}`).contains(message);
cy.get('div.post-pre-header').should('not.exist');
});
});
// # Close the RHS
cy.get('#searchResultsCloseButton').should('be.visible').click();
}
export function verifyUnsavedPost(postId: string) {
// * Check that the center save icon has been updated correctly
cy.get(`#post_${postId}`).trigger('mouseover', {force: true});
cy.get(`#CENTER_flagIcon_${postId}`).
should('have.class', 'post-menu__item').
and('have.attr', 'aria-label', 'save message');
// # Open the post-dotmenu
cy.clickPostDotMenu(postId, 'CENTER');
// * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Save Message').scrollIntoView().should('be.visible');
cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => {
// * Check that the post is not highlighted
cy.get(`#post_${postId}`).should('not.have.class', 'post--pinned-or-flagged');
// * Check that the post pre-header is not visible
cy.get('div.post-pre-header').should('not.exist');
// * Check that the post pre-header does not have the saved icon
cy.get('span.icon--post-pre-header').
should('not.exist');
// * Check that the post pre-header does not have the saved post link
cy.get('div.post-pre-header__text-container').
should('not.exist');
});
// * Check that the saved posts list is not open in RHS before clicking the link in the post pre-header
cy.get('#searchContainer').should('not.exist');
// # Click the link
cy.uiGetSavedPostButton().click();
// * Check that the saved posts list is open in RHS
cy.get('#searchContainer').should('be.visible').within(() => {
cy.get('.sidebar--right__title').
should('be.visible').
and('have.text', 'Saved messages');
// * Check that the post pre-header is not shown for the saved message in RHS
cy.get('#search-items-container').within(() => {
cy.get(`#rhsPostMessageText_${postId}`).should('not.exist');
});
});
// # Close the RHS
cy.get('#searchResultsCloseButton').should('be.visible').click();
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Edit last post with a new message
*
* @param {string} - message
*
* @example
* cy.editLastPostWithNewMessage('new message');
*/
editLastPostWithNewMessage: typeof editLastPostWithNewMessage;
/**
* Get post profile image of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostProfileImage();
*/
uiGetPostProfileImage: typeof uiGetPostProfileImage;
/**
* Get post header of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostHeader();
*/
uiGetPostHeader: typeof uiGetPostHeader;
/**
* Get post body of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostBody();
*/
uiGetPostBody: typeof uiGetPostBody;
/**
* Get post thread footer of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostThreadFooter();
*/
uiGetPostThreadFooter: typeof uiGetPostThreadFooter;
/**
* Get post embed container of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostEmbedContainer();
*/
uiGetPostEmbedContainer: typeof uiGetPostEmbedContainer;
/**
* Get post textbox
*
* @param {bool} option.exist - Set to false to check whether element should not exist. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetPostTextBox();
*/
uiGetPostTextBox: typeof uiGetPostTextBox;
/**
* Get reply textbox
*
* @param {bool} option.exist - Set to false to check whether element should not exist. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetReplyTextBox();
*/
uiGetReplyTextBox: typeof uiGetReplyTextBox;
getPost: typeof getPost;
/**
* Get mm_blocks root within a center-channel post.
*
* @param {string} postId
*
* @example
* cy.getPostMmBlocks(postId).within(() => { ... });
*/
getPostMmBlocks: typeof getPostMmBlocks;
/**
* Get mm_blocks root within an RHS/thread post row.
*
* @param {string} postId
*
* @example
* cy.getRhsPostMmBlocks(postId).within(() => { ... });
*/
getRhsPostMmBlocks: typeof getRhsPostMmBlocks;
}
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiClickCopyLink`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Click on "Copy Link" of post dropdown menu and verifies if the link is copied into the clipboard
* Created user has an option to log in after all are setup.
* @param {string} permalink - permalink to verify if copied into the clipboard
*
* @example
* const permalink = 'http://localhost:8065/team-name/pl/post-id';
* cy.uiClickCopyLink(permalink);
*/
uiClickCopyLink(permalink: string, postId: string): Chainable;
/**
* Click dropdown menu of a post by post ID.
* @param {String} postId - post ID
* @param {String} menuItem - e.g. "Pin to channel"
* @param {String} location - 'CENTER' (default), 'SEARCH', RHS_ROOT, RHS_COMMENT
*/
uiClickPostDropdownMenu(postId: string, menuItem: string, location?: string): Chainable;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {stubClipboard} from '../../utils';
Cypress.Commands.add('uiClickCopyLink', (permalink, postId) => {
stubClipboard().as('clipboard');
// * Verify initial state
cy.get('@clipboard').its('contents').should('eq', '');
// # Click on "Copy Link"
cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').within(() => {
cy.findByText('Copy Link').scrollIntoView().should('be.visible').click();
// * Verify if it's called with correct link value
cy.get('@clipboard').its('wasCalled').should('eq', true);
cy.get('@clipboard').its('contents').should('eq', permalink);
});
});
Cypress.Commands.add('uiClickPostDropdownMenu', (postId, menuItem, location = 'CENTER') => {
cy.clickPostDotMenu(postId, location);
cy.findAllByTestId(`unread_post_${postId}`).eq(0).should('be.visible');
cy.findByText(menuItem).scrollIntoView().should('be.visible').click({force: true});
});
Cypress.Commands.add('uiPostDropdownMenuShortcut', (postId, menuText, shortcutKey, location = 'CENTER') => {
cy.clickPostDotMenu(postId, location);
cy.findByText(menuText).scrollIntoView().should('be.visible');
cy.get('body').type(shortcutKey);
});

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '@/types';
function uiSearchPosts(searchTerm: string) {
// # Enter the search terms and hit enter to start the search
cy.get('#searchFormContainer').should('be.visible').click();
cy.get('#searchBox').find('input').clear().type(searchTerm).type('{enter}');
// * Wait for the RHS to open and the search results to appear
cy.contains('.sidebar--right__header', 'Search Results').should('be.visible');
cy.get('#searchContainer .LoadingSpinner').should('not.exist');
}
Cypress.Commands.add('uiSearchPosts', uiSearchPosts);
function uiJumpToSearchResult(postId: string) {
// # Find the post in the search results and click Jump
cy.get(`#searchResult_${postId}`).contains('a', 'Jump').click();
// * Verify the URL changes to the permalink URL
cy.url().should((url) => url.endsWith(`/${postId}`));
// * Verify that the permalinked post is highlighted in the center channel
cy.get(`#post_${postId}.post--highlight`).should('be.visible');
}
Cypress.Commands.add('uiJumpToSearchResult', uiJumpToSearchResult);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
uiSearchPosts(searchTerm: string): ChainableT<void>;
uiJumpToSearchResult(postId: string): ChainableT<void>;
}
}
}

View File

@@ -0,0 +1,273 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '../../types';
Cypress.Commands.add('uiGetLHS', () => {
return cy.get('#SidebarContainer').should('be.visible');
});
Cypress.Commands.add('uiGetLHSHeader', () => {
return cy.uiGetLHS().
find('#sidebarTeamMenuButton').
should('be.visible');
});
Cypress.Commands.add('uiOpenTeamMenu', (item = '') => {
// # Click on LHS header
cy.uiGetLHSHeader().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetLHSTeamMenu();
}
// # Click on a particular item
return cy.uiGetLHSTeamMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
});
Cypress.Commands.add('uiGetLHSAddChannelButton', () => {
return cy.uiGetLHS().findByRole('button', {name: 'Browse or create channels'});
});
Cypress.Commands.add('uiGetLHSTeamMenu', () => {
return cy.get('#sidebarTeamMenu');
});
function uiOpenSystemConsoleMenu(item = ''): ChainableT<JQuery> {
// # Click on LHS header button
cy.uiGetSystemConsoleButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetSystemConsoleMenu();
}
// # Click on a particular item
return cy.uiGetSystemConsoleMenu().
findByText(item).
scrollIntoView().
should('be.visible').
click();
}
Cypress.Commands.add('uiOpenSystemConsoleMenu', uiOpenSystemConsoleMenu);
function uiGetSystemConsoleButton(): ChainableT<JQuery> {
return cy.get('.admin-sidebar').
findByRole('button', {name: 'Menu Icon'});
}
Cypress.Commands.add('uiGetSystemConsoleButton', uiGetSystemConsoleButton);
function uiGetSystemConsoleMenu(): ChainableT<JQuery> {
return cy.get('.admin-sidebar').
find('.dropdown-menu').
should('be.visible');
}
Cypress.Commands.add('uiGetSystemConsoleMenu', uiGetSystemConsoleMenu);
Cypress.Commands.add('uiGetLhsSection', (section) => {
if (section === 'UNREADS') {
return cy.findByText(section).
parent().
parent().
parent();
}
return cy.findAllByRole('button', {name: section}).
first().
parent().
parent().
parent();
});
Cypress.Commands.add('uiBrowseOrCreateChannel', (menuitem) => {
cy.uiGetLHSAddChannelButton().should('be.visible').click();
cy.get('#browserOrAddChannelMenu').
should('exist').and('be.visible').
within(() => {
if (menuitem) {
cy.findByText(menuitem).should('exist').click();
}
});
});
Cypress.Commands.add('uiAddDirectMessage', () => {
return cy.findByRole('button', {name: 'Write a direct message'});
});
Cypress.Commands.add('uiGetFindChannels', () => {
return cy.get('#lhsNavigator').findByRole('button', {name: 'Find Channels'});
});
Cypress.Commands.add('uiOpenFindChannels', () => {
cy.uiGetFindChannels().click();
});
function uiGetSidebarThreadsButton(): ChainableT<JQuery> {
return cy.get('#sidebar-threads-button').should('be.visible');
}
Cypress.Commands.add('uiGetSidebarThreadsButton', uiGetSidebarThreadsButton);
Cypress.Commands.add('uiGetChannelSidebarMenu', (channelName, isChannelId = false) => {
cy.uiGetLHS().within(() => {
if (isChannelId) {
cy.get(`#sidebarItem_${channelName}`).should('be.visible').find('button').should('exist').click({force: true});
} else {
cy.findByText(channelName).should('be.visible').parents('a').find('button').should('exist').click({force: true});
}
});
return cy.findByRole('menu', {name: 'Edit channel menu'}).should('be.visible');
});
Cypress.Commands.add('uiClickSidebarItem', (name) => {
cy.uiGetSidebarItem(name).click({force: true});
if (name === 'threads') {
cy.get('body').then((body) => {
if (body.find('#genericModalLabel').length > 0) {
cy.uiCloseModal('A new way to view and follow threads');
}
});
cy.get('#tutorial-threads-mobile-header span.Button_label').contains('Followed threads');
} else {
cy.findAllByTestId('postView').last().scrollIntoView().should('be.visible');
}
});
Cypress.Commands.add('uiGetSidebarItem', (channelName) => {
return cy.get(`#sidebarItem_${channelName}`);
});
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Get LHS
*
* @example
* cy.uiGetLHS();
*/
uiGetLHS(): Chainable;
/**
* Get LHS header
*
* @example
* cy.uiGetLHSHeader().click();
*/
uiGetLHSHeader(): Chainable;
/**
* Open team menu
*
* @param {string} item - ex. 'Invite People', 'Team Settings', etc.
*
* @example
* cy.uiOpenTeamMenu();
*/
uiOpenTeamMenu(item?: string): Chainable;
/**
* Get LHS add channel button
*
* @example
* cy.uiGetLHSAddChannelButton().click();
*/
uiGetLHSAddChannelButton(): Chainable;
/**
* Get LHS team menu
*
* @example
* cy.uiGetLHSTeamMenu().should('not.exist);
*/
uiGetLHSTeamMenu(): Chainable;
/**
* Get LHS section
* @param {string} section - section such as UNREADS, CHANNELS, FAVORITES, DIRECT MESSAGES and other custom category
*
* @example
* cy.uiGetLhsSection('CHANNELS');
*/
uiGetLhsSection(section: string): Chainable;
/**
* Open menu to browse or create channel
* @param {string} item - dropdown menu. If set, it will do click action.
*
* @example
* cy.uiBrowseOrCreateChannel('Browse channels');
*/
uiBrowseOrCreateChannel(item: string): Chainable;
/**
* Get "+" button to write a direct message
* @example
* cy.uiAddDirectMessage();
*/
uiAddDirectMessage(): Chainable;
/**
* Get find channels button
* @example
* cy.uiGetFindChannels();
*/
uiGetFindChannels(): Chainable;
/**
* Open find channels
* @example
* cy.uiOpenFindChannels();
*/
uiOpenFindChannels(): Chainable;
/**
* Open menu of a channel in the sidebar
* @param {string} channelName - name of channel, ex. 'town-square'
* @param {boolean} isChannelId - default false. If true, it will use channel id instead of channel name
* @example
* cy.uiGetChannelSidebarMenu('Town Square');
* cy.uiGetChannelSidebarMenu('user1212__user333', true);
*/
uiGetChannelSidebarMenu(channelName: string, isChannelId?: boolean): Chainable;
/**
* Click sidebar item by channel or thread name
* @param {string} name - channel name for channels, and threads for Global Threads
*
* @example
* cy.uiClickSidebarItem('town-square');
*/
uiClickSidebarItem(name: string): Chainable;
/**
* Get sidebar item by channel or thread name
* @param {string} name - channel name for channels, and threads for Global Threads
*
* @example
* cy.uiGetSidebarItem('town-square').find('.badge').should('be.visible');
*/
uiGetSidebarItem(name: string): Chainable;
uiOpenSystemConsoleMenu: typeof uiOpenSystemConsoleMenu;
uiGetSystemConsoleButton: typeof uiGetSystemConsoleButton;
uiGetSystemConsoleMenu: typeof uiGetSystemConsoleMenu;
uiGetSidebarThreadsButton: typeof uiGetSidebarThreadsButton;
}
}
}

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiGetRHS`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get RHS container
*
* @param {bool} option.visible - Set to false to check whether RHS is not visible. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetRHS();
*/
uiGetRHS(option?: Record<string, boolean>): Chainable;
/**
* Close RHS
*
* @example
* cy.uiCloseRHS();
*/
uiCloseRHS(): Chainable;
/**
* Expand RHS
*
* @example
* cy.uiExpandRHS();
*/
uiExpandRHS(): Chainable;
/**
* Verify if RHS is expanded
*
* @example
* cy.uiGetRHS().isExpanded();
*/
isExpanded(): Chainable;
/**
* Get "Reply" button
*
* @example
* cy.uiGetReply();
*/
uiGetReply(): Chainable;
/**
* Reply by clicking "Reply" button
*
* @example
* cy.uiReply();
*/
uiReply(): Chainable;
/**
* Get RHS container
*
* @param {bool} option.visible - Set to false to check whether Search container at RHS is not visible. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetRHSSearchContainer();
*/
uiGetRHSSearchContainer(option: Record<string, boolean>): Chainable;
/**
* Get file filter button from RHS.
*
* @example
* cy.uiGetFileFilterButton().click();
*/
uiGetFileFilterButton(): Chainable;
/**
* Get file filter menu from RHS
*
* @param {bool} option.exist - Set to false to check whether file filter menu should not exist at RHS. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetFileFilterMenu();
*/
uiGetFileFilterMenu(): Chainable;
/**
* Open file filter menu from RHS
* @param {string} item - such as `'Documents'`, `'Spreadsheets'`, `'Presentations'`, `'Code'`, `'Images'`, `'Audio'` and `'Videos'`.
* @return the file filter menu
*
* @example
* cy.uiOpenFileFilterMenu();
*/
uiOpenFileFilterMenu(): Chainable;
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiGetRHS', (options = {visible: true}) => {
if (options.visible) {
return cy.get('#sidebar-right').should('be.visible');
}
return cy.get('#sidebar-right').should('not.be.exist');
});
Cypress.Commands.add('uiCloseRHS', () => {
cy.document().then((doc) => {
const closeButton = doc.querySelector('[aria-label="Close Sidebar Icon"]');
if (closeButton) {
closeButton.click();
}
});
});
Cypress.Commands.add('uiExpandRHS', () => {
cy.findByLabelText('Expand Sidebar Icon').click();
});
Cypress.Commands.add('isExpanded', {prevSubject: true}, (subject) => {
return cy.get(subject).should('have.class', 'sidebar--right--expanded');
});
Cypress.Commands.add('uiGetReply', () => {
return cy.get('#sidebar-right').findByTestId('SendMessageButton');
});
Cypress.Commands.add('uiReply', () => {
cy.uiGetReply().click();
});
// Sidebar search container
Cypress.Commands.add('uiGetRHSSearchContainer', (options = {visible: true}) => {
if (options.visible) {
return cy.get('#searchContainer').should('be.visible');
}
return cy.get('#searchContainer').should('not.exist');
});
// Sidebar files search
Cypress.Commands.add('uiGetFileFilterButton', () => {
return cy.get('.FilesFilterMenu').should('be.visible');
});
Cypress.Commands.add('uiGetFileFilterMenu', (option = {exist: true}) => {
if (option.exist) {
return cy.get('.FilesFilterMenu').
find('.dropdown-menu').
should('be.visible');
}
return cy.get('.FilesFilterMenu').
find('.dropdown-menu').
should('not.exist');
});
Cypress.Commands.add('uiOpenFileFilterMenu', (item = '') => {
// # Click on file filter button
cy.uiGetFileFilterButton().click();
if (!item) {
// # Return the menu if no item is passed
return cy.uiGetFileFilterMenu();
}
// # Click on a particular item
return cy.uiGetFileFilterMenu().
findByText(item).
should('be.visible').
click();
});

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {SimpleUser} from '@/integration/channels/autocomplete/helpers';
import type {ChainableT} from '@/types';
/**
* Verify user's at-mention in the suggestion list
* @param {SimpleUser} user - user object
* @param {boolean} isSelected - check if user is selected with false as default
* @param {string} sectionDividerName - name of the section in suggestion list, ex. "Channel Members"
*
* @example
* cy.uiVerifyAtMentionInSuggestionList(user, true, 'Channel Members');
*/
function uiVerifyAtMentionInSuggestionList(user: SimpleUser, isSelected = false, sectionDividerName: string | null = null): ChainableT {
// * Verify that the suggestion list is open and visible
return cy.get('#suggestionList').should('be.visible').within(() => {
if (sectionDividerName) {
// * Verify the section name is as expected
cy.get('.suggestion-list__divider').findByText(sectionDividerName).should('be.visible');
cy.get('.suggestion-list__divider').next().findByTestId(`mentionSuggestion_${user.username}`).should('be.visible');
}
// * Verify that the user is selected
return cy.uiVerifyAtMentionSuggestion(user, isSelected);
});
}
Cypress.Commands.add('uiVerifyAtMentionInSuggestionList', uiVerifyAtMentionInSuggestionList);
/**
* Verify user's at-mention suggestion
* @param {SimpleUser} user - user object
* @param {boolean} isSelected - check if user is selected with false as default
*
* @example
* cy.uiVerifyAtMentionSuggestion(user, true);
*/
function uiVerifyAtMentionSuggestion(user: SimpleUser, isSelected = false): ChainableT {
const {
username,
first_name: firstName,
last_name: lastName,
nickname,
} = user;
// * Verify that the user is selected
cy.findByTestId(`mentionSuggestion_${username}`).as('selectedMentionSuggestion').should('be.visible');
if (isSelected) {
cy.get('@selectedMentionSuggestion').should('have.class', 'suggestion--selected');
}
cy.get('@selectedMentionSuggestion').findByText(`@${username}`).should('be.visible');
cy.get('@selectedMentionSuggestion').findByText(`${firstName} ${lastName} (${nickname})`).should('be.visible');
return cy.findByTestId(`mentionSuggestion_${username}`);
}
Cypress.Commands.add('uiVerifyAtMentionSuggestion', uiVerifyAtMentionSuggestion);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
uiVerifyAtMentionInSuggestionList: typeof uiVerifyAtMentionInSuggestionList;
uiVerifyAtMentionSuggestion: typeof uiVerifyAtMentionSuggestion;
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiCheckLicenseExists`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Verify license exists via admin console.
*
* @example
* cy.uiCheckLicenseExists();
*/
uiCheckLicenseExists(): Chainable;
/**
* Reset system scheme permissions via System Console
*
* @example
* cy.uiResetPermissionsToDefault();
*/
uiResetPermissionsToDefault(): Chainable;
/**
* Save settings located in System Console
*
* @example
* cy.uiSaveConfig();
*/
uiSaveConfig(): Chainable;
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
Cypress.Commands.add('uiCheckLicenseExists', () => {
// # Go to system admin then verify admin console URL, header, and content
cy.visit('/admin_console/about/license');
cy.url().should('include', '/admin_console/about/license');
cy.get('.admin-console', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible').within(() => {
cy.get('.admin-console__header').should('be.visible').and('have.text', 'Edition and License');
cy.get('.admin-console__content').should('be.visible').and('not.contain', 'undefined').and('not.contain', 'Invalid');
cy.get('#remove-button').should('be.visible');
});
});
Cypress.Commands.add('uiResetPermissionsToDefault', () => {
// # Navigate to system scheme page
cy.visit('/admin_console/user_management/permissions/system_scheme');
// # Click reset to defaults and confirm
cy.findByTestId('resetPermissionsToDefault', {timeout: TIMEOUTS.HALF_MIN}).click();
cy.get('#confirmModalButton').click();
cy.uiSaveConfig();
});
Cypress.Commands.add('uiSaveConfig', ({confirm = true} = {}) => {
// # Save settings
cy.get('#saveSetting').should('be.enabled').click();
cy.wait(TIMEOUTS.HALF_SEC);
if (confirm) {
// # Wait until the UI shows the saving is done and revert the text to "Save"
cy.waitUntil(() => cy.get('#saveSetting').then((el) => {
return el[0].innerText === 'Save';
}));
} else {
cy.wait(TIMEOUTS.HALF_SEC);
}
});

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* Invites a member to the current team
* @param {string} username - the username
*/
function uiInviteMemberToCurrentTeam(username: string) {
// # Open member invite screen
cy.uiOpenTeamMenu('Invite people');
// # Open members section if licensed for guest accounts
cy.findByTestId('invitationModal').
then((container) => container.find('[data-testid="inviteMembersLink"]')).
then((link) => link?.click());
// # Enter bot username and submit
cy.get('.users-emails-input__control input').typeWithForce(username).as('input');
cy.get('.users-emails-input__option ').contains(`@${username}`);
cy.get('@input').typeWithForce('{enter}');
cy.findByTestId('inviteButton').click();
// * Verify user invited to team
cy.get('.invitation-modal-confirm--sent .InviteResultRow').
should('contain.text', `@${username}`).
and('contain.text', 'This member has been added to the team.');
// # Close, return
cy.findByTestId('confirm-done').click();
}
Cypress.Commands.add('uiInviteMemberToCurrentTeam', uiInviteMemberToCurrentTeam);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
uiInviteMemberToCurrentTeam: typeof uiInviteMemberToCurrentTeam;
}
}
}
export {};

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `ui` prefix, e.g. `uiGetToolTip`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get tooltip
*
* @param {string} text of the tooltip
*
* @example
* cy.uiGetToolTip('text');
*/
uiGetToolTip(text: string): Chainable;
}
}

View File

@@ -0,0 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('uiGetToolTip', (text) => {
cy.findByRole('tooltip').should('exist').and('contain', text);
});