vitest-qase-reporter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VitestQaseReporter = void 0;
4
+ const qase_javascript_commons_1 = require("qase-javascript-commons");
5
+ class VitestQaseReporter {
6
+ reporter;
7
+ currentSuite = undefined;
8
+ testMetadata = new Map();
9
+ /**
10
+ * @type {RegExp}
11
+ */
12
+ static qaseIdRegExp = /\(Qase ID: ([\d,]+)\)/;
13
+ /**
14
+ * @param {string} title
15
+ * @returns {number[]}
16
+ * @private
17
+ */
18
+ static getCaseId(title) {
19
+ const [, ids] = title.match(VitestQaseReporter.qaseIdRegExp) ?? [];
20
+ return ids ? ids.split(',').map((id) => Number(id)) : [];
21
+ }
22
+ /**
23
+ * @param {string} title
24
+ * @returns {string}
25
+ * @private
26
+ */
27
+ static removeQaseIdsFromTitle(title) {
28
+ const matches = title.match(VitestQaseReporter.qaseIdRegExp);
29
+ if (matches) {
30
+ return title.replace(matches[0], '').trimEnd();
31
+ }
32
+ return title;
33
+ }
34
+ constructor(options = {}) {
35
+ const composedOptions = (0, qase_javascript_commons_1.composeOptions)(options, {});
36
+ this.reporter = qase_javascript_commons_1.QaseReporter.getInstance({
37
+ ...composedOptions,
38
+ frameworkPackage: 'vitest',
39
+ frameworkName: 'vitest',
40
+ reporterName: 'vitest-qase-reporter',
41
+ });
42
+ }
43
+ /**
44
+ * Convert Vitest test result status to Qase TestStatusEnum
45
+ */
46
+ convertStatus(result) {
47
+ switch (result.state) {
48
+ case 'passed':
49
+ return qase_javascript_commons_1.TestStatusEnum.passed;
50
+ case 'failed':
51
+ return qase_javascript_commons_1.TestStatusEnum.failed;
52
+ case 'skipped':
53
+ return qase_javascript_commons_1.TestStatusEnum.skipped;
54
+ case 'pending':
55
+ return qase_javascript_commons_1.TestStatusEnum.skipped;
56
+ default:
57
+ return qase_javascript_commons_1.TestStatusEnum.skipped;
58
+ }
59
+ }
60
+ /**
61
+ * Create TestResultType from Vitest TestCase
62
+ */
63
+ createTestResult(testCase) {
64
+ const result = testCase.result();
65
+ const qaseIds = VitestQaseReporter.getCaseId(testCase.name);
66
+ const diagnostic = testCase.diagnostic();
67
+ const testId = testCase.id ?? testCase.name;
68
+ const metadata = this.testMetadata.get(testId);
69
+ // Use title from metadata if available, otherwise use test name
70
+ const testTitle = metadata?.title ?? VitestQaseReporter.removeQaseIdsFromTitle(testCase.name);
71
+ const testResult = new qase_javascript_commons_1.TestResultType(testTitle);
72
+ testResult.id = testCase.id ?? '';
73
+ testResult.signature = testCase.fullName ?? testCase.name;
74
+ // Set testops_id based on extracted qase IDs
75
+ if (qaseIds.length > 0) {
76
+ testResult.testops_id = qaseIds.length === 1 ? qaseIds[0] ?? null : qaseIds;
77
+ }
78
+ else {
79
+ testResult.testops_id = null;
80
+ }
81
+ // Set relations for test suite
82
+ const suiteToUse = metadata?.suite ?? this.currentSuite ?? this.extractSuiteFromTestCase(testCase);
83
+ if (suiteToUse) {
84
+ testResult.relations = {
85
+ suite: {
86
+ data: [],
87
+ },
88
+ };
89
+ const suites = suiteToUse.split(' - ');
90
+ suites.forEach((suite) => {
91
+ testResult.relations.suite.data.push({ title: suite.trim(), public_id: null });
92
+ });
93
+ }
94
+ // Set execution details
95
+ testResult.execution.status = this.convertStatus(result);
96
+ testResult.execution.start_time = diagnostic?.startTime ? diagnostic.startTime / 1000 : null;
97
+ testResult.execution.end_time = diagnostic?.startTime ? diagnostic.startTime / 1000 + diagnostic.duration : null;
98
+ testResult.execution.duration = Math.round(diagnostic?.duration || 0);
99
+ if (result?.errors && result.errors.length > 0) {
100
+ testResult.execution.stacktrace = result.errors.map((error) => {
101
+ if (error && typeof error === 'object' && 'stack' in error && 'message' in error) {
102
+ return error.stack ?? error.message ?? String(error);
103
+ }
104
+ return String(error);
105
+ }).join('\n');
106
+ testResult.message = result.errors[0] && typeof result.errors[0] === 'object' && 'message' in result.errors[0]
107
+ ? String(result.errors[0].message) ?? 'Test failed'
108
+ : 'Test failed';
109
+ }
110
+ if (result.state === 'skipped') {
111
+ testResult.message = result && typeof result === 'object' && 'note' in result ? result.note ?? null : null;
112
+ }
113
+ // Add metadata from annotations
114
+ if (metadata) {
115
+ // Add comment if available - store in message field since execution doesn't have comment
116
+ if (metadata.comment) {
117
+ testResult.message = metadata.comment;
118
+ }
119
+ // Add fields if available
120
+ if (metadata.fields) {
121
+ testResult.fields = metadata.fields;
122
+ }
123
+ // Add parameters if available
124
+ if (metadata.parameters) {
125
+ testResult.params = metadata.parameters;
126
+ }
127
+ // Add group parameters if available
128
+ if (metadata.groupParameters) {
129
+ testResult.group_params = metadata.groupParameters;
130
+ }
131
+ // Add steps if available - create proper TestStepType objects
132
+ if (metadata.steps.length > 0) {
133
+ testResult.steps = metadata.steps.map(step => {
134
+ const stepObj = new qase_javascript_commons_1.TestStepType();
135
+ stepObj.id = Math.random().toString(36).substr(2, 9);
136
+ stepObj.data = {
137
+ action: step.name,
138
+ expected_result: null,
139
+ data: null
140
+ };
141
+ stepObj.execution.status = step.status === 'failed' ? qase_javascript_commons_1.StepStatusEnum.failed : qase_javascript_commons_1.StepStatusEnum.passed;
142
+ return stepObj;
143
+ });
144
+ }
145
+ // Add attachments if available
146
+ if (metadata.attachments.length > 0) {
147
+ testResult.attachments = metadata.attachments.map(attachment => {
148
+ const attachmentModel = {
149
+ file_name: attachment.name,
150
+ mime_type: attachment.contentType || 'application/octet-stream',
151
+ file_path: attachment.path || null,
152
+ content: attachment.content || '',
153
+ size: attachment.content ? Buffer.byteLength(attachment.content) : 0,
154
+ id: Math.random().toString(36).substr(2, 9)
155
+ };
156
+ return attachmentModel;
157
+ });
158
+ }
159
+ }
160
+ // Clean up metadata after processing
161
+ this.testMetadata.delete(testId);
162
+ return testResult;
163
+ }
164
+ onTestRunStart() {
165
+ this.reporter.startTestRun();
166
+ }
167
+ async onTestRunEnd() {
168
+ await this.reporter.publish();
169
+ }
170
+ async onTestCaseResult(testCase) {
171
+ const testResult = this.createTestResult(testCase);
172
+ await this.reporter.addTestResult(testResult);
173
+ }
174
+ onTestCaseAnnotate(testCase, annotation) {
175
+ const testId = testCase.id ?? testCase.name;
176
+ // Initialize metadata if not exists
177
+ if (!this.testMetadata.has(testId)) {
178
+ this.testMetadata.set(testId, {
179
+ steps: [],
180
+ attachments: []
181
+ });
182
+ }
183
+ const metadata = this.testMetadata.get(testId);
184
+ if (!metadata)
185
+ return;
186
+ // Process qase annotations
187
+ // Check if this is a qase annotation by looking at the message pattern
188
+ if (annotation.message && annotation.message.startsWith('Qase ')) {
189
+ const qaseType = annotation.message.split(':')[0]?.replace('Qase ', '').toLowerCase().replace(' ', '-') ?? '';
190
+ switch (qaseType) {
191
+ case 'title': {
192
+ metadata.title = annotation.message.replace('Qase Title: ', '');
193
+ break;
194
+ }
195
+ case 'comment': {
196
+ metadata.comment = annotation.message.replace('Qase Comment: ', '');
197
+ break;
198
+ }
199
+ case 'suite': {
200
+ metadata.suite = annotation.message.replace('Qase Suite: ', '');
201
+ break;
202
+ }
203
+ case 'fields': {
204
+ const fieldsData = annotation.message.replace('Qase Fields: ', '');
205
+ try {
206
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
207
+ const parsed = JSON.parse(fieldsData);
208
+ if (typeof parsed === 'object' && parsed !== null) {
209
+ metadata.fields = parsed;
210
+ }
211
+ }
212
+ catch (e) {
213
+ console.warn('Failed to parse qase fields:', fieldsData);
214
+ }
215
+ break;
216
+ }
217
+ case 'parameters': {
218
+ const parametersData = annotation.message.replace('Qase Parameters: ', '');
219
+ try {
220
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
221
+ const parsed = JSON.parse(parametersData);
222
+ if (typeof parsed === 'object' && parsed !== null) {
223
+ metadata.parameters = parsed;
224
+ }
225
+ }
226
+ catch (e) {
227
+ console.warn('Failed to parse qase parameters:', parametersData);
228
+ }
229
+ break;
230
+ }
231
+ case 'group-parameters': {
232
+ const groupParametersData = annotation.message.replace('Qase Group Parameters: ', '');
233
+ try {
234
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
235
+ const parsed = JSON.parse(groupParametersData);
236
+ if (typeof parsed === 'object' && parsed !== null) {
237
+ metadata.groupParameters = parsed;
238
+ }
239
+ }
240
+ catch (e) {
241
+ console.warn('Failed to parse qase group parameters:', groupParametersData);
242
+ }
243
+ break;
244
+ }
245
+ case 'step-start': {
246
+ const stepStartData = annotation.message.replace('Qase Step Start: ', '');
247
+ metadata.currentStep = stepStartData;
248
+ break;
249
+ }
250
+ case 'step-end': {
251
+ if (metadata.currentStep) {
252
+ metadata.steps.push({ name: metadata.currentStep, status: 'end' });
253
+ delete metadata.currentStep;
254
+ }
255
+ break;
256
+ }
257
+ case 'step-failed': {
258
+ if (metadata.currentStep) {
259
+ metadata.steps.push({ name: metadata.currentStep, status: 'failed' });
260
+ delete metadata.currentStep;
261
+ }
262
+ break;
263
+ }
264
+ case 'attachment': {
265
+ const attachmentName = annotation.message.replace('Qase Attachment: ', '');
266
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
267
+ const attachment = {
268
+ name: attachmentName,
269
+ ...(annotation.attachment?.path && { path: annotation.attachment.path }),
270
+ ...(annotation.attachment?.body && {
271
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
272
+ content: typeof annotation.attachment.body === 'string'
273
+ ? annotation.attachment.body
274
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
275
+ : new TextDecoder().decode(annotation.attachment.body)
276
+ }),
277
+ ...(annotation.attachment?.contentType && { contentType: annotation.attachment.contentType })
278
+ };
279
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
280
+ metadata.attachments.push(attachment);
281
+ break;
282
+ }
283
+ }
284
+ }
285
+ }
286
+ onTestSuiteReady(testSuite) {
287
+ this.currentSuite = testSuite.name;
288
+ }
289
+ onTestSuiteResult() {
290
+ this.currentSuite = undefined;
291
+ }
292
+ extractSuiteFromTestCase(testCase) {
293
+ // Extract suite from testCase.fullName or testCase.name
294
+ // Format is usually "Suite Name > Test Name" or just "Test Name"
295
+ const fullName = testCase.fullName ?? testCase.name;
296
+ const parts = fullName.split(' > ');
297
+ if (parts.length > 1) {
298
+ // Return the suite part (everything except the last part)
299
+ return parts.slice(0, -1).join(' > ');
300
+ }
301
+ // If no suite separator found, return undefined
302
+ // The test will be assigned to the default suite
303
+ return undefined;
304
+ }
305
+ }
306
+ exports.VitestQaseReporter = VitestQaseReporter;
307
+ exports.default = VitestQaseReporter;
@@ -0,0 +1,41 @@
1
+ type StepFunction = () => Promise<void> | void;
2
+ export declare const addQaseId: (name: string, caseIds: number[]) => string;
3
+ type AnnotateFunction = (message: string, options?: unknown) => Promise<void>;
4
+ export interface QaseWrapper {
5
+ annotate(message: string, options?: unknown): Promise<void>;
6
+ title(value: string): Promise<void>;
7
+ comment(value: string): Promise<void>;
8
+ suite(value: string): Promise<void>;
9
+ fields(values: Record<string, string>): Promise<void>;
10
+ parameters(values: Record<string, string>): Promise<void>;
11
+ groupParameters(values: Record<string, string>): Promise<void>;
12
+ step(name: string, body: StepFunction): Promise<void>;
13
+ attach(attach: {
14
+ name?: string;
15
+ type?: string;
16
+ content?: string;
17
+ paths?: string[];
18
+ }): Promise<void>;
19
+ }
20
+ export interface TestContextWithQase {
21
+ qase: QaseWrapper;
22
+ annotate: AnnotateFunction;
23
+ }
24
+ /**
25
+ * Higher-order function that extends test context with qase functions
26
+ * @param testFn - Test function that receives context with qase
27
+ * @returns Wrapped test function
28
+ * @example
29
+ * test('hello world', withQase(async ({ qase, annotate }) => {
30
+ * await qase.title("My Test Title");
31
+ * await qase.comment("This is a test comment");
32
+ *
33
+ * if (condition) {
34
+ * await qase.annotate('this should\'ve errored', 'error');
35
+ * }
36
+ * }));
37
+ */
38
+ export declare const withQase: <T extends unknown[]>(testFn: (context: TestContextWithQase & T[0]) => unknown) => (context: T[0] & {
39
+ annotate: AnnotateFunction;
40
+ }) => Promise<unknown>;
41
+ export {};
package/dist/vitest.js ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withQase = exports.addQaseId = void 0;
7
+ const qase_javascript_commons_1 = require("qase-javascript-commons");
8
+ const path_1 = __importDefault(require("path"));
9
+ // Function to add Qase ID to test name
10
+ const addQaseId = (name, caseIds) => {
11
+ return `${name} (Qase ID: ${caseIds.join(',')})`;
12
+ };
13
+ exports.addQaseId = addQaseId;
14
+ /**
15
+ * Create qase wrapper for annotate function
16
+ * @param annotate - Vitest annotate function
17
+ * @returns QaseWrapper object with all qase methods
18
+ */
19
+ const createQaseWrapper = (annotate) => {
20
+ return {
21
+ async annotate(message, options) {
22
+ return await annotate(message, options);
23
+ },
24
+ /**
25
+ * Set a title for the test case
26
+ * @param {string} value
27
+ * @example
28
+ * test('test', withQase(async ({ qase }) => {
29
+ * await qase.title("Title");
30
+ * expect(true).toBe(true);
31
+ * }));
32
+ */
33
+ async title(value) {
34
+ return await annotate(`Qase Title: ${value}`, { type: 'qase-title', body: value });
35
+ },
36
+ /**
37
+ * Add a comment to the test case
38
+ * @param {string} value
39
+ * @example
40
+ * test('test', withQase(async ({ qase }) => {
41
+ * await qase.comment("Comment");
42
+ * expect(true).toBe(true);
43
+ * }));
44
+ */
45
+ async comment(value) {
46
+ return await annotate(`Qase Comment: ${value}`, { type: 'qase-comment', body: value });
47
+ },
48
+ /**
49
+ * Set a suite for the test case
50
+ * @param {string} value
51
+ * @example
52
+ * test('test', withQase(async ({ qase }) => {
53
+ * await qase.suite("Suite");
54
+ * expect(true).toBe(true);
55
+ * }));
56
+ */
57
+ async suite(value) {
58
+ return await annotate(`Qase Suite: ${value}`, { type: 'qase-suite', body: value });
59
+ },
60
+ /**
61
+ * Set fields for the test case
62
+ * @param {Record<string, string>} values
63
+ * @example
64
+ * test('test', withQase(async ({ qase }) => {
65
+ * await qase.fields({field: "value"});
66
+ * expect(true).toBe(true);
67
+ * }));
68
+ */
69
+ async fields(values) {
70
+ return await annotate(`Qase Fields: ${JSON.stringify(values)}`, { type: 'qase-fields', body: JSON.stringify(values) });
71
+ },
72
+ /**
73
+ * Set parameters for the test case
74
+ * @param {Record<string, string>} values
75
+ * @example
76
+ * test('test', withQase(async ({ qase }) => {
77
+ * await qase.parameters({param: "value"});
78
+ * expect(true).toBe(true);
79
+ * }));
80
+ */
81
+ async parameters(values) {
82
+ return await annotate(`Qase Parameters: ${JSON.stringify(values)}`, { type: 'qase-parameters', body: JSON.stringify(values) });
83
+ },
84
+ /**
85
+ * Set group parameters for the test case
86
+ * @param {Record<string, string>} values
87
+ * @example
88
+ * test('test', withQase(async ({ qase }) => {
89
+ * await qase.groupParameters({param: "value"});
90
+ * expect(true).toBe(true);
91
+ * }));
92
+ */
93
+ async groupParameters(values) {
94
+ return await annotate(`Qase Group Parameters: ${JSON.stringify(values)}`, { type: 'qase-group-parameters', body: JSON.stringify(values) });
95
+ },
96
+ /**
97
+ * Add a step to the test case
98
+ * @param name
99
+ * @param body
100
+ * @example
101
+ * test('test', withQase(async ({ qase }) => {
102
+ * await qase.step("Step", () => {
103
+ * expect(true).toBe(true);
104
+ * });
105
+ * expect(true).toBe(true);
106
+ * }));
107
+ */
108
+ async step(name, body) {
109
+ await annotate(`Qase Step Start: ${name}`, { type: 'qase-step-start', body: name });
110
+ try {
111
+ const runningStep = new qase_javascript_commons_1.QaseStep(name);
112
+ await runningStep.run(body, async (step) => {
113
+ const stepName = 'action' in step.data ? step.data.action : step.data.name;
114
+ await annotate(`Qase Step: ${stepName}`, { type: 'qase-step', body: stepName });
115
+ });
116
+ await annotate(`Qase Step End: ${name}`, { type: 'qase-step-end', body: name });
117
+ }
118
+ catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : String(error);
120
+ await annotate(`Qase Step Failed: ${name} - ${errorMessage}`, { type: 'qase-step-failed', body: `${name} - ${errorMessage}` });
121
+ throw error;
122
+ }
123
+ },
124
+ /**
125
+ * Add an attachment to the test case
126
+ * @param attach
127
+ * @example
128
+ * test('test', withQase(async ({ qase }) => {
129
+ * await qase.attach({ name: 'attachment.txt', content: 'Hello, world!', type: 'text/plain' });
130
+ * await qase.attach({ paths: ['/path/to/file', '/path/to/another/file']});
131
+ * expect(true).toBe(true);
132
+ * }));
133
+ */
134
+ async attach(attach) {
135
+ if (attach.paths) {
136
+ for (const file of attach.paths) {
137
+ const attachmentName = path_1.default.basename(file);
138
+ const contentType = (0, qase_javascript_commons_1.getMimeTypes)(file);
139
+ await annotate(`Qase Attachment: ${attachmentName}`, {
140
+ type: 'qase-attachment',
141
+ body: attachmentName,
142
+ attachment: {
143
+ path: file,
144
+ contentType: contentType
145
+ }
146
+ });
147
+ }
148
+ return;
149
+ }
150
+ if (attach.content) {
151
+ await annotate(`Qase Attachment: ${attach.name ?? 'attachment'}`, {
152
+ type: 'qase-attachment',
153
+ body: attach.content,
154
+ attachment: {
155
+ contentType: attach.type ?? 'application/octet-stream',
156
+ body: attach.content
157
+ }
158
+ });
159
+ }
160
+ }
161
+ };
162
+ };
163
+ /**
164
+ * Higher-order function that extends test context with qase functions
165
+ * @param testFn - Test function that receives context with qase
166
+ * @returns Wrapped test function
167
+ * @example
168
+ * test('hello world', withQase(async ({ qase, annotate }) => {
169
+ * await qase.title("My Test Title");
170
+ * await qase.comment("This is a test comment");
171
+ *
172
+ * if (condition) {
173
+ * await qase.annotate('this should\'ve errored', 'error');
174
+ * }
175
+ * }));
176
+ */
177
+ const withQase = (testFn) => {
178
+ return async (context) => {
179
+ const qase = createQaseWrapper(context.annotate);
180
+ return await testFn({ ...context, qase });
181
+ };
182
+ };
183
+ exports.withQase = withQase;