@camperaid/watest 2.5.1 → 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 (162) hide show
  1. package/README.md +274 -129
  2. package/bin/watest.js +36 -2
  3. package/core/core.js +43 -15
  4. package/core/deps.js +211 -0
  5. package/core/{process_args.js → process-args.js} +8 -0
  6. package/core/series.js +41 -40
  7. package/core/settings.js +27 -9
  8. package/core/util.js +1 -1
  9. package/index.js +5 -3
  10. package/interfaces/servicer.js +13 -2
  11. package/logging/logging.js +1 -1
  12. package/logging/logpipe.js +38 -21
  13. package/package.json +1 -1
  14. package/tests/base/{t_core.js → t-core.js} +10 -3
  15. package/tests/base/test.js +1 -2
  16. package/tests/deps/samples/nested/.watestrc.js +3 -0
  17. package/tests/deps/samples/nested/tests/meta.js +1 -0
  18. package/tests/deps/samples/nested/tests/services/meta.js +1 -0
  19. package/tests/deps/samples/nested/tests/services/ws/meta.js +1 -0
  20. package/tests/deps/samples/nested/tests/services/ws/webservice/meta.js +2 -0
  21. package/tests/deps/samples/nested/tests/services/ws/webservice/t-ws.js +3 -0
  22. package/tests/deps/samples/unified/.watestrc.js +3 -0
  23. package/tests/deps/samples/unified/tests/e2e/meta.js +4 -0
  24. package/tests/deps/samples/unified/tests/e2e/pages/meta.js +1 -0
  25. package/tests/deps/samples/unified/tests/e2e/pages/t-example.js +3 -0
  26. package/tests/deps/samples/unified/tests/e2e/t-example.js +3 -0
  27. package/tests/deps/samples/unified/tests/integration/meta.js +3 -0
  28. package/tests/deps/samples/unified/tests/lib/meta.js +0 -0
  29. package/tests/deps/samples/unified/tests/lib/t-example.js +3 -0
  30. package/tests/deps/samples/unified/tests/meta.js +8 -0
  31. package/tests/deps/samples/unified/tests/services/meta.js +3 -0
  32. package/tests/deps/samples/unified/tests/services/request/meta.js +1 -0
  33. package/tests/deps/samples/unified/tests/services/t-example.js +3 -0
  34. package/tests/deps/t-parse-cell-syntax.js +6 -0
  35. package/tests/deps/t-parse-grid-args.js +11 -0
  36. package/tests/deps/t-watest-deps.js +37 -0
  37. package/tests/deps/t-watest-grid.js +122 -0
  38. package/tests/deps/test.js +63 -0
  39. package/tests/e2e/samples/folder/package-lock.json +3 -1
  40. package/tests/e2e/samples/loader/package-lock.json +3 -1
  41. package/tests/e2e/samples/loader/tests/meta.js +1 -1
  42. package/tests/e2e/samples/{loader_mixed → loader-mixed}/package-lock.json +3 -1
  43. package/tests/e2e/samples/{loader_multiple/tests/core → loader-mixed/tests/ui}/meta.js +1 -1
  44. package/tests/e2e/samples/{loader_multiple → loader-multiple}/package-lock.json +3 -1
  45. package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/base/meta.js +1 -1
  46. package/tests/e2e/samples/{loader_mixed/tests/ui → loader-multiple/tests/core}/meta.js +1 -1
  47. package/tests/e2e/samples/single/package-lock.json +3 -1
  48. package/tests/e2e/samples/{wd_mixed → wd-mixed}/package-lock.json +3 -1
  49. package/tests/e2e/samples/{wd_single → wd-single}/package-lock.json +3 -1
  50. package/tests/e2e/{t_folder.js → t-folder.js} +2 -2
  51. package/tests/e2e/{t_loader_mixed.js → t-loader-mixed.js} +5 -5
  52. package/tests/e2e/{t_loader_multiple_patterns.js → t-loader-multiple-patterns.js} +6 -6
  53. package/tests/e2e/{t_loader_multiple.js → t-loader-multiple.js} +5 -5
  54. package/tests/e2e/{t_loader.js → t-loader.js} +2 -2
  55. package/tests/e2e/{t_single.js → t-single.js} +2 -2
  56. package/tests/e2e/{t_wd_firefox_chrome_pattern.js → t-wd-firefox-chrome-pattern.js} +6 -6
  57. package/tests/e2e/{t_wd_firefox_chrome.js → t-wd-firefox-chrome.js} +5 -5
  58. package/tests/e2e/{t_wd_firefox.js → t-wd-firefox.js} +3 -3
  59. package/tests/e2e/{t_wd_mixed_firefox_chrome.js → t-wd-mixed-firefox-chrome.js} +7 -7
  60. package/tests/e2e/{t_wd_mixed_firefox.js → t-wd-mixed-firefox.js} +5 -5
  61. package/tests/meta.js +1 -1
  62. package/tests/series/build/{t_webdriver_services.js → t-webdriver-services.js} +1 -0
  63. package/tests/series/logging/{t_failures.js → t-failures.js} +1 -1
  64. package/tests/series/logging/{t_success.js → t-success.js} +1 -1
  65. package/tests/series/logging/{t_verify.js → t-verify.js} +1 -1
  66. package/tests/series/servicer/mock-servicer.js +68 -0
  67. package/tests/series/servicer/t-nested-servicer-lifecycle.js +99 -0
  68. package/tests/series/servicer/t-servicer-no-services.js +53 -0
  69. package/tests/series/servicer/t-servicer-type-switching.js +139 -0
  70. package/tests/series/servicer/{t_servicer.js → t-servicer.js} +5 -38
  71. package/tests/series/test.js +1 -1
  72. package/tests/test.js +2 -0
  73. package/tests/webdriver/test.js +3 -3
  74. package/webdriver/{control_driver.js → control-driver.js} +1 -1
  75. package/webdriver/{driver_base.js → driver-base.js} +3 -1
  76. package/webdriver/driver.js +1 -1
  77. package/webdriver/session.js +57 -30
  78. /package/tests/base/{t_api.js → t-api.js} +0 -0
  79. /package/tests/base/{t_contains.js → t-contains.js} +0 -0
  80. /package/tests/base/{t_format.js → t-format.js} +0 -0
  81. /package/tests/base/{t_is_object.js → t-is-object.js} +0 -0
  82. /package/tests/base/{t_is_primitive.js → t-is-primitive.js} +0 -0
  83. /package/tests/base/{t_is_string.js → t-is-string.js} +0 -0
  84. /package/tests/base/{t_is.js → t-is.js} +0 -0
  85. /package/tests/base/{t_ok.js → t-ok.js} +0 -0
  86. /package/tests/base/{t_stringify.js → t-stringify.js} +0 -0
  87. /package/tests/base/{t_system.js → t-system.js} +0 -0
  88. /package/tests/base/{t_test_.js → t-test-.js} +0 -0
  89. /package/tests/base/{t_throws.js → t-throws.js} +0 -0
  90. /package/tests/e2e/samples/folder/tests/unit/{t_test.js → t-test.js} +0 -0
  91. /package/tests/e2e/samples/{loader_multiple/tests/module_mock.js → loader/tests/module-mock.js} +0 -0
  92. /package/tests/e2e/samples/loader/tests/{t_test.js → t-test.js} +0 -0
  93. /package/tests/e2e/samples/{loader_mixed → loader-mixed}/.watestrc.js +0 -0
  94. /package/tests/e2e/samples/{loader_mixed → loader-mixed}/package.json +0 -0
  95. /package/tests/e2e/samples/{loader_mixed → loader-mixed}/tests/meta.js +0 -0
  96. /package/tests/e2e/samples/{loader/tests/module_mock.js → loader-mixed/tests/module-mock.js} +0 -0
  97. /package/tests/e2e/samples/{loader_mixed → loader-mixed}/tests/module.js +0 -0
  98. /package/tests/e2e/samples/{loader_mixed/tests/ui/t_test.js → loader-mixed/tests/ui/t-test.js} +0 -0
  99. /package/tests/e2e/samples/{loader_mixed/tests/unit/t_test.js → loader-mixed/tests/unit/t-test.js} +0 -0
  100. /package/tests/e2e/samples/{loader_multiple → loader-multiple}/.watestrc.js +0 -0
  101. /package/tests/e2e/samples/{loader_multiple → loader-multiple}/package.json +0 -0
  102. /package/tests/e2e/samples/{loader_multiple/tests/base/t_btest.js → loader-multiple/tests/base/t-btest.js} +0 -0
  103. /package/tests/e2e/samples/{loader_multiple/tests/core/t_ctest.js → loader-multiple/tests/core/t-ctest.js} +0 -0
  104. /package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/meta.js +0 -0
  105. /package/tests/e2e/samples/{loader_mixed/tests/module_mock.js → loader-multiple/tests/module-mock.js} +0 -0
  106. /package/tests/e2e/samples/{loader_multiple → loader-multiple}/tests/module.js +0 -0
  107. /package/tests/e2e/samples/single/tests/{t_test.js → t-test.js} +0 -0
  108. /package/tests/e2e/samples/{wd_mixed → wd-mixed}/.watestrc.js +0 -0
  109. /package/tests/e2e/samples/{wd_mixed → wd-mixed}/package.json +0 -0
  110. /package/tests/e2e/samples/{wd_mixed → wd-mixed}/tests/meta.js +0 -0
  111. /package/tests/e2e/samples/{wd_mixed → wd-mixed}/tests/ui/meta.js +0 -0
  112. /package/tests/e2e/samples/{wd_mixed/tests/ui/t_test.js → wd-mixed/tests/ui/t-test.js} +0 -0
  113. /package/tests/e2e/samples/{wd_mixed/tests/unit/t_test.js → wd-mixed/tests/unit/t-test.js} +0 -0
  114. /package/tests/e2e/samples/{wd_single → wd-single}/.watestrc.js +0 -0
  115. /package/tests/e2e/samples/{wd_single → wd-single}/package.json +0 -0
  116. /package/tests/e2e/samples/{wd_single → wd-single}/tests/meta.js +0 -0
  117. /package/tests/e2e/samples/{wd_single/tests/t_test.js → wd-single/tests/t-test.js} +0 -0
  118. /package/tests/series/build/{t_adjust_names_webdriver.js → t-adjust-names-webdriver.js} +0 -0
  119. /package/tests/series/build/{t_adjust_names.js → t-adjust-names.js} +0 -0
  120. /package/tests/series/build/{t_expected_failures.js → t-expected-failures.js} +0 -0
  121. /package/tests/series/build/{t_loader_mixed.js → t-loader-mixed.js} +0 -0
  122. /package/tests/series/build/{t_loader.js → t-loader.js} +0 -0
  123. /package/tests/series/build/{t_mixed.js → t-mixed.js} +0 -0
  124. /package/tests/series/build/{t_nested.js → t-nested.js} +0 -0
  125. /package/tests/series/build/{t_pattern_filtering.js → t-pattern-filtering.js} +0 -0
  126. /package/tests/series/build/{t_patterns_loader.js → t-patterns-loader.js} +0 -0
  127. /package/tests/series/build/{t_patterns_webdriver.js → t-patterns-webdriver.js} +0 -0
  128. /package/tests/series/build/{t_webdriver_firefox_mixed.js → t-webdriver-firefox-mixed.js} +0 -0
  129. /package/tests/series/build/{t_webdriver_nested.js → t-webdriver-nested.js} +0 -0
  130. /package/tests/series/build/{t_webdriver.js → t-webdriver.js} +0 -0
  131. /package/tests/series/generic/{t_failures_info.js → t-failures-info.js} +0 -0
  132. /package/tests/series/{mock_series.js → mock-series.js} +0 -0
  133. /package/tests/series/perform/{t_failure_notest.js → t-failure-notest.js} +0 -0
  134. /package/tests/series/perform/{t_failure.js → t-failure.js} +0 -0
  135. /package/tests/series/perform/{t_intermittent_global.js → t-intermittent-global.js} +0 -0
  136. /package/tests/series/perform/{t_intermittent.js → t-intermittent.js} +0 -0
  137. /package/tests/series/perform/{t_missing_perma.js → t-missing-perma.js} +0 -0
  138. /package/tests/series/perform/{t_nested.js → t-nested.js} +0 -0
  139. /package/tests/series/perform/{t_perma.js → t-perma.js} +0 -0
  140. /package/tests/series/perform/{t_success.js → t-success.js} +0 -0
  141. /package/tests/series/run/{t_debunk_failure.js → t-debunk-failure.js} +0 -0
  142. /package/tests/series/run/{t_debunk_success.js → t-debunk-success.js} +0 -0
  143. /package/tests/series/run/{t_nested.js → t-nested.js} +0 -0
  144. /package/tests/series/run/{t_verify_webdriver.js → t-verify-webdriver.js} +0 -0
  145. /package/tests/series/run/{t_verify.js → t-verify.js} +0 -0
  146. /package/tests/webdriver/{t_app_driver_selectors.js → t-app-driver-selectors.js} +0 -0
  147. /package/tests/webdriver/{t_app_driver.js → t-app-driver.js} +0 -0
  148. /package/tests/webdriver/{t_attribute_all.js → t-attribute-all.js} +0 -0
  149. /package/tests/webdriver/{t_attribute.js → t-attribute.js} +0 -0
  150. /package/tests/webdriver/{t_doubleclick.js → t-doubleclick.js} +0 -0
  151. /package/tests/webdriver/{t_doubleclickat.js → t-doubleclickat.js} +0 -0
  152. /package/tests/webdriver/{t_execute.js → t-execute.js} +0 -0
  153. /package/tests/webdriver/{t_if_has_elements.js → t-if-has-elements.js} +0 -0
  154. /package/tests/webdriver/{t_if_no_elements.js → t-if-no-elements.js} +0 -0
  155. /package/tests/webdriver/{t_no_elements_or_not_visible.js → t-no-elements-or-not-visible.js} +0 -0
  156. /package/tests/webdriver/{t_properties.js → t-properties.js} +0 -0
  157. /package/tests/webdriver/{t_script.js → t-script.js} +0 -0
  158. /package/tests/webdriver/{t_select_all.js → t-select-all.js} +0 -0
  159. /package/tests/webdriver/{t_selection.js → t-selection.js} +0 -0
  160. /package/tests/webdriver/{t_text_all.js → t-text-all.js} +0 -0
  161. /package/tests/webdriver/{t_text.js → t-text.js} +0 -0
  162. /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();
