@paschal_cheps/cypress-ms-teams-reporter
Version:
A Cypress reporter for Microsoft Teams with mochawesome support
335 lines (302 loc) • 10.3 kB
text/typescript
import fs from "fs/promises";
import chalk from "chalk";
import { Command } from "commander";
import { getCIProvider } from "./ciProviders.js";
import { IncomingWebhook } from "ms-teams-webhook"; // Import IncomingWebhook
import path from "path"; // Import path for config file
import { pathExists } from "fs-extra"; // Import pathExists from fs-extra
// Define interfaces
interface Fact {
name: string;
value: string;
}
interface ReportConfig {
teamsWebhookUrl?: string;
reportDir?: string;
reportFilename?: string;
reportTitle?: string;
ciProvider?: string;
customFacts?: Fact[];
projectRoot?: string;
verbose?: boolean; // Added verbose option
reportUrl?: string; // Add reportUrl here
customUrl?: string; // Add customUrl here
}
// Load package.json version dynamically
const packageJson = JSON.parse(
await fs.readFile(new URL("../package.json", import.meta.url), "utf-8")
);
// CLI Command setup
const program = new Command();
program
.version(packageJson.version)
.option(
"--ci-provider <type>",
"CI Provider [github|bitbucket|circleci|jenkins|none|local]",
"github"
)
.option(
"--custom-url <type>",
"Custom test report URL (if --ci-provider=custom)",
""
)
.option(
"--report-dir <type>",
"Mochawesome report directory",
"cypress/reports"
)
.option(
"--screenshot-dir <type>",
"Cypress screenshot directory",
"cypress/screenshots"
)
.option("--video-dir <type>", "Cypress video directory", "cypress/videos")
.option("--verbose", "Show detailed log output", false)
.option("--only-failed", "Only send notifications for failed tests", false)
.option("--custom-text <type>", "Additional text in Teams message")
.option(
"--module-name <type>",
"Module name associated with the test run",
process.env.MODULE_NAME || ""
)
.option(
"--team-name <type>",
"Team name receiving the test report",
process.env.TEAM_NAME || ""
)
.option(
"--config-file <path>",
"Path to the configuration file for the Teams reporter",
"teamsReport.config.js" // Default config file name changed here
)
.parse(process.argv);
const options = program.opts();
const REPORT_PATH = `${options.reportDir}/html/index.json`; // Adjust this path based on your Mochawesome JSON output
const TEAMS_WEBHOOK_URL = process.env.TEAMS_WEBHOOK_URL;
const DEFAULT_CONFIG: ReportConfig = {
teamsWebhookUrl:
process.env.TEAMS_WEBHOOK_URL || "https://default-webhook-url", // Default Teams Webhook URL
reportDir: "cypress/reports",
reportFilename: "index.json",
reportTitle: "Cypress E2E Tests",
ciProvider: "github",
customFacts: [],
projectRoot: process.cwd(),
verbose: false, // Default verbose to false
};
async function loadConfig(
projectRoot: string = process.cwd()
): Promise<ReportConfig> {
const configFileName = options.configFile;
const configPath = path.resolve(projectRoot, configFileName);
let finalConfig: ReportConfig = {
...DEFAULT_CONFIG,
projectRoot,
verbose: options.verbose,
};
if (options.ciProvider && options.ciProvider !== "github") {
// Only override if a value other than default is provided
finalConfig.ciProvider = options.ciProvider;
}
if (options.verbose) {
console.log(
chalk.blue("Command-line CI Provider Option:"),
options.ciProvider
);
}
try {
if (await pathExists(configPath)) {
const importedConfig = await import(configPath);
const loadedConfig = importedConfig.default as ReportConfig; // Access the default export
if (options.verbose) {
console.log(chalk.yellow(`⚙️ Loaded config from: ${configPath}`));
}
finalConfig = { ...finalConfig, ...loadedConfig }; // Merge loaded config into the base config
} else {
if (options.verbose) {
console.log(
chalk.yellow(
`⚠️ No config file found at: ${configPath}. Using default settings.`
)
);
}
}
} catch (error) {
console.error(
chalk.red(`❌ Error loading config file from ${configPath}:`),
error
);
}
return finalConfig;
}
const config = await loadConfig(process.cwd());
// Fetch artifact URL based on CI Provider
const ciProvider = getCIProvider(config.ciProvider);
const artifactUrlFromProvider = await ciProvider.getArtifactUrl();
let REPORT_URL = options.customUrl;
if (!REPORT_URL && config.ciProvider === "local") {
REPORT_URL = config.customUrl || config.reportUrl;
}
if (!REPORT_URL) {
REPORT_URL = artifactUrlFromProvider || "No report URL available";
}
if (options.verbose) {
console.log(chalk.blue("Configured CI Provider:"), config.ciProvider);
console.log(
chalk.blue("Artifact URL from Provider:"),
artifactUrlFromProvider
);
console.log(chalk.blue("Using TEAMS_WEBHOOK_URL:"), config.teamsWebhookUrl); // Log the webhook URL in verbose mode
}
console.log(chalk.blueBright(`🚀 Running version: ${packageJson.version}`));
console.log(chalk.cyan(`📢 Report URL: ${REPORT_URL}`));
// Read and process the test report
async function displayTestResults(report: any) {
const stats = report.stats;
let allTests: any[] = [];
function collectTests(suite: any) {
allTests = allTests.concat(suite.tests || []);
(suite.suites || []).forEach(collectTests);
}
if (report.results && report.results.length > 0) {
const rootSuites = report.results[0].suites;
rootSuites.forEach(collectTests);
}
const total = stats.tests;
const passed = stats.passes;
const failed = stats.failures;
const pending = stats.pending;
const duration = (stats.duration / 1000).toFixed(2);
const start = new Date(stats.start).toLocaleString();
const end = new Date(stats.end).toLocaleString();
const passPercentage = ((passed / total) * 100).toFixed(1);
const failPercentage = ((failed / total) * 100).toFixed(1);
const pendingPercentage = ((pending / total) * 100).toFixed(1);
// Pie chart with 11 segments
const totalSegments = 11;
const passSegments = Math.round((passed / total) * totalSegments);
const failSegments = Math.round((failed / total) * totalSegments);
const pendingSegments = Math.round((pending / total) * totalSegments);
const emptySegments =
totalSegments - (passSegments + failSegments + pendingSegments);
let pieChart =
"🟢".repeat(passSegments) +
"🔴".repeat(failSegments) +
"⚠️".repeat(pendingSegments) +
"⚪".repeat(emptySegments);
const failedTests = allTests
.filter((test) => test.state === "failed")
.map((test) => ({
title: test.title,
error: test.err?.message || "No error message available",
context: test.context ? JSON.parse(test.context)[0].value : "",
}));
return {
total,
passed,
failed,
pending,
duration,
start,
end,
passPercentage,
failPercentage,
pendingPercentage,
pieChart,
failedTests,
};
}
// Send results to Microsoft Teams
async function sendTeamsReport(options: any) {
try {
const teamsWebhookUrl = config.teamsWebhookUrl; // Use the webhook URL from the loaded config
if (!teamsWebhookUrl) {
console.error(
chalk.red("❌ Missing TEAMS_WEBHOOK_URL in the configuration")
);
return;
}
const webhook = new IncomingWebhook(teamsWebhookUrl); // Instantiate IncomingWebhook
// Check if the report file exists
if (!(await pathExists(REPORT_PATH))) {
console.error(chalk.red(`❌ Report file not found at: ${REPORT_PATH}`));
return;
}
const reportData = await fs.readFile(REPORT_PATH, "utf-8");
const report = JSON.parse(reportData);
const {
total,
passed,
failed,
pending,
duration,
start,
end,
passPercentage,
failPercentage,
pendingPercentage,
pieChart,
failedTests,
} = await displayTestResults(report);
const allTestsPassed = failed === 0;
const buttonColor = allTestsPassed ? "00FF00" : "FF0000";
const buttonText = allTestsPassed
? "View Full Report (All Tests Passed)"
: "View Full Report (Some Tests Failed)";
const message = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
themeColor: buttonColor,
summary: "Cypress Test Results",
sections: [
{
activityTitle: `${options.moduleName} QA Test Report Summary`,
activitySubtitle: `Test Run: ${start} - ${end}\nTotal Tests: ${total} | Duration: ${duration}s`,
activityImage:
"https://d2ef4hkqu4id.cloudfront.net/cy_logo_a009bb69d4.jpeg",
facts: [
{ name: "Passed", value: `✅ ${passed} (${passPercentage}%)` },
{ name: "Failed", value: `❌ ${failed} (${failPercentage}%)` },
{ name: "Pending", value: `⚠️ ${pending} (${pendingPercentage}%)` },
],
markdown: true,
},
{
title: "Test Status Distribution",
text: `📊 **Pie Chart**: ${pieChart}\n✅ Passed: ${passPercentage}% 🟢\n❌ Failed: ${failPercentage}% 🔴\n⚠️ Pending: ${pendingPercentage}% ⚠️`,
markdown: true,
},
{
title: allTestsPassed ? "All Tests Passed!" : "Failed Tests",
text: allTestsPassed
? "All tests executed successfully!"
: failedTests
.map(
(t) =>
`❌ **${t.title}**\nError:\n\`${t.error}\`\nVideo: ${
t.context ? t.context : "N/A"
}`
)
.join("\n"),
markdown: true,
},
],
potentialAction: [
{
"@type": "OpenUri",
name: buttonText,
targets: [{ os: "default", uri: REPORT_URL }],
},
],
};
await webhook.send(message); // Use the send method of IncomingWebhook
console.log(chalk.green("✅ Teams notification sent successfully!"));
} catch (error: any) {
console.error(
chalk.red("❌ Failed to send Teams notification:", error.message)
);
console.error(chalk.red("Stack Trace:"), error.stack); // Log stack trace for debugging
}
}
export { sendTeamsReport };