@camperaid/watest 2.5.0 → 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 +49 -8
  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`,
@@ -1032,6 +1059,20 @@ class Series {
1032
1059
  }
1033
1060
  }
1034
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;
1035
1076
  }
1036
1077
 
1037
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.5.0",
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
+ }