mattermost/e2e-tests/cypress/tests/support/task_commands.ts
sabril 8bf422d6e2
E2E/Cypress: Upgrade dependencies (#33665)
* upgrade cypress and other dependecies

* fix eslint

* remove axios-retry and update eslint

* fix tests

* fix lint on trailing spaces

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2025-09-01 14:14:13 +08:00

299 lines
11 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {type AxiosResponse} from 'axios';
import {ChainableT} from '../types';
/**
* postMessageAs is a task which is wrapped as command with post-verification
* that a message is successfully posted by the user/sender
* @param {Object} sender - a user object who will post a message
* @param {String} message - message in a post
* @param {Object} channelId - where a post will be posted
*/
export interface PostMessageResp {
id: string;
status: number;
data: any;
}
interface PostMessageArg {
sender: {
username: string;
password: string;
};
message: string;
channelId: string;
rootId?: string;
createAt?: number;
}
interface PostMessageRequest {
token: string;
message: string;
props?;
channelId: string;
rootId?;
createAt?;
failOnStatus?: boolean;
}
function postMessageAs(arg: PostMessageArg): ChainableT<PostMessageResp> {
const {sender, message, channelId, rootId, createAt} = arg;
const baseUrl = Cypress.config('baseUrl');
return cy.task('postMessageAs', {sender, message, channelId, rootId, createAt, baseUrl}).then((response: AxiosResponse<{id: string}>) => {
const {status, data} = response;
expect(status).to.equal(201);
// # Return the data so it can be interacted in a test
return cy.wrap({id: data.id, status, data});
});
}
Cypress.Commands.add('postMessageAs', postMessageAs);
/**
* @param {string} [numberOfMessages = 30] - Number of messages
* @param {Object} sender - a user object who will post a message
* @param {String} message - message in a post
* @param {Object} channelId - where a post will be posted
*/
function postListOfMessages({numberOfMessages = 30, ...rest}): ChainableT<any> {
const baseUrl = Cypress.config('baseUrl');
return (cy as any).
task('postListOfMessages', {numberOfMessages, baseUrl, ...rest}, {timeout: numberOfMessages * 200}).
each((message) => expect(message.status).to.equal(201));
}
Cypress.Commands.add('postListOfMessages', postListOfMessages);
/**
* reactToMessageAs is a task wrapped as command with post-verification
* that a reaction is added successfully to a message by a user/sender
* @param {Object} sender - a user object who will post a message
* @param {String} postId - post on which reaction is intended
* @param {String} reaction - emoji text eg. smile
*/
Cypress.Commands.add('reactToMessageAs', ({sender, postId, reaction}) => {
const baseUrl = Cypress.config('baseUrl');
return cy.task('reactToMessageAs', {sender, postId, reaction, baseUrl}).then(({status, data}) => {
expect(status).to.equal(200);
// # Return the response after reaction is added
return cy.wrap({status, data});
});
});
/**
* postIncomingWebhook is a task which is wrapped as command with post-verification
* that the incoming webhook is successfully posted
* @param {String} url - incoming webhook URL
* @param {Object} data - payload on incoming webhook
*/
function postIncomingWebhook({url, data, waitFor}: {
url: string;
data: Record<string, any>;
waitFor?: string;
}): ChainableT {
cy.task('postIncomingWebhook', {url, data}).its('status').should('be.equal', 200);
if (!waitFor) {
return;
}
cy.waitUntil(() => cy.getLastPost().then((el) => {
switch (waitFor) {
case 'text': {
const textEl = el.find('.post-message__text > p')[0];
return Boolean(textEl && textEl.textContent.includes(data.text));
}
case 'attachment-pretext': {
const attachmentPretextEl = el.find('.attachment__thumb-pretext > p')[0];
return Boolean(attachmentPretextEl && attachmentPretextEl.textContent.includes(data.attachments[0].pretext));
}
default:
return false;
}
}));
}
Cypress.Commands.add('postIncomingWebhook', postIncomingWebhook);
interface ExternalRequestArg<T> {
user: Record<string, unknown>;
method: string;
path: string;
data?: T;
failOnStatusCode?: boolean;
}
function externalRequest<T=any, U=any>(arg: ExternalRequestArg<U>): ChainableT<Pick<AxiosResponse<T>, 'data' | 'status'>> {
const {user, method, path, data, failOnStatusCode = true} = arg;
const baseUrl = Cypress.config('baseUrl');
return cy.task('externalRequest', {baseUrl, user, method, path, data}).then((response: Pick<AxiosResponse<T & {id: string}>, 'data' | 'status'>) => {
// Temporarily ignore error related to Cloud
const cloudErrorId = [
'ent.cloud.request_error',
'api.cloud.get_subscription.error',
];
if (response.data && !cloudErrorId.includes(response.data.id) && failOnStatusCode) {
expect(response.status).to.be.oneOf([200, 201, 204]);
}
return cy.wrap(response);
});
}
Cypress.Commands.add('externalRequest', externalRequest);
/**
* postMessageAs is a task which is wrapped as command with post-verification
* that a message is successfully posted by the bot
* @param {String} message - message in a post
* @param {Object} channelId - where a post will be posted
*/
function postBotMessage({token, message, props, channelId, rootId, createAt, failOnStatus = true}: PostMessageRequest): ChainableT<PostMessageResp> {
const baseUrl = Cypress.config('baseUrl');
return cy.task('postBotMessage', {token, message, props, channelId, rootId, createAt, baseUrl}).then(({status, data}) => {
if (failOnStatus) {
expect(status).to.equal(201);
}
// # Return the data so it can be interacted in a test
return cy.wrap({id: data.id, status, data});
});
}
Cypress.Commands.add('postBotMessage', postBotMessage);
/**
* urlHealthCheck is a task wrapped as command that checks whether
* a URL is healthy and reachable.
* @param {String} name - name of service to check
* @param {String} url - URL to check
* @param {String} helperMessage - a message to display on error to help resolve the issue
* @param {String} method - a request using a specific method
* @param {String} httpStatus - expected HTTP status
*/
function urlHealthCheck({name, url, helperMessage, method, httpStatus}): ChainableT {
Cypress.log({name, message: `Checking URL health at ${url}`});
return cy.task('urlHealthCheck', {url, method}).then(({data, errorCode, status, success}) => {
const urlService = `__${name}__ at ${url}`;
const successMessage = success ?
`${urlService}: reachable` :
`${errorCode}: The test you're running requires ${urlService} to be reachable. \n${helperMessage}`;
expect(success, successMessage).to.equal(true);
const statusMessage = status === httpStatus ?
`${urlService}: responded with ${status} HTTP status` :
`${urlService}: expected to respond with ${httpStatus} but got ${status} HTTP status`;
expect(status, statusMessage).to.equal(httpStatus);
return cy.wrap({data, status});
});
}
Cypress.Commands.add('urlHealthCheck', urlHealthCheck);
Cypress.Commands.add('requireWebhookServer', () => {
const baseUrl = Cypress.config('baseUrl');
const webhookBaseUrl = Cypress.env('webhookBaseUrl');
const adminUsername = Cypress.env('adminUsername');
const adminPassword = Cypress.env('adminPassword');
const helperMessage = `
__Tips:__
1. In local development, you may run "__npm run start:webhook__" at "/e2e" folder.
2. If reachable from remote host, you may export it as env variable, like "__CYPRESS_webhookBaseUrl=[url] npm run cypress:open__".
`;
cy.urlHealthCheck({
name: 'Webhook Server',
url: webhookBaseUrl,
helperMessage,
method: 'get',
httpStatus: 200,
});
cy.task('postIncomingWebhook', {
url: `${webhookBaseUrl}/setup`,
data: {
baseUrl,
webhookBaseUrl,
adminUsername,
adminPassword,
}}).
its('status').should('be.equal', 201);
});
Cypress.Commands.overwrite('log', (subject, message) => cy.task('log', message));
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* externalRequest is a task which is wrapped as command with post-verification
* that the external request is successfully completed
* @param {Object} options
* @param {<UserProfile, 'username' | 'password'>} options.user - a user initiating external request
* @param {String} options.method - an HTTP method (e.g. get, post, etc)
* @param {String} options.path - API path that is relative to Cypress.config().baseUrl
* @param {Object} options.data - payload
* @param {Boolean} options.failOnStatusCode - whether to fail on status code, default is true
*
* @example
* cy.externalRequest({user: sysadmin, method: 'POST', path: 'config', data});
*/
externalRequest(options?: {
user: Pick<UserProfile, 'username' | 'password'>;
method: string;
path: string;
data?: Record<string, any>;
failOnStatusCode?: boolean;
}): Chainable<any>;
/**
* Adds a given reaction to a specific post from a user
* @param {Object} reactToMessageObject - Information on person and post to which a reaction needs to be added
* @param {Object} reactToMessageObject.sender - a user object who will post a message
* @param {string} reactToMessageObject.postId - post on which reaction is intended
* @param {string} reactToMessageObject.reaction - emoji text eg. smile
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.reactToMessageAs({sender:user2, postId:"ABC123", reaction: 'smile'});
*/
reactToMessageAs({sender, postId, reaction}: {sender: Record<string, unknown>; postId: string; reaction: string}): Chainable<any>;
/**
* Verify that the webhook server is accessible, and then sets up base URLs and credential.
*
* @example
* cy.requireWebhookServer();
*/
requireWebhookServer(): Chainable;
postMessageAs: typeof postMessageAs;
postListOfMessages: typeof postListOfMessages;
postIncomingWebhook: typeof postIncomingWebhook;
postBotMessage: typeof postBotMessage;
urlHealthCheck: typeof urlHealthCheck;
}
}
}