|
17 | 17 | import fs from 'fs'; |
18 | 18 | import path from 'path'; |
19 | 19 | import url from 'url'; |
20 | | -import { test as baseTest, expect as baseExpect, createImage } from './playwright-test-fixtures'; |
| 20 | +import * as yazl from 'yazl'; |
| 21 | +import { test as baseTest, expect as baseExpect, cliEntrypoint, createImage } from './playwright-test-fixtures'; |
21 | 22 | import { iso, utils } from '../../packages/playwright-core/lib/coreBundle'; |
22 | 23 |
|
23 | 24 | type HttpServer = utils.HttpServer; |
@@ -3524,6 +3525,84 @@ test('should support merge files option', async ({ runInlineTest, showReport, pa |
3524 | 3525 | `); |
3525 | 3526 | }); |
3526 | 3527 |
|
| 3528 | +test.describe('show-report .zip support', () => { |
| 3529 | + test('should serve a zipped report', async ({ runInlineTest, childProcess, findFreePort, page }, testInfo) => { |
| 3530 | + await runInlineTest({ |
| 3531 | + 'a.test.js': ` |
| 3532 | + import { test, expect } from '@playwright/test'; |
| 3533 | + test('passes', async ({}) => {}); |
| 3534 | + `, |
| 3535 | + }, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); |
| 3536 | + |
| 3537 | + const reportFolder = testInfo.outputPath('playwright-report'); |
| 3538 | + const zipPath = testInfo.outputPath('report.zip'); |
| 3539 | + await zipDirectory(reportFolder, zipPath); |
| 3540 | + |
| 3541 | + const port = await findFreePort(); |
| 3542 | + const proc = childProcess({ |
| 3543 | + command: ['node', cliEntrypoint, 'show-report', zipPath, `--port=${port}`], |
| 3544 | + cwd: testInfo.outputPath(), |
| 3545 | + env: { ...process.env, PLAYWRIGHT_HTML_OPEN: 'never' }, |
| 3546 | + }); |
| 3547 | + await proc.waitForOutput('Serving HTML report at'); |
| 3548 | + await page.goto(`http://localhost:${port}`); |
| 3549 | + await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('1'); |
| 3550 | + await expect(page.locator('.test-file-test-outcome-expected >> text=passes')).toBeVisible(); |
| 3551 | + }); |
| 3552 | + |
| 3553 | + test('should error on a non-zip non-directory path', async ({ runInlineTest, childProcess }, testInfo) => { |
| 3554 | + const filePath = testInfo.outputPath('not-a-report.txt'); |
| 3555 | + await fs.promises.writeFile(filePath, 'hello'); |
| 3556 | + const proc = childProcess({ |
| 3557 | + command: ['node', cliEntrypoint, 'show-report', filePath], |
| 3558 | + cwd: testInfo.outputPath(), |
| 3559 | + }); |
| 3560 | + const { exitCode } = await proc.exited; |
| 3561 | + expect(exitCode).toBe(1); |
| 3562 | + expect(proc.output).toContain(`No report found at "${filePath}"`); |
| 3563 | + }); |
| 3564 | + |
| 3565 | + test('should error when zip lacks a top-level index.html', async ({ childProcess }, testInfo) => { |
| 3566 | + const zipPath = testInfo.outputPath('nested.zip'); |
| 3567 | + const zipFile = new yazl.ZipFile(); |
| 3568 | + const finished = new Promise<void>(resolve => zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', () => resolve())); |
| 3569 | + zipFile.addBuffer(Buffer.from('<html></html>'), 'nested/index.html'); |
| 3570 | + zipFile.end(); |
| 3571 | + await finished; |
| 3572 | + |
| 3573 | + const proc = childProcess({ |
| 3574 | + command: ['node', cliEntrypoint, 'show-report', zipPath], |
| 3575 | + cwd: testInfo.outputPath(), |
| 3576 | + }); |
| 3577 | + const { exitCode } = await proc.exited; |
| 3578 | + expect(exitCode).toBe(1); |
| 3579 | + expect(proc.output).toContain(`No "index.html" found at the top level of "${zipPath}"`); |
| 3580 | + }); |
| 3581 | +}); |
| 3582 | + |
| 3583 | +async function zipDirectory(sourceDir: string, zipPath: string): Promise<void> { |
| 3584 | + const zipFile = new yazl.ZipFile(); |
| 3585 | + const finished = new Promise<void>((resolve, reject) => { |
| 3586 | + zipFile.outputStream.pipe(fs.createWriteStream(zipPath)) |
| 3587 | + .on('close', () => resolve()) |
| 3588 | + .on('error', reject); |
| 3589 | + }); |
| 3590 | + const walk = async (dir: string, relative: string) => { |
| 3591 | + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); |
| 3592 | + for (const entry of entries) { |
| 3593 | + const absolute = path.join(dir, entry.name); |
| 3594 | + const relativeEntry = relative ? `${relative}/${entry.name}` : entry.name; |
| 3595 | + if (entry.isDirectory()) |
| 3596 | + await walk(absolute, relativeEntry); |
| 3597 | + else if (entry.isFile()) |
| 3598 | + zipFile.addFile(absolute, relativeEntry); |
| 3599 | + } |
| 3600 | + }; |
| 3601 | + await walk(sourceDir, ''); |
| 3602 | + zipFile.end(); |
| 3603 | + await finished; |
| 3604 | +} |
| 3605 | + |
3527 | 3606 | function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> { |
3528 | 3607 | return new Promise(resolve => { |
3529 | 3608 | const chunks: Buffer[] = []; |
|
0 commit comments