From e7d420246f1c79e977a2e5eca5938f94e8e5a27e Mon Sep 17 00:00:00 2001 From: Dmitrii Gridnev Date: Wed, 10 Jun 2026 11:06:36 +0300 Subject: [PATCH] fix: gracefully handle non-positive Qase IDs in runtime APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #973: the previous fix filtered IDs at the parser layer, but the playwright and wdio reporters also expose runtime metadata APIs that write IDs straight into test metadata, bypassing all parsers: - playwright qase.id(N) → addMetadata({ ids: [N] }) - playwright qase(N, name) → PlaywrightQaseReporter.addIds([N], ...) - playwright qase.projects({}) → addMetadata({ projectMapping: ... }) - wdio qase.id(N) → process.emit('addQaseID', { ids: [N] }) Apply filterPositiveIds in each of these so IDs <= 0 are dropped with a warning and the result is reported without an ID, matching the parser behavior. Without this, qase.id(0) still produced HTTP 400 from the API. --- package-lock.json | 8 ++-- qase-playwright/changelog.md | 6 +++ qase-playwright/package.json | 4 +- qase-playwright/src/playwright.ts | 18 +++++-- qase-playwright/test/playwright.test.ts | 62 ++++++++++++++++++++++++- qase-wdio/changelog.md | 6 +++ qase-wdio/package.json | 4 +- qase-wdio/src/wdio.ts | 6 ++- qase-wdio/test/wdio.test.ts | 50 ++++++++++++++++++++ 9 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 qase-wdio/test/wdio.test.ts diff --git a/package-lock.json b/package-lock.json index b2293e14..bfa7b449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28376,11 +28376,11 @@ }, "qase-playwright": { "name": "playwright-qase-reporter", - "version": "2.5.3", + "version": "2.5.4", "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.2", - "qase-javascript-commons": "~2.7.3", + "qase-javascript-commons": "~2.7.4", "uuid": "11.1.1" }, "devDependencies": { @@ -28444,13 +28444,13 @@ }, "qase-wdio": { "name": "wdio-qase-reporter", - "version": "1.5.3", + "version": "1.5.4", "license": "Apache-2.0", "dependencies": { "@wdio/reporter": "^8.43.0", "@wdio/types": "^8.41.0", "csv-stringify": "^6.6.0", - "qase-javascript-commons": "~2.7.3", + "qase-javascript-commons": "~2.7.4", "strip-ansi": "^7.1.2", "uuid": "11.1.1" }, diff --git a/qase-playwright/changelog.md b/qase-playwright/changelog.md index e4057458..ba7b06c1 100644 --- a/qase-playwright/changelog.md +++ b/qase-playwright/changelog.md @@ -1,3 +1,9 @@ +# playwright-qase-reporter@2.5.4 + +## Fixed + +- Qase test case IDs that are `<= 0` passed via the runtime API (`qase(0, name)`, `qase.id(0)`, `qase.projects({ PROJ1: [0] })`) are now dropped with a warning instead of being written to the test metadata. Previously these calls bypassed the parser-level filter introduced in 2.5.3 and could still produce HTTP 400 from the API. Bumped `qase-javascript-commons` pin to `~2.7.4`. + # playwright-qase-reporter@2.5.3 ## Fixed diff --git a/qase-playwright/package.json b/qase-playwright/package.json index ae55e06c..6ccb98df 100644 --- a/qase-playwright/package.json +++ b/qase-playwright/package.json @@ -1,6 +1,6 @@ { "name": "playwright-qase-reporter", - "version": "2.5.3", + "version": "2.5.4", "description": "Qase TMS Playwright Reporter", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -49,7 +49,7 @@ "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.2", - "qase-javascript-commons": "~2.7.3", + "qase-javascript-commons": "~2.7.4", "uuid": "11.1.1" }, "peerDependencies": { diff --git a/qase-playwright/src/playwright.ts b/qase-playwright/src/playwright.ts index 0dc52e35..23d200bc 100644 --- a/qase-playwright/src/playwright.ts +++ b/qase-playwright/src/playwright.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { PlaywrightQaseReporter } from './reporter'; import * as path from 'path'; import { getMimeTypes, formatTitleWithProjectMapping } from 'qase-javascript-commons'; +import { filterPositiveIds } from 'qase-javascript-commons/internal'; export const ReporterContentType = 'application/qase.metadata+json'; const defaultContentType = 'application/octet-stream'; @@ -60,7 +61,7 @@ export const qase = ( const newName = `${name} (Qase ID: ${caseIds.join(',')})`; - PlaywrightQaseReporter.addIds(ids, newName); + PlaywrightQaseReporter.addIds(filterPositiveIds(ids), newName); return newName; }; @@ -79,9 +80,10 @@ export const qase = ( * */ qase.id = function(value: number | number[]) { - addMetadata({ - ids: Array.isArray(value) ? value : [value], - }); + const ids = filterPositiveIds(Array.isArray(value) ? value : [value]); + if (ids.length > 0) { + addMetadata({ ids }); + } return this; }; @@ -98,7 +100,13 @@ qase.projects = function(mapping: ProjectMapping) { const normalized: ProjectMapping = {}; for (const [code, ids] of Object.entries(mapping)) { if (Array.isArray(ids) && ids.length > 0) { - normalized[code] = ids.map((id) => (typeof id === 'number' ? id : parseInt(String(id), 10))).filter((n) => !Number.isNaN(n)); + const parsed = ids + .map((id) => (typeof id === 'number' ? id : parseInt(String(id), 10))) + .filter((n) => !Number.isNaN(n)); + const filtered = filterPositiveIds(parsed); + if (filtered.length > 0) { + normalized[code] = filtered; + } } } if (Object.keys(normalized).length > 0) { diff --git a/qase-playwright/test/playwright.test.ts b/qase-playwright/test/playwright.test.ts index 710a93d8..2264f974 100644 --- a/qase-playwright/test/playwright.test.ts +++ b/qase-playwright/test/playwright.test.ts @@ -205,4 +205,64 @@ describe('qase API', () => { expect(result).toBe('Click button QaseExpRes:: Button should be clicked QaseData:: Button data'); }); }); -}); + + describe('qase with non-positive ID (regression test)', () => { + it('drops zero before passing to PlaywrightQaseReporter.addIds, title is preserved for back-compat', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const title = qase(0, 'Test Name'); + expect(title).toBe('Test Name (Qase ID: 0)'); // unchanged + expect(warn).toHaveBeenCalledWith(expect.stringContaining('0')); + warn.mockRestore(); + }); + + it('keeps positive IDs and drops zero from a mixed list, title uses original IDs', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const title = qase([1, 0, 2], 'Test Name'); + expect(title).toBe('Test Name (Qase ID: 1,0,2)'); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('0')); + warn.mockRestore(); + }); + }); + + describe('qase.id with non-positive ID (regression test)', () => { + it('does not attach metadata when ID is zero', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + testInfoMock.attach.mockClear(); + qase.id(0); + expect(testInfoMock.attach).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + it('drops zero from a mixed list and attaches only positive IDs', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + testInfoMock.attach.mockClear(); + qase.id([1, 0, 2]); + expect(testInfoMock.attach).toHaveBeenCalledWith('qase-metadata.json', { + contentType: 'application/qase.metadata+json', + body: Buffer.from(JSON.stringify({ ids: [1, 2] }), 'utf8'), + }); + warn.mockRestore(); + }); + }); + + describe('qase.projects with non-positive ID (regression test)', () => { + it('omits a project entirely when all of its IDs are non-positive', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + testInfoMock.attach.mockClear(); + qase.projects({ PROJ1: [0], PROJ2: [5] }); + expect(testInfoMock.attach).toHaveBeenCalledWith('qase-metadata.json', { + contentType: 'application/qase.metadata+json', + body: Buffer.from(JSON.stringify({ projectMapping: { PROJ2: [5] } }), 'utf8'), + }); + warn.mockRestore(); + }); + + it('does not attach metadata when every project is empty after filtering', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + testInfoMock.attach.mockClear(); + qase.projects({ PROJ1: [0], PROJ2: [-1] }); + expect(testInfoMock.attach).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + }); +}); diff --git a/qase-wdio/changelog.md b/qase-wdio/changelog.md index 345a9088..306ebeeb 100644 --- a/qase-wdio/changelog.md +++ b/qase-wdio/changelog.md @@ -1,3 +1,9 @@ +# wdio-qase-reporter@1.5.4 + +## Fixed + +- Qase test case IDs that are `<= 0` passed to `qase.id(0)` are now dropped with a warning instead of being emitted as a metadata event. Previously this call bypassed the parser-level filter introduced in 1.5.3 and could still produce HTTP 400 from the API. Bumped `qase-javascript-commons` pin to `~2.7.4`. + # wdio-qase-reporter@1.5.3 ## Fixed diff --git a/qase-wdio/package.json b/qase-wdio/package.json index ed0f30ac..89f36ca4 100644 --- a/qase-wdio/package.json +++ b/qase-wdio/package.json @@ -1,6 +1,6 @@ { "name": "wdio-qase-reporter", - "version": "1.5.3", + "version": "1.5.4", "description": "Qase WebDriverIO Reporter", "homepage": "https://github.com/qase-tms/qase-javascript", "sideEffects": false, @@ -36,7 +36,7 @@ "@wdio/reporter": "^8.43.0", "@wdio/types": "^8.41.0", "csv-stringify": "^6.6.0", - "qase-javascript-commons": "~2.7.3", + "qase-javascript-commons": "~2.7.4", "strip-ansi": "^7.1.2", "uuid": "11.1.1" } diff --git a/qase-wdio/src/wdio.ts b/qase-wdio/src/wdio.ts index 6ce68374..4b418abf 100644 --- a/qase-wdio/src/wdio.ts +++ b/qase-wdio/src/wdio.ts @@ -1,5 +1,6 @@ import { events } from './events'; import { QaseStep, StepFunction, formatTitleWithProjectMapping } from 'qase-javascript-commons'; +import { filterPositiveIds } from 'qase-javascript-commons/internal'; /** * Send event to reporter @@ -82,7 +83,10 @@ qase.projects = (mapping: ProjectMapping, name: string): string => { * }); */ qase.id = (value: number | number[]) => { - sendEvent(events.addQaseID, { ids: Array.isArray(value) ? value : [value] }); + const ids = filterPositiveIds(Array.isArray(value) ? value : [value]); + if (ids.length > 0) { + sendEvent(events.addQaseID, { ids }); + } return this; }; diff --git a/qase-wdio/test/wdio.test.ts b/qase-wdio/test/wdio.test.ts new file mode 100644 index 00000000..eaf2f758 --- /dev/null +++ b/qase-wdio/test/wdio.test.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +import { describe, expect, it, jest } from '@jest/globals'; + +describe('qase.id (wdio runtime API)', () => { + beforeEach(() => { + jest.resetModules(); + }); + + it('drops zero and does not emit an event', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const emit = jest.spyOn(process, 'emit').mockImplementation(() => true); + const { qase } = require('../src/wdio'); + qase.id(0); + // events.addQaseID = 'qase:id' + expect(emit).not.toHaveBeenCalledWith('qase:id', expect.anything()); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('0')); + warn.mockRestore(); + emit.mockRestore(); + }); + + it('emits only the positive IDs from a mixed list', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const emit = jest.spyOn(process, 'emit').mockImplementation(() => true); + const { qase } = require('../src/wdio'); + qase.id([1, 0, 2]); + expect(emit).toHaveBeenCalledWith('qase:id', { ids: [1, 2] }); + warn.mockRestore(); + emit.mockRestore(); + }); + + it('does not emit when all IDs are non-positive', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const emit = jest.spyOn(process, 'emit').mockImplementation(() => true); + const { qase } = require('../src/wdio'); + qase.id([-1, 0]); + expect(emit).not.toHaveBeenCalledWith('qase:id', expect.anything()); + warn.mockRestore(); + emit.mockRestore(); + }); + + it('emits positive ID without filtering', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const emit = jest.spyOn(process, 'emit').mockImplementation(() => true); + const { qase } = require('../src/wdio'); + qase.id(42); + expect(emit).toHaveBeenCalledWith('qase:id', { ids: [42] }); + warn.mockRestore(); + emit.mockRestore(); + }); +});