Closed farooqmdqa closed 2 months ago
First, you must upload all files to S3, including all attachments such as images and videos.
If you are opening the index.html file directly from an email, you have to use absolute attachments URLs which is link to S3.
Try use option attachmentPath
//Reporter Options
{
name: 'the report name',
outputFile: './test-results/report.html',
// attachment path handler
attachmentPath: (currentPath, extras) => `https://your-domain/s3-path-to/${currentPath}`
// ...
}
@cenfun Thank you for your response. Still I am facing issue of unable to view test attachments(screenshots, video) embedded in tests in generated presigned url of index.html file in Monocart reporter UI after uploading the index.html in test-results to AWS S3 bucket whereas I can view the screenshots, video in index.html inside the zip file which is downloaded after generating presigned url for zip file after uploading to AWS S3 bucket. The reason to be visible in Zip file is I have uploaded all assests/media files to AWS S3 and created presigned urls for all assests files. but it is not working for presigned url of index.html file.
When inspected Monocart reporter through DEV tools observe that complete/full presigned url of the assessts/media file is not visible i.e. Url appended with X-Amz-AWS Signature till Getobject in img tag/src attribute. Attached screenshot for reference.
When I replaced complete presigned url in img tag/src attribute then screenshot is visbile. Not sure why complete presigned url is not loaded in Monocart reporter index.html file after uploading to AWS S3 bucket and genertating presigned url of it.
I believe that there is a issue of presigned url with Monocart reporter UI i.e. unable to view embeded complete/full presigned url under img tag/src attribute in DOM.
Could you please check below code and provide refactor code to view the embedded test attachment(screenshot,video) in generated presigned url of index.html. or Could you please provide working code inTypscript that resolve the issue which I am facing without any errors in Playwright.
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter'; import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { fromIni } from '@aws-sdk/credential-providers'; import as fs from 'fs'; import archiver from 'archiver'; import path from 'path'; import as nodemailer from 'nodemailer'; import * as aws from '@aws-sdk/client-ses'; import AdmZip from 'adm-zip';
class MyReporter implements Reporter { private totalTests = 0; private passedTests = 0; private skippedTests = 0; private failedTests = 0; private totalDuration = 0; private s3Client: S3Client; private reportDir: string; private zipPath: string; private bucketName = '';
constructor() {
this.s3Client = new S3Client({
region: '',
apiVersion: '',
endpoint: "",
credentials: fromIni({ profile: 'default' })
});
const timestamp = new Date().toISOString().slice(0, 10);
this.zipPath = './test-results.zip';
this.reportDir = './test-results';
}
onBegin(config: FullConfig, suite: Suite) {
this.totalTests = suite.allTests().length;
console.log('=========================================================================================================================================');
console.log(Starting the run with ${this.totalTests} tests);
console.log('=========================================================================================================================================');
}
onTestBegin(test: TestCase, result: TestResult) {
console.log(Starting test ${test.title});
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(Finished test ${test.title}: ${result.status});
this.totalDuration += result.duration;
if (result.status === 'passed') {
this.passedTests += 1;
} if (result.status === 'failed') {
this.failedTests += 1;
} else if (result.status === 'skipped') {
this.skippedTests += 1;
}
console.log('=========================================================================================================================================');
}
async onEnd(result: FullResult) {
console.log('=========================================================================================================================================');
console.log(Finished the run: ${result.status});
const summary = this.generateSummary();
console.log(summary);
console.log('=========================================================================================================================================');
}
async onExit(): Promise<void> {
try {
const zipPath = await this.zipMonocartReport();
const [indexHtmlUrl, zipUrl] = await this.uploadAndGetPreSignedUrls();
await this.sendEmail(indexHtmlUrl, zipUrl);
} catch (error) {
console.error('Error in report generation and sending:', error);
}
console.log('=========================================================================================================================================');
}
generateSummary() {
return
Test Suite Summary:
-------------------
Total Tests: ${this.totalTests}
Passed Tests: ${this.passedTests}
Failed Tests: ${this.failedTests}
Skipped Tests: ${this.skippedTests}
Total Duration: ${this.totalDuration}ms
;
}
private async zipMonocartReport(): Promise<string> {
return new Promise((resolve, reject) => {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(this.zipPath);
archive
.directory(this.reportDir, false)
.on('error', err => reject(err))
.pipe(stream);
stream.on('close', () => resolve(this.zipPath));
archive.finalize();
});
}
/*
private async uploadAssetsAndGetUrls(extractPath: string): Promise<Map<string, string>> {
const files = await this.getAllFiles(extractPath);
const assetUrls = new Map<string, string>();
for (const file of files) {
try {
const key = path.relative(extractPath, file);
const content = await fs.promises.readFile(file);
const contentType = this.getContentType(file);
// Only upload non-HTML files
if (path.extname(file).toLowerCase() !== '.html') {
await this.uploadFile(key, content, contentType);
const presignedUrl = await this.getPresignedUrl(key);
assetUrls.set(key, presignedUrl);
}
} catch (error) {
console.error(Error processing file ${file}:, error);
}
}
return assetUrls;
}
*/
private async uploadAssets(extractPath: string): Promise<void> {
const files = await this.getAllFiles(extractPath);
for (const file of files) {
try {
const key = path.relative(extractPath, file);
const content = await fs.promises.readFile(file);
const contentType = this.getContentType(file);
// Only upload non-HTML files directly
if (path.extname(file).toLowerCase() !== '.html') {
await this.uploadFile(key, content, contentType);
}
} catch (error) {
console.error(Error processing file ${file}:, error);
}
}
}
private async uploadAndGetPreSignedUrls(): Promise<[string, string]> {
try {
console.log('Starting uploadAndGetPreSignedUrls process...');
const zip = new AdmZip(this.zipPath);
const extractPath = path.join(path.dirname(this.zipPath), 'extracted');
await fs.promises.mkdir(extractPath, { recursive: true });
zip.extractAllTo(extractPath, true);
console.log(Files extracted to: ${extractPath});
// Upload all assets and get their presigned URLs
const assetUrls = await this.uploadAssetsAndGetUrls(extractPath);
console.log('Asset URLs generated:', assetUrls);
// Modify and upload index.html
const htmlFilePath = path.join(extractPath, 'index.html');
const modifiedHtmlContent = await this.modifyIndexHtml(htmlFilePath, assetUrls);
await this.uploadFile('index.html', modifiedHtmlContent, 'text/html');
console.log('Modified index.html uploaded successfully');
// Upload zip file
const zipFileName = path.basename(this.zipPath);
const zipContent = await fs.promises.readFile(this.zipPath);
await this.uploadFile(zipFileName, zipContent, 'application/zip');
console.log('ZIP file uploaded successfully');
// Generate presigned URLs for index.html and zip
const indexHtmlUrl = await this.getPresignedUrl('index.html');
const zipUrl = await this.getPresignedUrl(zipFileName);
console.log(Index HTML Pre-signed URL: ${indexHtmlUrl});
console.log(ZIP Pre-signed URL: ${zipUrl});
// Verify accessibility of assets
await this.verifyAssetAccessibility(assetUrls);
// Clean up extracted files
await fs.promises.rm(extractPath, { recursive: true, force: true });
console.log('Cleaned up extracted files');
return [indexHtmlUrl, zipUrl];
} catch (error) {
console.error('Error in uploadAndGetPreSignedUrls:', error);
throw error;
}
}
private async uploadAssetsAndGetUrls(extractPath: string): Promise<{ [key: string]: string }> {
const files = await this.getAllFiles(extractPath);
const assetUrls: { [key: string]: string } = {};
for (const file of files) {
try {
const key = path.relative(extractPath, file);
const content = await fs.promises.readFile(file);
const contentType = this.getContentType(file);
// Only upload non-HTML files
if (path.extname(file).toLowerCase() !== '.html') {
await this.uploadFile(key, content, contentType);
const presignedUrl = await this.getPresignedUrl(key);
assetUrls[key] = presignedUrl;
console.log(Uploaded and generated presigned URL for: ${key});
}
} catch (error) {
console.error(Error processing file ${file}:, error);
}
}
return assetUrls;
}
private async modifyIndexHtml(htmlFilePath: string, assetUrls: { [key: string]: string }): Promise<string> {
let htmlContent = await fs.promises.readFile(htmlFilePath, 'utf-8');
console.log('Original HTML content length:', htmlContent.length);
// Replace all asset references with their presigned URLs
Object.entries(assetUrls).forEach(([key, url]) => {
const regex = new RegExp((src|href)=['"](${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})['"], 'g');
const replacements = htmlContent.match(regex);
htmlContent = htmlContent.replace(regex, $1="${url}");
console.log(Replaced ${replacements?.length || 0} occurrences of ${key} with presigned URL);
});
// Inject the reportConfig script
const reportConfigScript =
<script>
window.reportConfig = {
attachmentPath: (currentPath) => {
console.log('Resolving path:', currentPath);
const url = new URL(currentPath, window.location.href);
console.log('Resolved URL:', url.href);
return url.href;
}
};
</script>
;
htmlContent = htmlContent.replace('</head>', ${reportConfigScript}</head>);
console.log('Injected reportConfig script');
console.log('Modified HTML content length:', htmlContent.length);
return htmlContent;
}
private async verifyAssetAccessibility(assetUrls: { [key: string]: string }): Promise<void> {
for (const [key, url] of Object.entries(assetUrls)) {
try {
const response = await fetch(url);
if (!response.ok) {
console.error(Asset ${key} is not accessible. Status: ${response.status});
} else {
console.log(Asset ${key} is accessible);
}
} catch (error) {
console.error(Error verifying asset ${key}:, error);
}
}
}
private async uploadFile(key: string, content: Buffer | string, contentType: string): Promise<void> {
try {
const uploadParams = {
Bucket: this.bucketName,
Key: key,
Body: content,
ContentType: contentType,
};
await this.s3Client.send(new PutObjectCommand(uploadParams));
console.log(Successfully uploaded ${key});
// Verify the upload
const headParams = {
Bucket: this.bucketName,
Key: key,
};
await this.s3Client.send(new HeadObjectCommand(headParams));
console.log(Verified upload of ${key});
} catch (error) {
console.error(Error uploading file ${key}:, error);
throw error;
}
}
private async getPresignedUrl(key: string): Promise<string> {
try {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 604800 }); // 7 days
console.log(Generated presigned URL for ${key});
return url;
} catch (error) {
console.error(Error generating presigned URL for ${key}:, error);
throw error;
}
}
private async getAllFiles(dir: string): Promise<string[]> {
const files = await fs.promises.readdir(dir, { withFileTypes: true });
const paths = await Promise.all(files.map((file) => {
const res = path.resolve(dir, file.name);
return file.isDirectory() ? this.getAllFiles(res) : res;
}));
return paths.flat();
}
private getContentType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.png':
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.gif':
return 'image/gif';
case '.mp4':
return 'video/mp4';
case '.webm':
return 'video/webm';
case '.html':
return 'text/html';
case '.css':
return 'text/css';
case '.js':
return 'application/javascript';
default:
return 'application/octet-stream';
}
}
async sendEmail(indexHtmlUrl: string, zipUrl: string) {
const ses = new aws.SES({
apiVersion: '',
region: '',
credentials: {
accessKeyId: '',
secretAccessKey: ''
}
});
const transporter = nodemailer.createTransport({
SES: { ses, aws },
});
const date = new Date().toISOString().slice(0, 10);
try {
const mailOptions = {
from: '', // Replace with verified sender email address
to: '', // Replace with recipient email address
//cc: '',
subject: Test Execution Summary Report - ${date},
html:
<p>Hello All,</p>
<h3>Test Execution Summary Report</h3>
<p>Total tests: ${this.totalTests}</p>
<p>Passed tests: ${this.passedTests}</p>
<p>Failed tests: ${this.failedTests}</p>
<p>Skipped Tests: ${this.skippedTests}</p>
<p>Total Duration: ${this.totalDuration}ms</p>
<p>To view the detailed report online: <a href="${indexHtmlUrl}">Click here</a>.</p>
<p>To download the full report ZIP file: <a href="${zipUrl}">Click here</a>.</p>
<p>These URLs will expire in 168 hours.</p>
<p>Thanks</p>
};
await transporter.sendMail(mailOptions);
console.log('Test summary email sent successfully with urls.');
} catch (error) {
console.error('Error sending test summary email:', error);
throw error;
}
}
}
export default MyReporter;
reporter: [
['list'], ['./src/test-report-scripts/custom-monocart-reporter.ts'],
['monocart-reporter', {
name: "Monocart Test Execution Summary Report",
outputFile: ./test-results/index.html
}]
],
Working perfectly fine for allure reporter which generates single index.html file embedded with all test attachments with acommand: allure generate --single-file ./allure-results and able to view those test attachments even in presigned url of index.html file generated after uploading to AWS S3 bucket.
But I really liked Monocart reporter reporting format and style. Because of the issue, I had to opt allure-reporter.
Would appreciate and opt Monocart reporter if you resolve the issue of unable to view the test attachements/assests files in presigned url of index.html file generated after uploading to AWS S3 bucket.
Thank you
I don't know why you create your own custom report ./src/test-report-scripts/custom-monocart-reporter.ts
, but please reconsider and try my approach.
// this folder include all attachments and html report
const outputDir = `./test-results`;
module.exports = {
outputDir: outputDir,
reporter: [
['list'],
['monocart-reporter', {
name: `Monocart Test Execution Summary Report`,
outputFile: `${outputDir}/index.html`,
// absolute link for email, replace your attachment's path here
attachmentPath: (currentPath, extras) => `https://your-s3-domain/path-to/${currentPath}`,
// your custom scripts
onEnd: async (reportData, helper) => {
// console.log(reportData.summary);
// filter failed cases
// const failedCases = helper.filter((item, parent) => item.type === 'case' && item.caseType === 'failed');
// console.log(failedCases.map((it) => it.title));
// Iterate all items
// helper.forEach((item, parent) => {
// do something
// });
// 1, zip whole `./test-results/` folder here
// 2, upload it to S3 here
// 3, send email here
// see more Integration Examples https://github.com/cenfun/playwright-reporter-integrations
}
}]
]
};
Because you have used two custom reports (monocart-reporter and your custom-monocart-reporter.ts), but I don't think they can be merged or share resources. Because I don't have a private AWS S3 bucket for test, I can't create an example for you, but I believe it should be easy to follow the approach I mentioned above.
here is a example to unzip files in S3 https://medium.com/@AliAzG/how-to-extract-large-zip-files-in-an-amazon-s3-bucket-by-using-aws-ec2-and-python-f3973491a5b3
As an alternative, we can directly upload all the files in the directory test-results
to S3.
BTW, I'd like to remind you that you may need to enable access permissions for S3 files in AWS CloudFront.
Hello,
I am facing issue of unable to view the screenshots, videos in test attachments in single index html file generated by Monocart reporter after uploading to AWS S3 bucket and generating presigned url. When clicked on the Download Image link then showing File wasn't available.
Generating presigned urls for zip file and index.html file. Presigned Url for index.html file generated after test execution to view it when clicked on the link in the email body. Presigned Url for zip file to zip the report folder and download when clicked on the link in the email body
Reason for zip file:
I tried by generating presigned URLs of all media files(.png, jpeg, webm) and also singl index.html file after uploading to AWS S3 bucket.
Here is the code which I am using to generate presigned urls for zip and as well as for single index.html file geneated after test execution. Still this code is not working.
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { fromIni } from '@aws-sdk/credential-providers'; import as fs from "fs"; import as path from "path"; import archiver from 'archiver'; import as nodemailer from 'nodemailer'; import as aws from '@aws-sdk/client-ses'; import { glob } from 'glob'; import * as cheerio from 'cheerio';
async function zipMonocartReport(sourceDir: string, outPath: string): Promise {
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(outPath);
}
class MyReporter implements Reporter { private totalTests = 0; private passedTests = 0; private skippedTests = 0; private failedTests = 0; private totalDuration = 0; private s3Client: S3Client; private reportDir: string; private zipPath: string; private bucketName: string;
}
export default MyReporter;
Monocart-reporter generating single index html file embedded with test attachments in the local/system but after uploading to AWS S3 bucket and we do not see the screenshots, videos as attached in generated presigned url. Not sure how Allure reporter generating single html file embedded with test attachments and able to view those test attachments in generated presigned url.
Could you please refactor above code so that we can view the screenshots, videos in test attachments in generated presigned url for single indext html file generated by Monocart reporter.
Thank you.