@@ -154,7 +154,7 @@ class Series {
154
154
  async runFor(patterns, name_postfix = '') {
155
155
  let tests = await this.build({
156
156
  patterns,
157
- folder: root_folder,
157
+ folder: rootFolder(),
158
158
  virtual_folder: this.invocation,
159
159
  });
160
160
 
@@ -178,8 +178,10 @@ class Series {
178
178
  }
179
179
 
180
180
  shutdown() {
181
- console.log(`Testsuite: shutdown`);
181
+ this.shutdownServicer();
182
182
  testflow.unlock();
183
+
184
+ console.log(`Testsuite: shutdown`);
183
185
  return this.failures.length > 0 ? this.failures : null;
184
186
  }
185
187
 
@@ -442,23 +444,16 @@ class Series {
442
444
  test_module.expected_failures || [],
443
445
  );
444
446
 
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
447
  // Initialize.
454
- if (test_module.services || test_module.init) {
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) {
455
453
  let init = async () => {
456
- // Start services if any.
457
- let chain = Promise.resolve();
458
- for (let service of test_module.services || []) {
459
- chain = chain.then(() => this.getServicer().start(service));
460
- }
461
- await chain;
454
+ // Initialize servicer with services (if any).
455
+ const servicer = this.getServicer(test_module.servicer);
456
+ await servicer.init(test_module.services);
462
457
 
463
458
  // Do initialization if any.
464
459
  if (test_module.init) {
@@ -506,23 +501,14 @@ class Series {
506
501
  }
507
502
 
508
503
  // Uninitialize.
509
- if (test_module.services || test_module.init) {
504
+ if (has_servicer || has_init) {
510
505
  let uninit = async () => {
511
506
  // Deinitalize test env.
512
507
  if (test_module.uninit) {
513
508
  await test_module.uninit();
514
509
  }
515
- // Stop services in reverse order.
516
- await Promise.all(
517
- [...(test_module.services || [])]
518
- .reverse()
519
- .map(s => this.getServicer().stop(s)),
520
- );
521
-
522
- // Clean up service-level servicer
523
- await this.#servicer?.shutdown();
524
- this.#servicer = null;
525
- this.#servicerType = null;
510
+ // Deinitialize servicer with services.
511
+ await this.#servicer?.deinit(test_module.services);
526
512
  };
527
513
  tests.push({
528
514
  name: `${virtual_folder}/uninit`,
@@ -937,7 +923,7 @@ class Series {
937
923
  .readdirSync(nodepath.join(root_dir, folder))
938
924
  .filter(
939
925
  n =>
940
- n.startsWith('t_') &&
926
+ settings.testFilePattern.test(n) &&
941
927
  (!settings.ignorePattern || !settings.ignorePattern.test(n)),
942
928
  );
943
929
  }
@@ -965,7 +951,10 @@ class Series {
965
951
  args.push('--webdriver', webdriver);
966
952
  }
967
953
 
968
- 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 =>
969
958
  this.processChildProcessOutput(name, buffer),
970
959
  ).catch(e => {
971
960
  log_error(e);
@@ -1060,9 +1049,15 @@ class Series {
1060
1049
  }
1061
1050
  }
1062
1051
 
1063
- getServicer() {
1064
- if (!this.#servicer) {
1065
- this.#servicer = this.createServicer(this.#servicerType);
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);
1066
1061
  }
1067
1062
  return this.#servicer;
1068
1063
  }
@@ -1071,8 +1066,14 @@ class Series {
1071
1066
  return settings.getServicer(servicerType);
1072
1067
  }
1073
1068
 
1069
+ shutdownServicer() {
1070
+ if (this.#servicer) {
1071
+ this.#servicer.shutdown();
1072
+ this.#servicer = null;
1073
+ }
1074
+ }
1075
+
1074
1076
  #servicer;
1075
- #servicerType;
1076
1077
  }
1077
1078
 
1078
1079
  export { Series };
package/core/settings.js CHANGED
@@ -7,9 +7,11 @@ 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 = (
@@ -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/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/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,10 +22,10 @@ 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';
29
30
  import {
30
31
  runCommand,
@@ -41,6 +42,7 @@ export {
41
42
  execCommand,
42
43
  failed,
43
44
  fail,
45
+ getServicer,
44
46
  group,
45
47
  is,
46
48
  is_output,
@@ -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,8 +27,7 @@ class Servicer {
15
27
  /**
16
28
  * Called when the testsuite gets shutdown.
17
29
  */
18
- shutdown() {
19
- }
30
+ shutdown() {}
20
31
 
21
32
  /**
22
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);
@@ -2,7 +2,7 @@ import path from 'path';
2
2
 
3
3
  import { FileStream as DefaultFileStream } from './filestream.js';
4
4
  import { log, log_error } from './logging.js';
5
- import settings from '../core/settings.js';
5
+ import { settings } from '../core/settings.js';
6
6
 
7
7
  /**
8
8
  * A single instance of a logpipe writing to std and file streams.
@@ -15,6 +15,7 @@ class LogPipeInstance {
15
15
 
16
16
  attach(invocation) {
17
17
  this.invocation = invocation;
18
+ this.run = settings.run;
18
19
  this.log_dir = path.join(settings.log_dir, this.invocation);
19
20
  if (this.suppress_logging) {
20
21
  return;
@@ -28,7 +29,7 @@ class LogPipeInstance {
28
29
  log_error(e);
29
30
  });
30
31
 
31
- return settings.logger.testRunStarted({ run: settings.run, invocation });
32
+ return settings.logger.testRunStarted({ run: this.run, invocation });
32
33
  }
33
34
 
34
35
  async logScreenshot(pic) {
@@ -41,7 +42,7 @@ class LogPipeInstance {
41
42
  log(`Screenshot is captured and written to ${stream.filepath}`);
42
43
 
43
44
  await settings.logger.writeLogFile({
44
- run: settings.run,
45
+ run: this.run,
45
46
  invocation: this.invocation,
46
47
  name,
47
48
  content,
@@ -50,30 +51,42 @@ class LogPipeInstance {
50
51
 
51
52
  logSourceMap() {
52
53
  return settings.logger.writeSourceMap({
53
- run: settings.run,
54
+ run: this.run,
54
55
  invocation: this.invocation,
55
56
  });
56
57
  }
57
58
 
58
59
  release() {
59
- return (
60
- !this.suppress_logging &&
61
- this.fstream
62
- .end()
63
- .then(() => this.fstream.readFile())
64
- .then(content =>
65
- settings.logger.writeLogFile({
66
- run: settings.run,
60
+ if (this.suppress_logging || !this.fstream) {
61
+ return Promise.resolve();
62
+ }
63
+
64
+ const filepath = this.fstream.filepath;
65
+
66
+ return this.fstream
67
+ .end()
68
+ .then(() => this.fstream.readFile())
69
+ .then(content =>
70
+ settings.logger.writeLogFile({
71
+ run: this.run,
72
+ invocation: this.invocation,
73
+ name: this.fname,
74
+ content,
75
+ zip: true,
76
+ }),
77
+ )
78
+ .catch(e => {
79
+ log_error(
80
+ `[logpipe:error] ✗ Logging shutdown failed for ${filepath}:`,
81
+ {
82
+ filepath,
67
83
  invocation: this.invocation,
68
- name: this.fname,
69
- content,
70
- zip: true,
71
- }),
72
- )
73
- .catch(e => {
74
- log_error(`Logging shutdown rejected: ${e}`);
75
- })
76
- );
84
+ error: e.message,
85
+ code: e.code,
86
+ stack: e.stack,
87
+ },
88
+ );
89
+ });
77
90
  }
78
91
  }
79
92
 
@@ -127,6 +140,10 @@ class LogPipe {
127
140
  // Remove the pipe from the stack to prevent anyone writing into it after
128
141
  // its release.
129
142
  let pipeToRelease = this.stack.pop();
143
+ if (!pipeToRelease) {
144
+ return Promise.resolve();
145
+ }
146
+
130
147
  await pipeToRelease.release(...args);
131
148
 
132
149
  if (this.suppress_logging) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camperaid/watest",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "Web Application Testsuite",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,14 +1,21 @@
1
1
  import { colorify, group, fail, success, is_output } from './test.js';
2
2
 
3
3
  export async function test() {
4
- // console.log
5
- await is_output(() => console.log(3), [`3\n`], [], `console.log(3)`);
4
+ // console.log - Node adds yellow color codes to numbers in TTY
5
+ const colored3 = `\x1b[33m3\x1b[39m\n`;
6
+
7
+ await is_output(
8
+ () => console.log(3),
9
+ [colored3],
10
+ [],
11
+ `console.log(3)`,
12
+ );
6
13
 
7
14
  // console.error
8
15
  await is_output(
9
16
  () => console.error(3),
10
17
  [],
11
- [`3\n`],
18
+ [colored3],
12
19
  `console.error(3)`,
13
20
  );
14
21