@camperaid/watest 2.4.12 → 2.5.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.
Files changed (38) hide show
  1. package/core/base.js +10 -3
  2. package/core/series.js +70 -15
  3. package/core/settings.js +1 -1
  4. package/core/system.js +68 -0
  5. package/eslint.config.js +1 -1
  6. package/index.js +10 -0
  7. package/interfaces/servicer.js +0 -1
  8. package/package.json +1 -1
  9. package/tests/base/t_system.js +59 -0
  10. package/tests/base/t_throws.js +67 -0
  11. package/tests/e2e/t_folder.js +1 -1
  12. package/tests/e2e/t_loader.js +2 -2
  13. package/tests/e2e/t_loader_mixed.js +2 -2
  14. package/tests/e2e/t_loader_multiple.js +3 -3
  15. package/tests/e2e/t_loader_multiple_patterns.js +3 -3
  16. package/tests/e2e/t_single.js +1 -1
  17. package/tests/e2e/t_wd_firefox.js +2 -2
  18. package/tests/e2e/t_wd_firefox_chrome.js +2 -2
  19. package/tests/e2e/t_wd_firefox_chrome_pattern.js +2 -2
  20. package/tests/e2e/t_wd_mixed_firefox.js +2 -2
  21. package/tests/e2e/t_wd_mixed_firefox_chrome.js +2 -2
  22. package/tests/series/build/t_pattern_filtering.js +175 -0
  23. package/tests/series/logging/t_verify.js +1 -1
  24. package/tests/series/meta.js +1 -0
  25. package/tests/series/perform/t_failure.js +1 -0
  26. package/tests/series/perform/t_failure_notest.js +1 -0
  27. package/tests/series/perform/t_intermittent.js +2 -0
  28. package/tests/series/perform/t_intermittent_global.js +1 -0
  29. package/tests/series/perform/t_missing_perma.js +2 -0
  30. package/tests/series/perform/t_nested.js +1 -0
  31. package/tests/series/perform/t_perma.js +1 -0
  32. package/tests/series/perform/t_success.js +2 -0
  33. package/tests/series/run/t_debunk_failure.js +1 -1
  34. package/tests/series/run/t_debunk_success.js +1 -1
  35. package/tests/series/run/t_nested.js +1 -1
  36. package/tests/series/run/t_verify.js +1 -1
  37. package/tests/series/run/t_verify_webdriver.js +1 -1
  38. package/tests/series/servicer/t_servicer.js +84 -0
package/core/base.js CHANGED
@@ -551,9 +551,16 @@ function is_out(got, expected, msg) {
551
551
  return false;
552
552
  }
553
553
 
554
- function throws(func, exception, msg) {
555
- const on_no_exception = () => fail(`${msg}: no '${exception}' exception`);
556
- const on_exception = e => is(e?.message, exception, msg);
554
+ function throws(func, expected, msg) {
555
+ const on_no_exception = () => fail(`${msg}: no '${expected}' exception`);
556
+ const on_exception = e =>
557
+ is(
558
+ expected instanceof RegExp || typeof expected === 'string'
559
+ ? e?.message
560
+ : e,
561
+ expected,
562
+ msg,
563
+ );
557
564
  return throw_internal(func, on_no_exception, on_exception);
558
565
  }
559
566
 
package/core/series.js CHANGED
@@ -145,9 +145,6 @@ class Series {
145
145
  await this.runFor(this.failures, '2');
146
146
  }
147
147
  }
148
-
149
- await settings.servicer.shutdown();
150
-
151
148
  log(`Elapsed: ${Date.now() - start_time}ms`);
152
149
  }
153
150
 
@@ -181,6 +178,7 @@ class Series {
181
178
  }
182
179
 
183
180
  shutdown() {
181
+ console.log(`Testsuite: shutdown`);
184
182
  testflow.unlock();
185
183
  return this.failures.length > 0 ? this.failures : null;
186
184
  }
@@ -367,8 +365,24 @@ class Series {
367
365
  return [];
368
366
  }
369
367
 
