@camperaid/watest 2.5.0 → 2.5.2

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 (166) hide show
  1. package/README.md +274 -129
  2. package/bin/watest.js +36 -2
  3. package/core/base.js +10 -3
  4. package/core/core.js +43 -15
  5. package/core/deps.js +211 -0
  6. package/core/{process_args.js → process-args.js} +8 -0
  7. package/core/series.js +70 -28
  8. package/core/settings.js +28 -10
  9. package/core/system.js +68 -0
  10. package/core/util.js +1 -1
  11. package/eslint.config.js +1 -1
  12. package/index.js +15 -3
  13. package/interfaces/servicer.js +13 -3
  14. package/logging/logging.js +1 -1
  15. package/logging/logpipe.js +38 -21
  16. package/package.json +1 -1
  17. package/tests/base/{t_core.js → t-core.js} +10 -3
  18. package/tests/base/t-system.js +59 -0
  19. package/tests/base/{t_throws.js → t-throws.js} +67 -0
  20. package/tests/base/test.js +1 -2
  21. package/tests/deps/samples/nested/.watestrc.js +3 -0
  22. package/tests/deps/samples/nested/tests/meta.js +1 -0
  23. package/tests/deps/samples/nested/tests/services/meta.js +1 -0
  24. package/tests/deps/samples/nested/tests/services/ws/meta.js +1 -0
  25. package/tests/deps/samples/nested/tests/services/ws/webservice/meta.js +2 -0
  26. package/tests/deps/samples/nested/tests/services/ws/webservice/t-ws.js +3 -0
  27. package/tests/deps/samples/unified/.watestrc.js +3 -0
  28. package/tests/deps/samples/unified/tests/e2e/meta.js +4 -0
  29. package/tests/deps/samples/unified/tests/e2e/pages/meta.js +1 -0
  30. package/tests/deps/samples/unified/tests/e2e/pages/t-example.js +3 -0
  31. package/tests/deps/samples/unified/tests/e2e/t-example.js +3 -0
  32. package/tests/deps/samples/unified/tests/integration/meta.js +3 -0
  33. package/tests/deps/samples/unified/tests/lib/meta.js +0 -0
  34. package/tests/deps/samples/unified/tests/lib/t-example.js +3 -0
  35. package/tests/deps/samples/unified/tests/meta.js +8 -0
  36. package/tests/deps/samples/unified/tests/services/meta.js +3 -0
  37. package/tests/deps/samples/unified/tests/services/request/meta.js +1 -0
  38. package/tests/deps/samples/unified/tests/services/t-example.js +3 -0
  39. package/tests/deps/t-parse-cell-syntax.js +6 -0
  40. package/tests/deps/t-parse-grid-args.js +11 -0
  41. package/tests/deps/t-watest-deps.js +37 -0
  42. package/tests/deps/t-watest-grid.js +122 -0
  43. package/tests/deps/test.js +63 -0
  44. package/tests/e2e/samples/folder/package-lock.json +3 -1
  45. package/tests/e2e/samples/loader/package-lock.json +3 -1
  46. package/tests/e2e/samples/loader/tests/meta.js +1 -1
  47. package/tests/e2e/samples/{loader_mixed → loader-mixed}/package-lock.json +3 -1
  48. package/tests/e2e/samples/{loader_multiple/tests/core → loader-mixed/tests/ui}/meta.js +1 -1
  49. package/tests/e2e/samples/{loader_multiple → loader-multiple}/package-lock.json +3 -1
  50. package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/base/meta.js +1 -1
  51. package/tests/e2e/samples/{loader_mixed/tests/ui → loader-multiple/tests/core}/meta.js +1 -1
  52. package/tests/e2e/samples/single/package-lock.json +3 -1
  53. package/tests/e2e/samples/{wd_mixed → wd-mixed}/package-lock.json +3 -1
  54. package/tests/e2e/samples/{wd_single → wd-single}/package-lock.json +3 -1
  55. package/tests/e2e/{t_folder.js → t-folder.js} +3 -3
  56. package/tests/e2e/{t_loader_mixed.js → t-loader-mixed.js} +7 -7
  57. package/tests/e2e/{t_loader_multiple_patterns.js → t-loader-multiple-patterns.js} +9 -9
  58. package/tests/e2e/{t_loader_multiple.js → t-loader-multiple.js} +8 -8
  59. package/tests/e2e/{t_loader.js → t-loader.js} +4 -4
  60. package/tests/e2e/{t_single.js → t-single.js} +3 -3
  61. package/tests/e2e/{t_wd_firefox_chrome_pattern.js → t-wd-firefox-chrome-pattern.js} +8 -8
  62. package/tests/e2e/{t_wd_firefox_chrome.js → t-wd-firefox-chrome.js} +7 -7
  63. package/tests/e2e/{t_wd_firefox.js → t-wd-firefox.js} +5 -5
  64. package/tests/e2e/{t_wd_mixed_firefox_chrome.js → t-wd-mixed-firefox-chrome.js} +9 -9
  65. package/tests/e2e/{t_wd_mixed_firefox.js → t-wd-mixed-firefox.js} +7 -7
  66. package/tests/meta.js +1 -1
  67. package/tests/series/build/t-pattern-filtering.js +175 -0
  68. package/tests/series/build/{t_webdriver_services.js → t-webdriver-services.js} +1 -0
  69. package/tests/series/logging/{t_failures.js → t-failures.js} +1 -1
  70. package/tests/series/logging/{t_success.js → t-success.js} +1 -1
  71. package/tests/series/logging/{t_verify.js → t-verify.js} +2 -2
  72. package/tests/series/meta.js +1 -0
  73. package/tests/series/perform/{t_failure_notest.js → t-failure-notest.js} +1 -0
  74. package/tests/series/perform/{t_failure.js → t-failure.js} +1 -0
  75. package/tests/series/perform/{t_intermittent_global.js → t-intermittent-global.js} +1 -0
  76. package/tests/series/perform/{t_intermittent.js → t-intermittent.js} +2 -0
  77. package/tests/series/perform/{t_missing_perma.js → t-missing-perma.js} +2 -0
  78. package/tests/series/perform/{t_nested.js → t-nested.js} +1 -0
  79. package/tests/series/perform/{t_perma.js → t-perma.js} +1 -0
  80. package/tests/series/perform/{t_success.js → t-success.js} +2 -0
  81. package/tests/series/servicer/mock-servicer.js +68 -0
  82. package/tests/series/servicer/t-nested-servicer-lifecycle.js +99 -0
  83. package/tests/series/servicer/t-servicer-no-services.js +53 -0
  84. package/tests/series/servicer/t-servicer-type-switching.js +139 -0
  85. package/tests/series/servicer/t-servicer.js +51 -0
  86. package/tests/series/test.js +1 -1
  87. package/tests/test.js +2 -0
  88. package/tests/webdriver/test.js +3 -3
  89. package/webdriver/{control_driver.js → control-driver.js} +1 -1
  90. package/webdriver/{driver_base.js → driver-base.js} +3 -1
  91. package/webdriver/driver.js +1 -1
  92. package/webdriver/session.js +57 -30
  93. package/tests/base/{t_api.js → t-api.js} +0 -0
  94. package/tests/base/{t_contains.js → t-contains.js} +0 -0
  95. package/tests/base/{t_format.js → t-format.js} +0 -0
  96. package/tests/base/{t_is_object.js → t-is-object.js} +0 -0
  97. package/tests/base/{t_is_primitive.js → t-is-primitive.js} +0 -0
  98. package/tests/base/{t_is_string.js → t-is-string.js} +0 -0
  99. package/tests/base/{t_is.js → t-is.js} +0 -0
  100. package/tests/base/{t_ok.js → t-ok.js} +0 -0
  101. package/tests/base/{t_stringify.js → t-stringify.js} +0 -0
  102. package/tests/base/{t_test_.js → t-test-.js} +0 -0
  103. package/tests/e2e/samples/folder/tests/unit/{t_test.js → t-test.js} +0 -0
  104. package/tests/e2e/samples/{loader_multiple/tests/module_mock.js → loader/tests/module-mock.js} +0 -0
  105. package/tests/e2e/samples/loader/tests/{t_test.js → t-test.js} +0 -0
  106. package/tests/e2e/samples/{loader_mixed → loader-mixed}/.watestrc.js +0 -0
  107. package/tests/e2e/samples/{loader_mixed → loader-mixed}/package.json +0 -0
  108. package/tests/e2e/samples/{loader_mixed → loader-mixed}/tests/meta.js +0 -0
  109. package/tests/e2e/samples/{loader/tests/module_mock.js → loader-mixed/tests/module-mock.js} +0 -0
  110. package/tests/e2e/samples/{loader_mixed → loader-mixed}/tests/module.js +0 -0
  111. package/tests/e2e/samples/{loader_mixed/tests/ui/t_test.js → loader-mixed/tests/ui/t-test.js} +0 -0
  112. package/tests/e2e/samples/{loader_mixed/tests/unit/t_test.js → loader-mixed/tests/unit/t-test.js} +0 -0
  113. package/tests/e2e/samples/{loader_multiple → loader-multiple}/.watestrc.js +0 -0
  114. package/tests/e2e/samples/{loader_multiple → loader-multiple}/package.json +0 -0
  115. package/tests/e2e/samples/{loader_multiple/tests/base/t_btest.js → loader-multiple/tests/base/t-btest.js} +0 -0
  116. package/tests/e2e/samples/{loader_multiple/tests/core/t_ctest.js → loader-multiple/tests/core/t-ctest.js} +0 -0
  117. package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/meta.js +0 -0
  118. package/tests/e2e/samples/{loader_mixed/tests/module_mock.js → loader-multiple/tests/module-mock.js} +0 -0
  119. package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/module.js +0 -0
  120. package/tests/e2e/samples/single/tests/{t_test.js → t-test.js} +0 -0
  121. package/tests/e2e/samples/{wd_mixed → wd-mixed}/.watestrc.js +0 -0
  122. package/tests/e2e/samples/{wd_mixed → wd-mixed}/package.json +0 -0
  123. package/tests/e2e/samples/{wd_mixed → wd-mixed}/tests/meta.js +0 -0
  124. package/tests/e2e/samples/{wd_mixed → wd-mixed}/tests/ui/meta.js +0 -0
  125. package/tests/e2e/samples/{wd_mixed/tests/ui/t_test.js → wd-mixed/tests/ui/t-test.js} +0 -0
  126. package/tests/e2e/samples/{wd_mixed/tests/unit/t_test.js → wd-mixed/tests/unit/t-test.js} +0 -0
  127. package/tests/e2e/samples/{wd_single → wd-single}/.watestrc.js +0 -0
  128. package/tests/e2e/samples/{wd_single → wd-single}/package.json +0 -0
  129. package/tests/e2e/samples/{wd_single → wd-single}/tests/meta.js +0 -0
  130. package/tests/e2e/samples/{wd_single/tests/t_test.js → wd-single/tests/t-test.js} +0 -0
  131. package/tests/series/build/{t_adjust_names_webdriver.js → t-adjust-names-webdriver.js} +0 -0
  132. package/tests/series/build/{t_adjust_names.js → t-adjust-names.js} +0 -0
  133. package/tests/series/build/{t_expected_failures.js → t-expected-failures.js} +0 -0
  134. package/tests/series/build/{t_loader_mixed.js → t-loader-mixed.js} +0 -0
  135. package/tests/series/build/{t_loader.js → t-loader.js} +0 -0
  136. package/tests/series/build/{t_mixed.js → t-mixed.js} +0 -0
  137. package/tests/series/build/{t_nested.js → t-nested.js} +0 -0
  138. package/tests/series/build/{t_patterns_loader.js → t-patterns-loader.js} +0 -0
  139. package/tests/series/build/{t_patterns_webdriver.js → t-patterns-webdriver.js} +0 -0
  140. package/tests/series/build/{t_webdriver_firefox_mixed.js → t-webdriver-firefox-mixed.js} +0 -0
  141. package/tests/series/build/{t_webdriver_nested.js → t-webdriver-nested.js} +0 -0
  142. package/tests/series/build/{t_webdriver.js → t-webdriver.js} +0 -0
  143. package/tests/series/generic/{t_failures_info.js → t-failures-info.js} +0 -0
  144. package/tests/series/{mock_series.js → mock-series.js} +0 -0
  145. package/tests/series/run/{t_debunk_failure.js → t-debunk-failure.js} +1 -1
  146. package/tests/series/run/{t_debunk_success.js → t-debunk-success.js} +1 -1
  147. package/tests/series/run/{t_nested.js → t-nested.js} +1 -1
  148. package/tests/series/run/{t_verify_webdriver.js → t-verify-webdriver.js} +1 -1
  149. package/tests/series/run/{t_verify.js → t-verify.js} +1 -1
  150. /package/tests/webdriver/{t_app_driver_selectors.js → t-app-driver-selectors.js} +0 -0
  151. /package/tests/webdriver/{t_app_driver.js → t-app-driver.js} +0 -0
  152. /package/tests/webdriver/{t_attribute_all.js → t-attribute-all.js} +0 -0
  153. /package/tests/webdriver/{t_attribute.js → t-attribute.js} +0 -0
  154. /package/tests/webdriver/{t_doubleclick.js → t-doubleclick.js} +0 -0
  155. /package/tests/webdriver/{t_doubleclickat.js → t-doubleclickat.js} +0 -0
  156. /package/tests/webdriver/{t_execute.js → t-execute.js} +0 -0
  157. /package/tests/webdriver/{t_if_has_elements.js → t-if-has-elements.js} +0 -0
  158. /package/tests/webdriver/{t_if_no_elements.js → t-if-no-elements.js} +0 -0
  159. /package/tests/webdriver/{t_no_elements_or_not_visible.js → t-no-elements-or-not-visible.js} +0 -0
  160. /package/tests/webdriver/{t_properties.js → t-properties.js} +0 -0
  161. /package/tests/webdriver/{t_script.js → t-script.js} +0 -0
  162. /package/tests/webdriver/{t_select_all.js → t-select-all.js} +0 -0
  163. /package/tests/webdriver/{t_selection.js → t-selection.js} +0 -0
  164. /package/tests/webdriver/{t_text_all.js → t-text-all.js} +0 -0
  165. /package/tests/webdriver/{t_text.js → t-text.js} +0 -0
  166. /package/webdriver/{app_driver.js → app-driver.js} +0 -0
