snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
12
e2e-tests/cypress/utils/constants.js
Normal file
12
e2e-tests/cypress/utils/constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const RESULTS_DIR = 'results';
|
||||
const MOCHAWESOME_REPORT_DIR = 'results/mochawesome-report';
|
||||
const AD_CYCLE_FILE = 'results/ad_cycle.json';
|
||||
|
||||
module.exports = {
|
||||
MOCHAWESOME_REPORT_DIR,
|
||||
RESULTS_DIR,
|
||||
AD_CYCLE_FILE,
|
||||
};
|
||||
159
e2e-tests/cypress/utils/dashboard.js
Normal file
159
e2e-tests/cypress/utils/dashboard.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/*
|
||||
* Environment:
|
||||
* AUTOMATION_DASHBOARD_URL=[url]
|
||||
* AUTOMATION_DASHBOARD_TOKEN=[token]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const readFile = require('util').promisify(fs.readFile);
|
||||
|
||||
const axios = require('axios');
|
||||
const chalk = require('chalk').default;
|
||||
const mime = require('mime-types');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const timeout = 60 * 1000;
|
||||
|
||||
const {
|
||||
AUTOMATION_DASHBOARD_URL,
|
||||
AUTOMATION_DASHBOARD_TOKEN,
|
||||
} = process.env;
|
||||
|
||||
const connectionErrors = ['ECONNABORTED', 'ECONNREFUSED'];
|
||||
|
||||
async function createAndStartCycle(data) {
|
||||
const response = await axios({
|
||||
url: `${AUTOMATION_DASHBOARD_URL}/cycles/start`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTOMATION_DASHBOARD_TOKEN}`,
|
||||
},
|
||||
method: 'post',
|
||||
timeout,
|
||||
data,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getSpecToTest({repo, branch, build, server}) {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${AUTOMATION_DASHBOARD_URL}/executions/specs/start?repo=${repo}&branch=${branch}&build=${build}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTOMATION_DASHBOARD_TOKEN}`,
|
||||
},
|
||||
method: 'post',
|
||||
timeout,
|
||||
data: {server},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(chalk.red('Failed to get spec to test'));
|
||||
if (connectionErrors.includes(err.code) || !err.response) {
|
||||
console.log(chalk.red(`Error code: ${err.code}`));
|
||||
return {code: err.code};
|
||||
}
|
||||
|
||||
return err.response && err.response.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function recordSpecResult(specId, spec, tests) {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${AUTOMATION_DASHBOARD_URL}/executions/specs/end?id=${specId}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTOMATION_DASHBOARD_TOKEN}`,
|
||||
},
|
||||
method: 'post',
|
||||
timeout,
|
||||
data: {spec, tests},
|
||||
});
|
||||
|
||||
console.log(chalk.green('Successfully recorded!'));
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(chalk.red('Failed to record spec result'));
|
||||
if (connectionErrors.includes(err.code) || !err.response) {
|
||||
console.log(chalk.red(`Error code: ${err.code}`));
|
||||
return {code: err.code};
|
||||
}
|
||||
|
||||
return err.response && err.response.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCycle(id, cyclePatch) {
|
||||
try {
|
||||
const response = await axios({
|
||||
url: `${AUTOMATION_DASHBOARD_URL}/cycles/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTOMATION_DASHBOARD_TOKEN}`,
|
||||
},
|
||||
method: 'put',
|
||||
timeout,
|
||||
data: cyclePatch,
|
||||
});
|
||||
|
||||
console.log(chalk.green('Successfully updated the cycle with test environment data!'));
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(chalk.red('Failed to update cycle'));
|
||||
if (connectionErrors.includes(err.code) || !err.response) {
|
||||
console.log(chalk.red(`Error code: ${err.code}`));
|
||||
return {code: err.code};
|
||||
}
|
||||
|
||||
return err.response && err.response.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadScreenshot(filePath, repo, branch, build) {
|
||||
try {
|
||||
const contentType = mime.lookup(filePath);
|
||||
const extension = mime.extension(contentType);
|
||||
|
||||
const {data} = await axios({
|
||||
url: `${AUTOMATION_DASHBOARD_URL}/upload-request`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${AUTOMATION_DASHBOARD_TOKEN}`,
|
||||
},
|
||||
method: 'get',
|
||||
timeout,
|
||||
data: {repo, branch, build, extension},
|
||||
});
|
||||
|
||||
const file = await readFile(filePath);
|
||||
|
||||
await axios({
|
||||
url: data.upload_url,
|
||||
method: 'put',
|
||||
headers: {'Content-Type': contentType},
|
||||
data: file,
|
||||
});
|
||||
|
||||
return data.object_url;
|
||||
} catch (err) {
|
||||
if (connectionErrors.includes(err.code) || !err.response) {
|
||||
console.log(chalk.red(`Error code: ${err.code}`));
|
||||
return {code: err.code};
|
||||
}
|
||||
|
||||
return {error: 'Failed to upload a screenshot.'};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createAndStartCycle,
|
||||
getSpecToTest,
|
||||
recordSpecResult,
|
||||
updateCycle,
|
||||
uploadScreenshot,
|
||||
};
|
||||
48
e2e-tests/cypress/utils/even_distribution.js
Normal file
48
e2e-tests/cypress/utils/even_distribution.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
function* distributeItems(total, divider) {
|
||||
if (divider === 0) {
|
||||
yield 0;
|
||||
} else {
|
||||
let rest = total % divider;
|
||||
const result = total / divider;
|
||||
|
||||
for (let i = 0; i < divider; i++) {
|
||||
if (rest-- > 0) {
|
||||
yield Math.ceil(result);
|
||||
} else {
|
||||
yield Math.floor(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTestFilesIdentifier(numberOfTestFiles, part, of) {
|
||||
const PART = parseInt(part, 10) || 1;
|
||||
const OF = parseInt(of, 10) || 1;
|
||||
if (PART > OF) {
|
||||
throw new Error(`"--part=${PART}" should not be greater than "--of=${OF}"`);
|
||||
}
|
||||
|
||||
const distributions = [];
|
||||
for (const member of distributeItems(numberOfTestFiles, OF)) {
|
||||
distributions.push(member);
|
||||
}
|
||||
|
||||
const indexedPart = (PART - 1);
|
||||
|
||||
let start = 0;
|
||||
for (let i = 0; i < indexedPart; i++) {
|
||||
start += distributions[i];
|
||||
}
|
||||
|
||||
const end = distributions[indexedPart] + start;
|
||||
const count = distributions[indexedPart];
|
||||
|
||||
return {start, end, count};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTestFilesIdentifier,
|
||||
};
|
||||
37
e2e-tests/cypress/utils/even_distribution.test.js
Normal file
37
e2e-tests/cypress/utils/even_distribution.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTestFilesIdentifier} from './even_distribution';
|
||||
|
||||
describe('getTestFilesIdentifier', () => {
|
||||
it('should return expected output', () => {
|
||||
const testCases = [
|
||||
{numberOfTestFiles: 5, part: 1, of: 4, outStart: 0, outEnd: 2, outCount: 2},
|
||||
{numberOfTestFiles: 5, part: 2, of: 4, outStart: 2, outEnd: 3, outCount: 1},
|
||||
{numberOfTestFiles: 5, part: 3, of: 4, outStart: 3, outEnd: 4, outCount: 1},
|
||||
{numberOfTestFiles: 5, part: 4, of: 4, outStart: 4, outEnd: 5, outCount: 1},
|
||||
|
||||
{numberOfTestFiles: 10, part: 1, of: 4, outStart: 0, outEnd: 3, outCount: 3},
|
||||
{numberOfTestFiles: 10, part: 2, of: 4, outStart: 3, outEnd: 6, outCount: 3},
|
||||
{numberOfTestFiles: 10, part: 3, of: 4, outStart: 6, outEnd: 8, outCount: 2},
|
||||
{numberOfTestFiles: 10, part: 4, of: 4, outStart: 8, outEnd: 10, outCount: 2},
|
||||
|
||||
{numberOfTestFiles: 410, part: 1, of: 8, outStart: 0, outEnd: 52, outCount: 52},
|
||||
{numberOfTestFiles: 410, part: 2, of: 8, outStart: 52, outEnd: 104, outCount: 52},
|
||||
{numberOfTestFiles: 410, part: 3, of: 8, outStart: 104, outEnd: 155, outCount: 51},
|
||||
{numberOfTestFiles: 410, part: 4, of: 8, outStart: 155, outEnd: 206, outCount: 51},
|
||||
{numberOfTestFiles: 410, part: 5, of: 8, outStart: 206, outEnd: 257, outCount: 51},
|
||||
{numberOfTestFiles: 410, part: 6, of: 8, outStart: 257, outEnd: 308, outCount: 51},
|
||||
{numberOfTestFiles: 410, part: 7, of: 8, outStart: 308, outEnd: 359, outCount: 51},
|
||||
{numberOfTestFiles: 410, part: 8, of: 8, outStart: 359, outEnd: 410, outCount: 51},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const actual = getTestFilesIdentifier(testCase.numberOfTestFiles, testCase.part, testCase.of);
|
||||
|
||||
expect(testCase.outStart).toEqual(actual.start);
|
||||
expect(testCase.outEnd).toEqual(actual.end);
|
||||
expect(testCase.outCount).toEqual(actual.count);
|
||||
});
|
||||
});
|
||||
});
|
||||
259
e2e-tests/cypress/utils/file.js
Normal file
259
e2e-tests/cypress/utils/file.js
Normal file
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
const intersection = require('lodash.intersection');
|
||||
const without = require('lodash.without');
|
||||
const shell = require('shelljs');
|
||||
const argv = require('yargs')(process.argv.slice(2)).
|
||||
default('includeFile', '').
|
||||
default('excludeFile', '').
|
||||
argv;
|
||||
|
||||
const TEST_DIR = 'tests';
|
||||
|
||||
const grepCommand = (word = '') => {
|
||||
// -r, recursive search on subdirectories
|
||||
// -I, ignore binary
|
||||
// -l, only names of files to stdout/return
|
||||
// -w, expression is searched for as a word
|
||||
return `grep -rIlw '${word}' ${TEST_DIR}`;
|
||||
};
|
||||
|
||||
const grepFiles = (command) => {
|
||||
return shell.exec(command, {silent: true}).stdout.
|
||||
split('\n').
|
||||
filter((f) => f.includes('spec.js') || f.includes('spec.ts'));
|
||||
};
|
||||
|
||||
const findFiles = (pattern) => {
|
||||
function diveOnFiles(dirPath, filesArr) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
let arrayOfFiles = filesArr || [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = `${dirPath}/${file}`;
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
arrayOfFiles = diveOnFiles(filePath, arrayOfFiles);
|
||||
} else {
|
||||
arrayOfFiles.push(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
|
||||
return shell.exec(`find ${TEST_DIR}/integration -name "${pattern}"`, {silent: true}).stdout.
|
||||
split('\n').
|
||||
filter((matched) => Boolean(matched)).
|
||||
map((fileOrDir) => {
|
||||
if (fs.statSync(`./${fileOrDir}`).isDirectory(fileOrDir)) {
|
||||
return diveOnFiles(`./${fileOrDir}`);
|
||||
}
|
||||
return fileOrDir;
|
||||
}).
|
||||
flat().
|
||||
filter((file) => file.includes('spec.js') || file.includes('spec.ts')).
|
||||
map((file) => file.replace('./', ''));
|
||||
};
|
||||
|
||||
function getBaseTestFiles() {
|
||||
const {invert, group, stage} = argv;
|
||||
|
||||
const allFiles = grepFiles(grepCommand());
|
||||
const stageFiles = getFilesByMetadata(stage);
|
||||
const groupFiles = getFilesByMetadata(group);
|
||||
|
||||
if (invert) {
|
||||
// Return no test file if no stage and withGroup, but inverted
|
||||
if (!stage && !group) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return all excluding stage files
|
||||
if (stage && !group) {
|
||||
return without(allFiles, ...stageFiles);
|
||||
}
|
||||
|
||||
// Return all excluding group files
|
||||
if (!stage && group) {
|
||||
return without(allFiles, ...groupFiles);
|
||||
}
|
||||
|
||||
// Return all excluding group and stage files
|
||||
return without(allFiles, ...intersection(stageFiles, groupFiles));
|
||||
}
|
||||
|
||||
// Return all files if no stage and group flags
|
||||
if (!stage && !group) {
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
// Return stage files if no group flag
|
||||
if (stage && !group) {
|
||||
return stageFiles;
|
||||
}
|
||||
|
||||
// Return group files if no stage flag
|
||||
if (!stage && group) {
|
||||
return groupFiles;
|
||||
}
|
||||
|
||||
// Return files if both in stage and group
|
||||
return intersection(stageFiles, groupFiles);
|
||||
}
|
||||
|
||||
function getWeightedFiles(metadata, sortFirst = true) {
|
||||
let weightedFiles = [];
|
||||
if (metadata) {
|
||||
metadata.split(',').forEach((word, i, arr) => {
|
||||
const files = getFilesByMetadata(word).map((file) => {
|
||||
return {
|
||||
file,
|
||||
sortWeight: sortFirst ? (i - arr.length) : (i + 1),
|
||||
};
|
||||
});
|
||||
weightedFiles.push(...files);
|
||||
});
|
||||
}
|
||||
|
||||
if (sortFirst) {
|
||||
weightedFiles = weightedFiles.reverse();
|
||||
}
|
||||
|
||||
return weightedFiles.reduce((acc, f) => {
|
||||
acc[f.file] = f;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function reorderFiles(files = {}, filesToReorder = {}) {
|
||||
const testFilesObject = Object.assign({}, files);
|
||||
|
||||
const validFiles = intersection(Object.keys(testFilesObject), Object.keys(filesToReorder));
|
||||
Object.entries(filesToReorder).forEach(([k, v]) => {
|
||||
if (validFiles.includes(k)) {
|
||||
testFilesObject[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
return testFilesObject;
|
||||
}
|
||||
|
||||
function removeFromFiles(files = {}, filesToRemove = []) {
|
||||
const testFilesObject = Object.assign({}, files);
|
||||
|
||||
const removedFiles = intersection(Object.keys(testFilesObject), filesToRemove);
|
||||
removedFiles.forEach((file) => {
|
||||
if (Object.hasOwn(testFilesObject, file)) {
|
||||
delete testFilesObject[file];
|
||||
}
|
||||
});
|
||||
|
||||
return {testFilesObject, removedFiles};
|
||||
}
|
||||
|
||||
function getSortedTestFiles(platform, browser, headless) {
|
||||
// Get test files based on stage, group and/or invert
|
||||
const baseTestFiles = getBaseTestFiles();
|
||||
|
||||
// Add files matched by spec metadata
|
||||
const includeFilesByGroup = getFilesByMetadata(argv.includeGroup);
|
||||
if (includeFilesByGroup.length) {
|
||||
printMessage(includeFilesByGroup, `\nIncluded test files due to --include-group="${argv.includeGroup}"`);
|
||||
}
|
||||
|
||||
// Add files matched by filename
|
||||
const includeFilesByFilename = argv.includeFile.split(',').
|
||||
map((pattern) => findFiles(pattern)).
|
||||
reduce((acc, files) => acc.concat(files), []);
|
||||
if (includeFilesByFilename.length) {
|
||||
printMessage(includeFilesByFilename, `\nIncluded test files due to --include-file="${argv.includeFile}"`);
|
||||
}
|
||||
|
||||
let testFilesObject = baseTestFiles.
|
||||
concat(includeFilesByGroup).
|
||||
concat(includeFilesByFilename).
|
||||
reduce((acc, file) => {
|
||||
acc[file] = {file, sortWeight: 0};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Remove skipped files due to test environment
|
||||
let removedFiles;
|
||||
const skippedFiles = getSkippedFiles(platform, browser, headless);
|
||||
({testFilesObject, removedFiles} = removeFromFiles(testFilesObject, skippedFiles));
|
||||
printMessage(removedFiles, `\nSkipped test files due to ${platform}/${browser} (${headless ? 'headless' : 'headed'})`);
|
||||
|
||||
// Remove files matched by spec metadata
|
||||
const excludeFilesByGroup = getFilesByMetadata(argv.excludeGroup);
|
||||
({testFilesObject, removedFiles} = removeFromFiles(testFilesObject, excludeFilesByGroup));
|
||||
if (excludeFilesByGroup.length) {
|
||||
printMessage(removedFiles, `\nExcluded test files due to --exclude-group="${argv.excludeGroup}"`);
|
||||
}
|
||||
|
||||
// Remove files matched by filename
|
||||
const excludeFilesByFilename = argv.excludeFile.split(',').
|
||||
map((pattern) => findFiles(pattern)).
|
||||
reduce((acc, files) => acc.concat(files), []);
|
||||
|
||||
({testFilesObject, removedFiles} = removeFromFiles(testFilesObject, excludeFilesByFilename));
|
||||
if (excludeFilesByFilename.length) {
|
||||
printMessage(removedFiles, `\nExcluded test files due to --exclude-file="${argv.excludeFile}"`);
|
||||
}
|
||||
|
||||
// Get files to be sorted first
|
||||
const firstFilesObject = getWeightedFiles(argv.sortFirst, true);
|
||||
testFilesObject = reorderFiles(testFilesObject, firstFilesObject);
|
||||
|
||||
// Get files to be sorted last
|
||||
const lastFilesObject = getWeightedFiles(argv.sortLast, false);
|
||||
testFilesObject = reorderFiles(testFilesObject, lastFilesObject);
|
||||
|
||||
const sortedFiles = Object.values(testFilesObject).
|
||||
sort((a, b) => {
|
||||
if (a.sortWeight > b.sortWeight) {
|
||||
return 1;
|
||||
} else if (a.sortWeight < b.sortWeight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a.file.localeCompare(b.file);
|
||||
}).
|
||||
map((sortedObj) => sortedObj.file);
|
||||
|
||||
return {sortedFiles, skippedFiles, weightedTestFiles: Object.values(testFilesObject)};
|
||||
}
|
||||
|
||||
function getFilesByMetadata(metadata) {
|
||||
if (!metadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const egc = grepCommand(metadata.split(',').join('\\|'));
|
||||
return grepFiles(egc);
|
||||
}
|
||||
|
||||
function printMessage(files = [], message) {
|
||||
console.log(chalk.cyan(`\n${message}:`));
|
||||
|
||||
files.forEach((file, index) => {
|
||||
console.log(chalk.cyan(`- [${index + 1}] ${file}`));
|
||||
});
|
||||
}
|
||||
|
||||
function getSkippedFiles(platform, browser, headless) {
|
||||
const platformFiles = getFilesByMetadata(`@${platform}`);
|
||||
const browserFiles = getFilesByMetadata(`@${browser}`);
|
||||
const headlessFiles = getFilesByMetadata(`@${headless ? 'headless' : 'headed'}`);
|
||||
|
||||
return platformFiles.concat(browserFiles, headlessFiles);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSortedTestFiles,
|
||||
};
|
||||
348
e2e-tests/cypress/utils/report.js
Normal file
348
e2e-tests/cypress/utils/report.js
Normal file
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const axios = require('axios');
|
||||
const fse = require('fs-extra');
|
||||
const dayjs = require('dayjs');
|
||||
const duration = require('dayjs/plugin/duration');
|
||||
dayjs.extend(duration);
|
||||
|
||||
const {MOCHAWESOME_REPORT_DIR, AD_CYCLE_FILE} = require('./constants');
|
||||
|
||||
const MAX_FAILED_TITLES = 5;
|
||||
|
||||
let incrementalDuration = 0;
|
||||
|
||||
function getAllTests(results) {
|
||||
const tests = [];
|
||||
results.forEach((result) => {
|
||||
result.tests.forEach((test) => {
|
||||
incrementalDuration += test.duration;
|
||||
tests.push({...test, incrementalDuration});
|
||||
});
|
||||
|
||||
if (result.suites.length > 0) {
|
||||
getAllTests(result.suites).forEach((test) => tests.push(test));
|
||||
}
|
||||
});
|
||||
|
||||
return tests;
|
||||
}
|
||||
|
||||
function generateStatsFieldValue(stats, failedFullTitles) {
|
||||
const startAt = dayjs(stats.start);
|
||||
const endAt = dayjs(stats.end);
|
||||
const statsDuration = dayjs.duration(endAt.diff(startAt)).format('H:mm:ss');
|
||||
|
||||
let statsFieldValue = `
|
||||
| Key | Value |
|
||||
|:---|:---|
|
||||
| Passing Rate | ${stats.passPercent.toFixed(2)}% |
|
||||
| Duration | ${statsDuration} |
|
||||
| Suites | ${stats.suites} |
|
||||
| Tests | ${stats.tests} |
|
||||
| :white_check_mark: Passed | ${stats.passes} |
|
||||
| :x: Failed | ${stats.failures} |
|
||||
| :fast_forward: Skipped | ${stats.skipped} |
|
||||
`;
|
||||
|
||||
// If present, add full title of failing tests.
|
||||
// Only show per maximum number of failed titles with the last item as "more..." if failing tests are more than that.
|
||||
let failedTests;
|
||||
if (failedFullTitles && failedFullTitles.length > 0) {
|
||||
const re = /[:'"\\]/gi;
|
||||
const failed = failedFullTitles;
|
||||
if (failed.length > MAX_FAILED_TITLES) {
|
||||
failedTests = failed.slice(0, MAX_FAILED_TITLES - 1).map((f) => `- ${f.replace(re, '')}`).join('\n');
|
||||
failedTests += '\n- more...';
|
||||
} else {
|
||||
failedTests = failed.map((f) => `- ${f.replace(re, '')}`).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (failedTests) {
|
||||
statsFieldValue += '###### Failed Tests:\n' + failedTests;
|
||||
}
|
||||
|
||||
return statsFieldValue;
|
||||
}
|
||||
|
||||
function generateShortSummary(report) {
|
||||
const {results, stats} = report;
|
||||
const tests = getAllTests(results);
|
||||
|
||||
const failedFullTitles = tests.filter((t) => t.fail).map((t) => t.fullTitle);
|
||||
const statsFieldValue = generateStatsFieldValue(stats, failedFullTitles);
|
||||
|
||||
// If AD Cycle file is found, we have data from the Automation Dashboard available
|
||||
// We are able to override the run stats with enriched information
|
||||
const adCycle = readJsonFromFile(AD_CYCLE_FILE);
|
||||
if (!(adCycle instanceof Error)) {
|
||||
stats.passes = adCycle.pass;
|
||||
stats.failures = adCycle.fail;
|
||||
stats.tests = adCycle.pass + adCycle.fail;
|
||||
stats.passPercent = 100 * (stats.passes / stats.tests);
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
statsFieldValue,
|
||||
};
|
||||
}
|
||||
|
||||
function removeOldGeneratedReports() {
|
||||
[
|
||||
'all.json',
|
||||
'summary.json',
|
||||
'mochawesome.html',
|
||||
].forEach((file) => fse.removeSync(`${MOCHAWESOME_REPORT_DIR}/${file}`));
|
||||
}
|
||||
|
||||
function writeJsonToFile(jsonObject, filename, dir) {
|
||||
fse.writeJson(`${dir}/${filename}`, jsonObject).
|
||||
then(() => console.log('Successfully written:', filename)).
|
||||
catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
function readJsonFromFile(file) {
|
||||
try {
|
||||
return fse.readJsonSync(file);
|
||||
} catch (err) {
|
||||
return {err};
|
||||
}
|
||||
}
|
||||
|
||||
const result = [
|
||||
{status: 'Passed', priority: 'none', cutOff: 100, color: '#43A047'},
|
||||
{status: 'Failed', priority: 'low', cutOff: 98, color: '#FFEB3B'},
|
||||
{status: 'Failed', priority: 'medium', cutOff: 95, color: '#FF9800'},
|
||||
{status: 'Failed', priority: 'high', cutOff: 0, color: '#F44336'},
|
||||
];
|
||||
|
||||
function generateTestReport(summary, isUploadedToS3, reportLink, environment, testCycleKey) {
|
||||
const {
|
||||
FULL_REPORT,
|
||||
TEST_CYCLE_LINK_PREFIX,
|
||||
MM_ENV,
|
||||
SERVER_TYPE,
|
||||
BUILD_ID,
|
||||
AUTOMATION_DASHBOARD_FRONTEND_URL,
|
||||
} = process.env;
|
||||
const {statsFieldValue, stats} = summary;
|
||||
const {
|
||||
cypress_version: cypressVersion,
|
||||
browser_name: browserName,
|
||||
browser_version: browserVersion,
|
||||
headless,
|
||||
os_name: osName,
|
||||
os_version: osVersion,
|
||||
node_version: nodeVersion,
|
||||
} = environment;
|
||||
|
||||
let testResult;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (stats.passPercent >= result[i].cutOff) {
|
||||
testResult = result[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const title = generateTitle();
|
||||
const runnerEnvValue = `cypress@${cypressVersion} | node@${nodeVersion} | ${browserName}@${browserVersion}${headless ? ' (headless)' : ''} | ${osName}@${osVersion}`;
|
||||
|
||||
if (FULL_REPORT === 'true') {
|
||||
let reportField;
|
||||
if (isUploadedToS3) {
|
||||
reportField = {
|
||||
short: false,
|
||||
title: 'Test Report',
|
||||
value: `[Link to the report](${reportLink})`,
|
||||
};
|
||||
}
|
||||
|
||||
let testCycleField;
|
||||
if (testCycleKey) {
|
||||
testCycleField = {
|
||||
short: false,
|
||||
title: 'Test Execution',
|
||||
value: `[Recorded test executions](${TEST_CYCLE_LINK_PREFIX}${testCycleKey})`,
|
||||
};
|
||||
}
|
||||
|
||||
let serverEnvField;
|
||||
if (MM_ENV) {
|
||||
serverEnvField = {
|
||||
short: false,
|
||||
title: 'Test Server Override',
|
||||
value: MM_ENV,
|
||||
};
|
||||
}
|
||||
|
||||
let serverTypeField;
|
||||
if (SERVER_TYPE) {
|
||||
serverTypeField = {
|
||||
short: false,
|
||||
title: 'Test Server',
|
||||
value: SERVER_TYPE,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
username: 'Cypress UI Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
attachments: [{
|
||||
color: testResult.color,
|
||||
author_name: 'Webapp End-to-end Testing',
|
||||
author_icon: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
author_link: 'https://www.mattermost.com',
|
||||
title,
|
||||
fields: [
|
||||
{
|
||||
short: false,
|
||||
title: 'Environment',
|
||||
value: runnerEnvValue,
|
||||
},
|
||||
serverTypeField,
|
||||
serverEnvField,
|
||||
reportField,
|
||||
testCycleField,
|
||||
{
|
||||
short: false,
|
||||
title: `Key metrics (required support: ${testResult.priority})`,
|
||||
value: statsFieldValue,
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
let quickSummary = `${stats.passPercent.toFixed(2)}% (${stats.passes}/${stats.tests}) in ${stats.suites} suites`;
|
||||
if (isUploadedToS3) {
|
||||
quickSummary = `[${quickSummary}](${reportLink})`;
|
||||
}
|
||||
|
||||
let testCycleLink = '';
|
||||
if (testCycleKey) {
|
||||
testCycleLink = testCycleKey ? `| [Recorded test executions](${TEST_CYCLE_LINK_PREFIX}${testCycleKey})` : '';
|
||||
}
|
||||
|
||||
const automationDashboardField = AUTOMATION_DASHBOARD_FRONTEND_URL ? `| [Automation Dashboard](${AUTOMATION_DASHBOARD_FRONTEND_URL}/cycle/${BUILD_ID})` : '';
|
||||
|
||||
const rollingReleaseMatchRegex = BUILD_ID.match(/-rolling(?<version>[^-]+)-/);
|
||||
const rollingReleaseFrom = rollingReleaseMatchRegex?.groups?.version;
|
||||
const rollingReleaseFromField = rollingReleaseFrom ? `\nRolling release upgrade from: ${rollingReleaseFrom}` : '';
|
||||
|
||||
const startAt = dayjs(stats.start);
|
||||
const endAt = dayjs(stats.end);
|
||||
const statsDuration = dayjs.duration(endAt.diff(startAt)).format('H:mm:ss');
|
||||
|
||||
return {
|
||||
username: 'Cypress UI Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
attachments: [{
|
||||
color: testResult.color,
|
||||
author_name: 'Webapp End-to-end Testing',
|
||||
author_icon: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
author_link: 'https://www.mattermost.com/',
|
||||
title,
|
||||
text: `${quickSummary} | ${statsDuration} ${testCycleLink} ${automationDashboardField}\n${runnerEnvValue}${SERVER_TYPE ? '\nTest server: ' + SERVER_TYPE : ''}${rollingReleaseFromField}${MM_ENV ? '\nTest server override: ' + MM_ENV : ''}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function generateTitle() {
|
||||
const {
|
||||
BRANCH,
|
||||
MM_DOCKER_IMAGE,
|
||||
MM_DOCKER_TAG,
|
||||
PULL_REQUEST,
|
||||
RELEASE_DATE,
|
||||
TYPE,
|
||||
} = process.env;
|
||||
|
||||
let dockerImageLink = '';
|
||||
if (MM_DOCKER_IMAGE && MM_DOCKER_TAG) {
|
||||
dockerImageLink = ` with [${MM_DOCKER_IMAGE}:${MM_DOCKER_TAG}](https://hub.docker.com/r/mattermostdevelopment/${MM_DOCKER_IMAGE}/tags?name=${MM_DOCKER_TAG})`;
|
||||
}
|
||||
|
||||
let releaseDate = '';
|
||||
if (RELEASE_DATE) {
|
||||
releaseDate = ` for ${RELEASE_DATE}`;
|
||||
}
|
||||
|
||||
let title;
|
||||
|
||||
switch (TYPE) {
|
||||
case 'PR':
|
||||
title = `E2E for Pull Request Build: [${BRANCH}](${PULL_REQUEST})${dockerImageLink}`;
|
||||
break;
|
||||
case 'RELEASE':
|
||||
title = `E2E for Release Build${dockerImageLink}${releaseDate}`;
|
||||
break;
|
||||
case 'MASTER':
|
||||
title = `E2E for Master Nightly Build (Prod tests)${dockerImageLink}`;
|
||||
break;
|
||||
case 'MASTER_UNSTABLE':
|
||||
title = `E2E for Master Nightly Build (Unstable tests)${dockerImageLink}`;
|
||||
break;
|
||||
case 'CLOUD':
|
||||
title = `E2E for Cloud Build (Prod tests)${dockerImageLink}${releaseDate}`;
|
||||
break;
|
||||
case 'CLOUD_UNSTABLE':
|
||||
title = `E2E for Cloud Build (Unstable tests)${dockerImageLink}`;
|
||||
break;
|
||||
default:
|
||||
title = `E2E for Build${dockerImageLink}`;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
function generateDiagnosticReport(summary, serverInfo) {
|
||||
const {BRANCH, BUILD_ID} = process.env;
|
||||
|
||||
return {
|
||||
username: 'Cypress UI Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
attachments: [{
|
||||
color: '#43A047',
|
||||
author_name: 'Cypress UI Test',
|
||||
author_icon: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
author_link: 'https://community.mattermost.com/core/channels/ui-test-automation',
|
||||
title: `Cypress UI Test Automation #${BUILD_ID}, **${BRANCH}** branch`,
|
||||
fields: [{
|
||||
short: false,
|
||||
value: `Start: **${summary.stats.start}**\nEnd: **${summary.stats.end}**\nUser ID: **${serverInfo.userId}**\nTeam ID: **${serverInfo.teamId}**`,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
async function sendReport(name, url, data) {
|
||||
const requestOptions = {method: 'POST', url, data};
|
||||
|
||||
try {
|
||||
const response = await axios(requestOptions);
|
||||
|
||||
if (response.data) {
|
||||
console.log(`Successfully sent ${name}.`);
|
||||
}
|
||||
return response;
|
||||
} catch (er) {
|
||||
console.log(`Something went wrong while sending ${name}.`, er);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateDiagnosticReport,
|
||||
generateShortSummary,
|
||||
generateTestReport,
|
||||
getAllTests,
|
||||
removeOldGeneratedReports,
|
||||
sendReport,
|
||||
readJsonFromFile,
|
||||
writeJsonToFile,
|
||||
};
|
||||
200
e2e-tests/cypress/utils/test_cases.js
Normal file
200
e2e-tests/cypress/utils/test_cases.js
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// See reference: https://support.smartbear.com/tm4j-cloud/api-docs/
|
||||
|
||||
const axios = require('axios');
|
||||
const chalk = require('chalk').default;
|
||||
|
||||
const {getAllTests} = require('./report');
|
||||
|
||||
const status = {
|
||||
passed: 'Pass',
|
||||
failed: 'Fail',
|
||||
pending: 'Pending',
|
||||
skipped: 'Skip',
|
||||
};
|
||||
|
||||
const environment = {
|
||||
chrome: 'Chrome',
|
||||
firefox: 'Firefox',
|
||||
};
|
||||
|
||||
function getStepStateResult(steps = []) {
|
||||
return steps.reduce((acc, item) => {
|
||||
if (acc[item.state]) {
|
||||
acc[item.state] += 1;
|
||||
} else {
|
||||
acc[item.state] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getStepStateSummary(steps = []) {
|
||||
const result = getStepStateResult(steps);
|
||||
|
||||
return Object.entries(result).map(([key, value]) => `${value} ${key}`).join(',');
|
||||
}
|
||||
|
||||
function getTM4JTestCases(report) {
|
||||
return getAllTests(report.results).
|
||||
filter((item) => (/^(MM-T)\w+/g).test(item.title)).
|
||||
map((item) => {
|
||||
return {
|
||||
title: item.title,
|
||||
duration: item.duration,
|
||||
incrementalDuration: item.incrementalDuration,
|
||||
state: item.state,
|
||||
pass: item.pass,
|
||||
fail: item.fail,
|
||||
pending: item.pending,
|
||||
};
|
||||
}).
|
||||
reduce((acc, item) => {
|
||||
// Extract the key to exactly match with "MM-T[0-9]+"
|
||||
const key = item.title.match(/(MM-T\d+)/)[0];
|
||||
|
||||
if (acc[key]) {
|
||||
acc[key].push(item);
|
||||
} else {
|
||||
acc[key] = [item];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function saveToEndpoint(url, data) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: process.env.TM4J_API_KEY,
|
||||
},
|
||||
data,
|
||||
}).catch((error) => {
|
||||
console.log('Something went wrong:', error.response.data.message);
|
||||
return error.response.data;
|
||||
});
|
||||
}
|
||||
|
||||
async function createTestCycle(startDate, endDate) {
|
||||
const {
|
||||
BRANCH,
|
||||
BUILD_ID,
|
||||
JIRA_PROJECT_KEY,
|
||||
TM4J_CYCLE_NAME,
|
||||
TM4J_FOLDER_ID,
|
||||
} = process.env;
|
||||
|
||||
const testCycle = {
|
||||
projectKey: JIRA_PROJECT_KEY,
|
||||
name: TM4J_CYCLE_NAME ? `${TM4J_CYCLE_NAME} (${BUILD_ID}-${BRANCH})` : `${BUILD_ID}-${BRANCH}`,
|
||||
description: `Cypress automated test with ${BRANCH}`,
|
||||
plannedStartDate: startDate,
|
||||
plannedEndDate: endDate,
|
||||
statusName: 'Done',
|
||||
folderId: TM4J_FOLDER_ID,
|
||||
};
|
||||
|
||||
const response = await saveToEndpoint('https://api.zephyrscale.smartbear.com/v2/testcycles', testCycle);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function createTestExecutions(report, testCycle) {
|
||||
const {
|
||||
BROWSER,
|
||||
JIRA_PROJECT_KEY,
|
||||
TM4J_ENVIRONMENT_NAME,
|
||||
} = process.env;
|
||||
|
||||
const testCases = getTM4JTestCases(report);
|
||||
const startDate = new Date(report.stats.start);
|
||||
const startTime = startDate.getTime();
|
||||
|
||||
const promises = [];
|
||||
Object.entries(testCases).forEach(([key, steps], index) => {
|
||||
const testScriptResults = steps.
|
||||
sort((a, b) => a.title.localeCompare(b.title)).
|
||||
map((item) => {
|
||||
return {
|
||||
statusName: status[item.state],
|
||||
actualEndDate: new Date(startTime + item.incrementalDuration).toISOString(),
|
||||
actualResult: 'Cypress automated test completed',
|
||||
};
|
||||
});
|
||||
|
||||
const stateResult = getStepStateResult(steps);
|
||||
|
||||
const testExecution = {
|
||||
projectKey: JIRA_PROJECT_KEY,
|
||||
testCaseKey: key,
|
||||
testCycleKey: testCycle.key,
|
||||
statusName: stateResult.passed && stateResult.passed === steps.length ? 'Pass' : 'Fail',
|
||||
testScriptResults,
|
||||
environmentName: TM4J_ENVIRONMENT_NAME || environment[BROWSER] || 'Chrome',
|
||||
actualEndDate: testScriptResults[testScriptResults.length - 1].actualEndDate,
|
||||
executionTime: steps.reduce((acc, prev) => {
|
||||
return acc + prev.duration;
|
||||
}, 0),
|
||||
comment: `Cypress automated test - ${getStepStateSummary(steps)}`,
|
||||
};
|
||||
|
||||
// Temporarily log to verify cases that were being saved.
|
||||
console.log(index, key);
|
||||
|
||||
promises.push(saveTestExecution(testExecution, index));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log('Successfully saved test cases into the Test Management System');
|
||||
}
|
||||
|
||||
const saveTestCases = async (allReport) => {
|
||||
const {start, end} = allReport.stats;
|
||||
|
||||
const testCycle = await createTestCycle(start, end);
|
||||
|
||||
await createTestExecutions(allReport, testCycle);
|
||||
};
|
||||
|
||||
const RETRY = [];
|
||||
|
||||
async function saveTestExecution(testExecution, index) {
|
||||
await axios({
|
||||
method: 'POST',
|
||||
url: 'https://api.zephyrscale.smartbear.com/v2/testexecutions',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
Authorization: process.env.TM4J_API_KEY,
|
||||
},
|
||||
data: testExecution,
|
||||
}).then(() => {
|
||||
console.log(chalk.green('Success:', index, testExecution.testCaseKey));
|
||||
}).catch((error) => {
|
||||
// Retry on 500 error code / internal server error
|
||||
if (!error.response || error.response.data.errorCode === 500) {
|
||||
if (RETRY[testExecution.testCaseKey]) {
|
||||
RETRY[testExecution.testCaseKey] += 1;
|
||||
} else {
|
||||
RETRY[testExecution.testCaseKey] = 1;
|
||||
}
|
||||
|
||||
saveTestExecution(testExecution, index);
|
||||
console.log(chalk.magenta('Retry:', index, testExecution.testCaseKey, `(${RETRY[testExecution.testCaseKey]}x)`));
|
||||
} else {
|
||||
console.log(chalk.red('Error:', index, testExecution.testCaseKey, error.response.data.message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createTestCycle,
|
||||
saveTestCases,
|
||||
createTestExecutions,
|
||||
};
|
||||
866
e2e-tests/cypress/utils/webhook_utils.js
Normal file
866
e2e-tests/cypress/utils/webhook_utils.js
Normal file
@@ -0,0 +1,866 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Helper function to create dialog base structure
|
||||
function createDialog(triggerId, webhookBaseUrl, dialogConfig) {
|
||||
const baseDialog = {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: dialogConfig.callback_id,
|
||||
title: dialogConfig.title,
|
||||
submit_label: dialogConfig.submit_label || 'Submit',
|
||||
notify_on_cancel: true,
|
||||
...dialogConfig.dialog_props,
|
||||
elements: dialogConfig.elements || [],
|
||||
},
|
||||
};
|
||||
|
||||
if (dialogConfig.icon_url) {
|
||||
baseDialog.dialog.icon_url = dialogConfig.icon_url;
|
||||
}
|
||||
|
||||
if (dialogConfig.introduction_text) {
|
||||
baseDialog.dialog.introduction_text = dialogConfig.introduction_text;
|
||||
}
|
||||
|
||||
if (dialogConfig.state) {
|
||||
baseDialog.dialog.state = dialogConfig.state;
|
||||
}
|
||||
|
||||
if (dialogConfig.source_url) {
|
||||
baseDialog.dialog.source_url = dialogConfig.source_url;
|
||||
}
|
||||
|
||||
return baseDialog;
|
||||
}
|
||||
|
||||
// Helper function to create form response structure
|
||||
function createFormResponse(formConfig) {
|
||||
return {
|
||||
callback_id: formConfig.callback_id,
|
||||
title: formConfig.title,
|
||||
submit_label: formConfig.submit_label || 'Submit',
|
||||
notify_on_cancel: true,
|
||||
elements: formConfig.elements || [],
|
||||
...formConfig.form_props,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to create common form elements
|
||||
function createElement(type, config) {
|
||||
const baseElement = {
|
||||
display_name: config.display_name,
|
||||
name: config.name,
|
||||
type,
|
||||
optional: config.optional || false,
|
||||
};
|
||||
|
||||
if (config.placeholder) {
|
||||
baseElement.placeholder = config.placeholder;
|
||||
}
|
||||
if (config.help_text) {
|
||||
baseElement.help_text = config.help_text;
|
||||
}
|
||||
if (config.default) {
|
||||
baseElement.default = config.default;
|
||||
}
|
||||
if (config.subtype) {
|
||||
baseElement.subtype = config.subtype;
|
||||
}
|
||||
if (config.min_length) {
|
||||
baseElement.min_length = config.min_length;
|
||||
}
|
||||
if (config.max_length) {
|
||||
baseElement.max_length = config.max_length;
|
||||
}
|
||||
if (config.data_source) {
|
||||
baseElement.data_source = config.data_source;
|
||||
}
|
||||
if (config.options) {
|
||||
baseElement.options = config.options;
|
||||
}
|
||||
if (config.refresh) {
|
||||
baseElement.refresh = config.refresh;
|
||||
}
|
||||
if (config.label_position) {
|
||||
baseElement.label_position = config.label_position;
|
||||
}
|
||||
if (config.matrix_config) {
|
||||
baseElement.matrix_config = config.matrix_config;
|
||||
}
|
||||
if (config.multiselect) {
|
||||
baseElement.multiselect = true;
|
||||
}
|
||||
if (config.action_button) {
|
||||
baseElement.action_button = config.action_button;
|
||||
}
|
||||
|
||||
return baseElement;
|
||||
}
|
||||
|
||||
// Standard icon URL
|
||||
const STANDARD_ICON = 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png';
|
||||
|
||||
const WAIVER_REASON_OPTIONS = [
|
||||
{text: 'Reason 1', value: 'reason_1'},
|
||||
{text: 'Reason 2', value: 'reason_2'},
|
||||
{text: 'Reason 3', value: 'reason_3'},
|
||||
];
|
||||
|
||||
const SEVERITY_MATRIX_COLUMNS = [
|
||||
{text: 'High', value: 'high'},
|
||||
{text: 'Severe', value: 'severe'},
|
||||
];
|
||||
|
||||
// Dialog configurations
|
||||
const DIALOG_CONFIGS = {
|
||||
full: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Full Dialog Test',
|
||||
icon_url: STANDARD_ICON,
|
||||
elements: [
|
||||
createElement('text', {display_name: 'Display Name', name: 'realname', default: 'default text', placeholder: 'placeholder', help_text: 'This a regular input in an interactive dialog triggered by a test integration.'}),
|
||||
createElement('text', {display_name: 'Email', name: 'someemail', subtype: 'email', placeholder: 'placeholder@bladekick.com', help_text: 'This a regular email input in an interactive dialog triggered by a test integration.'}),
|
||||
createElement('text', {display_name: 'Number', name: 'somenumber', subtype: 'number'}),
|
||||
createElement('text', {display_name: 'Password', name: 'somepassword', subtype: 'password', default: 'p@ssW0rd', placeholder: 'placeholder', help_text: 'This a password input in an interactive dialog triggered by a test integration.', optional: true}),
|
||||
createElement('textarea', {display_name: 'Display Name Long Text Area', name: 'realnametextarea', placeholder: 'placeholder', optional: true, min_length: 5, max_length: 100}),
|
||||
createElement('select', {display_name: 'User Selector', name: 'someuserselector', placeholder: 'Select a user...', data_source: 'users'}),
|
||||
createElement('select', {display_name: 'Channel Selector', name: 'somechannelselector', placeholder: 'Select a channel...', help_text: 'Choose a channel from the list.', data_source: 'channels', optional: true}),
|
||||
createElement('select', {display_name: 'Option Selector', name: 'someoptionselector', placeholder: 'Select an option...', options: [{text: 'Option1', value: 'opt1'}, {text: 'Option2', value: 'opt2'}, {text: 'Option3', value: 'opt3'}]}),
|
||||
createElement('radio', {display_name: 'Radio Option Selector', name: 'someradiooptions', help_text: '', options: [{text: 'Engineering', value: 'engineering'}, {text: 'Sales', value: 'sales'}]}),
|
||||
createElement('bool', {display_name: 'Boolean Selector', name: 'boolean_input', placeholder: 'Was this modal helpful?', default: 'True', optional: true, help_text: 'This is the help text'}),
|
||||
],
|
||||
dialog_props: {state: 'somestate'},
|
||||
},
|
||||
|
||||
simple: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test without elements',
|
||||
icon_url: STANDARD_ICON,
|
||||
submit_label: 'Submit Test',
|
||||
dialog_props: {state: 'somestate'},
|
||||
},
|
||||
|
||||
basic: {
|
||||
callback_id: 'basic_dialog_callback',
|
||||
title: 'Basic Dialog',
|
||||
icon_url: STANDARD_ICON,
|
||||
submit_label: 'Submit',
|
||||
introduction_text: 'Required text, optional radio, single select, and optional multiselect.',
|
||||
elements: [
|
||||
createElement('text', {
|
||||
display_name: 'Title',
|
||||
name: 'title',
|
||||
placeholder: 'Enter a title',
|
||||
help_text: 'Required text field.',
|
||||
}),
|
||||
createElement('radio', {
|
||||
display_name: 'Department',
|
||||
name: 'department',
|
||||
help_text: 'Optional.',
|
||||
optional: true,
|
||||
options: [
|
||||
{text: 'Engineering', value: 'engineering'},
|
||||
{text: 'Sales', value: 'sales'},
|
||||
{text: 'Support', value: 'support'},
|
||||
],
|
||||
}),
|
||||
createElement('select', {
|
||||
display_name: 'Priority',
|
||||
name: 'priority',
|
||||
placeholder: 'Choose priority...',
|
||||
help_text: 'Optional single select. Use the × control to clear.',
|
||||
optional: true,
|
||||
options: [
|
||||
{text: 'Low', value: 'low'},
|
||||
{text: 'Medium', value: 'medium'},
|
||||
{text: 'High', value: 'high'},
|
||||
],
|
||||
}),
|
||||
createElement('select', {
|
||||
display_name: 'Categories',
|
||||
name: 'categories',
|
||||
multiselect: true,
|
||||
placeholder: 'Select one or more...',
|
||||
help_text: 'Optional multiselect (`type: select` + `multiselect: true`). Submits a string array.',
|
||||
optional: true,
|
||||
options: [
|
||||
{text: 'Bug', value: 'bug'},
|
||||
{text: 'Feature', value: 'feature'},
|
||||
{text: 'Documentation', value: 'docs'},
|
||||
],
|
||||
}),
|
||||
],
|
||||
dialog_props: {state: 'basic_dialog_state'},
|
||||
},
|
||||
|
||||
userAndChannel: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test with user and channel element',
|
||||
icon_url: STANDARD_ICON,
|
||||
submit_label: 'Submit Test',
|
||||
elements: [
|
||||
createElement('select', {display_name: 'User Selector', name: 'someuserselector', placeholder: 'Select a user...', data_source: 'users'}),
|
||||
createElement('select', {display_name: 'Channel Selector', name: 'somechannelselector', placeholder: 'Select a channel...', help_text: 'Choose a channel from the list.', data_source: 'channels', optional: true}),
|
||||
],
|
||||
dialog_props: {state: 'somestate'},
|
||||
},
|
||||
|
||||
boolean: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test with boolean element',
|
||||
icon_url: STANDARD_ICON,
|
||||
submit_label: 'Submit Test',
|
||||
elements: [
|
||||
createElement('bool', {display_name: 'Boolean Selector', name: 'boolean_input', placeholder: 'Was this modal helpful?', default: 'True', optional: true, help_text: 'This is the help text'}),
|
||||
],
|
||||
dialog_props: {state: 'somestate'},
|
||||
},
|
||||
|
||||
checkboxGroup: {
|
||||
callback_id: 'checkbox_group_callback',
|
||||
title: 'Checkbox Dialog Feature Demo',
|
||||
icon_url: STANDARD_ICON,
|
||||
submit_label: 'Submit',
|
||||
introduction_text: 'Two checkbox groups (label before vs after), plus related v1 dialog features for manual QA.',
|
||||
elements: [
|
||||
createElement('checkbox_group', {
|
||||
display_name: 'Group A: label before',
|
||||
name: 'waiver_group_label_before',
|
||||
help_text: 'Required. Option text is left of each checkbox (`label_position: before`). Default: reason_1 + reason_3.',
|
||||
default: 'reason_1,reason_3',
|
||||
label_position: 'before',
|
||||
options: WAIVER_REASON_OPTIONS,
|
||||
}),
|
||||
createElement('checkbox_group', {
|
||||
display_name: 'Group B: label after',
|
||||
name: 'waiver_group_label_after',
|
||||
help_text: 'Optional. Option text is right of each checkbox (`label_position: after`, default). Clear all → submits [].',
|
||||
default: 'reason_2',
|
||||
label_position: 'after',
|
||||
optional: true,
|
||||
options: WAIVER_REASON_OPTIONS,
|
||||
}),
|
||||
createElement('radio', {
|
||||
display_name: 'Department (optional)',
|
||||
name: 'department',
|
||||
help_text: 'Optional radio with default + Clear selection link.',
|
||||
default: 'sales',
|
||||
optional: true,
|
||||
label_position: 'after',
|
||||
options: [
|
||||
{text: 'Engineering', value: 'engineering'},
|
||||
{text: 'Sales', value: 'sales'},
|
||||
{text: 'Administration', value: 'administration'},
|
||||
],
|
||||
}),
|
||||
createElement('bool', {
|
||||
display_name: 'Acknowledgement',
|
||||
name: 'acknowledged',
|
||||
placeholder: 'I understand the waiver terms',
|
||||
help_text: 'Bool with label before the checkbox.',
|
||||
label_position: 'before',
|
||||
default: 'false',
|
||||
}),
|
||||
createElement('checkbox_matrix', {
|
||||
display_name: 'Severity (multi per row)',
|
||||
name: 'severity_multiple',
|
||||
help_text: 'Matrix with row_selection: multiple — multiple columns per row allowed.',
|
||||
optional: true,
|
||||
matrix_config: {
|
||||
row_selection: 'multiple',
|
||||
rows: WAIVER_REASON_OPTIONS,
|
||||
columns: SEVERITY_MATRIX_COLUMNS,
|
||||
},
|
||||
}),
|
||||
createElement('checkbox_matrix', {
|
||||
display_name: 'Priority (one per row)',
|
||||
name: 'priority_single',
|
||||
help_text: 'Matrix with row_selection: single — radio-style, one column per row.',
|
||||
optional: true,
|
||||
matrix_config: {
|
||||
row_selection: 'single',
|
||||
rows: WAIVER_REASON_OPTIONS,
|
||||
columns: [
|
||||
{text: 'Low', value: 'low'},
|
||||
{text: 'Medium', value: 'medium'},
|
||||
{text: 'High', value: 'high'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
dialog_props: {state: 'checkbox_group_state'},
|
||||
},
|
||||
|
||||
fieldRefresh: {
|
||||
callback_id: 'field_refresh_callback',
|
||||
title: 'Field Refresh Demo',
|
||||
introduction_text: 'Enter project name then select type to see different fields',
|
||||
elements: [
|
||||
createElement('text', {display_name: 'Project Name', name: 'project_name', placeholder: 'Enter project name'}),
|
||||
createElement('select', {display_name: 'Project Type', name: 'project_type', refresh: true, placeholder: 'Select project type...', options: [{text: 'Web Application', value: 'web'}, {text: 'Mobile App', value: 'mobile'}, {text: 'API Service', value: 'api'}]}),
|
||||
],
|
||||
},
|
||||
|
||||
multistepStep1: {
|
||||
callback_id: 'multistep_callback',
|
||||
title: 'Step 1 - Personal Info',
|
||||
introduction_text: 'Multi-step registration - Step 1 of 3',
|
||||
submit_label: 'Next Step',
|
||||
elements: [
|
||||
createElement('text', {display_name: 'First Name', name: 'first_name', placeholder: 'Enter your first name'}),
|
||||
createElement('text', {display_name: 'Email', name: 'email', subtype: 'email', placeholder: 'Enter your email address'}),
|
||||
],
|
||||
dialog_props: {state: 'step1'},
|
||||
},
|
||||
|
||||
multistepStep2: {
|
||||
callback_id: 'multistep_callback',
|
||||
title: 'Step 2 - Work Info',
|
||||
introduction_text: 'Multi-step registration - Step 2 of 3',
|
||||
submit_label: 'Next Step',
|
||||
elements: [
|
||||
createElement('select', {display_name: 'Department', name: 'department', placeholder: 'Select department...', options: [{text: 'Engineering', value: 'engineering'}, {text: 'Marketing', value: 'marketing'}, {text: 'Sales', value: 'sales'}]}),
|
||||
createElement('radio', {display_name: 'Experience Level', name: 'experience_level', options: [{text: 'Junior', value: 'junior'}, {text: 'Mid-level', value: 'mid'}, {text: 'Senior', value: 'senior'}]}),
|
||||
],
|
||||
form_props: {state: 'step2'},
|
||||
},
|
||||
|
||||
multistepStep3: {
|
||||
callback_id: 'multistep_callback',
|
||||
title: 'Step 3 - Final Details',
|
||||
introduction_text: 'Multi-step registration - Step 3 of 3',
|
||||
submit_label: 'Complete Registration',
|
||||
elements: [
|
||||
createElement('textarea', {display_name: 'Comments', name: 'comments', placeholder: 'Any additional comments...', optional: true}),
|
||||
createElement('bool', {display_name: 'Terms & Conditions', name: 'terms_accepted'}),
|
||||
],
|
||||
form_props: {state: 'step3'},
|
||||
},
|
||||
|
||||
actionButtonParent: {
|
||||
callback_id: 'action_button_parent_callback',
|
||||
title: 'Parent Dialog with Action Button',
|
||||
elements: [],
|
||||
},
|
||||
|
||||
actionButtonChild: {
|
||||
callback_id: 'child_callback',
|
||||
title: 'Child Dialog',
|
||||
elements: [
|
||||
createElement('text', {display_name: 'Child Input', name: 'child_input', placeholder: 'Enter value', optional: true}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Public API functions
|
||||
function getFullDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.full);
|
||||
}
|
||||
|
||||
function getSimpleDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.simple);
|
||||
}
|
||||
|
||||
function getBasicDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.basic);
|
||||
}
|
||||
|
||||
function getUserAndChannelDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.userAndChannel);
|
||||
}
|
||||
|
||||
function getBooleanDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.boolean);
|
||||
}
|
||||
|
||||
function getCheckboxGroupDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.checkboxGroup);
|
||||
}
|
||||
|
||||
function getFieldRefreshDialog(triggerId, webhookBaseUrl) {
|
||||
const config = {...DIALOG_CONFIGS.fieldRefresh};
|
||||
config.source_url = `${webhookBaseUrl}/field_refresh_source`;
|
||||
return createDialog(triggerId, webhookBaseUrl, config);
|
||||
}
|
||||
|
||||
function getMultistepStep1Dialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, DIALOG_CONFIGS.multistepStep1);
|
||||
}
|
||||
|
||||
function getMultistepStep2Dialog(triggerId, webhookBaseUrl) {
|
||||
const config = {...DIALOG_CONFIGS.multistepStep2};
|
||||
config.dialog_props = {url: `${webhookBaseUrl}/dialog_submit`, ...config.form_props};
|
||||
return createFormResponse(config);
|
||||
}
|
||||
|
||||
function getMultistepStep3Dialog(triggerId, webhookBaseUrl) {
|
||||
const config = {...DIALOG_CONFIGS.multistepStep3};
|
||||
config.dialog_props = {url: `${webhookBaseUrl}/dialog_submit`, ...config.form_props};
|
||||
return createFormResponse(config);
|
||||
}
|
||||
|
||||
function getMultiSelectDialog(triggerId, webhookBaseUrl, includeDefaults = false) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test with multiselect elements',
|
||||
icon_url:
|
||||
'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
submit_label: 'Submit Multiselect Test',
|
||||
notify_on_cancel: true,
|
||||
state: 'somestate',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Multi Option Selector',
|
||||
name: 'multiselect_options',
|
||||
type: 'select',
|
||||
multiselect: true,
|
||||
default: includeDefaults ? 'opt1,opt3' : '',
|
||||
placeholder: 'Select multiple options...',
|
||||
help_text: 'You can select multiple options from this list.',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [
|
||||
{
|
||||
text: 'Engineering',
|
||||
value: 'opt1',
|
||||
},
|
||||
{
|
||||
text: 'Sales',
|
||||
value: 'opt2',
|
||||
},
|
||||
{
|
||||
text: 'Marketing',
|
||||
value: 'opt3',
|
||||
},
|
||||
{
|
||||
text: 'Support',
|
||||
value: 'opt4',
|
||||
},
|
||||
{
|
||||
text: 'Product',
|
||||
value: 'opt5',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display_name: 'Multi User Selector',
|
||||
name: 'multiselect_users',
|
||||
type: 'select',
|
||||
multiselect: true,
|
||||
default: '',
|
||||
placeholder: 'Select multiple users...',
|
||||
help_text: 'Choose multiple users from the team.',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: 'users',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
display_name: 'Single Option Selector',
|
||||
name: 'single_select_options',
|
||||
type: 'select',
|
||||
multiselect: false,
|
||||
default: includeDefaults ? 'single2' : '',
|
||||
placeholder: 'Select one option...',
|
||||
help_text: 'This is a regular single-select for comparison.',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
data_source: '',
|
||||
options: [
|
||||
{
|
||||
text: 'Single Option 1',
|
||||
value: 'single1',
|
||||
},
|
||||
{
|
||||
text: 'Single Option 2',
|
||||
value: 'single2',
|
||||
},
|
||||
{
|
||||
text: 'Single Option 3',
|
||||
value: 'single3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDynamicSelectDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test with dynamic select element',
|
||||
icon_url:
|
||||
'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
submit_label: 'Submit Dynamic Select Test',
|
||||
notify_on_cancel: true,
|
||||
state: 'somestate',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Dynamic Role Selector',
|
||||
name: 'dynamic_role_selector',
|
||||
type: 'select',
|
||||
data_source: 'dynamic',
|
||||
data_source_url: `${webhookBaseUrl}/dynamic_select_source`,
|
||||
default: '',
|
||||
placeholder: 'Search for a role...',
|
||||
help_text: 'Start typing to search for available roles. Options are loaded dynamically.',
|
||||
optional: false,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
},
|
||||
{
|
||||
display_name: 'Optional Dynamic Selector',
|
||||
name: 'optional_dynamic_selector',
|
||||
type: 'select',
|
||||
data_source: 'dynamic',
|
||||
data_source_url: `${webhookBaseUrl}/dynamic_select_source`,
|
||||
default: 'backend_eng',
|
||||
placeholder: 'Search for another role...',
|
||||
help_text: 'This field is optional and has a default value.',
|
||||
optional: true,
|
||||
min_length: 0,
|
||||
max_length: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Basic date field test - MM-T2530A
|
||||
function getBasicDateDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/datetime_dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'basic_date_callback',
|
||||
title: 'DateTime Fields Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Event Date',
|
||||
name: 'event_date',
|
||||
type: 'date',
|
||||
default: '',
|
||||
placeholder: 'Select a date',
|
||||
help_text: 'Select the date for your event',
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
submit_label: 'Submit',
|
||||
notify_on_cancel: true,
|
||||
state: 'datetime_state',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Basic datetime field test - MM-T2530B
|
||||
function getBasicDateTimeDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/datetime_dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'basic_datetime_callback',
|
||||
title: 'DateTime Fields Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Event Date',
|
||||
name: 'event_date',
|
||||
type: 'date',
|
||||
default: '',
|
||||
placeholder: 'Select a date',
|
||||
help_text: 'Select the date for your event',
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
display_name: 'Meeting Time',
|
||||
name: 'meeting_time',
|
||||
type: 'datetime',
|
||||
default: '',
|
||||
placeholder: 'Select date and time',
|
||||
help_text: 'Select the date and time for your meeting',
|
||||
optional: false,
|
||||
datetime_config: {
|
||||
time_interval: 60,
|
||||
},
|
||||
},
|
||||
],
|
||||
submit_label: 'Submit',
|
||||
notify_on_cancel: true,
|
||||
state: 'datetime_state',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Date field with min_date constraint - MM-T2530C
|
||||
function getMinDateConstraintDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/datetime_dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'mindate_callback',
|
||||
title: 'DateTime Fields Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Future Date Only',
|
||||
name: 'future_date',
|
||||
type: 'date',
|
||||
default: '',
|
||||
placeholder: 'Select a future date',
|
||||
help_text: 'Must be today or later',
|
||||
optional: true,
|
||||
datetime_config: {
|
||||
min_date: 'today',
|
||||
},
|
||||
},
|
||||
],
|
||||
submit_label: 'Submit',
|
||||
notify_on_cancel: true,
|
||||
state: 'datetime_state',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// DateTime field with custom time interval - MM-T2530D
|
||||
function getCustomIntervalDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/datetime_dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'interval_callback',
|
||||
title: 'DateTime Fields Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Custom Interval Time',
|
||||
name: 'interval_time',
|
||||
type: 'datetime',
|
||||
default: '',
|
||||
placeholder: 'Select time (30min intervals)',
|
||||
help_text: 'Time picker with 30-minute intervals',
|
||||
optional: true,
|
||||
datetime_config: {
|
||||
time_interval: 30,
|
||||
},
|
||||
},
|
||||
],
|
||||
submit_label: 'Submit',
|
||||
notify_on_cancel: true,
|
||||
state: 'datetime_state',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Relative date values test - MM-T2530F
|
||||
function getRelativeDateDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/datetime_dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'relative_callback',
|
||||
title: 'DateTime Fields Test',
|
||||
icon_url: 'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Relative Date Example',
|
||||
name: 'relative_date',
|
||||
type: 'date',
|
||||
default: 'today',
|
||||
placeholder: 'Today by default',
|
||||
help_text: 'Defaults to today using relative date',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Relative DateTime Example',
|
||||
name: 'relative_datetime',
|
||||
type: 'datetime',
|
||||
default: '+1d',
|
||||
placeholder: 'Tomorrow by default',
|
||||
help_text: 'Defaults to tomorrow using relative date',
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
submit_label: 'Submit',
|
||||
notify_on_cancel: true,
|
||||
state: 'datetime_state',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy function for backward compatibility - returns basic datetime dialog
|
||||
function getDateTimeDialog(triggerId, webhookBaseUrl) {
|
||||
return getBasicDateTimeDialog(triggerId, webhookBaseUrl);
|
||||
}
|
||||
|
||||
function getTimezoneManualDialog(triggerId, webhookBaseUrl) {
|
||||
return createDialog(triggerId, webhookBaseUrl, {
|
||||
callback_id: 'timezone_manual',
|
||||
title: 'Timezone & Manual Entry Demo',
|
||||
introduction_text: '**Timezone & Manual Entry Demo**\n\n' +
|
||||
'This dialog demonstrates timezone support and manual time entry features.',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Your Local Time (Manual Entry)',
|
||||
name: 'local_manual',
|
||||
type: 'datetime',
|
||||
help_text: 'Type any time: 9am, 14:30, 3:45pm - no rounding',
|
||||
datetime_config: {
|
||||
manual_time_entry: true,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
display_name: 'London Office Hours (Dropdown)',
|
||||
name: 'london_dropdown',
|
||||
type: 'datetime',
|
||||
help_text: 'Times shown in GMT - select from 60 min intervals',
|
||||
datetime_config: {
|
||||
location_timezone: 'Europe/London',
|
||||
time_interval: 60,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
display_name: 'London Office Hours (Manual Entry)',
|
||||
name: 'london_manual',
|
||||
type: 'datetime',
|
||||
help_text: 'Type time in GMT: 9am, 14:30, 3:45pm - no rounding',
|
||||
datetime_config: {
|
||||
location_timezone: 'Europe/London',
|
||||
manual_time_entry: true,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function getFileUploadDialog(triggerId, webhookBaseUrl) {
|
||||
return {
|
||||
trigger_id: triggerId,
|
||||
url: `${webhookBaseUrl}/dialog_submit`,
|
||||
dialog: {
|
||||
callback_id: 'somecallbackid',
|
||||
title: 'Title for Dialog Test with file upload element',
|
||||
icon_url:
|
||||
'https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png',
|
||||
submit_label: 'Submit File Upload Test',
|
||||
notify_on_cancel: true,
|
||||
state: 'somestate',
|
||||
elements: [
|
||||
{
|
||||
display_name: 'Upload Single Document',
|
||||
name: 'single_document',
|
||||
type: 'file',
|
||||
placeholder: 'Select one document...',
|
||||
help_text: 'Upload a single document (replaces previous selection).',
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
display_name: 'Upload Multiple Files',
|
||||
name: 'multiple_files',
|
||||
type: 'file',
|
||||
allow_multiple: true,
|
||||
placeholder: 'Select multiple files...',
|
||||
help_text: 'Upload multiple files (can select and add more).',
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
display_name: 'Description',
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
subtype: '',
|
||||
default: '',
|
||||
placeholder: 'Describe the uploaded files...',
|
||||
help_text: 'Provide a description for the uploaded files.',
|
||||
optional: true,
|
||||
min_length: 0,
|
||||
max_length: 500,
|
||||
data_source: '',
|
||||
options: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getActionButtonParentDialog(triggerId, webhookBaseUrl) {
|
||||
const config = {
|
||||
...DIALOG_CONFIGS.actionButtonParent,
|
||||
elements: [
|
||||
createElement('text', {display_name: 'Your Name', name: 'your_name', placeholder: 'Enter your name', optional: true}),
|
||||
|
||||
// Two action buttons on the same dialog. Each carries a distinct
|
||||
// context.source so the child dialog can reflect which one was pressed.
|
||||
createElement('action_button', {
|
||||
display_name: 'Open Details',
|
||||
name: 'open_details',
|
||||
action_button: {
|
||||
url: `${webhookBaseUrl}/dialog/open_child`,
|
||||
context: {source: 'Details'},
|
||||
},
|
||||
}),
|
||||
createElement('action_button', {
|
||||
display_name: 'Open Summary',
|
||||
name: 'open_summary',
|
||||
action_button: {
|
||||
url: `${webhookBaseUrl}/dialog/open_child`,
|
||||
context: {source: 'Summary'},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
return createDialog(triggerId, webhookBaseUrl, config);
|
||||
}
|
||||
|
||||
// `source` comes from the pressed action button's context.source and is reflected
|
||||
// in the child dialog's title and introduction text, so a test can verify which
|
||||
// button opened it.
|
||||
function getActionButtonChildDialog(triggerId, webhookBaseUrl, source) {
|
||||
const label = source || 'Unknown';
|
||||
const config = {
|
||||
...DIALOG_CONFIGS.actionButtonChild,
|
||||
title: `${label} Dialog`,
|
||||
introduction_text: `This child dialog was opened from the "${label}" action button.`,
|
||||
};
|
||||
return createDialog(triggerId, webhookBaseUrl, config);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFullDialog,
|
||||
getSimpleDialog,
|
||||
getBasicDialog,
|
||||
getUserAndChannelDialog,
|
||||
getBooleanDialog,
|
||||
getCheckboxGroupDialog,
|
||||
getFieldRefreshDialog,
|
||||
getMultistepStep1Dialog,
|
||||
getMultistepStep2Dialog,
|
||||
getMultistepStep3Dialog,
|
||||
getMultiSelectDialog,
|
||||
getDynamicSelectDialog,
|
||||
getDateTimeDialog,
|
||||
getBasicDateDialog,
|
||||
getBasicDateTimeDialog,
|
||||
getMinDateConstraintDialog,
|
||||
getCustomIntervalDialog,
|
||||
getRelativeDateDialog,
|
||||
getTimezoneManualDialog,
|
||||
getFileUploadDialog,
|
||||
getActionButtonParentDialog,
|
||||
getActionButtonChildDialog,
|
||||
};
|
||||
Reference in New Issue
Block a user