@checksum-ai/runtime 1.0.6 → 1.0.7

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.
@@ -23,10 +23,5 @@ export default defineConfig({
23
23
  name: "chromium",
24
24
  use: { ...devices["Desktop Chrome"] },
25
25
  },
26
- {
27
- name: "checksumpage",
28
- use: { ...devices["Desktop Chrome"] },
29
- testDir: "./src/lib/runtime",
30
- },
31
26
  ],
32
27
  });
package/cli.ts ADDED
@@ -0,0 +1,599 @@
1
+ import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
2
+ import * as childProcess from "child_process";
3
+ import { join } from "path";
4
+ import {
5
+ CHECKSUM_ROOT_FOLDER,
6
+ ChecksumConfig,
7
+ TestSuiteSession,
8
+ } from "./types";
9
+
10
+ class ChecksumCLI {
11
+ private readonly UPLOAD_AGENT_PATH = join(__dirname, "upload-agent.js");
12
+ private readonly CHECKSUM_API_URL = "http://localhost:3000";
13
+
14
+ private testSession: TestSuiteSession;
15
+ private volatileChecksumConfig;
16
+ private uploadAgent;
17
+ private config: ChecksumConfig;
18
+
19
+ private didFail = false;
20
+ private mock = true;
21
+
22
+ private completeIndicators = {
23
+ upload: false,
24
+ tests: false,
25
+ report: false,
26
+ };
27
+
28
+ constructor() {}
29
+
30
+ // rename install to init
31
+ // rename run to test
32
+ // rename import of playwright to our alias
33
+ async execute() {
34
+ switch (process.argv[2]) {
35
+ case "init":
36
+ this.install();
37
+ break;
38
+ case "test":
39
+ if (process.argv?.[3] === "--help") {
40
+ await this.printHelp("test");
41
+ break;
42
+ }
43
+ await this.test(process.argv.slice(3));
44
+ break;
45
+ case "show-report":
46
+ this.showReport(process.argv.slice(3));
47
+ break;
48
+ default:
49
+ // should we simply forward to playwright in default case?
50
+ await this.printHelp();
51
+ }
52
+ process.exit(0);
53
+ }
54
+
55
+ async execCmd(cmdString) {
56
+ const child = await childProcess.spawn(cmdString, {
57
+ shell: true,
58
+ stdio: "inherit",
59
+ });
60
+
61
+ const exitPromise = new Promise((resolve, reject) => {
62
+ child.on("exit", (code) => {
63
+ if (code === 0) {
64
+ resolve(true);
65
+ } else {
66
+ reject(new Error(`Checsum failed execution with code: ${code} `));
67
+ }
68
+ });
69
+ });
70
+
71
+ return exitPromise;
72
+ }
73
+
74
+ async getCmdOutput(cmdString): Promise<string> {
75
+ return new Promise<string>(function (resolve, reject) {
76
+ childProcess.exec(cmdString, (error, stdout, stderr) => {
77
+ if (error) {
78
+ reject(`Error executing command: ${error.message}`);
79
+ return;
80
+ }
81
+
82
+ resolve(stdout);
83
+ });
84
+ });
85
+
86
+ // return promise;
87
+ }
88
+
89
+ async printHelp(command?: string) {
90
+ switch (command) {
91
+ default:
92
+ console.log(`
93
+ Checksum CLI
94
+ Usage: checksum [command] [options]
95
+
96
+ Commands:
97
+ init installs checksum files and folders
98
+ test [options] [test-filter...] runs checksum tests
99
+ help prints this help message
100
+ show-report [options] [report] show HTML report
101
+ `);
102
+ break;
103
+ case "test":
104
+ try {
105
+ const cmd = `npx playwright test --help`;
106
+ const testHelp = (await this.getCmdOutput(cmd))
107
+ .replace(/npx playwright/g, "yarn checksum")
108
+ .split("\n");
109
+ testHelp
110
+ .splice(
111
+ 5,
112
+ 0,
113
+ " --checksum-config=<config> Checksum configuration in JSON format"
114
+ )
115
+ .join("\n");
116
+ console.log(testHelp.join("\n"));
117
+ } catch (e) {
118
+ console.log("Error", e.message);
119
+ }
120
+
121
+ break;
122
+ }
123
+ }
124
+
125
+ async showReport(args: string[]) {
126
+ const cmd = `npx playwright show-report ${args.join(" ")}`;
127
+
128
+ try {
129
+ await this.execCmd(cmd);
130
+ } catch (e) {
131
+ console.log("Error showing report", e.message);
132
+ }
133
+ }
134
+
135
+ async test(args: string[]) {
136
+ // check for checksum config in command
137
+ args = this.getChecksumConfigFromCommand(args);
138
+
139
+ // load checksum config
140
+ this.setChecksumConfig();
141
+
142
+ // init new test session
143
+ await this.getSession();
144
+
145
+ // start upload agent
146
+ let uploadAgentListeningPort;
147
+ try {
148
+ const { uuid, uploadURL } = this.testSession;
149
+ // load upload agent, timout after 20 seconds
150
+ uploadAgentListeningPort = await this.guardReturn(
151
+ this.startUploadAgent(uuid, uploadURL),
152
+ 10_000,
153
+ "Upload agent timeout"
154
+ );
155
+ } catch (e) {
156
+ console.log(
157
+ "Error starting upload agent. Test results will not be available on checksum."
158
+ );
159
+ }
160
+
161
+ // write volatile config if set
162
+ this.buildVolatileConfig();
163
+
164
+ // build shell command
165
+ const cmd = `${
166
+ uploadAgentListeningPort
167
+ ? `CHECKSUM_UPLOAD_AGENT_PORT=${uploadAgentListeningPort} `
168
+ : ""
169
+ } PWDEBUG=console npx playwright test --config ${this.getPlaywrightConfigFile()} ${args.join(
170
+ " "
171
+ )}`;
172
+
173
+ try {
174
+ // run tests
175
+ await this.execCmd(cmd);
176
+ } catch (e) {
177
+ this.didFail = true;
178
+ console.log("Error during test", e.message);
179
+ } finally {
180
+ const reportFile = this.getPlaywrightReportPath();
181
+ if (!existsSync(reportFile)) {
182
+ console.log(`Could not find report file at ${reportFile}`);
183
+ } else {
184
+ this.uploadAgent.stdin.write(`cli:report=${reportFile}`);
185
+ }
186
+ // upload report
187
+ this.completeIndicators.tests = true;
188
+ await this.handleCompleteMessage();
189
+ }
190
+ }
191
+
192
+ getPlaywrightReportPath() {
193
+ // default path
194
+ let reportFolder = join(process.cwd(), "playwright-report");
195
+ // check for playwright report path in config file
196
+ // for now, ignore cases of multiple reporters
197
+ const playwrightConfig = require(this.getPlaywrightConfigFile());
198
+ const { reporter } = playwrightConfig;
199
+ if (
200
+ reporter instanceof Array &&
201
+ reporter.length > 1 &&
202
+ reporter[1]?.outputFolder
203
+ ) {
204
+ reportFolder = reporter[1]?.outputFolder;
205
+ }
206
+
207
+ // check for env variable
208
+ if (process.env.PLAYWRIGHT_HTML_REPORT) {
209
+ reportFolder = process.env.PLAYWRIGHT_HTML_REPORT;
210
+ }
211
+
212
+ return join(reportFolder, "index.html");
213
+ }
214
+
215
+ getPlaywrightConfigFile() {
216
+ return join(this.getRootDirPath(), "playwright.config.ts");
217
+ }
218
+
219
+ startUploadAgent(sessionId: string, uploadURL: string) {
220
+ return new Promise((resolve, reject) => {
221
+ console.log("Starting upload agent");
222
+ this.uploadAgent = childProcess.spawn("node", [
223
+ this.UPLOAD_AGENT_PATH,
224
+ JSON.stringify({
225
+ sessionId,
226
+ checksumApiURL: this.CHECKSUM_API_URL,
227
+ apiKey: this.config.apiKey,
228
+ }),
229
+ ...(this.mock ? ["mock"] : []),
230
+ ]);
231
+
232
+ // Listen for messages from the upload agent
233
+ this.uploadAgent.stdout.on("data", (data) => {
234
+ const message = data.toString().trim();
235
+
236
+ // if not a formatted message from upload agent - ignore
237
+ if (!message.startsWith("upag:")) {
238
+ // console.log(`Message from upload agent: ${message}`); // remove
239
+ return;
240
+ }
241
+
242
+ const [key, value] = message.substring(5).split("=");
243
+ if (key === "port") {
244
+ console.log("Received port from upload agent", value);
245
+ resolve(value);
246
+ } else {
247
+ this.handleUploadAgentMessage(key, value);
248
+ }
249
+ });
250
+
251
+ // Handle exit event
252
+ this.uploadAgent.on("exit", (code, signal) => {
253
+ console.log(
254
+ `upload agent process exited with code ${code} and signal ${signal}`
255
+ );
256
+ });
257
+
258
+ // Handle errors
259
+ this.uploadAgent.on("error", (err) => {
260
+ console.error(`Error starting upload agent: ${err.message}`);
261
+ });
262
+ });
263
+ }
264
+
265
+ private async handleUploadAgentMessage(key: any, value: any) {
266
+ switch (key) {
267
+ case "complete":
268
+ // console.log("Received upload complete message from upload agent");
269
+ this.sendUploadsComplete().then(() => {
270
+ this.completeIndicators.upload = true;
271
+ });
272
+ break;
273
+ case "report-uploaded":
274
+ // console.log("Received report uploaded message from upload agent");
275
+ const reportURL = await this.sendTestrunEnd();
276
+ this.completeIndicators.report = true;
277
+ if (reportURL) {
278
+ console.log(
279
+ `*******************\n* Checksum report URL: ${reportURL}\n*******************`
280
+ );
281
+ }
282
+ break;
283
+
284
+ default:
285
+ console.warn(`Unhandled upload agent message: ${key}=${value}`);
286
+ }
287
+ }
288
+
289
+ async handleCompleteMessage() {
290
+ while (true) {
291
+ if (
292
+ Object.keys(this.completeIndicators).find(
293
+ (key) => !this.completeIndicators[key]
294
+ )
295
+ ) {
296
+ await this.awaitSleep(1000);
297
+ } else {
298
+ console.log("Tests complete");
299
+ this.shutdown(this.didFail ? 1 : 0);
300
+ }
301
+ }
302
+ }
303
+
304
+ shutdown(code = 0) {
305
+ this.cleanup();
306
+ process.exit(code);
307
+ }
308
+
309
+ buildVolatileConfig() {
310
+ // if no volatile config set - return
311
+ if (!this.volatileChecksumConfig) {
312
+ return;
313
+ }
314
+
315
+ const configPath = this.getVolatileConfigPath();
316
+ const configString = `
317
+ import { RunMode, getChecksumConfig } from "@checksum-ai/runtime";
318
+
319
+ export default getChecksumConfig(${JSON.stringify(this.config, null, 2)});
320
+ `;
321
+
322
+ writeFileSync(configPath, configString);
323
+ }
324
+
325
+ cleanup() {
326
+ this.deleteVolatileConfig();
327
+ this.uploadAgent.stdin.write("cli:shutdown");
328
+ this.uploadAgent.kill();
329
+ }
330
+
331
+ async getSession() {
332
+ try {
333
+ if (this.mock) {
334
+ this.testSession = {
335
+ uuid: "session-id-1234",
336
+ uploadURL: "http://localhost:3000/upload",
337
+ };
338
+ return;
339
+ }
340
+
341
+ const apiKey = this.config.apiKey;
342
+ if (!apiKey || apiKey === "<API key>") {
343
+ console.error("No API key found in checksum config");
344
+ this.shutdown(1);
345
+ }
346
+
347
+ const body = JSON.stringify(await this.getEnvInfo());
348
+ const res = await fetch(`${this.CHECKSUM_API_URL}/client-api/test-runs`, {
349
+ method: "POST",
350
+ headers: {
351
+ Accept: "application/json",
352
+ "Content-Type": "application/json",
353
+ ChecksumAppCode: apiKey,
354
+ },
355
+ body,
356
+ });
357
+
358
+ this.testSession = await res.json();
359
+ } catch (e) {
360
+ console.error("Error getting checksum test session", e);
361
+ this.shutdown(1);
362
+ }
363
+ }
364
+
365
+ async sendTestrunEnd() {
366
+ try {
367
+ if (this.mock) {
368
+ return "https://mock.report.url";
369
+ }
370
+ const { uuid: id } = this.testSession;
371
+ const body = JSON.stringify({
372
+ failed: this.didFail ? 1 : 0,
373
+ passed: 0,
374
+ healed: 0,
375
+ endedAt: Date.now(),
376
+ });
377
+
378
+ const { reportURL } = await this.updateTestRun(
379
+ `${this.CHECKSUM_API_URL}/client-api/test-runs/${id}`,
380
+ "PATCH",
381
+ body
382
+ );
383
+ return reportURL;
384
+ } catch (e) {
385
+ console.log("Error sending test run end", e.message);
386
+ return null;
387
+ }
388
+ }
389
+
390
+ async sendUploadsComplete() {
391
+ if (this.mock) {
392
+ return;
393
+ }
394
+ try {
395
+ const { uuid: id } = this.testSession;
396
+
397
+ await this.updateTestRun(
398
+ `${this.CHECKSUM_API_URL}/client-api/test-runs/${id}/uploads-completed`,
399
+ "PATCH"
400
+ );
401
+ } catch (e) {
402
+ console.log("Error sending test run uploads complete", e.message);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Sends update to the test run API
408
+ *
409
+ * @param url API endpoint
410
+ * @param method HTTP method
411
+ * @param body request body
412
+ * @returns JSON response from API
413
+ */
414
+ async updateTestRun(
415
+ url: string,
416
+ method: string,
417
+ body: string | undefined = undefined
418
+ ) {
419
+ const res = await fetch(url, {
420
+ method,
421
+ headers: {
422
+ Accept: "application/json",
423
+ "Content-Type": "application/json",
424
+ ChecksumAppCode: this.config.apiKey,
425
+ },
426
+ body,
427
+ });
428
+
429
+ return res.json();
430
+ }
431
+
432
+ async getEnvInfo() {
433
+ const info = {
434
+ commitHash: "",
435
+ branch: "branch",
436
+ environment: process.env.CI ? "CI" : "local",
437
+ name: "name",
438
+ startedAt: Date.now(),
439
+ };
440
+
441
+ try {
442
+ info.commitHash = (await this.getCmdOutput(`git rev-parse HEAD`))
443
+ .toString()
444
+ .trim();
445
+ } catch (e) {
446
+ console.log("Error getting git hash", e.message);
447
+ }
448
+
449
+ try {
450
+ info.branch = (await this.getCmdOutput(`git rev-parse --abbrev-ref HEAD`))
451
+ .toString()
452
+ .trim();
453
+ } catch (e) {
454
+ console.log("Error getting branch name", e.message);
455
+ }
456
+
457
+ return info;
458
+ }
459
+
460
+ getVolatileConfigPath() {
461
+ return join(this.getRootDirPath(), "checksum.config.tmp.ts");
462
+ }
463
+
464
+ deleteVolatileConfig() {
465
+ const configPath = this.getVolatileConfigPath();
466
+ if (existsSync(configPath)) {
467
+ rmSync(configPath);
468
+ }
469
+ }
470
+
471
+ setChecksumConfig() {
472
+ this.config = {
473
+ ...(require(join(this.getRootDirPath(), "checksum.config.ts")).default ||
474
+ {}),
475
+ ...(this.volatileChecksumConfig || {}),
476
+ };
477
+ }
478
+
479
+ /**
480
+ * Search for checksum config in the command arguments.
481
+ * If found, parse and remove from args.
482
+ *
483
+ * @param args arguments passed to the command
484
+ * @returns args without checksum config
485
+ */
486
+ getChecksumConfigFromCommand(args) {
487
+ // delete any old config if exists
488
+ this.deleteVolatileConfig();
489
+
490
+ for (const arg of args) {
491
+ if (arg.startsWith("--checksum-config")) {
492
+ try {
493
+ this.volatileChecksumConfig = JSON.parse(arg.split("=")[1]);
494
+ return args.filter((a) => a !== arg);
495
+ } catch (e) {
496
+ console.log("Error parsing checksum config", e.message);
497
+ this.volatileChecksumConfig = undefined;
498
+ }
499
+ }
500
+ }
501
+
502
+ return args;
503
+ }
504
+
505
+ install() {
506
+ console.log(
507
+ "Creating Checksum directory and necessary files to run your tests"
508
+ );
509
+
510
+ const checksumRoot = this.getRootDirPath();
511
+
512
+ if (!existsSync(this.getRootDirPath())) {
513
+ mkdirSync(checksumRoot);
514
+ }
515
+
516
+ if (!existsSync(this.getChecksumRootOrigin())) {
517
+ throw new Error(
518
+ "Could not find checksum root directory, please install @checksum-ai/runtime package"
519
+ );
520
+
521
+ // automatically install?
522
+ }
523
+
524
+ // copy sources
525
+ [
526
+ "checksum.config.ts",
527
+ "playwright.config.ts",
528
+ "login.ts",
529
+ "README.md",
530
+ ].forEach((file) => {
531
+ copyFileSync(
532
+ join(this.getChecksumRootOrigin(), file),
533
+ join(checksumRoot, file)
534
+ );
535
+ });
536
+
537
+ // create tests folder
538
+ mkdirSync(join(checksumRoot, "tests"), {
539
+ recursive: true,
540
+ });
541
+
542
+ // create test data directories
543
+ ["esra", "har", "trace", "log"].forEach((folder) => {
544
+ mkdirSync(join(checksumRoot, "test-data", folder), {
545
+ recursive: true,
546
+ });
547
+ });
548
+ }
549
+
550
+ getRootDirPath() {
551
+ return join(process.cwd(), CHECKSUM_ROOT_FOLDER);
552
+ }
553
+
554
+ getChecksumRootOrigin() {
555
+ return join(
556
+ process.cwd(),
557
+ "node_modules",
558
+ "@checksum-ai",
559
+ "runtime",
560
+ "checksum-root"
561
+ );
562
+ }
563
+
564
+ /**
565
+ * Adds a timeout limit for a promise to resolve
566
+ * Will throw an error if the promise does not resolve within the timeout limit
567
+ *
568
+ * @param promise promise to add timeout to
569
+ * @param timeout timeout in milliseconds
570
+ * @param errMessage error message to throw if timeout is reached
571
+ * @returns promise that resolves if the original promise resolves within the timeout limit
572
+ */
573
+ guardReturn = async (
574
+ promise,
575
+ timeout = 1_000,
576
+ errMessage = "action hang guard timed out"
577
+ ) => {
578
+ const timeoutStringIdentifier = "guard-timed-out";
579
+ const guard = async () => {
580
+ await this.awaitSleep(timeout + 1_000);
581
+ return timeoutStringIdentifier;
582
+ };
583
+
584
+ const res = await Promise.race([promise, guard()]);
585
+ if (typeof res === "string" && res === timeoutStringIdentifier) {
586
+ throw new Error(errMessage);
587
+ }
588
+ return res;
589
+ };
590
+
591
+ awaitSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
592
+ }
593
+
594
+ /**
595
+ * Trigger the main function
596
+ */
597
+ (async () => {
598
+ await new ChecksumCLI().execute();
599
+ })();
package/index.d.ts CHANGED
@@ -53,7 +53,6 @@ export type ChecksumConfig = {
53
53
  */
54
54
  baseURL: string;
55
55
 
56
- apiURL: string;
57
56
  /**
58
57
  * Account's username that will be used
59
58
  * to login into your testing environment
@@ -64,6 +63,7 @@ export type ChecksumConfig = {
64
63
  * to login into your testing environment
65
64
  */
66
65
  password?: string;
66
+
67
67
  options?: Partial<RuntimeOptions>;
68
68
  };
69
69