@bigbinary/neeto-playwright-reporter 2.2.0 → 3.0.0-beta.1

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.
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import * as R from "ramda";
5
+ import smartOrchestrationApi from "./smartOrchestration.js";
6
+
7
+ const pickTest = R.pick(["id", "title"]);
8
+
9
+ const collectSpecs = suite =>
10
+ R.concat(
11
+ R.map(pickTest, suite.specs || []),
12
+ R.chain(collectSpecs, suite.suites || [])
13
+ );
14
+
15
+ const extractTestTitlesAndIds = R.pipe(R.prop("suites"), R.chain(collectSpecs));
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.length === 0 || args[0] !== "playwright") {
20
+ console.error(`Usage: neetoplaydash playwright [options] <playwright commands and args>
21
+
22
+ Required Options (via flags or environment variables):
23
+ --projectKey <key> Project key for API calls (or NEETO_PROJECT_KEY env var)
24
+ --apiKey <key> API key for authentication (or NEETO_API_KEY env var)
25
+ --ciBuildId <id> CI build ID for tracking (or NEETO_CI_BUILD_ID env var)
26
+ `);
27
+ process.exit(1);
28
+ }
29
+
30
+ const extractNeetoOption = (args, optionName) => {
31
+ const index = args.findIndex(
32
+ arg => arg === `--${optionName}` || arg.startsWith(`--${optionName}=`)
33
+ );
34
+ if (index === -1) return { value: null, filteredArgs: args };
35
+
36
+ const arg = args[index];
37
+ let value;
38
+ let argsToRemove = [index];
39
+
40
+ if (arg === `--${optionName}` && index + 1 < args.length) {
41
+ value = args[index + 1];
42
+ argsToRemove.push(index + 1);
43
+ } else if (arg.startsWith(`--${optionName}=`)) {
44
+ value = arg.substring(`--${optionName}=`.length);
45
+ }
46
+
47
+ const filteredArgs = args.filter((_, i) => !argsToRemove.includes(i));
48
+ return { value, filteredArgs };
49
+ };
50
+
51
+ let remainingArgs = args.slice(1);
52
+ const neetoOptions = {};
53
+
54
+ ["projectKey", "apiKey", "ciBuildId"].forEach(option => {
55
+ const { value, filteredArgs } = extractNeetoOption(remainingArgs, option);
56
+ if (value) neetoOptions[option] = value;
57
+ remainingArgs = filteredArgs;
58
+ });
59
+
60
+ const projectKey = neetoOptions.projectKey || process.env.NEETO_PROJECT_KEY;
61
+ const apiKey = neetoOptions.apiKey || process.env.NEETO_API_KEY;
62
+ const ciBuildId = neetoOptions.ciBuildId || process.env.NEETO_CI_BUILD_ID;
63
+
64
+ const missingOptions = [];
65
+ if (!projectKey) missingOptions.push("--projectKey or NEETO_PROJECT_KEY");
66
+ if (!apiKey) missingOptions.push("--apiKey or NEETO_API_KEY");
67
+ if (!ciBuildId) missingOptions.push("--ciBuildId or NEETO_CI_BUILD_ID");
68
+
69
+ if (missingOptions.length > 0) {
70
+ console.error(
71
+ `Error: Missing required options: ${missingOptions.join(", ")}`
72
+ );
73
+ console.error(
74
+ `\nUsage: neetoplaydash playwright --projectKey <key> --apiKey <key> --ciBuildId <id> <playwright commands and args>`
75
+ );
76
+ console.error(
77
+ `Or set environment variables: NEETO_PROJECT_KEY, NEETO_API_KEY, NEETO_CI_BUILD_ID`
78
+ );
79
+ process.exit(1);
80
+ }
81
+
82
+ const playwrightArgs = remainingArgs;
83
+
84
+ const escapeShellArg = arg => {
85
+ if (arg.includes(" ") || arg.includes('"') || arg.includes("'")) {
86
+ return `"${arg.replace(/"/g, '\\"')}"`;
87
+ }
88
+ return arg;
89
+ };
90
+
91
+ const escapeRegexSpecialChars = str =>
92
+ str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+
94
+ const extractShardInfo = args => {
95
+ let currentShard = null;
96
+ let totalShards = null;
97
+
98
+ const shardIndex = args.findIndex(arg => arg.startsWith("--shard"));
99
+
100
+ if (shardIndex !== -1) {
101
+ const arg = args[shardIndex];
102
+ let shardValue;
103
+
104
+ if (arg === "--shard" && shardIndex + 1 < args.length) {
105
+ shardValue = args[shardIndex + 1];
106
+ } else if (arg.startsWith("--shard=")) {
107
+ shardValue = arg.substring("--shard=".length);
108
+ }
109
+
110
+ if (shardValue) {
111
+ const match = shardValue.match(/^(\d+)\/(\d+)$/);
112
+ if (match) {
113
+ currentShard = parseInt(match[1]);
114
+ totalShards = parseInt(match[2]);
115
+ }
116
+ }
117
+ }
118
+
119
+ return { currentShard, totalShards };
120
+ };
121
+
122
+ const filterArgs = (args, filterableArg) => {
123
+ return args.filter((arg, index) => {
124
+ if (arg === filterableArg || arg.startsWith(`${filterableArg}=`)) {
125
+ return false;
126
+ }
127
+ if (index > 0 && args[index - 1] === filterableArg) {
128
+ return false;
129
+ }
130
+ return true;
131
+ });
132
+ };
133
+
134
+ const filterShardArgs = args => filterArgs(args, "--shard");
135
+ const filterGrepArgs = args => filterArgs(args, "--grep");
136
+
137
+ const removeConsecutiveDuplicates = (args, value) => {
138
+ return args.filter((arg, index) => {
139
+ if (arg === value && index > 0 && args[index - 1] === value) {
140
+ return false;
141
+ }
142
+ return true;
143
+ });
144
+ };
145
+
146
+ const { currentShard, totalShards } = extractShardInfo(playwrightArgs);
147
+ const filteredPlaywrightArgs = filterGrepArgs(filterShardArgs(playwrightArgs));
148
+
149
+ const env = {
150
+ ...process.env,
151
+ DOTENV_CONFIG_QUIET: "true",
152
+ NODE_OPTIONS: "--no-warnings",
153
+ };
154
+
155
+ env.NEETO_PROJECT_KEY = projectKey;
156
+ env.NEETO_API_KEY = apiKey;
157
+ env.NEETO_CI_BUILD_ID = ciBuildId;
158
+
159
+ if (currentShard !== null) {
160
+ env.NEETO_PLAYDASH_CURRENT_SHARD = currentShard.toString();
161
+ }
162
+
163
+ if (totalShards !== null) {
164
+ env.NEETO_PLAYDASH_TOTAL_SHARDS = totalShards.toString();
165
+ }
166
+
167
+ const finalArgs = [...playwrightArgs, "--reporter=json", "--list"];
168
+ const escapedFinalArgs = finalArgs.map(escapeShellArg);
169
+ const listCommand = `npx playwright ${escapedFinalArgs.join(" ")}`;
170
+
171
+ const child = spawn(listCommand, {
172
+ env,
173
+ stdio: ["inherit", "pipe", "pipe"],
174
+ shell: true,
175
+ });
176
+
177
+ let stdout = "";
178
+
179
+ child.stdout.on("data", data => {
180
+ stdout += data.toString();
181
+ });
182
+
183
+ child.on("close", async code => {
184
+ const output = stdout;
185
+
186
+ const firstBrace = output.indexOf("{");
187
+ const lastBrace = output.lastIndexOf("}");
188
+
189
+ if (firstBrace === -1 || lastBrace === -1 || firstBrace >= lastBrace) {
190
+ console.error("Command invalid. Please check the command and try again.");
191
+ process.exit(code || 1);
192
+ }
193
+
194
+ const jsonString = output.substring(firstBrace, lastBrace + 1);
195
+
196
+ try {
197
+ const json = JSON.parse(jsonString);
198
+ const tests = extractTestTitlesAndIds(json);
199
+ const historyIds = R.pluck("id", tests);
200
+
201
+ const smartOrchestrationPayload = {
202
+ total_shards: totalShards,
203
+ current_shard: currentShard,
204
+ ci_build_id: ciBuildId,
205
+ test_history_ids: historyIds,
206
+ };
207
+
208
+ const smartOrchestrationResponse = await smartOrchestrationApi.create({
209
+ payload: smartOrchestrationPayload,
210
+ apiKey,
211
+ projectKey,
212
+ });
213
+ const executableTests = smartOrchestrationResponse.data.tests;
214
+ const greppedTests = executableTests.map(escapeRegexSpecialChars).join("|");
215
+ const deduplicatedArgs = removeConsecutiveDuplicates(
216
+ filteredPlaywrightArgs,
217
+ "test"
218
+ );
219
+ const escapedArgs = deduplicatedArgs.map(escapeShellArg);
220
+ const command = `npx playwright ${escapedArgs.join(
221
+ " "
222
+ )} --reporter="@bigbinary/neeto-playwright-reporter" --grep="${greppedTests}"`;
223
+ const runChild = spawn(command, {
224
+ env,
225
+ stdio: "inherit",
226
+ shell: true,
227
+ });
228
+
229
+ runChild.on("close", exitCode => {
230
+ process.exit(exitCode || 0);
231
+ });
232
+
233
+ runChild.on("error", error => {
234
+ console.error(`Error executing test command: ${error.message}`);
235
+ process.exit(1);
236
+ });
237
+ } catch (error) {
238
+ console.error("Failed to parse JSON:", error.message);
239
+ process.exit(code || 1);
240
+ }
241
+ });
242
+
243
+ child.on("error", error => {
244
+ console.error(`Error executing command: ${error.message}`);
245
+ process.exit(1);
246
+ });
@@ -0,0 +1,24 @@
1
+ import axios from "axios";
2
+
3
+ const API_BASE_URL =
4
+ process.env.NEETO_PLAYDASH_API_BASE_URL ||
5
+ "https://connect.neetoplaydash.com";
6
+
7
+ const create = ({ payload, apiKey, projectKey }) => {
8
+ return axios.post(
9
+ "/api/v2/reporter/smart_orchestration/distribute_tests",
10
+ { smart_orchestration: payload },
11
+ {
12
+ baseURL: API_BASE_URL,
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ "X-Api-Key": apiKey,
16
+ "Project-Key": projectKey,
17
+ },
18
+ }
19
+ );
20
+ };
21
+
22
+ const smartOrchestrationApi = { create };
23
+
24
+ export default smartOrchestrationApi;