368
+ // Filter subfolders to only process those that match the patterns
369
+ let filteredSubfolders = subfolders;
370
+ if (patterns.length > 0) {
371
+ filteredSubfolders = subfolders.filter(subfolder => {
372
+ const subfolderPath = `${folder}/${subfolder}`;
373
+ return (
374
+ this.matchedPatterns({
375
+ path: subfolderPath,
376
+ webdriver,
377
+ patterns,
378
+ path_is_not_final: true,
379
+ }).length > 0
380
+ );
381
+ });
382
+ }
383
+
370
384
  let subtests_for_subfolders = await Promise.all(
371
- subfolders.map(subfolder =>
385
+ filteredSubfolders.map(subfolder =>
372
386
  this.build({
373
387
  patterns,
374
388
  folder: `${folder}/${subfolder}`,
@@ -428,13 +442,21 @@ class Series {
428
442
  test_module.expected_failures || [],
429
443
  );
430
444
 
431
- // Initialize
445
+ // Set services if given.
446
+ if (test_module.servicer) {
447
+ if (this.#servicerType) {
448
+ throw new Error(`No nested servicers are supported`);
449
+ }
450
+ this.#servicerType = test_module.servicer;
451
+ }
452
+
453
+ // Initialize.
432
454
  if (test_module.services || test_module.init) {
433
455
  let init = async () => {
434
456
  // Start services if any.
435
457
  let chain = Promise.resolve();
436
458
  for (let service of test_module.services || []) {
437
- chain = chain.then(() => settings.servicer.start(service));
459
+ chain = chain.then(() => this.getServicer().start(service));
438
460
  }
439
461
  await chain;
440
462
 
@@ -460,7 +482,7 @@ class Series {
460
482
  // A function to notify the servicer the test is about to start and then
461
483
  // to invoke the test.
462
484
  const test_wrap = () =>
463
- Promise.resolve(settings.servicer.ontest(name)).then(test);
485
+ Promise.resolve(this.#servicer?.ontest(name)).then(test);
464
486
 
465
487
  let failures_info = Series.failuresInfo({
466
488
  failures: expected_failures,
@@ -494,8 +516,13 @@ class Series {
494
516
  await Promise.all(
495
517
  [...(test_module.services || [])]
496
518
  .reverse()
497
- .map(s => settings.servicer.stop(s)),
519
+ .map(s => this.getServicer().stop(s)),
498
520
  );
521
+
522
+ // Clean up service-level servicer
523
+ await this.#servicer?.shutdown();
524
+ this.#servicer = null;
525
+ this.#servicerType = null;
499
526
  };
500
527
  tests.push({
501
528
  name: `${virtual_folder}/uninit`,
@@ -641,6 +668,16 @@ class Series {
641
668
  let kungFuDeathGrip = null;
642
669
  let kungFuDeathGripResolve = null;
643
670
  let kungFuDeathGripTimer = 0;
671
+
672
+ // Take snapshots before the test runs
673
+ const testSnapshots = {
674
+ fcnt: this.core.failureCount,
675
+ icnt: this.core.intermittentCount,
676
+ tcnt: this.core.todoCount,
677
+ wcnt: this.core.warningCount,
678
+ ocnt: this.core.okCount,
679
+ };
680
+
644
681
  try {
645
682
  this.core.setExpectedFailures(failures_info);
646
683
 
@@ -687,6 +724,7 @@ class Series {
687
724
  init_or_uninit,
688
725
  path,
689
726
  webdriver,
727
+ snapshots: testSnapshots,
690
728
  });
691
729
 
692
730
  // If failed, then stop running the current tests.
@@ -715,12 +753,12 @@ class Series {
715
753
  await this.LogPipe.release();
716
754
  }
717
755
 
718
- recordStats({ name, init_or_uninit, path, webdriver }) {
756
+ recordStats({ name, init_or_uninit, path, webdriver, snapshots }) {
719
757
  let hasChanged = false;
720
758
  let hasFailures = false;
721
759
 
722
760
  // Record intermittents.
723
- let delta = this.core.intermittentCount - this.icnt;
761
+ let delta = this.core.intermittentCount - snapshots.icnt;
724
762
  if (delta > 0) {
725
763
  log(`>${name} has ${delta} intermittent(s)`);
726
764
  this.icnt = this.core.intermittentCount;
@@ -728,7 +766,7 @@ class Series {
728
766
  }
729
767
 
730
768
  // Record todos.
731
- delta = this.core.todoCount - this.tcnt;
769
+ delta = this.core.todoCount - snapshots.tcnt;
732
770
  if (delta > 0) {
733
771
  log(`>${name} has ${delta} todo(s)`);
734
772
  this.tcnt = this.core.todoCount;
@@ -736,7 +774,7 @@ class Series {
736
774
  }
737
775
 
738
776
  // Record warnings.
739
- delta = this.core.warningCount - this.wcnt;
777
+ delta = this.core.warningCount - snapshots.wcnt;
740
778
  if (delta > 0) {
741
779
  log(`>${name} has ${delta} warnings(s)`);
742
780
  this.wcnt = this.core.warningCount;
@@ -744,14 +782,14 @@ class Series {
744
782
  }
745
783
 
746
784
  // Record successful test count.
747
- delta = this.core.okCount - this.ocnt;
785
+ delta = this.core.okCount - snapshots.ocnt;
748
786
  if (delta > 0) {
749
787
  this.ocnt = this.core.okCount;
750
788
  hasChanged = true;
751
789
  }
752
790
 
753
791
  // Fail if no changes.
754
- delta = this.core.failureCount - this.fcnt;
792
+ delta = this.core.failureCount - snapshots.fcnt;
755
793
  if (!init_or_uninit) {
756
794
  if (delta == 0 && !hasChanged) {
757
795
  delta = 1;
@@ -908,7 +946,10 @@ class Series {
908
946
  let args = [];
909
947
  if (loader) {
910
948
  // Use the new --import flag with register() API instead of deprecated --loader
911
- args.push('--import', `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("${loader}", pathToFileURL("./"));`);
949
+ args.push(
950
+ '--import',
951
+ `data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("${loader}", pathToFileURL("./"));`,
952
+ );
912
953
  }
913
954
  const watest_bin = nodepath.join(__dirname, '../bin/watest.js');
914
955
  args.push(
@@ -1018,6 +1059,20 @@ class Series {
1018
1059
  }
1019
1060
  }
1020
1061
  }
1062
+
1063
+ getServicer() {
1064
+ if (!this.#servicer) {
1065
+ this.#servicer = this.createServicer(this.#servicerType);
1066
+ }
1067
+ return this.#servicer;
1068
+ }
1069
+
1070
+ createServicer(servicerType) {
1071
+ return settings.getServicer(servicerType);
1072
+ }
1073
+
1074
+ #servicer;
1075
+ #servicerType;
1021
1076
  }
1022
1077
 
1023
1078
  export { Series };
package/core/settings.js CHANGED
@@ -16,7 +16,7 @@ class Settings {
16
16
  await import(this.rc.logger || '../interfaces/logger.js')
17
17
  ).default;
18
18
 
19
- this.servicer = (
19
+ this.getServicer = (
20
20
  await import(this.rc.servicer || '../interfaces/servicer.js')
21
21
  ).default;
22
22
 
package/core/system.js ADDED
@@ -0,0 +1,68 @@
1
+ import { spawn } from './spawn.js';
2
+
3
+ /**
4
+ * Run a shell command and capture its output
5
+ * @param {string} cmd - Command to run
6
+ * @param {string[]} args - Command arguments
7
+ * @param {object} options - Spawn options
8
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number, stdoutLines: string[], stderrLines: string[]}>}
9
+ */
10
+ export async function runCommand(cmd, args = [], options = {}) {
11
+ let stdout = [];
12
+ let stderr = [];
13
+ let exitCode = 0;
14
+
15
+ try {
16
+ exitCode = await spawn(cmd, args, options, buffer => {
17
+ for (let { str_data, is_stdout } of buffer) {
18
+ const lines = str_data.split('\n').filter(line => line);
19
+ if (is_stdout) {
20
+ stdout.push(...lines);
21
+ } else {
22
+ stderr.push(...lines);
23
+ }
24
+ }
25
+ });
26
+ } catch (code) {
27
+ exitCode = code;
28
+ }
29
+
30
+ return {
31
+ stdout: stdout.join('\n'),
32
+ stderr: stderr.join('\n'),
33
+ exitCode,
34
+ stdoutLines: stdout,
35
+ stderrLines: stderr,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Run a bash script with proper error handling
41
+ * @param {string} script - Bash script content
42
+ * @param {string[]} args - Script arguments
43
+ * @param {object} options - Spawn options
44
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number, stdoutLines: string[], stderrLines: string[]}>}
45
+ */
46
+ export async function runBashScript(script, args = [], options = {}) {
47
+ return runCommand('bash', ['-c', script, '--', ...args], options);
48
+ }
49
+
50
+ /**
51
+ * Run a shell command and return only stdout (for simple cases)
52
+ * @param {string} cmd - Command to run
53
+ * @param {string[]} args - Command arguments
54
+ * @param {object} options - Spawn options
55
+ * @returns {Promise<string>} stdout content
56
+ */
57
+ export async function execCommand(cmd, args = [], options = {}) {
58
+ const result = await runCommand(cmd, args, options);
59
+ if (result.exitCode !== 0) {
60
+ throw new Error(
61
+ `Command failed with exit code ${result.exitCode}: ${result.stderr}`,
62
+ );
63
+ }
64
+ return result.stdout;
65
+ }
66
+
67
+ // Export the raw spawn function for advanced use cases
68
+ export { spawn };
package/eslint.config.js CHANGED
@@ -6,7 +6,7 @@ export default [
6
6
  nodePlugin.configs['flat/recommended-script'],
7
7
  {
8
8
  languageOptions: {
9
- ecmaVersion: 2021,
9
+ ecmaVersion: 2022,
10
10
  sourceType: 'module',
11
11
  },
12
12
  },
package/index.js CHANGED
@@ -26,12 +26,19 @@ import { inspect } from './core/util.js';
26
26
  import { AppDriver } from './webdriver/app_driver.js';
27
27
  import { ControlDriver } from './webdriver/control_driver.js';
28
28
  import { start_session, scope } from './webdriver/session.js';
29
+ import {
30
+ runCommand,
31
+ runBashScript,
32
+ execCommand,
33
+ spawn,
34
+ } from './core/system.js';
29
35
 
30
36
  export {
31
37
  AppDriver,
32
38
  ControlDriver,
33
39
  assert,
34
40
  contains,
41
+ execCommand,
35
42
  failed,
36
43
  fail,
37
44
  group,
@@ -42,7 +49,10 @@ export {
42
49
  not_reached,
43
50
  no_throws,
44
51
  ok,
52
+ runBashScript,
53
+ runCommand,
45
54
  scope,
55
+ spawn,
46
56
  start_session,
47
57
  success,
48
58
  todo,
@@ -16,7 +16,6 @@ class Servicer {
16
16
  * Called when the testsuite gets shutdown.
17
17
  */
18
18
  shutdown() {
19
- console.log(`Testsuite: shutdown`);
20
19
  }
21
20
 
22
21
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camperaid/watest",
3
- "version": "2.4.12",
3
+ "version": "2.5.1",
4
4
  "description": "Web Application Testsuite",
5
5
  "type": "module",
6
6
  "engines": {
@@ -0,0 +1,59 @@
1
+ import { ok, contains } from '../../index.js';
2
+ import { runCommand, runBashScript, execCommand } from '../../core/system.js';
3
+
4
+ export async function test() {
5
+ // Test 1: runCommand with echo
6
+ {
7
+ const result = await runCommand('echo', ['hello', 'world']);
8
+ ok(result.exitCode === 0, 'echo command should succeed');
9
+ ok(result.stdout === 'hello world', 'stdout should contain echo output');
10
+ ok(result.stderr === '', 'stderr should be empty');
11
+ ok(result.stdoutLines.length === 1, 'should have one stdout line');
12
+ ok(result.stdoutLines[0] === 'hello world', 'stdout line should match');
13
+ }
14
+
15
+ // Test 2: runBashScript with simple script
16
+ {
17
+ const script = 'echo "test script"; echo "line 2"';
18
+ const result = await runBashScript(script);
19
+ ok(result.exitCode === 0, 'bash script should succeed');
20
+ contains(result.stdout, 'test script', 'should contain first echo');
21
+ contains(result.stdout, 'line 2', 'should contain second echo');
22
+ ok(result.stdoutLines.length === 2, 'should have two stdout lines');
23
+ }
24
+
25
+ // Test 3: execCommand for simple cases
26
+ {
27
+ const output = await execCommand('echo', ['simple test']);
28
+ ok(output === 'simple test', 'execCommand should return stdout directly');
29
+ }
30
+
31
+ // Test 4: runCommand with failing command
32
+ {
33
+ const result = await runCommand('bash', ['-c', 'echo "error" >&2; exit 1']);
34
+ ok(result.exitCode === 1, 'failing command should return exit code 1');
35
+ ok(result.stdout === '', 'stdout should be empty');
36
+ ok(result.stderr === 'error', 'stderr should contain error message');
37
+ }
38
+
39
+ // Test 5: execCommand should throw on failure
40
+ {
41
+ let threw = false;
42
+ try {
43
+ await execCommand('bash', ['-c', 'exit 1']);
44
+ } catch (error) {
45
+ threw = true;
46
+ contains(error.message, 'exit code 1', 'error should mention exit code');
47
+ }
48
+ ok(threw, 'execCommand should throw on non-zero exit code');
49
+ }
50
+
51
+ // Test 6: runBashScript with arguments
52
+ {
53
+ const script = 'echo "arg1: $1, arg2: $2"';
54
+ const result = await runBashScript(script, ['first', 'second']);
55
+ ok(result.exitCode === 0, 'bash script with args should succeed');
56
+ contains(result.stdout, 'arg1: first', 'should pass first argument');
57
+ contains(result.stdout, 'arg2: second', 'should pass second argument');
58
+ }
59
+ }
@@ -136,4 +136,71 @@ unexpected character: '2' at 6 pos, expected: '1' at '' line
136
136
  ],
137
137
  `no_throws fail (async)`,
138
138
  );
139
+
140
+ // throws: accept object descriptor (statusCode + JSON body)
141
+ await is_output(
142
+ () =>
143
+ throws(
144
+ () => {
145
+ const err = new Error(
146
+ 'HTTP error code 422 for https://api.digitalocean.com/v2/droplets',
147
+ );
148
+ err.statusCode = 422;
149
+ err.responseBody = JSON.stringify({
150
+ id: 'unprocessable_entity',
151
+ message: 'You specified an invalid image for Droplet creation.',
152
+ });
153
+ throw err;
154
+ },
155
+ {
156
+ statusCode: 422,
157
+ responseBody: JSON.stringify({
158
+ id: 'unprocessable_entity',
159
+ message: 'You specified an invalid image for Droplet creation.',
160
+ }),
161
+ },
162
+ `Throws error descriptor`,
163
+ ),
164
+ [
165
+ `Ok: Throws error descriptor, got: {statusCode: 422, responseBody: '{"id":"unprocessable_entity","message":"You specified an invalid image for Droplet creation."}'}`,
166
+ ],
167
+ [],
168
+ `throws accept object descriptor`,
169
+ );
170
+
171
+ // throws: accept RegExp for message matching
172
+ await is_output(
173
+ () =>
174
+ throws(
175
+ () => {
176
+ throw new Error(
177
+ 'Unexpected 404 response code for https://example.com/test from droplet 1',
178
+ );
179
+ },
180
+ /404|Unexpected/,
181
+ `Throws with RegExp`,
182
+ ),
183
+ [
184
+ `Ok: Throws with RegExp 'Unexpected 404 response code for https://example.com/test from droplet 1' matches /404|Unexpected/ regexp`,
185
+ ],
186
+ [],
187
+ `throws accept RegExp pattern`,
188
+ );
189
+
190
+ // throws: fail RegExp that doesn't match
191
+ await is_output(
192
+ () =>
193
+ throws(
194
+ () => {
195
+ throw new Error('Some other error message');
196
+ },
197
+ /404|Unexpected/,
198
+ `RegExp should match`,
199
+ ),
200
+ [],
201
+ [
202
+ `Failed: RegExp should match 'Some other error message' doesn't match /404|Unexpected/ regexp`,
203
+ ],
204
+ `throws fail RegExp no match`,
205
+ );
139
206
  }
@@ -13,8 +13,8 @@ export async function test() {
13
13
  '\x1B[38;5;243mCompleted\x1B[0m sample/unit',
14
14
  '\x1B[102mSuccess!\x1B[0m Total: 1',
15
15
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
16
- 'Testsuite: shutdown',
17
16
  'Elapsed:',
17
+ 'Testsuite: shutdown',
18
18
  ],
19
19
  'stdout',
20
20
  );
@@ -11,12 +11,12 @@ export async function test() {
11
11
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
12
12
  '>sample/t_test.js completed in',
13
13
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
14
- 'Testsuite: shutdown',
15
14
  'Elapsed:',
15
+ 'Testsuite: shutdown',
16
16
  '\x1B[102mSuccess!\x1B[0m Total: 1',
17
17
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
18
- 'Testsuite: shutdown',
19
18
  'Elapsed:',
19
+ 'Testsuite: shutdown',
20
20
  ],
21
21
  'stdout',
22
22
  );
@@ -16,12 +16,12 @@ export async function test() {
16
16
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
17
17
  '>sample/ui/t_test.js completed in',
18
18
  '\x1B[38;5;243mCompleted\x1B[0m sample/ui',
19
- 'Testsuite: shutdown',
20
19
  'Elapsed:',
20
+ 'Testsuite: shutdown',
21
21
  '\x1B[102mSuccess!\x1B[0m Total: 2',
22
22
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
23
- 'Testsuite: shutdown',
24
23
  'Elapsed:',
24
+ 'Testsuite: shutdown',
25
25
  ],
26
26
  'stdout',
27
27
  );
@@ -11,19 +11,19 @@ export async function test() {
11
11
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
12
12
  '>sample/base/t_btest.js completed in',
13
13
  '\x1B[38;5;243mCompleted\x1B[0m sample/base',
14
- 'Testsuite: shutdown',
15
14
  'Elapsed:',
15
+ 'Testsuite: shutdown',
16
16
  '\x1B[38;5;99mStarted\x1B[0m sample/core',
17
17
  '!Running: sample/core/t_ctest.js, path: tests/core/t_ctest.js',
18
18
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
19
19
  '>sample/core/t_ctest.js completed in',
20
20
  '\x1B[38;5;243mCompleted\x1B[0m sample/core',
21
- 'Testsuite: shutdown',
22
21
  'Elapsed:',
22
+ 'Testsuite: shutdown',
23
23
  '\x1B[102mSuccess!\x1B[0m Total: 2',
24
24
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
25
- 'Testsuite: shutdown',
26
25
  'Elapsed:',
26
+ 'Testsuite: shutdown',
27
27
  ],
28
28
  'stdout',
29
29
  );
@@ -13,19 +13,19 @@ export async function test() {
13
13
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
14
14
  '>sample/base/t_btest.js completed in',
15
15
  '\x1B[38;5;243mCompleted\x1B[0m sample/base',
16
- 'Testsuite: shutdown',
17
16
  'Elapsed:',
17
+ 'Testsuite: shutdown',
18
18
  '\x1B[38;5;99mStarted\x1B[0m sample/core',
19
19
  '!Running: sample/core/t_ctest.js, path: tests/core/t_ctest.js',
20
20
  '\x1B[32mOk:\x1B[0m Mocked!, got: mocked',
21
21
  '>sample/core/t_ctest.js completed in',
22
22
  '\x1B[38;5;243mCompleted\x1B[0m sample/core',
23
- 'Testsuite: shutdown',
24
23
  'Elapsed:',
24
+ 'Testsuite: shutdown',
25
25
  '\x1B[102mSuccess!\x1B[0m Total: 2',
26
26
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
27
- 'Testsuite: shutdown',
28
27
  'Elapsed:',
28
+ 'Testsuite: shutdown',
29
29
  ],
30
30
  'stdout',
31
31
  );
@@ -11,8 +11,8 @@ export async function test() {
11
11
  '>sample/t_test.js completed in',
12
12
  '\x1B[102mSuccess!\x1B[0m Total: 1',
13
13
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
14
- 'Testsuite: shutdown',
15
14
  'Elapsed:',
15
+ 'Testsuite: shutdown',
16
16
  ],
17
17
  'stdout',
18
18
  );
@@ -13,12 +13,12 @@ export async function test() {
13
13
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
14
14
  '>sample/firefox/t_test.js completed in',
15
15
  '\x1B[38;5;243mCompleted\x1B[0m sample/firefox',
16
- 'Testsuite: shutdown',
17
16
  'Elapsed:',
17
+ 'Testsuite: shutdown',
18
18
  '\x1B[102mSuccess!\x1B[0m Total: 1',
19
19
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
20
- 'Testsuite: shutdown',
21
20
  'Elapsed:',
21
+ 'Testsuite: shutdown',
22
22
  ],
23
23
  'stdout',
24
24
  );
@@ -13,8 +13,8 @@ export async function test() {
13
13
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
14
14
  '>sample/firefox/t_test.js completed in',
15
15
  '\x1B[38;5;243mCompleted\x1B[0m sample/firefox',
16
- 'Testsuite: shutdown',
17
16
  'Elapsed:',
17
+ 'Testsuite: shutdown',
18
18
  '\x1B[38;5;99mStarted\x1B[0m sample/chrome',
19
19
  '!Running: sample/chrome/t_test.js, path: tests/t_test.js',
20
20
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
@@ -22,8 +22,8 @@ export async function test() {
22
22
  '\x1B[38;5;243mCompleted\x1B[0m sample/chrome',
23
23
  '\x1B[102mSuccess!\x1B[0m Total: 2',
24
24
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
25
- 'Testsuite: shutdown',
26
25
  'Elapsed:',
26
+ 'Testsuite: shutdown',
27
27
  ],
28
28
  'stdout',
29
29
  );
@@ -14,8 +14,8 @@ export async function test() {
14
14
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
15
15
  '>sample/firefox/t_test.js completed in',
16
16
  '\x1B[38;5;243mCompleted\x1B[0m sample/firefox',
17
- 'Testsuite: shutdown',
18
17
  'Elapsed:',
18
+ 'Testsuite: shutdown',
19
19
  '\x1B[38;5;99mStarted\x1B[0m sample/chrome',
20
20
  '!Running: sample/chrome/t_test.js, path: tests/t_test.js',
21
21
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
@@ -23,8 +23,8 @@ export async function test() {
23
23
  '\x1B[38;5;243mCompleted\x1B[0m sample/chrome',
24
24
  '\x1B[102mSuccess!\x1B[0m Total: 2',
25
25
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
26
- 'Testsuite: shutdown',
27
26
  'Elapsed:',
27
+ 'Testsuite: shutdown',
28
28
  ],
29
29
  'stdout',
30
30
  );
@@ -19,13 +19,13 @@ export async function test() {
19
19
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
20
20
  '>sample/ui/firefox/t_test.js completed in',
21
21
  '\x1B[38;5;243mCompleted\x1B[0m sample/ui/firefox',
22
- 'Testsuite: shutdown',
23
22
  'Elapsed:',
23
+ 'Testsuite: shutdown',
24
24
  '\x1B[38;5;243mCompleted\x1B[0m sample/ui',
25
25
  '\x1B[102mSuccess!\x1B[0m Total: 2',
26
26
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
27
- 'Testsuite: shutdown',
28
27
  'Elapsed:',
28
+ 'Testsuite: shutdown',
29
29
  ],
30
30
  'stdout',
31
31
  );
@@ -19,8 +19,8 @@ export async function test() {
19
19
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
20
20
  '>sample/ui/firefox/t_test.js completed in',
21
21
  '\x1B[38;5;243mCompleted\x1B[0m sample/ui/firefox',
22
- 'Testsuite: shutdown',
23
22
  'Elapsed:',
23
+ 'Testsuite: shutdown',
24
24
  '\x1B[38;5;99mStarted\x1B[0m sample/ui/chrome',
25
25
  '!Running: sample/ui/chrome/t_test.js, path: tests/ui/t_test.js',
26
26
  '\x1B[32mOk:\x1B[0m Webdriver Works!',
@@ -29,8 +29,8 @@ export async function test() {
29
29
  '\x1B[38;5;243mCompleted\x1B[0m sample/ui',
30
30
  '\x1B[102mSuccess!\x1B[0m Total: 3',
31
31
  '\x1B[38;5;243mCompleted\x1B[0m sample/',
32
- 'Testsuite: shutdown',
33
32
  'Elapsed:',
33
+ 'Testsuite: shutdown',
34
34
  ],
35
35
  'stdout',
36
36
  );
@@ -0,0 +1,175 @@
1
+ import { is, MockSeries } from '../test.js';
2
+
3
+ export async function test() {
4
+ // Test that buildSubtests only processes subfolders that match patterns
5
+ // This test tracks which directories are actually accessed to verify the optimization
6
+
7
+ const accessedDirs = [];
8
+
9
+ class TrackingMockSeries extends MockSeries {
10
+ loadTestMeta(folder) {
11
+ accessedDirs.push(folder);
12
+ return super.loadTestMeta(folder);
13
+ }
14
+
15
+ getTestFileList(folder) {
16
+ accessedDirs.push(folder);
17
+ return super.getTestFileList(folder);
18
+ }
19
+ }
20
+
21
+ const ts = {
22
+ 'tests': {
23
+ meta: {
24
+ folders: ['unit', 'integration', 'e2e'],
25
+ },
26
+ },
27
+ 'tests/unit': {
28
+ meta: {
29
+ folders: ['base', 'core'],
30
+ },
31
+ },
32
+ 'tests/unit/base': {
33
+ files: ['t_test1.js'],
34
+ },
35
+ 'tests/unit/base/t_test1.js': {
36
+ test() {},
37
+ },
38
+ 'tests/unit/core': {
39
+ files: ['t_test2.js'],
40
+ },
41
+ 'tests/unit/core/t_test2.js': {
42
+ test() {},
43
+ },
44
+ 'tests/integration': {
45
+ files: ['t_integration.js'],
46
+ },
47
+ 'tests/integration/t_integration.js': {
48
+ test() {},
49
+ },
50
+ 'tests/e2e': {
51
+ files: ['t_e2e.js'],
52
+ },
53
+ 'tests/e2e/t_e2e.js': {
54
+ test() {},
55
+ },
56
+ };
57
+
58
+ const series = new TrackingMockSeries([], { ts });
59
+
60
+ // Test 1: When targeting a specific test, only relevant folders should be processed
61
+ accessedDirs.length = 0; // Clear the array
62
+
63
+ const tests = await series.build({
64
+ patterns: [
65
+ {
66
+ path: 'tests/unit/base/t_test1.js',
67
+ webdriver: '',
68
+ },
69
+ ],
70
+ folder: 'tests',
71
+ virtual_folder: 'mac',
72
+ });
73
+ series.shutdown();
74
+
75
+ // Should only build the unit folder and its relevant subfolders
76
+ // Should NOT build integration or e2e folders
77
+ const testNames = getAllTestNames(tests);
78
+
79
+ // Should include the targeted test and its path
80
+ is(
81
+ testNames.includes('mac/unit/base/t_test1.js'),
82
+ true,
83
+ 'Should include the targeted test',
84
+ );
85
+
86
+ // Should NOT include tests from unrelated folders
87
+ is(
88
+ testNames.includes('mac/integration/t_integration.js'),
89
+ false,
90
+ 'Should not include integration tests when targeting unit test',
91
+ );
92
+
93
+ is(
94
+ testNames.includes('mac/e2e/t_e2e.js'),
95
+ false,
96
+ 'Should not include e2e tests when targeting unit test',
97
+ );
98
+
99
+ // Check directory access optimization - this will fail with commented optimization
100
+ is(
101
+ accessedDirs.includes('tests/integration'),
102
+ false,
103
+ 'Should not access integration directory when targeting unit test',
104
+ );
105
+
106
+ is(
107
+ accessedDirs.includes('tests/e2e'),
108
+ false,
109
+ 'Should not access e2e directory when targeting unit test',
110
+ );
111
+
112
+ // Test 2: When no patterns specified, all folders should be processed
113
+ accessedDirs.length = 0; // Clear the array
114
+
115
+ const allTests = await series.build({
116
+ patterns: [],
117
+ folder: 'tests',
118
+ virtual_folder: 'mac',
119
+ });
120
+
121
+ const allTestNames = getAllTestNames(allTests);
122
+
123
+ // Should include all tests when no patterns specified
124
+ is(
125
+ allTestNames.includes('mac/unit/base/t_test1.js'),
126
+ true,
127
+ 'Should include unit test when no patterns',
128
+ );
129
+
130
+ is(
131
+ allTestNames.includes('mac/integration/t_integration.js'),
132
+ true,
133
+ 'Should include integration test when no patterns',
134
+ );
135
+
136
+ is(
137
+ allTestNames.includes('mac/e2e/t_e2e.js'),
138
+ true,
139
+ 'Should include e2e test when no patterns',
140
+ );
141
+
142
+ // When no patterns, all directories should be accessed
143
+ is(
144
+ accessedDirs.includes('tests/integration'),
145
+ true,
146
+ 'Should access integration directory when no patterns specified',
147
+ );
148
+
149
+ is(
150
+ accessedDirs.includes('tests/e2e'),
151
+ true,
152
+ 'Should access e2e directory when no patterns specified',
153
+ );
154
+ }
155
+
156
+ function getAllTestNames(tests) {
157
+ const names = [];
158
+
159
+ function collectNames(testList) {
160
+ for (const test of testList) {
161
+ if (test.subtests) {
162
+ collectNames(test.subtests);
163
+ } else if (
164
+ test.name &&
165
+ !test.name.includes('/init') &&
166
+ !test.name.includes('/uninit')
167
+ ) {
168
+ names.push(test.name);
169
+ }
170
+ }
171
+ }
172
+
173
+ collectNames(tests);
174
+ return names;
175
+ }
@@ -146,7 +146,7 @@ export async function test() {
146
146
  is(
147
147
  buffers,
148
148
  [
149
- ['log', ['Testsuite: shutdown', got => got.startsWith('Elapsed:')]],
149
+ ['log', [got => got.startsWith('Elapsed:')]],
150
150
  [
151
151
  'mac/log',
152
152
  [
@@ -5,4 +5,5 @@ export var folders = [
5
5
  'run',
6
6
  'logging',
7
7
  'loader',
8
+ 'servicer',
8
9
  ];
@@ -21,6 +21,7 @@ export async function test() {
21
21
  '!Running: t_testo.js, path: t_testo.js',
22
22
  '>t_testo.js completed in',
23
23
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
24
+ 'Testsuite: shutdown',
24
25
  ];
25
26
  const expected_stderr = [
26
27
  '\x1B[31mFailed:\x1B[0m Failio',
@@ -16,6 +16,7 @@ export async function test() {
16
16
  '!Running: t_testo.js, path: t_testo.js',
17
17
  '>t_testo.js completed in',
18
18
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
19
+ 'Testsuite: shutdown',
19
20
  ],
20
21
  [
21
22
  '\x1B[31mFailed:\x1B[0m Neighter failure nor success in t_testo.js',
@@ -26,6 +26,7 @@ export async function test() {
26
26
  '>t_testo.js has 1 warnings(s)',
27
27
  '>t_testo.js completed in',
28
28
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
29
+ 'Testsuite: shutdown',
29
30
  ];
30
31
  let expected_stderr = [];
31
32
 
@@ -77,6 +78,7 @@ export async function test() {
77
78
  '>t_testo_2.js has 3 warnings(s)',
78
79
  '>t_testo_2.js completed in',
79
80
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
81
+ 'Testsuite: shutdown',
80
82
  ];
81
83
  expected_stderr = [];
82
84
 
@@ -71,6 +71,7 @@ export async function test() {
71
71
  '\x1B[38;5;243mCompleted\x1B[0m unit/core',
72
72
  '\x1B[38;5;243mCompleted\x1B[0m unit',
73
73
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
74
+ 'Testsuite: shutdown',
74
75
  ],
75
76
  [],
76
77
  'global intermittents',
@@ -33,6 +33,7 @@ export async function test() {
33
33
  '!Running: t_testo.js, path: t_testo.js',
34
34
  '>t_testo.js completed in',
35
35
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
36
+ 'Testsuite: shutdown',
36
37
  ],
37
38
  [
38
39
  '\x1B[31mFailed:\x1B[0m Server busy',
@@ -79,6 +80,7 @@ export async function test() {
79
80
  '>t_testo.js has 1 warnings(s)',
80
81
  '>t_testo.js completed in',
81
82
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
83
+ 'Testsuite: shutdown',
82
84
  ],
83
85
  [],
84
86
  'no perma but intermittent',
@@ -50,6 +50,7 @@ export async function test() {
50
50
  '\x1B[38;5;243mCompleted\x1B[0m unit/core',
51
51
  '\x1B[38;5;243mCompleted\x1B[0m unit',
52
52
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
53
+ 'Testsuite: shutdown',
53
54
  ],
54
55
  [],
55
56
  'nested',
@@ -36,6 +36,7 @@ export async function test() {
36
36
  '>t_testo.js has 1 warnings(s)',
37
37
  '>t_testo.js completed in',
38
38
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
39
+ 'Testsuite: shutdown',
39
40
  ],
40
41
  [],
41
42
  'perma',
@@ -18,6 +18,7 @@ export async function test() {
18
18
  '\x1B[32mOk:\x1B[0m Successio!',
19
19
  '>t_testo.js completed in',
20
20
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
21
+ 'Testsuite: shutdown',
21
22
  ],
22
23
  [],
23
24
  'success',
@@ -53,6 +54,7 @@ export async function test() {
53
54
  '\x1B[32mOk:\x1B[0m Successio!',
54
55
  '>t_testo.js completed in',
55
56
  '\x1B[38;5;243mCompleted\x1B[0m tests/',
57
+ 'Testsuite: shutdown',
56
58
  ],
57
59
  [],
58
60
  'success #2',
@@ -31,9 +31,9 @@ export async function test() {
31
31
  '\x1B[41m\x1B[37mFailed!\x1B[0m Passed: 0. Failed: 1',
32
32
  '\x1B[38;5;243mCompleted\x1B[0m mac/',
33
33
  'Logs are written to',
34
- 'Testsuite: shutdown',
35
34
  'Elapsed:',
36
35
  'Logs are written to',
36
+ 'Testsuite: shutdown',
37
37
  ];
38
38
 
39
39
  const expected_stderr = [
@@ -40,9 +40,9 @@ export async function test() {
40
40
  ...expected_out_for_success,
41
41
  ...expected_out_for_success,
42
42
 
43
- 'Testsuite: shutdown',
44
43
  'Elapsed:',
45
44
  'Logs are written to',
45
+ 'Testsuite: shutdown',
46
46
  ];
47
47
 
48
48
  const expected_stderr = [];
@@ -55,9 +55,9 @@ export async function test() {
55
55
  '\x1B[102mSuccess!\x1B[0m Total: 2',
56
56
  '\x1B[38;5;243mCompleted\x1B[0m mac/',
57
57
  'Logs are written to',
58
- 'Testsuite: shutdown',
59
58
  'Elapsed:',
60
59
  'Logs are written to',
60
+ 'Testsuite: shutdown',
61
61
  ],
62
62
  [],
63
63
  'nested',
@@ -66,9 +66,9 @@ export async function test() {
66
66
  '\x1B[41m\x1B[37mFailed!\x1B[0m Passed: 0. Failed: 1',
67
67
  '\x1B[38;5;243mCompleted\x1B[0m mac/',
68
68
  'Logs are written to',
69
- 'Testsuite: shutdown',
70
69
  'Elapsed:',
71
70
  'Logs are written to',
71
+ 'Testsuite: shutdown',
72
72
  ];
73
73
 
74
74
  const expected_stderr = [
@@ -84,9 +84,9 @@ export async function test() {
84
84
  '\x1B[102mSuccess!\x1B[0m Total: 1',
85
85
  '\x1B[38;5;243mCompleted\x1B[0m mac/',
86
86
  'Logs are written to',
87
- 'Testsuite: shutdown',
88
87
  'Elapsed:',
89
88
  'Logs are written to',
89
+ 'Testsuite: shutdown',
90
90
  ];
91
91
 
92
92
  const expected_stderr = [
@@ -0,0 +1,84 @@
1
+ import { is_test_output, success } from '../../base/test.js';
2
+ import { log } from '../../../logging/logging.js';
3
+ import { MockSeries } from '../mock_series.js';
4
+
5
+ // Mock servicer that logs to console for output validation
6
+ class MockServicer {
7
+ constructor(type) {
8
+ this.type = type;
9
+ }
10
+
11
+ async start(service) {
12
+ log(`MockServicer:${this.type} starting ${service}`);
13
+ return { started: service, type: this.type };
14
+ }
15
+
16
+ async stop(service) {
17
+ log(`MockServicer:${this.type} stopping ${service}`);
18
+ return { stopped: service, type: this.type };
19
+ }
20
+
21
+ async shutdown() {
22
+ log(`MockServicer:${this.type} shutdown`);
23
+ return { shutdown: true, type: this.type };
24
+ }
25
+
26
+ async ontest() {
27
+ // Optional: log test notifications
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // Extended MockSeries that uses our logging servicer
33
+ class MockSeriesWithServicer extends MockSeries {
34
+ createServicer(type) {
35
+ return new MockServicer(type || 'default');
36
+ }
37
+ }
38
+
39
+ export async function test() {
40
+ const ts = {
41
+ 'tests': {
42
+ meta: {
43
+ servicer: 'docker',
44
+ services: ['mysql', 'redis'],
45
+ },
46
+ files: ['t_example.js'],
47
+ },
48
+ 'tests/t_example.js': {
49
+ test() {
50
+ success('Servicer test example works');
51
+ },
52
+ },
53
+ };
54
+
55
+ await is_test_output(
56
+ () => MockSeriesWithServicer.run([], { ts }),
57
+ [
58
+ 'Settings: no temporary storage dir',
59
+ 'Settings: logging into /tmp',
60
+ 'Settings: chrome webdrivers',
61
+ '\x1B[38;5;99mStarted\x1B[0m mac/',
62
+ '!Running: mac/init, path: tests/meta.js',
63
+ 'MockServicer:docker starting mysql',
64
+ 'MockServicer:docker starting redis',
65
+ '>mac/init completed in',
66
+ '!Running: mac/t_example.js, path: tests/t_example.js',
67
+ '\x1B[32mOk:\x1B[0m Servicer test example works',
68
+ '>mac/t_example.js completed in',
69
+ '!Running: mac/uninit, path: tests/meta.js',
70
+ 'MockServicer:docker stopping redis',
71
+ 'MockServicer:docker stopping mysql',
72
+ 'MockServicer:docker shutdown',
73
+ '>mac/uninit completed in',
74
+ '\x1B[102mSuccess!\x1B[0m Total: 1',
75
+ '\x1B[38;5;243mCompleted\x1B[0m mac/',
76
+ 'Logs are written to',
77
+ 'Elapsed:',
78
+ 'Logs are written to',
79
+ 'Testsuite: shutdown'
80
+ ],
81
+ [],
82
+ 'servicer lifecycle',
83
+ );
84
+ }