diff --git a/src/tests/fixtures/allure/matching-tcases/005-result.json b/src/tests/fixtures/allure/matching-tcases/005-result.json
index 16f04ab..8698bb2 100644
--- a/src/tests/fixtures/allure/matching-tcases/005-result.json
+++ b/src/tests/fixtures/allure/matching-tcases/005-result.json
@@ -10,7 +10,7 @@
"attachments": [
{
"name": "attachment",
- "source": "shared-attachment.txt",
+ "source": "006-container.json",
"type": "text/plain"
}
],
diff --git a/src/tests/fixtures/allure/missing-attachments/004-result.json b/src/tests/fixtures/allure/missing-attachments/004-result.json
index 68b92c2..5f08586 100644
--- a/src/tests/fixtures/allure/missing-attachments/004-result.json
+++ b/src/tests/fixtures/allure/missing-attachments/004-result.json
@@ -10,7 +10,7 @@
"attachments": [
{
"name": "attachment",
- "source": "existing-attachment.txt",
+ "source": "001-result.json",
"type": "text/plain"
}
]
diff --git a/src/tests/fixtures/junit-xml/matching-tcases.xml b/src/tests/fixtures/junit-xml/matching-tcases.xml
index fda43a5..e66c4c2 100644
--- a/src/tests/fixtures/junit-xml/matching-tcases.xml
+++ b/src/tests/fixtures/junit-xml/matching-tcases.xml
@@ -173,7 +173,7 @@
diff --git a/src/tests/fixtures/junit-xml/missing-attachments.xml b/src/tests/fixtures/junit-xml/missing-attachments.xml
index 2df1b69..1825ecc 100644
--- a/src/tests/fixtures/junit-xml/missing-attachments.xml
+++ b/src/tests/fixtures/junit-xml/missing-attachments.xml
@@ -173,7 +173,7 @@
diff --git a/src/tests/fixtures/playwright-json/matching-tcases.json b/src/tests/fixtures/playwright-json/matching-tcases.json
index 96728e2..1fefbe2 100644
--- a/src/tests/fixtures/playwright-json/matching-tcases.json
+++ b/src/tests/fixtures/playwright-json/matching-tcases.json
@@ -149,7 +149,7 @@
{
"name": "attachment",
"contentType": "application/json",
- "path": "./matching-tcases.json"
+ "path": "./empty-tsuite.json"
}
]
}
diff --git a/src/tests/fixtures/playwright-json/missing-attachments.json b/src/tests/fixtures/playwright-json/missing-attachments.json
index 48d85ca..6517230 100644
--- a/src/tests/fixtures/playwright-json/missing-attachments.json
+++ b/src/tests/fixtures/playwright-json/missing-attachments.json
@@ -149,7 +149,7 @@
{
"name": "attachment",
"contentType": "application/json",
- "path": "./matching-tcases.json"
+ "path": "./empty-tsuite.json"
}
]
}
diff --git a/src/tests/fixtures/playwright-json/multi-annotation-with-attachments.json b/src/tests/fixtures/playwright-json/multi-annotation-with-attachments.json
new file mode 100644
index 0000000..a6ae8cc
--- /dev/null
+++ b/src/tests/fixtures/playwright-json/multi-annotation-with-attachments.json
@@ -0,0 +1,81 @@
+{
+ "suites": [
+ {
+ "title": "multi-annotation.spec.ts",
+ "specs": [
+ {
+ "title": "Login flow covers multiple cases",
+ "tags": [],
+ "tests": [
+ {
+ "annotations": [
+ {
+ "type": "test case",
+ "description": "https://qas.eu1.qasphere.com/project/TEST/tcase/10427"
+ },
+ {
+ "type": "test case",
+ "description": "https://qas.eu1.qasphere.com/project/TEST/tcase/10427"
+ },
+ {
+ "type": "test case",
+ "description": "https://qas.eu1.qasphere.com/project/TEST/tcase/10428"
+ }
+ ],
+ "expectedStatus": "passed",
+ "projectName": "chromium",
+ "results": [
+ {
+ "status": "passed",
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "duration": 2500,
+ "attachments": [
+ {
+ "name": "attachment",
+ "contentType": "application/json",
+ "path": "./multi-annotation-with-attachments.json"
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ]
+ },
+ {
+ "title": "Navigation bar items TEST-006",
+ "tags": [],
+ "tests": [
+ {
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectName": "chromium",
+ "results": [
+ {
+ "status": "passed",
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "duration": 1000,
+ "attachments": [
+ {
+ "name": "attachment",
+ "contentType": "application/json",
+ "path": "./multi-annotation-with-attachments.json"
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ]
+ }
+ ],
+ "suites": []
+ }
+ ]
+}
diff --git a/src/tests/fixtures/testcases.ts b/src/tests/fixtures/testcases.ts
index be3c773..71f019d 100644
--- a/src/tests/fixtures/testcases.ts
+++ b/src/tests/fixtures/testcases.ts
@@ -118,4 +118,38 @@ export const runTestCases = [
pos: 1,
},
},
+ {
+ id: '1CBd7Qsn1_abc10427xyz',
+ version: 1,
+ folderId: 13,
+ pos: 4,
+ seq: 10427,
+ title: 'Login flow case 1',
+ priority: 'medium',
+ status: 'open',
+ folder: {
+ id: 13,
+ parentId: 7,
+ title: 'multi-annotation.spec.ts',
+ projectId: '1CBd7PKmG_khB9xTm8gL2oe',
+ pos: 1,
+ },
+ },
+ {
+ id: '1CBd7Qsn2_abc10428xyz',
+ version: 1,
+ folderId: 13,
+ pos: 5,
+ seq: 10428,
+ title: 'Login flow case 2',
+ priority: 'medium',
+ status: 'open',
+ folder: {
+ id: 13,
+ parentId: 7,
+ title: 'multi-annotation.spec.ts',
+ projectId: '1CBd7PKmG_khB9xTm8gL2oe',
+ pos: 1,
+ },
+ },
]
diff --git a/src/tests/marker-parser.spec.ts b/src/tests/marker-parser.spec.ts
index 0d43317..e444ca5 100644
--- a/src/tests/marker-parser.spec.ts
+++ b/src/tests/marker-parser.spec.ts
@@ -216,6 +216,48 @@ describe('nameMatchesTCase', () => {
test('no match for wrong seq', () => {
expect(junit.nameMatchesTCase('TEST-002 Cart', 'TEST', 3)).toBe(false)
})
+
+ describe('does not prefix-match longer sequences', () => {
+ test('QS1-104 does not match name containing QS1-10427', () => {
+ expect(playwright.nameMatchesTCase('QS1-10427: some test', 'QS1', 104)).toBe(false)
+ })
+
+ test('QS1-107 does not match name containing QS1-10775', () => {
+ expect(playwright.nameMatchesTCase('QS1-10775: some test', 'QS1', 107)).toBe(false)
+ })
+
+ test('QS1-10427 still matches itself', () => {
+ expect(playwright.nameMatchesTCase('QS1-10427: some test', 'QS1', 10427)).toBe(true)
+ })
+
+ test('QS1-104 matches when it appears exactly', () => {
+ expect(playwright.nameMatchesTCase('QS1-104: some test', 'QS1', 104)).toBe(true)
+ })
+
+ test('QS1-104 matches at end of name', () => {
+ expect(playwright.nameMatchesTCase('some test QS1-104', 'QS1', 104)).toBe(true)
+ })
+
+ test('QS1-104 matches in middle of name with boundaries', () => {
+ expect(playwright.nameMatchesTCase('some QS1-104 test', 'QS1', 104)).toBe(true)
+ })
+
+ test('marker surrounded by parens still matches', () => {
+ expect(playwright.nameMatchesTCase('some test (QS1-10427)', 'QS1', 10427)).toBe(true)
+ })
+
+ test('marker surrounded by parens does not prefix-match', () => {
+ expect(playwright.nameMatchesTCase('some test (QS1-10427)', 'QS1', 104)).toBe(false)
+ })
+
+ test('marker adjacent to underscore still matches', () => {
+ expect(playwright.nameMatchesTCase('test_QS1-104_login', 'QS1', 104)).toBe(true)
+ })
+
+ test('marker adjacent to bracket still matches', () => {
+ expect(playwright.nameMatchesTCase('case[QS1-104]', 'QS1', 104)).toBe(true)
+ })
+ })
})
describe('separator-bounded hyphenless (JUnit only)', () => {
diff --git a/src/tests/playwright-json-parsing.spec.ts b/src/tests/playwright-json-parsing.spec.ts
index ced352f..5e9ec7d 100644
--- a/src/tests/playwright-json-parsing.spec.ts
+++ b/src/tests/playwright-json-parsing.spec.ts
@@ -379,6 +379,138 @@ describe('Playwright JSON parsing', () => {
expect(testcases[2].name).toBe('PRJ-789: PRJ-456: Test with marker in name and annotation')
})
+ test('Should fan out multiple results for test with multiple annotations', async () => {
+ const jsonPath = `${playwrightJsonBasePath}/multi-annotation-with-attachments.json`
+ const jsonContent = await readFile(jsonPath, 'utf8')
+
+ const { testCaseResults: testcases } = await parsePlaywrightJson(jsonContent, '', {
+ skipStdout: 'never',
+ skipStderr: 'never',
+ })
+
+ // Fixture has 1 test with 3 annotations (2x 10427 deduped to 1, plus 10428) + 1 test with no annotations = 3 results
+ expect(testcases).toHaveLength(3)
+ expect(testcases[0].name).toBe('TEST-10427: Login flow covers multiple cases')
+ expect(testcases[1].name).toBe('TEST-10428: Login flow covers multiple cases')
+ expect(testcases[2].name).toBe('Navigation bar items TEST-006')
+
+ // The two fan-out entries share the same status, duration, folder
+ for (const tc of testcases.slice(0, 2)) {
+ expect(tc.status).toBe('passed')
+ expect(tc.timeTaken).toBe(2500)
+ expect(tc.folder).toBe('multi-annotation.spec.ts')
+ }
+
+ // All three entries have attachments from the fixture
+ for (const tc of testcases) {
+ expect(tc.attachments).toHaveLength(1)
+ }
+ })
+
+ test('Should still produce one result for single annotation', async () => {
+ const jsonContent = JSON.stringify({
+ suites: [
+ {
+ title: 'single.spec.ts',
+ specs: [
+ {
+ title: 'Simple test',
+ tags: [],
+ tests: [
+ {
+ annotations: [
+ {
+ type: 'test case',
+ description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/100',
+ },
+ ],
+ expectedStatus: 'passed',
+ projectName: 'chromium',
+ results: [
+ {
+ status: 'passed',
+ errors: [],
+ stdout: [],
+ stderr: [],
+ retry: 0,
+ duration: 1000,
+ attachments: [],
+ },
+ ],
+ status: 'expected',
+ },
+ ],
+ },
+ ],
+ suites: [],
+ },
+ ],
+ })
+
+ const { testCaseResults: testcases } = await parsePlaywrightJson(jsonContent, '', {
+ skipStdout: 'never',
+ skipStderr: 'never',
+ })
+
+ expect(testcases).toHaveLength(1)
+ expect(testcases[0].name).toBe('PRJ-100: Simple test')
+ })
+
+ test('Should fan out by annotations even when name has a marker', async () => {
+ const jsonContent = JSON.stringify({
+ suites: [
+ {
+ title: 'precedence.spec.ts',
+ specs: [
+ {
+ title: 'PRJ-999: Test with marker in name',
+ tags: [],
+ tests: [
+ {
+ annotations: [
+ {
+ type: 'test case',
+ description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/100',
+ },
+ {
+ type: 'test case',
+ description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/200',
+ },
+ ],
+ expectedStatus: 'passed',
+ projectName: 'chromium',
+ results: [
+ {
+ status: 'passed',
+ errors: [],
+ stdout: [],
+ stderr: [],
+ retry: 0,
+ duration: 1000,
+ attachments: [],
+ },
+ ],
+ status: 'expected',
+ },
+ ],
+ },
+ ],
+ suites: [],
+ },
+ ],
+ })
+
+ const { testCaseResults: testcases } = await parsePlaywrightJson(jsonContent, '', {
+ skipStdout: 'never',
+ skipStderr: 'never',
+ })
+
+ // Annotations take precedence — two results, not one from the name marker
+ expect(testcases).toHaveLength(2)
+ expect(testcases[0].name).toBe('PRJ-100: PRJ-999: Test with marker in name')
+ expect(testcases[1].name).toBe('PRJ-200: PRJ-999: Test with marker in name')
+ })
+
test('Should map test status correctly', async () => {
const jsonContent = JSON.stringify({
suites: [
diff --git a/src/tests/result-upload.spec.ts b/src/tests/result-upload.spec.ts
index 548ef27..66e64c3 100644
--- a/src/tests/result-upload.spec.ts
+++ b/src/tests/result-upload.spec.ts
@@ -1,7 +1,7 @@
import { HttpResponse, http } from 'msw'
import { setupServer } from 'msw/node'
import { unlinkSync, readdirSync } from 'node:fs'
-import { afterAll, beforeAll, beforeEach, expect, test, describe, afterEach } from 'vitest'
+import { afterAll, beforeAll, beforeEach, expect, test, describe, afterEach, vi } from 'vitest'
import { run } from '../commands/main'
import {
CreateTCasesRequest,
@@ -156,6 +156,16 @@ const countCreateTCasesApiCalls = () =>
const countRunLogApiCalls = () =>
countMockedApiCalls(server, (req) => new URL(req.url).pathname.endsWith(`/run/${runId}/log`))
+const countIndividualFileUploads = () => {
+ let count = 0
+ server.events.on('request:start', async (e) => {
+ if (!new URL(e.request.url).pathname.endsWith('/file/batch')) return
+ const form = await e.request.clone().formData()
+ count += form.getAll('files').length
+ })
+ return () => count
+}
+
const getMappingFiles = () =>
new Set(
readdirSync('.').filter((f) => f.startsWith('qasphere-automapping-') && f.endsWith('.txt'))
@@ -337,6 +347,17 @@ fileTypesWithAllure.forEach((fileType) => {
expect(numResultUploadCalls()).toBe(3) // 5 results total
})
+ test('Upload success message should report results and unique test cases', async () => {
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
+ await run(
+ `${fileType.command} -r ${runURL} ${fixtureInputPath(fileType, 'matching-tcases')}`
+ )
+ expect(logSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Uploaded 5 results to 5 test cases')
+ )
+ logSpy.mockRestore()
+ })
+
test('Test cases on reports with a missing test case on QAS should throw an error', async () => {
const numFileUploadCalls = countFileUploadApiCalls()
const numResultUploadCalls = countResultUploadApiCalls()
@@ -391,14 +412,18 @@ fileTypesWithAllure.forEach((fileType) => {
})
describe('Uploading with attachments', () => {
- test('Attachments should be uploaded', async () => {
+ test('Attachments should be uploaded and deduplicated', async () => {
const numFileUploadCalls = countFileUploadApiCalls()
const numResultUploadCalls = countResultUploadApiCalls()
+ const totalFilesUploaded = countIndividualFileUploads()
setMaxResultsInRequest(3)
await run(
`${fileType.command} -r ${runURL} --attachments ${fixtureInputPath(fileType, 'matching-tcases')}`
)
- expect(numFileUploadCalls()).toBe(1) // all 5 files in one batch
+ // matching-tcases fixtures have 4 tests with the same attachment + 1 unique
+ // Should upload 2 unique files, not 5
+ expect(numFileUploadCalls()).toBe(1)
+ expect(totalFilesUploaded()).toBe(2)
expect(numResultUploadCalls()).toBe(2) // 5 results total
})
test('Missing attachments should throw an error', async () => {
@@ -415,11 +440,15 @@ fileTypesWithAllure.forEach((fileType) => {
test('Missing attachments should be successful when forced', async () => {
const numFileUploadCalls = countFileUploadApiCalls()
const numResultUploadCalls = countResultUploadApiCalls()
+ const totalFilesUploaded = countIndividualFileUploads()
setMaxResultsInRequest(1)
await run(
`${fileType.command} -r ${runURL} --attachments --force ${fixtureInputPath(fileType, 'missing-attachments')}`
)
- expect(numFileUploadCalls()).toBe(1) // all 4 files in one batch
+ // missing-attachments fixtures have 3 tests with the same attachment + 1 unique + 1 missing
+ // Should upload 2 unique valid files, not 4
+ expect(numFileUploadCalls()).toBe(1)
+ expect(totalFilesUploaded()).toBe(2)
expect(numResultUploadCalls()).toBe(5) // 5 results total
})
})
@@ -615,6 +644,36 @@ describe('Allure invalid result file handling', () => {
})
})
+describe('Multi-annotation Playwright upload', () => {
+ test('Should deduplicate file uploads when fan-out entries share the same attachment', async () => {
+ const totalFilesUploaded = countIndividualFileUploads()
+
+ await run(
+ `playwright-json-upload -r ${runURL} --attachments ./src/tests/fixtures/playwright-json/multi-annotation-with-attachments.json`
+ )
+
+ // The fixture has 1 test with 3 annotations (2x 10427 deduped, 1x 10428 = 2 fan-out results)
+ // and 1 test with no annotations. All 3 results reference the same attachment file.
+ // Only 1 unique file should be uploaded, not 3.
+ expect(totalFilesUploaded()).toBe(1)
+ })
+
+ test('Upload message should distinguish results from unique test cases', async () => {
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+ await run(
+ `playwright-json-upload -r ${runURL} ./src/tests/fixtures/playwright-json/multi-annotation-with-attachments.json`
+ )
+
+ // Fixture: 2 fan-out results (10427 deduped, 10428) + 1 regular (TEST-006)
+ // = 3 results mapping to 3 unique test cases
+ expect(logSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Uploaded 3 results to 3 test cases')
+ )
+ logSpy.mockRestore()
+ })
+})
+
describe('Run-level log upload', () => {
const junitBasePath = './src/tests/fixtures/junit-xml'
const allureBasePath = './src/tests/fixtures/allure'
diff --git a/src/utils/result-upload/MarkerParser.ts b/src/utils/result-upload/MarkerParser.ts
index c3e5702..acf4a7e 100644
--- a/src/utils/result-upload/MarkerParser.ts
+++ b/src/utils/result-upload/MarkerParser.ts
@@ -150,9 +150,10 @@ export class MarkerParser {
* Used by ResultUploader to map results → run test cases.
*/
nameMatchesTCase(name: string, projectCode: string, seq: number): boolean {
- // 1. Hyphenated: case-insensitive check with hyphenated marker (e.g., TEST-002)
+ // 1. Hyphenated: case-insensitive check with exact hyphenated marker using word boundaries
const hyphenated = formatMarker(projectCode, seq)
- if (name.toLowerCase().includes(hyphenated.toLowerCase())) {
+ const hyphenatedPattern = new RegExp(`(? r.tcase.id)).size
+ console.log(`Uploaded ${mappedResults.length} results to ${uniqueTCases} test cases`)
}
}
@@ -208,30 +210,39 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')}
return results
}
- // Collect all attachments from all test cases
- const allAttachments: Array<{
- attachment: Attachment
- tcaseIndex: number
- }> = []
+ // Collect unique attachments, deduplicating by file path
+ const uniqueAttachments = new Map()
results.forEach((item, index) => {
item.result.attachments.forEach((attachment) => {
if (attachment.buffer !== null) {
- allAttachments.push({ attachment, tcaseIndex: index })
+ const existing = uniqueAttachments.get(attachment.filePath)
+ if (existing) {
+ existing.tcaseIndices.push(index)
+ } else {
+ uniqueAttachments.set(attachment.filePath, {
+ attachment,
+ tcaseIndices: [index],
+ })
+ }
}
})
})
- if (allAttachments.length === 0) {
+ if (uniqueAttachments.size === 0) {
return results
}
+ const uniqueEntries = [...uniqueAttachments.values()]
+ const totalRefs = uniqueEntries.reduce((sum, e) => sum + e.tcaseIndices.length, 0)
+ const duplicateCount = totalRefs - uniqueEntries.length
+
// Group attachments into batches where total size <= MAX_BATCH_SIZE_BYTES
- const batches: Array = []
- let currentBatch: typeof allAttachments = []
+ const batches: Array = []
+ let currentBatch: typeof uniqueEntries = []
let currentBatchSize = 0
- for (const item of allAttachments) {
+ for (const item of uniqueEntries) {
const size = item.attachment.buffer!.byteLength
if (
currentBatch.length > 0 &&
@@ -251,7 +262,8 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')}
// Upload batches concurrently with progress tracking
let uploadedCount = 0
- loader.start(`Uploading attachments: 0/${allAttachments.length} files uploaded`)
+ const duplicateMsg = duplicateCount > 0 ? ` (${duplicateCount} duplicates skipped)` : ''
+ loader.start(`Uploading attachments: 0/${uniqueEntries.length} files uploaded${duplicateMsg}`)
const batchResults = await this.processConcurrently(
batches,
@@ -265,11 +277,11 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')}
uploadedCount += batch.length
loader.setText(
- `Uploading attachments: ${uploadedCount}/${allAttachments.length} files uploaded`
+ `Uploading attachments: ${uploadedCount}/${uniqueEntries.length} files uploaded${duplicateMsg}`
)
return batch.map((item, i) => ({
- tcaseIndex: item.tcaseIndex,
+ tcaseIndices: item.tcaseIndices,
url: uploaded[i].url,
name: item.attachment.filename,
}))
@@ -278,14 +290,16 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')}
)
loader.stop()
- // Flatten batch results and group by test case index
+ // Flatten batch results and distribute URLs to all referencing test cases
const attachmentsByTCase = new Map>()
for (const batchResult of batchResults) {
- for (const { tcaseIndex, url, name } of batchResult) {
- if (!attachmentsByTCase.has(tcaseIndex)) {
- attachmentsByTCase.set(tcaseIndex, [])
+ for (const { tcaseIndices, url, name } of batchResult) {
+ for (const tcaseIndex of tcaseIndices) {
+ if (!attachmentsByTCase.has(tcaseIndex)) {
+ attachmentsByTCase.set(tcaseIndex, [])
+ }
+ attachmentsByTCase.get(tcaseIndex)!.push({ url, name })
}
- attachmentsByTCase.get(tcaseIndex)!.push({ url, name })
}
}
@@ -391,6 +405,6 @@ interface TCaseWithResult {
const makeListHtml = (list: { name: string; url: string }[]) => {
return ``
}
diff --git a/src/utils/result-upload/parsers/playwrightJsonParser.ts b/src/utils/result-upload/parsers/playwrightJsonParser.ts
index e05f651..858fbac 100644
--- a/src/utils/result-upload/parsers/playwrightJsonParser.ts
+++ b/src/utils/result-upload/parsers/playwrightJsonParser.ts
@@ -104,35 +104,43 @@ export const parsePlaywrightJson: Parser = async (
return // Can this happen?
}
- const markerFromAnnotations = getTCaseMarkerFromAnnotations(test.annotations) // What about result.annotations?
+ const markers = getAllTCaseMarkersFromAnnotations(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
- name: markerFromAnnotations
- ? `${markerFromAnnotations}: ${titlePrefix}${spec.title}`
- : `${titlePrefix}${spec.title}`,
- folder: topLevelSuite,
- status,
- message: buildMessage(result, status, options),
- timeTaken: result.duration,
- attachments: [],
- })
-
- const attachmentPaths = []
+ const message = buildMessage(result, status, options)
+ const baseName = `${titlePrefix}${spec.title}`
+
+ const attachmentPaths: string[] = []
for (const out of result.attachments || []) {
if (out.path) {
attachmentPaths.push(out.path)
}
}
- attachmentsPromises.push({
- index: numTestcases - 1,
- // Attachment paths are absolute, but in tests we are using relative paths
- promise: getAttachments(
- attachmentPaths,
- attachmentPaths[0]?.startsWith('/') ? undefined : attachmentBaseDirectory
- ),
- })
+ const attachmentPromise = getAttachments(
+ attachmentPaths,
+ attachmentPaths[0]?.startsWith('/') ? undefined : attachmentBaseDirectory
+ )
+
+ // Fan out: one TestCaseResult per unique annotation, or one with no prefix if no annotations
+ const uniqueMarkers = [...new Set(markers)]
+ const resultNames =
+ uniqueMarkers.length > 0
+ ? uniqueMarkers.map((marker) => `${marker}: ${baseName}`)
+ : [baseName]
+
+ for (const name of resultNames) {
+ const numTestcases = testcases.push({
+ name,
+ folder: topLevelSuite,
+ status,
+ message,
+ timeTaken: result.duration,
+ attachments: [],
+ })
+ attachmentsPromises.push({
+ index: numTestcases - 1,
+ promise: attachmentPromise,
+ })
+ }
}
// Recursively process nested suites
@@ -151,7 +159,8 @@ export const parsePlaywrightJson: Parser = async (
const attachments = await Promise.all(attachmentsPromises.map((p) => p.promise))
attachments.forEach((tcaseAttachment, i) => {
const tcaseIndex = attachmentsPromises[i].index
- testcases[tcaseIndex].attachments = tcaseAttachment
+ // Clone to avoid affecting other test cases if any downstream code mutates the array
+ testcases[tcaseIndex].attachments = [...tcaseAttachment]
})
// Build runFailureLogs from top-level errors
@@ -164,15 +173,17 @@ export const parsePlaywrightJson: Parser = async (
return { testCaseResults: testcases, runFailureLogs: runFailureLogParts.join('') }
}
-const getTCaseMarkerFromAnnotations = (annotations: Annotation[]) => {
+const getAllTCaseMarkersFromAnnotations = (annotations: Annotation[]): string[] => {
+ const markers: string[] = []
for (const annotation of annotations) {
if (annotation.type.toLowerCase().includes('test case') && annotation.description) {
const res = parseTCaseUrl(annotation.description)
if (res) {
- return formatMarker(res.project, res.tcaseSeq)
+ markers.push(formatMarker(res.project, res.tcaseSeq))
}
}
}
+ return markers
}
const mapPlaywrightStatus = (status: Status): ResultStatus => {
diff --git a/src/utils/result-upload/types.ts b/src/utils/result-upload/types.ts
index a16f5da..b2cc2ce 100644
--- a/src/utils/result-upload/types.ts
+++ b/src/utils/result-upload/types.ts
@@ -2,6 +2,7 @@ import { ResultStatus } from '../../api/schemas'
export interface Attachment {
filename: string
+ filePath: string
buffer: Buffer | null
error: Error | null
}
diff --git a/src/utils/result-upload/utils.ts b/src/utils/result-upload/utils.ts
index db4f748..25a5d5f 100644
--- a/src/utils/result-upload/utils.ts
+++ b/src/utils/result-upload/utils.ts
@@ -1,5 +1,5 @@
import { readFile } from 'fs/promises'
-import path, { basename } from 'path'
+import path, { basename, resolve } from 'path'
import { Attachment } from './types'
const getFile = async (filePath: string, basePath?: string): Promise => {
@@ -26,6 +26,7 @@ export const getAttachments = async (
return Promise.allSettled(filePaths.map((p) => getFile(p, basePath))).then((results) => {
return results.map((p, i) => ({
filename: basename(filePaths[i]),
+ filePath: resolve(basePath ?? '.', filePaths[i]),
buffer: p.status === 'fulfilled' ? p.value : null,
error: p.status === 'fulfilled' ? null : p.reason,
}))