cenfun / monocart-reporter

A playwright test reporter (Node.js)
https://cenfun.github.io/monocart-reporter/
MIT License
207 stars 12 forks source link

Unable to view the screenshots, videos in test attachments in presigned url generated after uploading index.html file to AWS S3 bucket #135

Closed farooqmdqa closed 2 months ago

farooqmdqa commented 3 months ago

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.

image

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);

return new Promise((resolve, reject) => {
    archive
        .directory(sourceDir, false)
        .on('error', err => reject(err))
        .pipe(stream);

    stream.on('close', () => resolve(outPath));
    archive.finalize();
});

}

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;

constructor() {
    this.s3Client = new S3Client({
        region: '',
        apiVersion: '',
        endpoint: "",
        credentials: fromIni({ profile: 'default' })
    });

    const timestamp = new Date().toISOString().slice(0, 10);
    this.zipPath = `./playwright-monocart-report-${timestamp}.zip`;
    this.reportDir = `./playwright-monocart-report-${timestamp}`;
    this.bucketName = '';
}

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 {
        await this.uploadMediaFiles();
        await this.updateReportWithPresignedUrls();
        await this.zipMonocartReport();
        const [presignedUrl, zipUrl] = await this.uploadAndGetPreSignedUrls();
        await this.sendEmail(presignedUrl, zipUrl);
    } catch (error) {
        console.error('Error in report generation and sending:', error);
    }
}

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 uploadMediaFiles(): Promise<void> {
    const timestamp = new Date().toISOString().slice(0, 10);
    const mediaFiles = await glob(['**/*.png', '**/*.jpg', '**/*.webm','**/*.mp4,'], { cwd: `./playwright-monocart-report-${timestamp}` });

    for (const file of mediaFiles) {
        const filePath = path.join(`./playwright-monocart-report-${timestamp}`, file);
        const fileContent = await fs.promises.readFile(filePath);

        const uploadParams = {
            Bucket: this.bucketName,
            Key: `media/${file}`,
            Body: fileContent,
            ContentType: this.getContentType(file)
        };

        await this.s3Client.send(new PutObjectCommand(uploadParams));
        console.log(`Uploaded ${file} to S3`);
    }
}

private getContentType(filename: string): string {
    const ext = path.extname(filename).toLowerCase();
    switch (ext) {
        case '.png': return 'image/png';
        case '.jpg': case '.jpeg': return 'image/jpeg';
        case '.webm': return 'video/webm';
        case '.mp4': return 'video/mp4';
        default: return 'application/octet-stream';
    }
}

private async updateReportWithPresignedUrls(): Promise<void> {
    const indexPath = path.join(this.reportDir, 'index.html');
    const htmlContent = await fs.promises.readFile(indexPath, 'utf-8');
    const $ = cheerio.load(htmlContent);

    const mediaElements = $('img, video').toArray();
    const updatePromises = mediaElements.map(async (elem) => {
        const $elem = $(elem);
        const src = $elem.attr('src');
        if (src && (src.endsWith('.png') || src.endsWith('.jpg') || src.endsWith('.webm'))) {
            const presignedUrl = await this.getPresignedUrl(`media/${src}`);
            $elem.attr('src', presignedUrl);
        }
    });

    await Promise.all(updatePromises);

    await fs.promises.writeFile(indexPath, $.html());
    console.log('Updated report with presigned URLs for media files');
}

private async getPresignedUrl(key: string): Promise<string> {
    const command = new GetObjectCommand({
        Bucket: this.bucketName,
        Key: key
    });
    return getSignedUrl(this.s3Client, command, { expiresIn: 604800 });
}

private async uploadAndGetPreSignedUrls(): Promise<[string, string]> {
    const bucketName = '';
    const htmlFileName = 'index.html';
    const zipFileName = path.basename(this.zipPath);
    const htmlFilePath = path.join(this.reportDir, htmlFileName);

    try {
        const htmlContent = await fs.promises.readFile(htmlFilePath);
        const zipContent = await fs.promises.readFile(this.zipPath);

        const uploadHtmlParams = {
            Bucket: this.bucketName,
            Key: htmlFileName,
            Body: htmlContent,
            ContentType: 'text/html',
            CacheControl: 'no-cache'
        };

        const uploadZipParams = {
            Bucket: this.bucketName,
            Key: zipFileName,
            Body: zipContent,
            ContentType: 'application/zip',
            CacheControl: 'no-cache'
        };

        await this.s3Client.send(new PutObjectCommand(uploadHtmlParams));
        await this.s3Client.send(new PutObjectCommand(uploadZipParams));

        const getHtmlCommand = new GetObjectCommand({ Bucket: bucketName, Key: htmlFileName });
        const getZipCommand = new GetObjectCommand({ Bucket: bucketName, Key: zipFileName });

        const presignedUrl = await getSignedUrl(this.s3Client, getHtmlCommand, { expiresIn: 604800 });
        const zipUrl = await getSignedUrl(this.s3Client, getZipCommand, { expiresIn: 604800 });

        console.log(`HTML Pre-signed URL: ${presignedUrl}`);
        console.log(`ZIP Pre-signed URL: ${zipUrl}`);

        return [presignedUrl, zipUrl];
    } catch (error) {
        console.error('Error uploading and getting presigned URLs:', error);
        throw error;
    }
}

private async zipMonocartReport(): Promise<void> {
    try {
        await zipMonocartReport(this.reportDir, this.zipPath);
        console.log(`Monocart report zipped to ${this.zipPath}`);
    } catch (error) {
        console.error('Error zipping Monocart report:', error);
        throw error;
    }
}

private async sendEmail(presignedUrl: 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
            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 (with embedded screenshots and videos): <a href="${presignedUrl}">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 links.');
    } catch (error) {
        console.error('Error sending test summary email:', error);
        throw error;
    }
}

}

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.

cenfun commented 3 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}`

    // ...
}
farooqmdqa commented 2 months ago

@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.

image

Sample presigned url: https://xxxxxxxxxxxxxxxxxxxxxxxxxx.amazonaws.com/src-tests-suites-ClientsPa-18932-ilter-by-Onboarding-Clients-Google-Chrome%5Ctest-failed-1.png?**X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=XXXXXXXXX%2F20240804%2Fxxxxx%2Fs3%2Faws4_request&X-Amz-Date=XXXXXX&X-Amz-Expires=xxxxxx&X-Amz-Signature=&X-Amz-SignedHeaders=host&x-id=GetObject**

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

cenfun commented 2 months ago

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.