@test-station/adapter-playwright 0.1.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.
Files changed (2) hide show
  1. package/package.json +15 -0
  2. package/src/index.js +276 -0
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@test-station/adapter-playwright",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "node ../../scripts/check-package.mjs ./src/index.js",
13
+ "lint": "node ../../scripts/lint-syntax.mjs ./src"
14
+ }
15
+ }
package/src/index.js ADDED
@@ -0,0 +1,276 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ export const id = 'playwright';
5
+ export const description = 'Playwright adapter';
6
+
7
+ export function createPlaywrightAdapter() {
8
+ return {
9
+ id,
10
+ description,
11
+ phase: 3,
12
+ async run({ project, suite }) {
13
+ const commandSpec = parseCommandSpec(suite.command);
14
+ const execution = await spawnCommand(commandSpec.command, appendPlaywrightJsonArgs(commandSpec.args), {
15
+ cwd: suite.cwd || project.rootDir,
16
+ env: resolveSuiteEnv(suite.env),
17
+ });
18
+
19
+ const payload = extractJsonPayload(execution.stdout || execution.stderr);
20
+ const parsed = parsePlaywrightReport(payload, suite.cwd || project.rootDir);
21
+
22
+ return {
23
+ status: deriveSuiteStatus(parsed.summary, execution.exitCode),
24
+ durationMs: execution.durationMs,
25
+ summary: parsed.summary,
26
+ coverage: null,
27
+ tests: parsed.tests,
28
+ warnings: [],
29
+ output: {
30
+ stdout: execution.stdout,
31
+ stderr: execution.stderr,
32
+ },
33
+ rawArtifacts: [
34
+ {
35
+ relativePath: `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}-playwright.json`,
36
+ content: JSON.stringify(payload, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ },
41
+ };
42
+ }
43
+
44
+ function parseCommandSpec(command) {
45
+ if (Array.isArray(command) && command.length > 0) {
46
+ return { command: String(command[0]), args: command.slice(1).map((entry) => String(entry)) };
47
+ }
48
+ if (typeof command === 'string' && command.trim().length > 0) {
49
+ const tokens = tokenizeCommand(command);
50
+ return { command: tokens[0], args: tokens.slice(1) };
51
+ }
52
+ throw new Error('Playwright adapter requires suite.command as a non-empty string or array.');
53
+ }
54
+
55
+ function appendPlaywrightJsonArgs(args) {
56
+ const filtered = [];
57
+ for (let index = 0; index < args.length; index += 1) {
58
+ const token = args[index];
59
+ if (token === '--reporter') {
60
+ index += 1;
61
+ continue;
62
+ }
63
+ if (token.startsWith('--reporter=')) {
64
+ continue;
65
+ }
66
+ filtered.push(token);
67
+ }
68
+ return [...filtered, '--reporter=json'];
69
+ }
70
+
71
+ function tokenizeCommand(command) {
72
+ const tokens = [];
73
+ let current = '';
74
+ let quote = null;
75
+ for (let index = 0; index < command.length; index += 1) {
76
+ const char = command[index];
77
+ if (quote) {
78
+ if (char === quote) {
79
+ quote = null;
80
+ } else if (char === '\\' && quote === '"' && index + 1 < command.length) {
81
+ current += command[index + 1];
82
+ index += 1;
83
+ } else {
84
+ current += char;
85
+ }
86
+ continue;
87
+ }
88
+ if (char === '"' || char === "'") {
89
+ quote = char;
90
+ continue;
91
+ }
92
+ if (/\s/.test(char)) {
93
+ if (current.length > 0) {
94
+ tokens.push(current);
95
+ current = '';
96
+ }
97
+ continue;
98
+ }
99
+ current += char;
100
+ }
101
+ if (current.length > 0) {
102
+ tokens.push(current);
103
+ }
104
+ return tokens;
105
+ }
106
+
107
+ function spawnCommand(command, args, options) {
108
+ return new Promise((resolve) => {
109
+ const startedAt = Date.now();
110
+ const child = spawn(command, args, {
111
+ cwd: options.cwd,
112
+ env: options.env,
113
+ stdio: ['ignore', 'pipe', 'pipe'],
114
+ });
115
+
116
+ let stdout = '';
117
+ let stderr = '';
118
+ child.stdout.on('data', (chunk) => {
119
+ stdout += chunk.toString();
120
+ });
121
+ child.stderr.on('data', (chunk) => {
122
+ stderr += chunk.toString();
123
+ });
124
+ child.on('error', (error) => {
125
+ stderr += `${error.message}\n`;
126
+ resolve({ exitCode: 1, stdout, stderr, durationMs: Date.now() - startedAt });
127
+ });
128
+ child.on('close', (code) => {
129
+ resolve({
130
+ exitCode: Number.isInteger(code) ? code : 1,
131
+ stdout,
132
+ stderr,
133
+ durationMs: Date.now() - startedAt,
134
+ });
135
+ });
136
+ });
137
+ }
138
+
139
+ function extractJsonPayload(text) {
140
+ const value = String(text || '').trim();
141
+ if (!value) {
142
+ return {};
143
+ }
144
+ try {
145
+ return JSON.parse(value);
146
+ } catch {
147
+ const start = value.indexOf('{');
148
+ const end = value.lastIndexOf('}');
149
+ if (start >= 0 && end > start) {
150
+ return JSON.parse(value.slice(start, end + 1));
151
+ }
152
+ throw new Error('Playwright adapter could not parse JSON reporter output.');
153
+ }
154
+ }
155
+
156
+ function parsePlaywrightReport(report, workspaceDir) {
157
+ const tests = [];
158
+ const rootDir = report.config?.rootDir ? path.resolve(report.config.rootDir) : path.resolve(workspaceDir);
159
+ for (const suite of report.suites || []) {
160
+ collectPlaywrightTests(suite, [], tests, rootDir);
161
+ }
162
+ return {
163
+ summary: createSummary({
164
+ total: tests.length,
165
+ passed: tests.filter((test) => test.status === 'passed').length,
166
+ failed: tests.filter((test) => test.status === 'failed').length,
167
+ skipped: tests.filter((test) => test.status === 'skipped').length,
168
+ }),
169
+ tests: tests.sort(sortTests),
170
+ };
171
+ }
172
+
173
+ function collectPlaywrightTests(suite, titleTrail, bucket, rootDir) {
174
+ const nextTrail = suite.title && !suite.title.endsWith('.spec.js') ? [...titleTrail, suite.title] : titleTrail;
175
+ for (const spec of suite.specs || []) {
176
+ const latestTest = spec.tests?.[spec.tests.length - 1];
177
+ const latestResult = latestTest?.results?.[latestTest.results.length - 1] || {};
178
+ const errors = (latestResult.errors || []).map((error) => {
179
+ if (typeof error?.message === 'string' && error.message.length > 0) {
180
+ return trimForReport(error.message, 1000);
181
+ }
182
+ return trimForReport(JSON.stringify(error, null, 2), 1000);
183
+ });
184
+ bucket.push({
185
+ name: spec.title,
186
+ fullName: [...nextTrail, spec.title].join(' '),
187
+ status: normalizeStatus(latestResult.status || latestTest?.status || 'passed'),
188
+ durationMs: Number.isFinite(latestResult.duration) ? latestResult.duration : 0,
189
+ file: spec.file ? path.resolve(rootDir, spec.file) : null,
190
+ line: Number.isFinite(spec.line) ? spec.line : null,
191
+ column: Number.isFinite(spec.column) ? spec.column : null,
192
+ failureMessages: errors,
193
+ rawDetails: {
194
+ retries: Array.isArray(latestTest?.results) ? latestTest.results.length - 1 : 0,
195
+ },
196
+ });
197
+ }
198
+ for (const child of suite.suites || []) {
199
+ collectPlaywrightTests(child, nextTrail, bucket, rootDir);
200
+ }
201
+ }
202
+
203
+ function createSummary(values = {}) {
204
+ return {
205
+ total: Number.isFinite(values.total) ? values.total : 0,
206
+ passed: Number.isFinite(values.passed) ? values.passed : 0,
207
+ failed: Number.isFinite(values.failed) ? values.failed : 0,
208
+ skipped: Number.isFinite(values.skipped) ? values.skipped : 0,
209
+ };
210
+ }
211
+
212
+ function normalizeStatus(status) {
213
+ if (status === 'passed' || status === 'expected') return 'passed';
214
+ if (status === 'pending' || status === 'skipped' || status === 'todo') return 'skipped';
215
+ return 'failed';
216
+ }
217
+
218
+ function deriveSuiteStatus(summary, exitCode) {
219
+ if (exitCode !== 0 || summary.failed > 0) {
220
+ return 'failed';
221
+ }
222
+ if (summary.total === 0 || summary.skipped === summary.total) {
223
+ return 'skipped';
224
+ }
225
+ return 'passed';
226
+ }
227
+
228
+ function sortTests(left, right) {
229
+ const leftFile = left.file || '';
230
+ const rightFile = right.file || '';
231
+ if (leftFile !== rightFile) {
232
+ return leftFile.localeCompare(rightFile);
233
+ }
234
+ if ((left.line || 0) !== (right.line || 0)) {
235
+ return (left.line || 0) - (right.line || 0);
236
+ }
237
+ return left.name.localeCompare(right.name);
238
+ }
239
+
240
+ function trimForReport(value, limit) {
241
+ if (typeof value !== 'string') {
242
+ return '';
243
+ }
244
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
245
+ }
246
+
247
+ function slugify(value) {
248
+ return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
249
+ }
250
+
251
+ function sanitizeEnv(env) {
252
+ const nextEnv = { ...env };
253
+ delete nextEnv.NODE_TEST_CONTEXT;
254
+ return nextEnv;
255
+ }
256
+
257
+ function resolveSuiteEnv(suiteEnv) {
258
+ return {
259
+ ...sanitizeEnv(process.env),
260
+ ...normalizeEnvRecord(suiteEnv),
261
+ };
262
+ }
263
+
264
+ function normalizeEnvRecord(env) {
265
+ if (!env || typeof env !== 'object') {
266
+ return {};
267
+ }
268
+ const normalized = {};
269
+ for (const [key, value] of Object.entries(env)) {
270
+ if (value === undefined || value === null) {
271
+ continue;
272
+ }
273
+ normalized[key] = String(value);
274
+ }
275
+ return normalized;
276
+ }