From 5635f5823f5bdf4185c00ec2d147d89264c949f9 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Mon, 10 Nov 2025 19:07:35 +0400 Subject: [PATCH 1/4] Add --skip-stdout and --skip-stderr options and extend attachment extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new command-line options for junit-upload and playwright-json-upload commands: - --skip-stdout: Control when to skip stdout from test results (on-success|never) - --skip-stderr: Control when to skip stderr from test results (on-success|never) Both options default to "never" (current behavior - always include output). When set to "on-success", stdout/stderr is excluded from passed tests but always included for failed, blocked, or skipped tests. This helps reduce payload size when tests have verbose logging while preserving debugging information for failures. Additionally, attachment extraction for JUnit XML has been extended to parse attachment markers from failure, error, and skipped element message attributes and text content, not just from system-out. This supports custom test framework configurations that embed attachment paths in error messages. Changes: - Added --skip-stdout and --skip-stderr command-line options to ResultUploadCommandModule - Updated Parser type signature to accept ParserOptions - Implemented conditional stdout/stderr filtering in junitXmlParser and playwrightJsonParser - Extended junitXmlParser to extract attachments from failure/error/skipped elements - Added comprehensive test coverage for new functionality - Updated README.md with usage examples Version bump: 0.4.1 → 0.4.2 --- README.md | 14 + package.json | 2 +- src/commands/resultUpload.ts | 12 + .../fixtures/junit-xml/webdriverio-real.xml | 133 +++++++++ src/tests/junit-xml-parsing.spec.ts | 143 +++++++++- src/tests/playwright-json-parsing.spec.ts | 262 +++++++++++++++++- .../ResultUploadCommandHandler.ts | 22 +- src/utils/result-upload/junitXmlParser.ts | 120 +++++--- .../result-upload/playwrightJsonParser.ts | 20 +- 9 files changed, 674 insertions(+), 54 deletions(-) create mode 100644 src/tests/fixtures/junit-xml/webdriverio-real.xml diff --git a/README.md b/README.md index f41f96f..83314c4 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ The `junit-upload` and `playwright-json-upload` commands upload test results fro - `--attachments` - Try to detect and upload any attachments with the test result - `--force` - Ignore API request errors, invalid test cases, or attachments - `--ignore-unmatched` - Suppress individual unmatched test messages, show summary only +- `--skip-stdout` - Control when to skip stdout from test results (choices: `on-success`, `never`; default: `never`) +- `--skip-stderr` - Control when to skip stderr from test results (choices: `on-success`, `never`; default: `never`) - `-h, --help` - Show command help ### Run Name Template Placeholders @@ -137,6 +139,18 @@ Ensure the required environment variables are defined before running these comma ``` This will show only a summary like "Skipped 5 unmatched tests" instead of individual error messages for each unmatched test. +9. Skip stdout/stderr for passed tests to reduce result payload size: + ```bash + qasphere junit-upload --skip-stdout on-success ./test-results.xml + ``` + This will exclude stdout from passed tests while still including it for failed, blocked, or skipped tests. + +10. Skip both stdout and stderr for passed tests: + ```bash + qasphere junit-upload --skip-stdout on-success --skip-stderr on-success ./test-results.xml + ``` + This is useful when you have verbose logging in tests but only want to see output for failures. + ## Test Report Requirements The QAS CLI requires test cases in your reports (JUnit XML or Playwright JSON) to reference corresponding test cases in QA Sphere. These references are used to map test results from your automation to the appropriate test cases in QA Sphere. If a report lacks these references or the referenced test case doesn't exist in QA Sphere, the tool will display an error message. diff --git a/package.json b/package.json index fe8eed8..5f5ea4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qas-cli", - "version": "0.4.1", + "version": "0.4.2", "description": "QAS CLI is a command line tool for submitting your automation test results to QA Sphere at https://qasphere.com/", "type": "module", "main": "./build/bin/qasphere.js", diff --git a/src/commands/resultUpload.ts b/src/commands/resultUpload.ts index b24acc1..ba1d0b0 100644 --- a/src/commands/resultUpload.ts +++ b/src/commands/resultUpload.ts @@ -53,6 +53,18 @@ export class ResultUploadCommandModule implements CommandModule + + + + + + + + + + (/Users/a/Developer/Hypersequent/bistro-e2e-webdriver/test/specs/contents.e2e.ts:12:21)]]> + + (/Users/a/Developer/Hypersequent/bistro-e2e-webdriver/test/specs/contents.e2e.ts:12:21) +]]> + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tests/junit-xml-parsing.spec.ts b/src/tests/junit-xml-parsing.spec.ts index 2b1b934..32c0636 100644 --- a/src/tests/junit-xml-parsing.spec.ts +++ b/src/tests/junit-xml-parsing.spec.ts @@ -10,7 +10,10 @@ describe('Junit XML parsing', () => { const xmlContent = await readFile(xmlPath, 'utf8') // This should not throw any exceptions - const testcases = await parseJUnitXml(xmlContent, xmlBasePath) + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) // Verify that we got the expected number of test cases expect(testcases).toHaveLength(12) @@ -48,7 +51,10 @@ describe('Junit XML parsing', () => { const xmlPath = `${xmlBasePath}/comprehensive-test.xml` const xmlContent = await readFile(xmlPath, 'utf8') - const testcases = await parseJUnitXml(xmlContent, xmlBasePath) + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) // Test specific scenarios from our comprehensive test const failureTests = testcases.filter((tc) => tc.status === 'failed') @@ -78,7 +84,10 @@ describe('Junit XML parsing', () => { const xmlPath = `${xmlBasePath}/empty-system-err.xml` const xmlContent = await readFile(xmlPath, 'utf8') - const testcases = await parseJUnitXml(xmlContent, xmlBasePath) + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(1) // Should parse as success (no failure/error/skipped present) @@ -92,7 +101,10 @@ describe('Junit XML parsing', () => { const xmlPath = `${xmlBasePath}/jest-failure-type-missing.xml` const xmlContent = await readFile(xmlPath, 'utf8') - const testcases = await parseJUnitXml(xmlContent, xmlBasePath) + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(3) // Verify test result types @@ -110,4 +122,127 @@ describe('Junit XML parsing', () => { expect(failedTest?.name).toContain('subtracts two numbers correctly') expect(failedTest?.message).toContain('expect(received).toBe(expected)') }) + + test('Should extract attachments from failure/error message attributes (WebDriverIO style)', async () => { + const xmlPath = `${xmlBasePath}/webdriverio-real.xml` + const xmlContent = await readFile(xmlPath, 'utf8') + + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) + expect(testcases).toHaveLength(4) + + // Find the test case BD-055 which has a failure element with attachment in the message attribute + const bd055 = testcases.find((tc) => tc.name.includes('BD-055')) + expect(bd055).toBeDefined() + expect(bd055?.status).toBe('failed') + + // This test should have an attachment extracted from the failure message attribute + // This is the WebDriverIO style where attachments are embedded in the failure message + expect(bd055?.attachments.length).toBeGreaterThan(0) + expect(bd055?.attachments[0].filename).toContain('BD_055') + expect(bd055?.attachments[0].filename).toContain('.png') + }) + + test('Should include stdout/stderr when skipStdout and skipStderr are set to "never"', async () => { + const xmlPath = `${xmlBasePath}/empty-system-err.xml` + const xmlContent = await readFile(xmlPath, 'utf8') + + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'never', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + // Should include stdout content + expect(testcases[0].message).toContain('ViewManager initialized') + }) + + test('Should skip stdout for passed tests when skipStdout is set to "on-success"', async () => { + const xmlPath = `${xmlBasePath}/empty-system-err.xml` + const xmlContent = await readFile(xmlPath, 'utf8') + + const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + skipStdout: 'on-success', + skipStderr: 'never', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + // Should NOT include stdout content for passed tests + expect(testcases[0].message).not.toContain('ViewManager initialized') + expect(testcases[0].message).toBe('') + }) + + test('Should skip stderr for passed tests when skipStderr is set to "on-success"', async () => { + const xml = ` + + + + stdout content + stderr content + + +` + + const testcases = await parseJUnitXml(xml, xmlBasePath, { + skipStdout: 'never', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + // Should include stdout but not stderr for passed tests + expect(testcases[0].message).toContain('stdout content') + expect(testcases[0].message).not.toContain('stderr content') + }) + + test('Should include stdout/stderr for failed tests even when skip options are set to "on-success"', async () => { + const xml = ` + + + + Failure details + stdout from failed test + stderr from failed test + + +` + + const testcases = await parseJUnitXml(xml, xmlBasePath, { + skipStdout: 'on-success', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('failed') + // Should include both stdout and stderr for failed tests + expect(testcases[0].message).toContain('Failure details') + expect(testcases[0].message).toContain('stdout from failed test') + expect(testcases[0].message).toContain('stderr from failed test') + }) + + test('Should skip both stdout and stderr for passed tests when both skip options are set to "on-success"', async () => { + const xml = ` + + + + stdout content + stderr content + + +` + + const testcases = await parseJUnitXml(xml, xmlBasePath, { + skipStdout: 'on-success', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + // Should not include stdout or stderr for passed tests + expect(testcases[0].message).toBe('') + }) }) diff --git a/src/tests/playwright-json-parsing.spec.ts b/src/tests/playwright-json-parsing.spec.ts index a42911c..a88850a 100644 --- a/src/tests/playwright-json-parsing.spec.ts +++ b/src/tests/playwright-json-parsing.spec.ts @@ -10,7 +10,10 @@ describe('Playwright JSON parsing', () => { const jsonContent = await readFile(jsonPath, 'utf8') // This should not throw any exceptions - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) // Verify that we got the expected number of test cases expect(testcases).toHaveLength(12) @@ -46,7 +49,10 @@ describe('Playwright JSON parsing', () => { const jsonPath = `${playwrightJsonBasePath}/empty-tsuite.json` const jsonContent = await readFile(jsonPath, 'utf8') - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) // Should only have the one test from ui.cart.spec.ts, not the empty ui.contents.spec.ts expect(testcases).toHaveLength(1) @@ -95,7 +101,10 @@ describe('Playwright JSON parsing', () => { ], }) - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(1) // Should use the last result (passed on retry) @@ -166,7 +175,10 @@ describe('Playwright JSON parsing', () => { ], }) - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(2) // Verify folder is set to top-level suite title @@ -225,7 +237,10 @@ describe('Playwright JSON parsing', () => { ], }) - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(1) // Verify ANSI codes are stripped from message @@ -330,7 +345,10 @@ describe('Playwright JSON parsing', () => { ], }) - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(3) // Test with annotation should have marker prefixed @@ -451,7 +469,10 @@ describe('Playwright JSON parsing', () => { ], }) - const testcases = await parsePlaywrightJson(jsonContent, '') + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) expect(testcases).toHaveLength(4) expect(testcases[0].status).toBe('passed') // expected @@ -459,4 +480,231 @@ describe('Playwright JSON parsing', () => { expect(testcases[2].status).toBe('passed') // flaky (passed on retry) expect(testcases[3].status).toBe('skipped') // skipped }) + + test('Should include stdout/stderr when skipStdout and skipStderr are set to "never"', async () => { + const jsonContent = JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }) + + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'never', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + expect(testcases[0].message).toContain('stdout content') + expect(testcases[0].message).toContain('stderr content') + }) + + test('Should skip stdout for passed tests when skipStdout is set to "on-success"', async () => { + const jsonContent = JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }) + + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'on-success', + skipStderr: 'never', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + expect(testcases[0].message).not.toContain('stdout content') + expect(testcases[0].message).toContain('stderr content') + }) + + test('Should skip stderr for passed tests when skipStderr is set to "on-success"', async () => { + const jsonContent = JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }) + + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'never', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + expect(testcases[0].message).toContain('stdout content') + expect(testcases[0].message).not.toContain('stderr content') + }) + + test('Should include stdout/stderr for failed tests even when skip options are set to "on-success"', async () => { + const jsonContent = JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Failed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [{ message: 'Test failed' }], + stdout: [{ text: 'stdout from failed test' }], + stderr: [{ text: 'stderr from failed test' }], + retry: 0, + attachments: [], + }, + ], + status: 'unexpected', + }, + ], + }, + ], + suites: [], + }, + ], + }) + + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'on-success', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('failed') + expect(testcases[0].message).toContain('Test failed') + expect(testcases[0].message).toContain('stdout from failed test') + expect(testcases[0].message).toContain('stderr from failed test') + }) + + test('Should skip both stdout and stderr for passed tests when both skip options are set to "on-success"', async () => { + const jsonContent = JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }) + + const testcases = await parsePlaywrightJson(jsonContent, '', { + skipStdout: 'on-success', + skipStderr: 'on-success', + }) + + expect(testcases).toHaveLength(1) + expect(testcases[0].status).toBe('passed') + expect(testcases[0].message).not.toContain('stdout content') + expect(testcases[0].message).not.toContain('stderr content') + expect(testcases[0].message).toBe('') + }) }) diff --git a/src/utils/result-upload/ResultUploadCommandHandler.ts b/src/utils/result-upload/ResultUploadCommandHandler.ts index d05db1d..285150c 100644 --- a/src/utils/result-upload/ResultUploadCommandHandler.ts +++ b/src/utils/result-upload/ResultUploadCommandHandler.ts @@ -12,7 +12,18 @@ import { parsePlaywrightJson } from './playwrightJsonParser' export type UploadCommandType = 'junit-upload' | 'playwright-json-upload' -export type Parser = (data: string, attachmentBaseDirectory: string) => Promise +export type SkipOutputOption = 'on-success' | 'never' + +export interface ParserOptions { + skipStdout: SkipOutputOption + skipStderr: SkipOutputOption +} + +export type Parser = ( + data: string, + attachmentBaseDirectory: string, + options: ParserOptions +) => Promise export interface ResultUploadCommandArgs { type: UploadCommandType @@ -22,6 +33,8 @@ export interface ResultUploadCommandArgs { force: boolean attachments: boolean ignoreUnmatched: boolean + skipStdout: SkipOutputOption + skipStderr: SkipOutputOption } interface FileResults { @@ -91,9 +104,14 @@ export class ResultUploadCommandHandler { protected async parseFiles(): Promise { const results: FileResults[] = [] + const parserOptions: ParserOptions = { + skipStdout: this.args.skipStdout, + skipStderr: this.args.skipStderr, + } + for (const file of this.args.files) { const fileData = readFileSync(file).toString() - const fileResults = await commandTypeParsers[this.type](fileData, dirname(file)) + const fileResults = await commandTypeParsers[this.type](fileData, dirname(file), parserOptions) results.push({ file, results: fileResults }) } diff --git a/src/utils/result-upload/junitXmlParser.ts b/src/utils/result-upload/junitXmlParser.ts index a2d75d9..2b652fb 100644 --- a/src/utils/result-upload/junitXmlParser.ts +++ b/src/utils/result-upload/junitXmlParser.ts @@ -2,7 +2,7 @@ import escapeHtml from 'escape-html' import xml from 'xml2js' import z from 'zod' import { Attachment, TestCaseResult } from './types' -import { Parser } from './ResultUploadCommandHandler' +import { Parser, ParserOptions } from './ResultUploadCommandHandler' import { ResultStatus } from '../../api/schemas' import { getAttachments } from './utils' @@ -75,7 +75,8 @@ const junitXmlSchema = z.object({ export const parseJUnitXml: Parser = async ( xmlString: string, - attachmentBaseDirectory: string + attachmentBaseDirectory: string, + options ): Promise => { const xmlData = await xml.parseStringPromise(xmlString, { explicitCharkey: true, @@ -90,7 +91,7 @@ export const parseJUnitXml: Parser = async ( for (const suite of validated.testsuites.testsuite) { for (const tcase of suite.testcase ?? []) { - const result = getResult(tcase) + const result = getResult(tcase, options) const index = testcases.push({ folder: suite.$.name ?? '', @@ -100,6 +101,8 @@ export const parseJUnitXml: Parser = async ( }) - 1 const attachmentPaths = [] + + // Extract from system-out for (const out of tcase['system-out'] || []) { const text = typeof out === 'string' ? out : out._ ?? '' if (text) { @@ -107,6 +110,50 @@ export const parseJUnitXml: Parser = async ( } } + // Extract from failure elements (message attribute and text content) + for (const failure of tcase.failure || []) { + if (typeof failure === 'object') { + // Check message attribute + if (failure.$?.message) { + attachmentPaths.push(...extractAttachmentPaths(failure.$.message)) + } + // Check text content + if (failure._) { + attachmentPaths.push(...extractAttachmentPaths(failure._)) + } + } + } + + // Extract from error elements (message attribute and text content) + for (const error of tcase.error || []) { + if (typeof error === 'object') { + // Check message attribute + if (error.$?.message) { + attachmentPaths.push(...extractAttachmentPaths(error.$.message)) + } + // Check text content + if (error._) { + attachmentPaths.push(...extractAttachmentPaths(error._)) + } + } + } + + // Extract from skipped elements (message attribute and text content) + for (const skipped of tcase.skipped || []) { + if (typeof skipped === 'string') { + attachmentPaths.push(...extractAttachmentPaths(skipped)) + } else if (typeof skipped === 'object') { + // Check message attribute + if (skipped.$?.message) { + attachmentPaths.push(...extractAttachmentPaths(skipped.$.message)) + } + // Check text content + if (skipped._) { + attachmentPaths.push(...extractAttachmentPaths(skipped._)) + } + } + } + attachmentsPromises.push({ index, promise: getAttachments(attachmentPaths, attachmentBaseDirectory), @@ -124,40 +171,47 @@ export const parseJUnitXml: Parser = async ( } const getResult = ( - tcase: z.infer + tcase: z.infer, + options: ParserOptions ): { status: ResultStatus; message: string } => { const err = tcase['system-err'] || [] const out = tcase['system-out'] || [] - if (tcase.error) - return { - status: 'blocked', - message: getResultMessage( - { result: tcase.error, type: 'code' }, - { result: out, type: 'code' }, - { result: err, type: 'code' } - ), - } - if (tcase.failure) - return { - status: 'failed', - message: getResultMessage( - { result: tcase.failure, type: 'code' }, - { result: out, type: 'code' }, - { result: err, type: 'code' } - ), - } - if (tcase.skipped) - return { - status: 'skipped', - message: getResultMessage( - { result: tcase.skipped, type: 'code' }, - { result: out, type: 'code' }, - { result: err, type: 'code' } - ), - } + + // Determine test status first + let status: ResultStatus + let mainResult: GetResultMessageOption | undefined + + if (tcase.error) { + status = 'blocked' + mainResult = { result: tcase.error, type: 'code' } + } else if (tcase.failure) { + status = 'failed' + mainResult = { result: tcase.failure, type: 'code' } + } else if (tcase.skipped) { + status = 'skipped' + mainResult = { result: tcase.skipped, type: 'code' } + } else { + status = 'passed' + } + + // Conditionally include stdout/stderr based on status and options + const includeStdout = !(status === 'passed' && options.skipStdout === 'on-success') + const includeStderr = !(status === 'passed' && options.skipStderr === 'on-success') + + const messageOptions: GetResultMessageOption[] = [] + if (mainResult) { + messageOptions.push(mainResult) + } + if (includeStdout) { + messageOptions.push({ result: out, type: 'code' }) + } + if (includeStderr) { + messageOptions.push({ result: err, type: 'code' }) + } + return { - status: 'passed', - message: getResultMessage({ result: out, type: 'code' }, { result: err, type: 'code' }), + status, + message: getResultMessage(...messageOptions), } } diff --git a/src/utils/result-upload/playwrightJsonParser.ts b/src/utils/result-upload/playwrightJsonParser.ts index 44c6351..8067273 100644 --- a/src/utils/result-upload/playwrightJsonParser.ts +++ b/src/utils/result-upload/playwrightJsonParser.ts @@ -2,7 +2,7 @@ import z from 'zod' import escapeHtml from 'escape-html' import stripAnsi from 'strip-ansi' import { Attachment, TestCaseResult } from './types' -import { Parser } from './ResultUploadCommandHandler' +import { Parser, ParserOptions } from './ResultUploadCommandHandler' import { ResultStatus } from '../../api/schemas' import { parseTCaseUrl } from '../misc' import { getAttachments } from './utils' @@ -80,7 +80,8 @@ const playwrightJsonSchema = z.object({ export const parsePlaywrightJson: Parser = async ( jsonString: string, - attachmentBaseDirectory: string + attachmentBaseDirectory: string, + options ): Promise => { const jsonData = JSON.parse(jsonString) const validated = playwrightJsonSchema.parse(jsonData) @@ -101,6 +102,7 @@ export const parsePlaywrightJson: Parser = async ( } const markerFromAnnotations = getTCaseMarkerFromAnnotations(test.annotations) // What about result.annotations? + const status = mapPlaywrightStatus(test.status) const numTestcases = testcases.push({ // Use markerFromAnnotations as name prefix, so that it takes precedence over any // other marker present. Prefixing it to name also helps in detectProjectCode @@ -108,8 +110,8 @@ export const parsePlaywrightJson: Parser = async ( ? `${markerFromAnnotations}: ${titlePrefix}${spec.title}` : `${titlePrefix}${spec.title}`, folder: topLevelSuite, - status: mapPlaywrightStatus(test.status), - message: buildMessage(result), + status, + message: buildMessage(result, status, options), attachments: [], }) @@ -177,7 +179,7 @@ const mapPlaywrightStatus = (status: Status): ResultStatus => { } } -const buildMessage = (result: Result) => { +const buildMessage = (result: Result, status: ResultStatus, options: ParserOptions) => { let message = '' if (result.retry) { @@ -194,7 +196,9 @@ const buildMessage = (result: Result) => { }) } - if (result.stdout.length > 0) { + // Conditionally include stdout based on status and options + const includeStdout = !(status === 'passed' && options.skipStdout === 'on-success') + if (includeStdout && result.stdout.length > 0) { message += '

