@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/README.md CHANGED
@@ -1,215 +1,360 @@
1
1
  # watest
2
2
 
3
- WATest stands for Web Application Testsuite. It is a lightweight, minimal dependency
4
- testsuite designed for webdriver-based testing. It also can great be for
5
- unit and integration testing.
3
+ WATest (Web Application Testsuite) is a lightweight, minimal-dependency test framework designed for webdriver-based E2E testing. Also works great for unit and integration tests.
6
4
 
7
5
  ## Install
8
6
 
9
- Invoke
10
-
11
- ```
7
+ ```bash
12
8
  npm install @camperaid/watest --save
13
9
  ```
14
10
 
15
- in the project root.
16
-
17
- Use `watest` command to run tests located in `tests/` folder. For example, add
11
+ Add to `package.json`:
18
12
 
19
- ```
20
- scripts: {
21
- test: 'watest'
13
+ ```json
14
+ {
15
+ "scripts": {
16
+ "test": "watest"
17
+ }
22
18
  }
23
19
  ```
24
20
 
25
- in `package.json` and then run the tests by
21
+ Run tests:
26
22
 
27
- ```
23
+ ```bash
28
24
  npm test
29
25
  ```
30
26
 
31
- ## Structure
27
+ ## Project Structure
28
+
29
+ ```
30
+ tests/
31
+ ├── meta.js # Root test configuration
32
+ ├── unit/
33
+ │ ├── meta.js # Folder-specific config
34
+ │ ├── t_foo.js # Test file (t_ prefix)
35
+ │ └── t_bar.js
36
+ └── e2e/
37
+ ├── meta.js
38
+ └── t_login.js
39
+ ```
40
+
41
+ ### meta.js Options
32
42
 
33
- Tests has to be located in `tests/` folder. Each test folder can contain
34
- `meta.js` file describing the test flow.
43
+ Each test folder can have a `meta.js` file:
35
44
 
36
- `init` function used to initialize tests in a folder
37
- `uninit` function used to initialize tests in a folder
38
- `folders` is a list of nested folders containing tests
39
- `services` is a list of services to run
45
+ ```javascript
46
+ // tests/e2e/meta.js
47
+ export const folders = ['login', 'checkout']; // Nested test folders
48
+ export const services = ['db', 'ws']; // Services to start
49
+ export const servicer = 'docker'; // Service manager: 'docker' | 'kubernetes'
50
+ export const webdriver = true; // Enable browser testing
51
+ export const init = async () => { /* setup */ };
52
+ export const uninit = async () => { /* teardown */ };
53
+ ```
40
54
 
41
55
  ## Configuration
42
56
 
43
- `.watestrc.js` is used to define configuration.
57
+ ### Project Configuration File
58
+
59
+ Create `.watestrc.js` in your **project root** (not in watest):
60
+
61
+ ```javascript
62
+ export default {
63
+ // Test run identifiers (from env or auto-generated)
64
+ run: process.env.WATEST_RUN,
65
+ invocation: process.env.WATEST_INVOCATION,
66
+
67
+ // Directories
68
+ log_dir: process.env.WATEST_LOG_DIR,
69
+ tmp_dir: process.env.WATEST_TMP_DIR || '/tmp',
70
+
71
+ // Test behavior
72
+ timeout: process.env.WATEST_TIMEOUT || 30000,
73
+ debunk_limit: process.env.WATEST_DEBUNK_LIMIT || 5,
74
+
75
+ // WebDriver settings
76
+ webdrivers: process.env.WATEST_WEBDRIVERS,
77
+ webdriver_headless: process.env.WATEST_WEBDRIVER_HEADLESS,
78
+ webdriver_loglevel: process.env.WATEST_WEBDRIVER_LOGLEVEL,
79
+ webdriver_window_width: process.env.WATEST_WEBDRIVER_WINDOW_WIDTH,
80
+ webdriver_window_height: process.env.WATEST_WEBDRIVER_WINDOW_HEIGHT,
81
+
82
+ // Integration hooks (absolute paths to your project files)
83
+ servicer: './tests/servicer.js', // Manages test services
84
+ logger: './tests/logserver.js', // Remote test logging
85
+
86
+ // Optional: ignore pattern
87
+ ignore_pattern: process.env.WATEST_IGNORE_PATTERN,
88
+ };
89
+ ```
90
+
91
+ **Note**: This file should be created in your project root (where you run `watest`), not inside the watest directory itself.
92
+
93
+ ### Environment Variables
94
+
95
+ Watest reads configuration from environment variables. Create `.env` in your project root or export them:
96
+
97
+ ```bash
98
+ # Test execution
99
+ WATEST_RUN=run-123 # Optional: Group multiple test invocations
100
+ WATEST_INVOCATION=inv-456 # Optional: Unique invocation identifier
101
+ WATEST_TIMEOUT=30000 # Test timeout in milliseconds
102
+ WATEST_DEBUNK_LIMIT=5 # Max retry attempts for flaky tests
103
+
104
+ # Directories
105
+ WATEST_LOG_DIR=/tmp/watest # Test logs directory
106
+ WATEST_TMP_DIR=/tmp # Temporary files directory
107
+
108
+ # WebDriver
109
+ WATEST_WEBDRIVERS='["chrome","firefox"]' # Browser list (JSON array)
110
+ WATEST_WEBDRIVER_HEADLESS=true # Run browsers headless
111
+ WATEST_WEBDRIVER_LOGLEVEL=info # WebDriver log level
112
+ WATEST_WEBDRIVER_WINDOW_WIDTH=1280 # Browser window width
113
+ WATEST_WEBDRIVER_WINDOW_HEIGHT=1024 # Browser window height
114
+
115
+ # Integration
116
+ WATEST_LOGGER_MODULE=./tests/logserver.js # Path to logger module
117
+ WATEST_SERVICER_MODULE=./tests/servicer.js # Path to servicer module
118
+ WATEST_IGNORE_PATTERN=_browser\.js$ # Regex for files to skip
119
+ ```
120
+
121
+ ### Webdrivers
122
+
123
+ Built-in webdriver configurations:
124
+
125
+ - `chrome` - Chrome browser
126
+ - `chrome-mobile` - Chrome with iPhone emulation
127
+ - `firefox` - Firefox browser
128
+ - `safari` - Safari browser
129
+
130
+ ## CLI Options
131
+
132
+ ```bash
133
+ watest [options] [patterns...]
134
+
135
+ Options:
136
+ --skip-on-fail Skip remaining tests in folder after first failure
137
+ --timeout Set webdriver condition timeout (ms)
138
+ --debunk Re-run passing test until it fails (flakiness detection)
139
+ -v, --verify Re-run failing tests
140
+ --deps Output metadata (servicers, services) as JSON
141
+ --grid Output grid metadata for distributed testing
142
+ -h, --help Show help
143
+ ```
144
+
145
+ ### Examples
44
146
 
45
- Pre-defined webdrivers:
147
+ ```bash
148
+ watest # Run all tests
149
+ watest tests/unit # Run specific folder
150
+ watest tests/e2e/t_login.js # Run specific file
151
+ watest chrome firefox # Run with multiple browsers
152
+ watest --debunk tests/e2e # Find flaky tests
153
+ ```
154
+
155
+ ## Test API
46
156
 
47
- - `chrome` to run tests in Chrome
48
- - `chrome-mobile` to run tests in Chrome on iPhone
49
- - `firefox` to run tests in Firefox
50
- - `safari` to run tests in Safari
157
+ ### Assertions
51
158
 
52
- You can create `.env` file in the root directory to
53
- define enviropment variables used for configuration.
54
- Here's an example of `.env` file:
159
+ ```javascript
160
+ import { ok, is, contains, throws, no_throws } from '@camperaid/watest';
55
161
 
162
+ ok(value, 'value is truthy');
163
+ is(got, expected, 'values match');
164
+ contains(array, expected, 'array contains values');
165
+ throws(() => fn(), 'throws error');
166
+ no_throws(() => fn(), 'does not throw');
56
167
  ```
57
- WATEST_DEBUNK_LIMIT=5
58
- WATEST_LOG_DIR=/tmp
59
- WATEST_TIMEOUT=3000
60
- WATEST_WEBDRIVERS=["chrome", "firefox"]
61
- WATEST_WEBDRIVER_HEADLESS=true
62
- WATEST_WEBDRIVER_LOGLEVEL=info
168
+
169
+ ### Reporting
170
+
171
+ ```javascript
172
+ import { success, fail, info, warn, todo, group } from '@camperaid/watest';
173
+
174
+ group('User authentication');
175
+ success('login completed');
176
+ fail('validation failed');
177
+ info('processing 100 items');
178
+ warn('deprecated API used');
179
+ todo('implement logout');
63
180
  ```
64
181
 
65
- ## Unit testing
182
+ ### Utilities
66
183
 
67
- The testsuite has basic functions:
184
+ ```javascript
185
+ import { assert, not_reached, inspect } from '@camperaid/watest';
68
186
 
69
- - `fail(msg)` to record test failure
70
- - `success(msg)` to record test success
71
- - `ok(cond, msg)` to test a boolean value
72
- - `is(got, expected, msg)` to compare two values
187
+ assert(condition, 'condition must be true'); // Fails with stack trace
188
+ not_reached('should not get here'); // Always fails with stack trace
189
+ inspect(object); // Pretty-print object
190
+ ```
191
+
192
+ ### Test Helpers
73
193
 
74
- Other functions:
194
+ ```javascript
195
+ import { test_is, test_contains } from '@camperaid/watest';
75
196
 
76
- - `info(msg)`to print an info message
77
- - `assert(expression, msg)` to print an assert message if an expression fails
78
- - `not_reached(msg)` to fail with stack trace printed
79
- - `group(msg)` prints a grouping message, useful to logically group a test checks
80
- - `intermittent(msg)` to print an intermittent message
81
- - `todo(msg)` to print a todo message
82
- - `warn(msg)` to print a warning message
197
+ // Silent checks (no success/fail logging)
198
+ if (test_is(got, expected)) { /* ... */ }
199
+ if (test_contains(array, subset)) { /* ... */ }
200
+ ```
83
201
 
84
- ## Integration testing
202
+ ## Integration Testing
85
203
 
86
- Make sure to add `servicer` into `watestrc.js` configuration, which refers to an object managing the services. See interfaces/servicer.js for the API, which should be implemented by a `servicer`.
204
+ Configure a servicer in `.watestrc.js` to manage services. The servicer interface (`interfaces/servicer.js`) must implement:
87
205
 
88
- Each time when the testsuite encounters `services` directive in `meta.js` file, it pokes into `servicer` object to start the referred services, and then to stop them, when leaving the folder.
206
+ - `start(services)` - Start services
207
+ - `stop()` - Stop services
89
208
 
90
- ## E2E testing
209
+ When watest encounters `services` in `meta.js`, it calls the servicer to start/stop services.
91
210
 
92
- The testuiste provides two helper classes to simplify webdriver testing.
211
+ ## E2E Testing
93
212
 
94
213
  ### Driver
95
214
 
96
- `Driver` is a chainable wrapper around selenium webdriver. It provides a set
97
- of handy functions used to implement high level application drivers.
215
+ Chainable wrapper around Selenium WebDriver:
216
+
217
+ ```javascript
218
+ import { scope } from '@camperaid/watest';
219
+
220
+ export const test = scope('https://example.com', async session => {
221
+ await session.driver
222
+ .get('/')
223
+ .findElement('#login')
224
+ .click()
225
+ .findElement('#email')
226
+ .sendKeys('user@example.com');
227
+ });
228
+ ```
98
229
 
99
230
  ### AppDriver
100
231
 
101
- This is a base class for all application testing blocks. A typical scenario of
102
- application block would look the following way. Let's you have a chat
103
- webapp, so you might want to have `Chat` appdriver like this:
232
+ Base class for page object patterns:
104
233
 
105
- ```
106
- class Chat extends AppDriver {
107
- sendMessage({ from, to, message }) {
108
- return this.chain(() => this.action(`Send a message from ${from} to ${to})).
109
- sendKeys(this.MainInput, message, `send keys to main input field`).
110
- hitEnter());
111
- }
234
+ ```javascript
235
+ import { AppDriver } from '@camperaid/watest';
112
236
 
113
- checkMessages({ messages }) {
114
- return this.chain(() => this.action(`Check messages`).
115
- getTextAll(this.Messages, messages, `check message boxes`));
237
+ class LoginPage extends AppDriver {
238
+ login({ email, password }) {
239
+ return this.chain(() =>
240
+ this.action('Login')
241
+ .sendKeys(this.Email, email)
242
+ .sendKeys(this.Password, password)
243
+ .click(this.Submit)
244
+ );
116
245
  }
117
246
 
118
247
  getSelectors() {
119
248
  return {
120
- Self: '#chat',
121
- MainInput: '#main-input',
122
- Messages: '.messages',
249
+ Self: '#login-page',
250
+ Email: '#email',
251
+ Password: '#password',
252
+ Submit: 'button[type=submit]',
123
253
  };
124
254
  }
125
255
  }
126
- module.exports = Chat;
127
256
  ```
128
257
 
129
- and the test will be like
258
+ ### ControlDriver
130
259
 
131
- ```
132
- module.exports.test = scope(url, async session => {
133
- await session.Chat.get().
134
- sendMessage({ from: 'me', to: 'you', 'hi' }).
135
- checkMessages({ messages: ['hi']});
136
- })
137
- ```
260
+ Base class for reusable UI component drivers (dropdowns, modals, etc.).
138
261
 
139
- ## Intermittent and perma failures
262
+ ## Expected Failures
140
263
 
141
- `meta.js` supports `expected_failures` instruction to handle intermittent and
142
- perma failures:
264
+ Handle known intermittent or permanent failures in `meta.js`:
143
265
 
144
- ```
145
- module.exports.expected_failures = [
266
+ ```javascript
267
+ export const expected_failures = [
146
268
  [
147
- test_file,
269
+ 'test_file.js', // or '*' for all files
148
270
  [
149
- [platform, failure_type, test_group, ...failures],
271
+ [platform, type, group, failures, description],
150
272
  ],
151
273
  ],
152
274
  ];
153
275
  ```
154
276
 
155
- where
277
+ - `platform`: `'all'`, `'darwin'`, `'linux'`, `'darwin-chrome'`, etc.
278
+ - `type`: `'perma'` or `'intermittent'`
279
+ - `group`: Test group name or `'*'`
280
+ - `failures`: Array of failure message patterns
281
+ - `description`: Human-readable description
156
282
 
157
- - `platform` is a value of `process.platform` or in case of webdriver tests
158
- it is a dash separated `process.platform` and webdriver name, for example,
159
- `darwin-chrome`, can be `all` to indicate that failure can happen on any
160
- platform
161
- - `failure_type` is either 'perma' or 'intermittent'
162
- - `test_group` is a test group name, can be `*`
163
- - `...failures` is a list of expected failures
283
+ Example:
164
284
 
165
- For example:
166
-
167
- ```
168
- module.exports.expected_failures = [
285
+ ```javascript
286
+ export const expected_failures = [
169
287
  [
170
288
  '*',
171
289
  [
172
- ['all', 'intermittent', '*', [`socket hang up`], `Socket hang up`],
173
- [
174
- 'all',
175
- 'intermittent',
176
- '*',
177
- [`[map:bounds] map retrieveBounds timeout`, `*`],
178
- `GoogleMaps is not loaded`,
179
- ],
180
- [
181
- 'darwin-safari',
182
- 'intermittent',
183
- '*',
184
- [`Wait for LocSearch focus`, `Waiting until element is focused`],
185
- `LocSearch is not focused`,
186
- ],
290
+ ['all', 'intermittent', '*', ['socket hang up'], 'Network flakiness'],
291
+ ['darwin-safari', 'perma', '*', ['WebDriver timeout'], 'Safari bug'],
187
292
  ],
188
293
  ],
189
294
  ];
190
295
  ```
191
296
 
192
- ## Mocking
297
+ ## ES Module Mocking
193
298
 
194
- ES6 modules mocking is supported only. You should add `meta.js` to provide ES6
195
- module loader. For example, you can substitute a module path by a mock module
196
- path this way:
299
+ Enable custom module resolution in `meta.js`:
197
300
 
198
- ```
301
+ ```javascript
199
302
  export const loader = true;
200
303
 
201
304
  export async function resolve(specifier, context, defaultResolve) {
202
- switch (specifier) {
203
- case './base.mjs':
204
- specifier = './base_mock.mjs';
205
- break;
305
+ if (specifier === './api.js') {
306
+ specifier = './api_mock.js';
206
307
  }
207
308
  return defaultResolve(specifier, context, defaultResolve);
208
309
  }
209
310
  ```
210
311
 
211
- ## Testsuite options
312
+ ## Grid Metadata for Distributed Testing
313
+
314
+ Watest provides metadata helpers for external orchestrators to distribute tests across multiple workers. Define grid splits in `meta.js`:
315
+
316
+ ```javascript
317
+ // tests/meta.js
318
+ export const grid = {
319
+ 'e2e+': ['tests/e2e'], // '+' = split per browser
320
+ 'www+': ['tests/www'],
321
+ 'services': ['tests/services', 'tests/integration'],
322
+ 'misc': ['tests/lib', 'tests/ops'],
323
+ };
324
+ ```
325
+
326
+ Query metadata for orchestration:
327
+
328
+ ```bash
329
+ # Get grid metadata as JSON
330
+ watest --grid
331
+ watest --grid chrome firefox
332
+ watest --grid tests/e2e tests/www
333
+
334
+ # Get dependency metadata (servicers, services)
335
+ watest --deps tests/e2e
336
+ ```
337
+
338
+ **Note**: Watest doesn't implement distributed test execution. These flags output metadata (JSON) that orchestration tools use to spawn workers and split test workload.
339
+
340
+ ## System Commands
341
+
342
+ ```javascript
343
+ import { runCommand, execCommand, spawn, runBashScript } from '@camperaid/watest';
344
+
345
+ // Run command, log output
346
+ await runCommand('npm', ['install']);
347
+
348
+ // Run command, capture output
349
+ const { stdout, stderr, code } = await execCommand('git', ['status']);
350
+
351
+ // Spawn with custom handling
352
+ const proc = spawn('node', ['server.js']);
353
+
354
+ // Run bash script
355
+ await runBashScript('setup.sh');
356
+ ```
357
+
358
+ ## License
212
359
 
213
- - `--debunk` to enable debunk mode which will run a test the number of times or until it fails whichever is first
214
- - `-v` or `--verify` to re-run failing tests
215
- - `--timeout` to set up a custom timeout for webdriver tests, for example, to break wd condition early
360
+ MPL-2.0
package/bin/watest.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { ProcessArgs } from '../core/process_args.js';
4
- import '../core/settings.js';
3
+ import { ProcessArgs } from '../core/process-args.js';
5
4
  import { runSeries } from '../core/series.js';
5
+ import { generateGridTasks, collectDeps, parseGridArgs } from '../core/deps.js';
6
+ import { settings } from '../core/settings.js';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { join } from 'node:path';
6
9
 
7
10
  const args = ProcessArgs.asObject();
11
+
8
12
  if (args.showHelp) {
9
13
  console.log(`Runs tests.`);
10
14
  console.log('Usage: watest [options] patterns');
@@ -14,9 +18,39 @@ Options:
14
18
  --timeout\t sets a timeout for webdriver tests, any webdriver condition is aborted after timeout making the test fail
15
19
  --debunk\t re-runs succeeding test until it fails or debunk limit is reached
16
20
  -v|--verify\t re-runs all failing tests
21
+ --deps\t outputs metadata (servicers, services) for given paths
22
+ --grid\t outputs grid metadata as JSON for distributed testing
17
23
  -h|--help\t shows this help
18
24
  `);
19
25
  process.exit();
20
26
  }
21
27
 
28
+ // Handle --deps flag (simple metadata from paths, for container rebuild decisions)
29
+ if (args.deps) {
30
+ await settings.initialize({ silent: true });
31
+ const { paths } = parseGridArgs(args.patterns);
32
+ const targetPaths = paths.length === 0 ? [settings.testsFolder] : paths;
33
+ const result = await collectDeps(targetPaths);
34
+ console.log(JSON.stringify(result));
35
+ process.exit(0);
36
+ }
37
+
38
+ // Handle --grid flag
39
+ if (args.grid) {
40
+ await settings.initialize({ silent: true });
41
+ const metaPath = join(process.cwd(), settings.testsFolder, 'meta.js');
42
+ const metaUrl = pathToFileURL(metaPath).href;
43
+
44
+ try {
45
+ const meta = await import(metaUrl);
46
+ const grid = meta.grid || {};
47
+ const tasks = await generateGridTasks(grid, args.patterns);
48
+ console.log(JSON.stringify(tasks, null, 2));
49
+ process.exit(0);
50
+ } catch (err) {
51
+ console.error('Error generating grid metadata:', err.message);
52
+ process.exit(1);
53
+ }
54
+ }
55
+
22
56
  runSeries(args.patterns, args).then(failures => failures && process.exit(1));
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/core.js CHANGED
@@ -57,7 +57,8 @@ class Core {
57
57
  fail(msg) {
58
58
  if (typeof msg == 'object') {
59
59
  inspect(msg);
60
- this.unconditional_fail('Unexpected exception');
60
+ const errorMessage = msg?.message || msg?.toString() || 'Unexpected exception';
61
+ this.unconditional_fail(errorMessage);
61
62
  return;
62
63
  }
63
64
 
@@ -200,28 +201,55 @@ class Core {
200
201
  }
201
202
 
202
203
  let primeCore = new Core();
203
- let currentCore = primeCore;
204
+
205
+ // Stack-based context management for nested operations
206
+ const contextStack = [];
207
+
208
+ function getCurrentContext() {
209
+ return contextStack.length > 0
210
+ ? contextStack[contextStack.length - 1]
211
+ : { core: primeCore, series: null };
212
+ }
204
213
 
205
214
  export { Core };
206
215
 
207
216
  export const testflow = {
208
- lock({ core, timeout } = {}) {
209
- currentCore = core || new Core(timeout);
217
+ lock({ core, timeout, series } = {}) {
218
+ const newCore = core || new Core(timeout);
219
+ contextStack.push({
220
+ core: newCore,
221
+ series,
222
+ });
210
223
  },
224
+
211
225
  unlock() {
212
- currentCore = primeCore;
226
+ if (contextStack.length > 0) {
227
+ contextStack.pop();
228
+ }
229
+ // Always maintain at least one context (the prime core)
213
230
  },
231
+
214
232
  get core() {
215
- return currentCore;
233
+ return getCurrentContext().core;
234
+ },
235
+
236
+ get series() {
237
+ return getCurrentContext().series;
238
+ },
239
+
240
+ // Debug helper to see stack depth
241
+ get stackDepth() {
242
+ return contextStack.length;
216
243
  },
217
244
  };
218
245
 
219
- export const assert = (...args) => currentCore.assert(...args);
220
- export const info = (...args) => currentCore.info(...args);
221
- export const not_reached = (...args) => currentCore.not_reached(...args);
222
- export const group = (...args) => currentCore.group(...args);
223
- export const fail = (...args) => currentCore.fail(...args);
224
- export const todo = (...args) => currentCore.todo(...args);
225
- export const warn = (...args) => currentCore.warn(...args);
226
- export const success = (...args) => currentCore.success(...args);
227
- export const failed = () => currentCore.failed();
246
+ export const assert = (...args) => testflow.core.assert(...args);
247
+ export const info = (...args) => testflow.core.info(...args);
248
+ export const not_reached = (...args) => testflow.core.not_reached(...args);
249
+ export const group = (...args) => testflow.core.group(...args);
250
+ export const fail = (...args) => testflow.core.fail(...args);
251
+ export const todo = (...args) => testflow.core.todo(...args);
252
+ export const warn = (...args) => testflow.core.warn(...args);
253
+ export const success = (...args) => testflow.core.success(...args);
254
+ export const failed = () => testflow.core.failed();
255
+ export const getServicer = () => testflow.series?.getServicer();