package/core/deps.js ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Dependency and grid metadata extraction for distributed testing.
3
+ * Parses test folder metadata and generates grid configurations.
4
+ */
5
+
6
+ import { pathToFileURL } from 'node:url';
7
+ import { join } from 'node:path';
8
+ import { settings } from './settings.js';
9
+
10
+ /**
11
+ * Parse cell syntax: 'e2e+' → { name: 'e2e', split: true }
12
+ */
13
+ export function parseCellSyntax(cellKey) {
14
+ if (cellKey.endsWith('+')) {
15
+ return { name: cellKey.slice(0, -1), split: true };
16
+ }
17
+ return { name: cellKey, split: false };
18
+ }
19
+
20
+ /**
21
+ * Separate paths (contain '/') from webdrivers (don't contain '/')
22
+ */
23
+ export function parseGridArgs(args) {
24
+ const paths = [];
25
+ const webdrivers = [];
26
+
27
+ for (const arg of args) {
28
+ if (arg.includes('/')) {
29
+ paths.push(arg);
30
+ } else {
31
+ webdrivers.push(arg);
32
+ }
33
+ }
34
+
35
+ return { paths, webdrivers };
36
+ }
37
+
38
+ /**
39
+ * Extract service name from service definition.
40
+ * Services can be either:
41
+ * - A string: 'db'
42
+ * - An array: ['nginx', { env: {...} }] where first element is the name
43
+ */
44
+ function getServiceName(service) {
45
+ return Array.isArray(service) ? service[0] : service;
46
+ }
47
+
48
+ /**
49
+ * Check if a folder path should be traversed based on target paths.
50
+ * Similar to series.js matchedPatterns logic:
51
+ * - folderPath starts with target: we're IN or PAST the target
52
+ * - target starts with folderPath: we're ON THE WAY to the target
53
+ */
54
+ function matchesPath(folderPath, targetPaths) {
55
+ return targetPaths.some(
56
+ target => folderPath.startsWith(target) || target.startsWith(folderPath),
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Collect metadata from a single directory's meta.js
62
+ * Helper for collectDeps
63
+ */
64
+ async function collectMetaFromDir(dirPath, result) {
65
+ try {
66
+ const metaPath = join(process.cwd(), dirPath, 'meta.js');
67
+ const metaUrl = pathToFileURL(metaPath).href;
68
+ const meta = await import(metaUrl);
69
+
70
+ if (meta.servicer && !result.servicers.includes(meta.servicer)) {
71
+ result.servicers.push(meta.servicer);
72
+ }
73
+ if (meta.webdriver) {
74
+ result.webdriver = true;
75
+ }
76
+ if (meta.services) {
77
+ for (const service of meta.services) {
78
+ const serviceName = getServiceName(service);
79
+ if (!result.services.includes(serviceName)) {
80
+ result.services.push(serviceName);
81
+ }
82
+ }
83
+ }
84
+
85
+ return meta;
86
+ } catch {
87
+ // No meta.js or error loading it - that's ok
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Recursively collect metadata from nested meta.js files.
94
+ * Starts from root folder, walks DOWN the tree, only following branches that match target paths.
95
+ * Similar to how series.js builds tests - uses bidirectional path matching.
96
+ *
97
+ * @param {string} folder - Current folder being traversed
98
+ * @param {string[]} targetPaths - The specific test paths we're collecting deps for
99
+ * @param {Object} result - Accumulated result object
100
+ */
101
+ async function collectDepsRecursive(folder, targetPaths, result) {
102
+ // Collect metadata from current folder
103
+ const meta = await collectMetaFromDir(folder, result);
104
+
105
+ // If this folder has subfolders, filter and recurse
106
+ if (meta?.folders) {
107
+ for (const subfolder of meta.folders) {
108
+ const subfolderPath = join(folder, subfolder);
109
+
110
+ // Only follow this branch if it matches any target path
111
+ if (matchesPath(subfolderPath, targetPaths)) {
112
+ await collectDepsRecursive(subfolderPath, targetPaths, result);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Collect metadata for given test paths.
120
+ * Walks the tree from tests root, following only branches that lead to target paths.
121
+ *
122
+ * @param {string[]} paths - Test paths to collect metadata for (defaults to entire tree)
123
+ * @param {Object} result - Initial result object
124
+ */
125
+ export async function collectDeps(
126
+ paths = [settings.testsFolder],
127
+ result = { servicers: [], webdriver: false, services: [] },
128
+ ) {
129
+ const rootFolder = settings.testsFolder;
130
+
131
+ // Filter to paths under root folder
132
+ const targetPaths = paths.filter(
133
+ p => p === rootFolder || p.startsWith(rootFolder + '/'),
134
+ );
135
+
136
+ if (targetPaths.length > 0) {
137
+ await collectDepsRecursive(rootFolder, targetPaths, result);
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * Generate expanded grid from grid config and args.
145
+ * Returns cell→metadata mapping with + cells expanded per browser.
146
+ *
147
+ * Example:
148
+ * watest --grid-meta tests/e2e firefox chrome
149
+ * → {
150
+ * "e2e-firefox": { paths: ["tests/e2e"], webdriver: "firefox", servicers: [...], services: [...] },
151
+ * "e2e-chrome": { paths: ["tests/e2e"], webdriver: "chrome", servicers: [...], services: [...] }
152
+ * }
153
+ */
154
+ export async function generateGridTasks(gridConfig, args) {
155
+ const { paths, webdrivers } = parseGridArgs(args);
156
+ const browsers =
157
+ webdrivers.length > 0 ? webdrivers : settings.webdrivers || [];
158
+
159
+ // Build cell → paths mapping
160
+ const cellPathsMap = new Map();
161
+
162
+ if (paths.length > 0) {
163
+ // Map each requested path to its cell
164
+ for (const [cellKey, cellPaths] of Object.entries(gridConfig)) {
165
+ const matchingPaths = paths.filter(p =>
166
+ cellPaths.some(cp => p.startsWith(cp)),
167
+ );
168
+ if (matchingPaths.length > 0) {
169
+ cellPathsMap.set(cellKey, matchingPaths);
170
+ }
171
+ }
172
+ } else {
173
+ // Use all cells with their configured paths
174
+ for (const [cellKey, cellPaths] of Object.entries(gridConfig)) {
175
+ cellPathsMap.set(cellKey, cellPaths);
176
+ }
177
+ }
178
+
179
+ // Build expanded grid with metadata
180
+ const expandedGrid = {};
181
+ for (const [cellKey, cellPaths] of cellPathsMap) {
182
+ const { name, split } = parseCellSyntax(cellKey);
183
+ const meta = await collectDeps(cellPaths);
184
+
185
+ if (split && meta.webdriver && browsers.length > 1) {
186
+ // Split: one entry per browser
187
+ for (const wd of browsers) {
188
+ expandedGrid[`${name}-${wd}`] = {
189
+ paths: cellPaths,
190
+ webdrivers: wd,
191
+ servicers: meta.servicers,
192
+ services: meta.services,
193
+ };
194
+ }
195
+ } else {
196
+ // No split: single entry with all browsers as space-separated string
197
+ let webdriversValue = '';
198
+ if (meta.webdriver && browsers.length > 0) {
199
+ webdriversValue = browsers.join(' ');
200
+ }
201
+ expandedGrid[name] = {
202
+ paths: cellPaths,
203
+ webdrivers: webdriversValue,
204
+ servicers: meta.servicers,
205
+ services: meta.services,
206
+ };
207
+ }
208
+ }
209
+
210
+ return expandedGrid;
211
+ }
@@ -28,6 +28,14 @@ class ProcessArgs {
28
28
  obj.showHelp = true;
29
29
  break;
30
30
 
31
+ case '--grid':
32
+ obj.grid = true;
33
+ break;
34
+
35
+ case '--deps':
36
+ obj.deps = true;
37
+ break;
38
+
31
39
  case '--child-process':
32
40
  obj.childProcess = true;
33
41
  break;
package/core/series.js CHANGED
@@ -4,13 +4,13 @@ import { fileURLToPath } from 'url';
4
4
 
5
5
  import { assert, fail, testflow } from './core.js';
6
6
  import { parse, parse_failure } from './format.js';
7
- import { ProcessArgs } from './process_args.js';
8
- import settings from './settings.js';
7
+ import { ProcessArgs } from './process-args.js';
8
+ import { settings } from './settings.js';
9
9
  import { spawn } from './spawn.js';
10
10
  import { stringify } from './util.js';
11
11
  import { log, log_error } from '../logging/logging.js';
12
12
  import { LogPipe } from '../logging/logpipe.js';
13
- import { DriverBase } from '../webdriver/driver_base.js';
13
+ import { DriverBase } from '../webdriver/driver-base.js';
14
14
 
15
15
  import {
16
16
  format_started,
@@ -27,7 +27,7 @@ import {
27
27
  const __filename = fileURLToPath(import.meta.url);
28
28
  const __dirname = nodepath.dirname(__filename);
29
29
 
30
- const root_folder = 'tests';
30
+ const rootFolder = () => settings.testsFolder;
31
31
  const root_dir = nodepath.resolve('.');
32
32
 
33
33
  const kKungFuDeathGripTimeout = {};
@@ -107,7 +107,7 @@ class Series {
107
107
  this.ocnt = 0;
108
108
 
109
109
  this.core = core || testflow.core;
110
- testflow.lock({ core: this.core });
110
+ testflow.lock({ core: this.core, series: this });
111
111
 
112
112
  this.core.setTimeout(timeout);
113
113
  this.core.clearStats();
@@ -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
 
@@ -157,7 +154,7 @@ class Series {
157
154
  async runFor(patterns, name_postfix = '') {
158
155
  let tests = await this.build({
159
156
  patterns,
160
- folder: root_folder,
157
+ folder: rootFolder(),
161
158
  virtual_folder: this.invocation,
162
159
  });
163
160
 
@@ -181,7 +178,10 @@ class Series {
181
178
  }
182
179
 
183
180
  shutdown() {
181
+ this.shutdownServicer();
184
182
  testflow.unlock();
183
+
184
+ console.log(`Testsuite: shutdown`);
185
185
  return this.failures.length > 0 ? this.failures : null;
186
186
  }
187
187
 
@@ -367,8 +367,24 @@ class Series {
367
367
  return [];
368
368
  }
369
369
 
370
+ // Filter subfolders to only process those that match the patterns
371
+ let filteredSubfolders = subfolders;
372
+ if (patterns.length > 0) {
373
+ filteredSubfolders = subfolders.filter(subfolder => {
374
+ const subfolderPath = `${folder}/${subfolder}`;
375
+ return (
376
+ this.matchedPatterns({
377
+ path: subfolderPath,
378
+ webdriver,
379
+ patterns,
380
+ path_is_not_final: true,
381
+ }).length > 0
382
+ );
383
+ });
384
+ }
385
+
370
386
  let subtests_for_subfolders = await Promise.all(
371
- subfolders.map(subfolder =>
387
+ filteredSubfolders.map(subfolder =>
372
388
  this.build({
373
389
  patterns,
374
390
  folder: `${folder}/${subfolder}`,
@@ -428,15 +444,16 @@ class Series {
428
444
  test_module.expected_failures || [],
429
445
  );
430
446
 
431
- // Initialize
432
- if (test_module.services || test_module.init) {
447
+ // Initialize.
448
+ // Servicer is only created when explicitly requested via 'servicer' property.
449
+ // Tests with just 'services' but no 'servicer' won't trigger servicer creation.
450
+ const has_servicer = test_module.servicer !== undefined;
451
+ const has_init = test_module.init;
452
+ if (has_servicer || has_init) {
433
453
  let init = async () => {
434
- // Start services if any.
435
- let chain = Promise.resolve();
436
- for (let service of test_module.services || []) {
437
- chain = chain.then(() => settings.servicer.start(service));
438
- }
439
- await chain;
454
+ // Initialize servicer with services (if any).
455
+ const servicer = this.getServicer(test_module.servicer);
456
+ await servicer.init(test_module.services);
440
457
 
441
458
  // Do initialization if any.
442
459
  if (test_module.init) {
@@ -460,7 +477,7 @@ class Series {
460
477
  // A function to notify the servicer the test is about to start and then
461
478
  // to invoke the test.
462
479
  const test_wrap = () =>
463
- Promise.resolve(settings.servicer.ontest(name)).then(test);
480
+ Promise.resolve(this.#servicer?.ontest(name)).then(test);
464
481
 
465
482
  let failures_info = Series.failuresInfo({
466
483
  failures: expected_failures,
@@ -484,18 +501,14 @@ class Series {
484
501
  }
485
502
 
486
503
  // Uninitialize.
487
- if (test_module.services || test_module.init) {
504
+ if (has_servicer || has_init) {
488
505
  let uninit = async () => {
489
506
  // Deinitalize test env.
490
507
  if (test_module.uninit) {
491
508
  await test_module.uninit();
492
509
  }
493
- // Stop services in reverse order.
494
- await Promise.all(
495
- [...(test_module.services || [])]
496
- .reverse()
497
- .map(s => settings.servicer.stop(s)),
498
- );
510
+ // Deinitialize servicer with services.
511
+ await this.#servicer?.deinit(test_module.services);
499
512
  };
500
513
  tests.push({
501
514
  name: `${virtual_folder}/uninit`,
@@ -910,7 +923,7 @@ class Series {
910
923
  .readdirSync(nodepath.join(root_dir, folder))
911
924
  .filter(
912
925
  n =>
913
- n.startsWith('t_') &&
926
+ settings.testFilePattern.test(n) &&
914
927
  (!settings.ignorePattern || !settings.ignorePattern.test(n)),
915
928
  );
916
929
  }
@@ -938,7 +951,10 @@ class Series {
938
951
  args.push('--webdriver', webdriver);
939
952
  }
940
953
 
941
- return spawn('node', args, {}, buffer =>
954
+ // Pass run ID to child process to ensure consistent test artifact IDs
955
+ const env = { ...process.env, WATEST_RUN: settings.run };
956
+
957
+ return spawn('node', args, { env }, buffer =>
942
958
  this.processChildProcessOutput(name, buffer),
943
959
  ).catch(e => {
944
960
  log_error(e);
@@ -1032,6 +1048,32 @@ class Series {
1032
1048
  }
1033
1049
  }
1034
1050
  }
1051
+
1052
+ getServicer(requestedType) {
1053
+ // Create servicer if none exists or if switching to a different type.
1054
+ // Note: we don't shutdown the old servicer here - the servicer factory
1055
+ // handles stopping conflicting services for better performance.
1056
+ if (
1057
+ !this.#servicer ||
1058
+ (requestedType !== undefined && this.#servicer.type !== requestedType)
1059
+ ) {
1060
+ this.#servicer = this.createServicer(requestedType);
1061
+ }
1062
+ return this.#servicer;
1063
+ }
1064
+
1065
+ createServicer(servicerType) {
1066
+ return settings.getServicer(servicerType);
1067
+ }
1068
+
1069
+ shutdownServicer() {
1070
+ if (this.#servicer) {
1071
+ this.#servicer.shutdown();
1072
+ this.#servicer = null;
1073
+ }
1074
+ }
1075
+
1076
+ #servicer;
1035
1077
  }
1036
1078
 
1037
1079
  export { Series };
package/core/settings.js CHANGED
@@ -7,16 +7,18 @@ class Settings {
7
7
  this.tmp_storage_dir = '';
8
8
  this.logger = null;
9
9
  this.servicer = null;
10
+ this.silent = false;
10
11
  }
11
12
 
12
- async initialize() {
13
+ async initialize(options = {}) {
14
+ this.silent = options.silent || false;
13
15
  this.rc = (await import(path.resolve('.', './.watestrc.js'))).default;
14
16
 
15
17
  this.logger = (
16
18
  await import(this.rc.logger || '../interfaces/logger.js')
17
19
  ).default;
18
20
 
19
- this.servicer = (
21
+ this.getServicer = (
20
22
  await import(this.rc.servicer || '../interfaces/servicer.js')
21
23
  ).default;
22
24
 
@@ -47,33 +49,49 @@ class Settings {
47
49
  return parseInt(this.rc.debunk_limit) || 5;
48
50
  }
49
51
 
52
+ get testsFolder() {
53
+ return this.rc?.tests_folder ?? 'tests';
54
+ }
55
+
56
+ get testFilePattern() {
57
+ return this.rc.test_file_pattern || /^t[-_]/;
58
+ }
59
+
50
60
  get timeout() {
51
61
  return parseInt(this.rc.timeout) || 0;
52
62
  }
53
63
 
54
64
  setupTmpStorageDir() {
55
65
  if (!this.rc.tmp_dir) {
56
- console.log(`Settings: no temporary storage dir`);
66
+ if (!this.silent) {
67
+ console.log(`Settings: no temporary storage dir`);
68
+ }
57
69
  return;
58
70
  }
59
71
 
60
72
  this.tmp_storage_dir = path.join(this.rc.tmp_dir, 'watest-tmpstorage');
61
- console.log(
62
- `Settings: temporary storage dir is at ${this.tmp_storage_dir}`,
63
- );
73
+ if (!this.silent) {
74
+ console.log(
75
+ `Settings: temporary storage dir is at ${this.tmp_storage_dir}`,
76
+ );
77
+ }
64
78
  }
65
79
 
66
80
  setupLogDir() {
67
81
  const log_dir = this.rc.log_dir;
68
82
  if (!log_dir) {
69
- console.log('Settings: no file logging');
83
+ if (!this.silent) {
84
+ console.log('Settings: no file logging');
85
+ }
70
86
  return;
71
87
  }
72
88
 
73
89
  this.run = this.rc.run || `${parseInt(Date.now() / 1000)}`;
74
90
 
75
91
  this.log_dir = path.join(log_dir, this.run);
76
- console.log(`Settings: logging into ${log_dir}`);
92
+ if (!this.silent) {
93
+ console.log(`Settings: logging into ${log_dir}`);
94
+ }
77
95
  }
78
96
 
79
97
  setupWebdrivers() {
@@ -100,10 +118,10 @@ class Settings {
100
118
  this.webdriver_window_height =
101
119
  parseInt(this.rc.webdriver_window_height) || 768;
102
120
 
103
- if (this.webdrivers) {
121
+ if (this.webdrivers && !this.silent) {
104
122
  console.log(`Settings: ${this.webdrivers.join(', ')} webdrivers`);
105
123
  }
106
124
  }
107
125
  }
108
126
 
109
- export default new Settings();
127
+ export const settings = new Settings();
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/core/util.js CHANGED
@@ -3,7 +3,7 @@ import querystring from 'querystring';
3
3
  import util from 'util';
4
4
 
5
5
  import { log } from '../logging/logging.js';
6
- import settings from './settings.js';
6
+ import { settings } from './settings.js';
7
7
 
8
8
  /**
9
9
  * Logs object in console colored.
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
@@ -8,6 +8,7 @@ import {
8
8
  success,
9
9
  todo,
10
10
  warn,
11
+ getServicer,
11
12
  } from './core/core.js';
12
13
 
13
14
  import {
@@ -21,19 +22,27 @@ import {
21
22
  test_contains,
22
23
  } from './core/base.js';
23
24
 
24
- import settings from './core/settings.js';
25
+ import { settings } from './core/settings.js';
25
26
  import { inspect } from './core/util.js';
26
- import { AppDriver } from './webdriver/app_driver.js';
27
- import { ControlDriver } from './webdriver/control_driver.js';
27
+ import { AppDriver } from './webdriver/app-driver.js';
28
+ import { ControlDriver } from './webdriver/control-driver.js';
28
29
  import { start_session, scope } from './webdriver/session.js';
30
+ import {
31
+ runCommand,
32
+ runBashScript,
33
+ execCommand,
34
+ spawn,
35
+ } from './core/system.js';
29
36
 
30
37
  export {
31
38
  AppDriver,
32
39
  ControlDriver,
33
40
  assert,
34
41
  contains,
42
+ execCommand,
35
43
  failed,
36
44
  fail,
45
+ getServicer,
37
46
  group,
38
47
  is,
39
48
  is_output,
@@ -42,7 +51,10 @@ export {
42
51
  not_reached,
43
52
  no_throws,
44
53
  ok,
54
+ runBashScript,
55
+ runCommand,
45
56
  scope,
57
+ spawn,
46
58
  start_session,
47
59
  success,
48
60
  todo,
@@ -2,6 +2,18 @@
2
2
  * Manages services requested by a testsuite.
3
3
  */
4
4
  class Servicer {
5
+ /**
6
+ * Initialize servicer and optionally start services.
7
+ * Called at the beginning of a test folder.
8
+ */
9
+ async init(/* services */) {}
10
+
11
+ /**
12
+ * Deinitialize servicer and stop services.
13
+ * Called at the end of a test folder.
14
+ */
15
+ async deinit(/* services */) {}
16
+
5
17
  /**
6
18
  * Starts a service.
7
19
  */
@@ -15,9 +27,7 @@ class Servicer {
15
27
  /**
16
28
  * Called when the testsuite gets shutdown.
17
29
  */
18
- shutdown() {
19
- console.log(`Testsuite: shutdown`);
20
- }
30
+ shutdown() {}
21
31
 
22
32
  /**
23
33
  * Called when test is started.
@@ -1,4 +1,4 @@
1
- import { ProcessArgs } from '../core/process_args.js';
1
+ import { ProcessArgs } from '../core/process-args.js';
2
2
 
3
3
  function log(...args) {
4
4
  console.log(...args);