Output:

' result.stdout.forEach((out) => { const content = 'text' in out ? out.text : out.buffer @@ -205,7 +209,9 @@ const buildMessage = (result: Result) => { }) } - if (result.stderr.length > 0) { + // Conditionally include stderr based on status and options + const includeStderr = !(status === 'passed' && options.skipStderr === 'on-success') + if (includeStderr && result.stderr.length > 0) { message += '

Errors (stderr):

' result.stderr.forEach((err) => { const content = 'text' in err ? err.text : err.buffer From e183513c643fde281fb1669dd5343279798ba10c Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Wed, 12 Nov 2025 19:07:50 +0400 Subject: [PATCH 2/4] Rename flags to --skip-report-stdout/stderr and refactor attachment extraction - Rename command-line flags from --skip-stdout/--skip-stderr to --skip-report-stdout/--skip-report-stderr to clarify these control stdout/stderr blocks from test reports, not qas-cli output - Update README documentation to reflect new flag names - Refactor duplicated attachment extraction logic into reusable helper function extractAttachmentsFromElements() --- README.md | 8 +-- src/commands/resultUpload.ts | 4 +- .../ResultUploadCommandHandler.ts | 8 +-- src/utils/result-upload/junitXmlParser.ts | 57 ++++++------------- 4 files changed, 28 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 83314c4..ef011a0 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ The `junit-upload` and `playwright-json-upload` commands upload test results fro - `--attachments` - Try to detect and upload any attachments with the test result - `--force` - Ignore API request errors, invalid test cases, or attachments - `--ignore-unmatched` - Suppress individual unmatched test messages, show summary only -- `--skip-stdout` - Control when to skip stdout from test results (choices: `on-success`, `never`; default: `never`) -- `--skip-stderr` - Control when to skip stderr from test results (choices: `on-success`, `never`; default: `never`) +- `--skip-report-stdout` - Control when to skip stdout blocks from test report (choices: `on-success`, `never`; default: `never`) +- `--skip-report-stderr` - Control when to skip stderr blocks from test report (choices: `on-success`, `never`; default: `never`) - `-h, --help` - Show command help ### Run Name Template Placeholders @@ -141,13 +141,13 @@ Ensure the required environment variables are defined before running these comma 9. Skip stdout/stderr for passed tests to reduce result payload size: ```bash - qasphere junit-upload --skip-stdout on-success ./test-results.xml + qasphere junit-upload --skip-report-stdout on-success ./test-results.xml ``` This will exclude stdout from passed tests while still including it for failed, blocked, or skipped tests. 10. Skip both stdout and stderr for passed tests: ```bash - qasphere junit-upload --skip-stdout on-success --skip-stderr on-success ./test-results.xml + qasphere junit-upload --skip-report-stdout on-success --skip-report-stderr on-success ./test-results.xml ``` This is useful when you have verbose logging in tests but only want to see output for failures. diff --git a/src/commands/resultUpload.ts b/src/commands/resultUpload.ts index ba1d0b0..df5cac0 100644 --- a/src/commands/resultUpload.ts +++ b/src/commands/resultUpload.ts @@ -53,13 +53,13 @@ export class ResultUploadCommandModule implements CommandModule { + for (const element of elements || []) { + if (typeof element === 'string') { + attachmentPaths.push(...extractAttachmentPaths(element)) + } else if (typeof element === 'object') { + if (element.$?.message) { + attachmentPaths.push(...extractAttachmentPaths(element.$.message)) + } + if (element._) { + attachmentPaths.push(...extractAttachmentPaths(element._)) + } } } } - // Extract from skipped elements (message attribute and text content) - for (const skipped of tcase.skipped || []) { - if (typeof skipped === 'string') { - attachmentPaths.push(...extractAttachmentPaths(skipped)) - } else if (typeof skipped === 'object') { - // Check message attribute - if (skipped.$?.message) { - attachmentPaths.push(...extractAttachmentPaths(skipped.$.message)) - } - // Check text content - if (skipped._) { - attachmentPaths.push(...extractAttachmentPaths(skipped._)) - } - } - } + // Extract attachments from failure, error, and skipped elements + extractAttachmentsFromElements(tcase.failure) + extractAttachmentsFromElements(tcase.error) + extractAttachmentsFromElements(tcase.skipped) attachmentsPromises.push({ index, From 19835756d75fda2d11515378a84878ea984810b7 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Wed, 12 Nov 2025 19:45:25 +0400 Subject: [PATCH 3/4] Improve attachment handling and type safety - Use Set to deduplicate attachment paths in JUnit parser (prevents processing the same attachment multiple times when referenced in both system-out and failure/error messages) - Add explicit ParserOptions type annotation to both parsers for better type safety and code clarity --- src/utils/result-upload/junitXmlParser.ts | 14 +++++++------- src/utils/result-upload/playwrightJsonParser.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/result-upload/junitXmlParser.ts b/src/utils/result-upload/junitXmlParser.ts index c8d5786..45d1bc4 100644 --- a/src/utils/result-upload/junitXmlParser.ts +++ b/src/utils/result-upload/junitXmlParser.ts @@ -76,7 +76,7 @@ const junitXmlSchema = z.object({ export const parseJUnitXml: Parser = async ( xmlString: string, attachmentBaseDirectory: string, - options + options: ParserOptions ): Promise => { const xmlData = await xml.parseStringPromise(xmlString, { explicitCharkey: true, @@ -100,13 +100,13 @@ export const parseJUnitXml: Parser = async ( attachments: [], }) - 1 - const attachmentPaths = [] + const attachmentPaths = new Set() // Extract from system-out for (const out of tcase['system-out'] || []) { const text = typeof out === 'string' ? out : out._ ?? '' if (text) { - attachmentPaths.push(...extractAttachmentPaths(text)) + extractAttachmentPaths(text).forEach((path) => attachmentPaths.add(path)) } } @@ -116,13 +116,13 @@ export const parseJUnitXml: Parser = async ( ) => { for (const element of elements || []) { if (typeof element === 'string') { - attachmentPaths.push(...extractAttachmentPaths(element)) + extractAttachmentPaths(element).forEach((path) => attachmentPaths.add(path)) } else if (typeof element === 'object') { if (element.$?.message) { - attachmentPaths.push(...extractAttachmentPaths(element.$.message)) + extractAttachmentPaths(element.$.message).forEach((path) => attachmentPaths.add(path)) } if (element._) { - attachmentPaths.push(...extractAttachmentPaths(element._)) + extractAttachmentPaths(element._).forEach((path) => attachmentPaths.add(path)) } } } @@ -135,7 +135,7 @@ export const parseJUnitXml: Parser = async ( attachmentsPromises.push({ index, - promise: getAttachments(attachmentPaths, attachmentBaseDirectory), + promise: getAttachments(Array.from(attachmentPaths), attachmentBaseDirectory), }) } } diff --git a/src/utils/result-upload/playwrightJsonParser.ts b/src/utils/result-upload/playwrightJsonParser.ts index 8067273..c322dfc 100644 --- a/src/utils/result-upload/playwrightJsonParser.ts +++ b/src/utils/result-upload/playwrightJsonParser.ts @@ -81,7 +81,7 @@ const playwrightJsonSchema = z.object({ export const parsePlaywrightJson: Parser = async ( jsonString: string, attachmentBaseDirectory: string, - options + options: ParserOptions ): Promise => { const jsonData = JSON.parse(jsonString) const validated = playwrightJsonSchema.parse(jsonData) From 090587ab3e5e3b4ce3976a03ba09607c0e5e6b98 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Thu, 13 Nov 2025 08:05:44 +0400 Subject: [PATCH 4/4] README change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef011a0..a223cc6 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Ensure the required environment variables are defined before running these comma ``` This will exclude stdout from passed tests while still including it for failed, blocked, or skipped tests. -10. Skip both stdout and stderr for passed tests: + Skip both stdout and stderr for passed tests: ```bash qasphere junit-upload --skip-report-stdout on-success --skip-report-stderr on-success ./test-results.xml ```