@camperaid/watest 2.6.1 → 2.6.3

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.
package/.watestrc.js CHANGED
@@ -54,6 +54,12 @@ const cfg = {
54
54
  */
55
55
  webdrivers: process.env.WATEST_WEBDRIVERS,
56
56
 
57
+ /**
58
+ * Project-defined webdriver aliases. JSON object mapping alias names to
59
+ * { browser, isMobile?, chrome?, windowWidth?, windowHeight? }.
60
+ */
61
+ webdriver_aliases: process.env.WATEST_WEBDRIVER_ALIASES,
62
+
57
63
  /**
58
64
  * Logger module.
59
65
  */
package/README.md CHANGED
@@ -121,6 +121,7 @@ WATEST_TMP_DIR=/tmp # Temporary files directory
121
121
 
122
122
  # WebDriver
123
123
  WATEST_WEBDRIVERS='["chrome","firefox"]' # Browser list (JSON array)
124
+ WATEST_WEBDRIVER_ALIASES='{"chrome-mobile":{"browser":"chrome","isMobile":true,"chrome":{"mobileEmulation":{"deviceName":"iPhone 7"}}}}'
124
125
  WATEST_WEBDRIVER_HEADLESS=true # Run browsers headless
125
126
  WATEST_WEBDRIVER_LOGLEVEL=info # WebDriver log level
126
127
  WATEST_WEBDRIVER_WINDOW_WIDTH=1280 # Browser window width
@@ -134,12 +135,38 @@ WATEST_IGNORE_PATTERN=_browser\.js$ # Regex for files to skip
134
135
 
135
136
  ### Webdrivers
136
137
 
137
- Built-in webdriver configurations:
138
+ Built-in browsers:
138
139
 
139
140
  - `chrome` - Chrome browser
140
- - `chrome-mobile` - Chrome with iPhone emulation
141
141
  - `firefox` - Firefox browser
142
142
  - `safari` - Safari browser
143
+ - `edge` - Edge browser
144
+
145
+ Project-defined aliases go in `.watestrc.js` as `webdriver_aliases` (or
146
+ `WATEST_WEBDRIVER_ALIASES` JSON env var). Each alias maps a name to a base
147
+ browser plus overrides:
148
+
149
+ ```js
150
+ webdriver_aliases: {
151
+ 'chrome-mobile': {
152
+ browser: 'chrome',
153
+ isMobile: true,
154
+ chrome: {
155
+ mobileEmulation: { deviceName: 'iPhone 7' },
156
+ },
157
+ },
158
+ 'chrome-iphone10': {
159
+ browser: 'chrome',
160
+ isMobile: true,
161
+ chrome: {
162
+ mobileEmulation: { deviceName: 'iPhone X' },
163
+ },
164
+ },
165
+ },
166
+ ```
167
+
168
+ Supported alias fields: `browser` (required), `isMobile`, `browserLoggingOn`,
169
+ `chrome.mobileEmulation`, `windowWidth`, `windowHeight`.
143
170
 
144
171
  ## CLI Options
145
172
 
package/core/base.js CHANGED
@@ -213,7 +213,7 @@ function is_primitive(
213
213
 
214
214
  // Overlimit got: keep it small to not pollute output.
215
215
  let printable_got =
216
- String(got).length > limit ? `${String(got).substring(0, limit)}…` : got;
216
+ String(got).length > limit ? `${String(got).substring(0, limit)}...` : got;
217
217
  if (typeof got == 'string') {
218
218
  printable_got = `'${printable_got}'`;
219
219
  }
@@ -310,8 +310,22 @@ function is_object_impl(
310
310
  ) {
311
311
  const contextmsg = `${msg}${(fieldpath && ` '${fieldpath}' field`) || ''}`;
312
312
 
313
- // Primitive values
313
+ // When got is null / a primitive but expected is a plain object or array,
314
+ // report a clear type mismatch rather than falling into is_primitive where
315
+ // the value would stringify as [object Object].
316
+ // Functions and RegExps are excluded: is_primitive handles them as
317
+ // predicates / pattern matchers against the primitive got value.
314
318
  if (!(got instanceof Object)) {
319
+ if (
320
+ expected instanceof Object &&
321
+ typeof expected !== 'function' &&
322
+ !(expected instanceof RegExp)
323
+ ) {
324
+ fail_(
325
+ `${contextmsg} value mismatch, got: ${stringify(got)}, expected: ${stringify(expected)}`,
326
+ );
327
+ return false;
328
+ }
315
329
  return is_primitive(got, expected, contextmsg, {
316
330
  enrich_failure_msg: true,
317
331
  fail_,
package/core/series.js CHANGED
@@ -692,6 +692,9 @@ class Series {
692
692
  // Set a webdiver on stack if not empty.
693
693
  if (webdriver) {
694
694
  settings.webdriver = webdriver;
695
+ process.env.WATEST_WEBDRIVER = webdriver;
696
+ } else {
697
+ delete process.env.WATEST_WEBDRIVER;
695
698
  }
696
699
 
697
700
  // Subtests.
package/core/settings.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import path from 'path';
2
2
 
3
+ const baseBrowsers = ['chrome', 'firefox', 'safari', 'edge'];
4
+
5
+ const baseBrowserDefaults = {
6
+ chrome: { chrome: true, browserLoggingOn: true },
7
+ firefox: { firefox: true },
8
+ safari: { safari: true },
9
+ edge: { edge: true },
10
+ };
11
+
3
12
  class Settings {
4
13
  constructor() {
5
14
  this.log_dir = '';
@@ -8,6 +17,11 @@ class Settings {
8
17
  this.logger = null;
9
18
  this.servicer = null;
10
19
  this.silent = false;
20
+ this.webdriver_aliases = {};
21
+ }
22
+
23
+ get baseBrowsers() {
24
+ return baseBrowsers;
11
25
  }
12
26
 
13
27
  async initialize(options = {}) {
@@ -94,7 +108,128 @@ class Settings {
94
108
  }
95
109
  }
96
110
 
111
+ parseJsonObject(value, label) {
112
+ if (value == null || value === '') {
113
+ return null;
114
+ }
115
+ if (typeof value == 'object') {
116
+ return value;
117
+ }
118
+ if (typeof value == 'string') {
119
+ try {
120
+ return JSON.parse(value);
121
+ } catch (e) {
122
+ console.error(`Settings: failed to parse ${label} '${value}'`, e);
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ setupWebdriverAliases() {
129
+ this.webdriver_aliases = {};
130
+
131
+ const aliases = this.parseJsonObject(
132
+ this.rc.webdriver_aliases,
133
+ 'webdriver_aliases',
134
+ );
135
+ if (!aliases) {
136
+ return;
137
+ }
138
+
139
+ for (const [name, alias] of Object.entries(aliases)) {
140
+ if (baseBrowsers.includes(name)) {
141
+ throw new Error(
142
+ `Settings: webdriver alias '${name}' conflicts with built-in browser`,
143
+ );
144
+ }
145
+ if (!alias || typeof alias != 'object') {
146
+ throw new Error(
147
+ `Settings: webdriver alias '${name}' must be an object`,
148
+ );
149
+ }
150
+ if (!alias.browser) {
151
+ throw new Error(
152
+ `Settings: webdriver alias '${name}' is missing required 'browser' field`,
153
+ );
154
+ }
155
+ if (!baseBrowsers.includes(alias.browser)) {
156
+ throw new Error(
157
+ `Settings: webdriver alias '${name}' has unexpected browser '${alias.browser}'`,
158
+ );
159
+ }
160
+ this.webdriver_aliases[name] = alias;
161
+ }
162
+ }
163
+
164
+ isKnownWebdriver(name) {
165
+ return baseBrowsers.includes(name) || name in this.webdriver_aliases;
166
+ }
167
+
168
+ resolveWebdriver(name) {
169
+ if (!name) {
170
+ throw new Error(`Settings: no webdriver name provided`);
171
+ }
172
+
173
+ const alias = this.webdriver_aliases[name];
174
+ if (alias) {
175
+ const flags = {
176
+ ...baseBrowserDefaults[alias.browser],
177
+ };
178
+ if (alias.isMobile) {
179
+ flags.isMobile = true;
180
+ }
181
+ if (alias.browserLoggingOn != null) {
182
+ flags.browserLoggingOn = alias.browserLoggingOn;
183
+ }
184
+
185
+ return {
186
+ name,
187
+ browser: alias.browser,
188
+ flags,
189
+ chrome: alias.chrome || null,
190
+ windowWidth: alias.windowWidth,
191
+ windowHeight: alias.windowHeight,
192
+ };
193
+ }
194
+
195
+ if (baseBrowsers.includes(name)) {
196
+ return {
197
+ name,
198
+ browser: name,
199
+ flags: { ...baseBrowserDefaults[name] },
200
+ chrome: null,
201
+ windowWidth: undefined,
202
+ windowHeight: undefined,
203
+ };
204
+ }
205
+
206
+ throw new Error(
207
+ `Settings: unexpected webdriver '${name}', define it in webdriver_aliases`,
208
+ );
209
+ }
210
+
211
+ validateWebdrivers() {
212
+ if (!this.webdrivers) {
213
+ return;
214
+ }
215
+ if (!(this.webdrivers instanceof Array)) {
216
+ throw new Error(
217
+ `Settings: webdrivers must be an array, got: ${JSON.stringify(this.webdrivers)}`,
218
+ );
219
+ }
220
+
221
+ for (const webdriver of this.webdrivers) {
222
+ if (!this.isKnownWebdriver(webdriver)) {
223
+ throw new Error(
224
+ `Settings: unexpected webdriver '${webdriver}', define it in webdriver_aliases`,
225
+ );
226
+ }
227
+ }
228
+ }
229
+
97
230
  setupWebdrivers() {
231
+ this.setupWebdriverAliases();
232
+
98
233
  this.webdrivers = this.rc.webdrivers;
99
234
  if (typeof this.rc.webdrivers == 'string') {
100
235
  try {
@@ -107,6 +242,8 @@ class Settings {
107
242
  }
108
243
  }
109
244
 
245
+ this.validateWebdrivers();
246
+
110
247
  this.webdriver = null;
111
248
  this.webdriver_headless =
112
249
  this.rc.webdriver_headless == true ||
@@ -142,6 +279,13 @@ class Settings {
142
279
  if (this.webdrivers && !this.silent) {
143
280
  console.log(`Settings: ${this.webdrivers.join(', ')} webdrivers`);
144
281
  }
282
+
283
+ const aliasCount = Object.keys(this.webdriver_aliases).length;
284
+ if (aliasCount > 0 && !this.silent) {
285
+ console.log(
286
+ `Settings: ${aliasCount} webdriver alias(es): ${Object.keys(this.webdriver_aliases).join(', ')}`,
287
+ );
288
+ }
145
289
  }
146
290
  }
147
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camperaid/watest",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "Web Application Testsuite",
5
5
  "type": "module",
6
6
  "engines": {
@@ -0,0 +1,179 @@
1
+ import { is, throws } from '../base/test.js';
2
+ import { settings } from '../../core/settings.js';
3
+ import { DriverBase } from '../../webdriver/driver-base.js';
4
+
5
+ function withSettings(rcPatch, fn) {
6
+ const saved = {
7
+ rc: { ...settings.rc },
8
+ webdriver_aliases: { ...settings.webdriver_aliases },
9
+ webdrivers: settings.webdrivers,
10
+ };
11
+ settings.rc = { ...settings.rc, ...rcPatch };
12
+ settings.silent = true;
13
+ try {
14
+ settings.setupWebdrivers();
15
+ return fn();
16
+ } finally {
17
+ settings.rc = saved.rc;
18
+ settings.webdriver_aliases = saved.webdriver_aliases;
19
+ settings.webdrivers = saved.webdrivers;
20
+ settings.silent = false;
21
+ }
22
+ }
23
+
24
+ export function test() {
25
+ const chromeMobileAlias = {
26
+ browser: 'chrome',
27
+ isMobile: true,
28
+ chrome: {
29
+ mobileEmulation: { deviceName: 'iPhone 7' },
30
+ },
31
+ };
32
+
33
+ withSettings(
34
+ {
35
+ webdriver_aliases: {
36
+ 'chrome-mobile': chromeMobileAlias,
37
+ },
38
+ },
39
+ () => {
40
+ is(
41
+ settings.resolveWebdriver('chrome-mobile'),
42
+ {
43
+ name: 'chrome-mobile',
44
+ browser: 'chrome',
45
+ flags: {
46
+ chrome: true,
47
+ browserLoggingOn: true,
48
+ isMobile: true,
49
+ },
50
+ chrome: {
51
+ mobileEmulation: { deviceName: 'iPhone 7' },
52
+ },
53
+ windowWidth: undefined,
54
+ windowHeight: undefined,
55
+ },
56
+ 'resolve chrome-mobile alias',
57
+ );
58
+
59
+ is(
60
+ settings.resolveWebdriver('chrome'),
61
+ {
62
+ name: 'chrome',
63
+ browser: 'chrome',
64
+ flags: {
65
+ chrome: true,
66
+ browserLoggingOn: true,
67
+ },
68
+ chrome: null,
69
+ windowWidth: undefined,
70
+ windowHeight: undefined,
71
+ },
72
+ 'resolve chrome base browser',
73
+ );
74
+
75
+ is(settings.isKnownWebdriver('chrome-mobile'), true, 'known alias');
76
+ is(settings.isKnownWebdriver('firefox'), true, 'known base browser');
77
+ is(settings.isKnownWebdriver('unknown'), false, 'unknown webdriver');
78
+ },
79
+ );
80
+
81
+ withSettings(
82
+ {
83
+ webdriver_aliases: JSON.stringify({
84
+ 'firefox-mobile': {
85
+ browser: 'firefox',
86
+ isMobile: true,
87
+ },
88
+ }),
89
+ },
90
+ () => {
91
+ is(
92
+ settings.resolveWebdriver('firefox-mobile').browser,
93
+ 'firefox',
94
+ 'parse alias JSON string',
95
+ );
96
+ is(
97
+ DriverBase.isStdOutLogging('firefox-mobile'),
98
+ true,
99
+ 'firefox alias uses stdout logging',
100
+ );
101
+ },
102
+ );
103
+
104
+ withSettings(
105
+ {
106
+ webdriver_aliases: {
107
+ 'safari-mobile': {
108
+ browser: 'safari',
109
+ isMobile: true,
110
+ windowWidth: 375,
111
+ windowHeight: 667,
112
+ },
113
+ },
114
+ },
115
+ () => {
116
+ is(
117
+ settings.resolveWebdriver('safari-mobile'),
118
+ {
119
+ name: 'safari-mobile',
120
+ browser: 'safari',
121
+ flags: {
122
+ safari: true,
123
+ isMobile: true,
124
+ },
125
+ chrome: null,
126
+ windowWidth: 375,
127
+ windowHeight: 667,
128
+ },
129
+ 'resolve safari-mobile alias',
130
+ );
131
+ },
132
+ );
133
+
134
+ withSettings(
135
+ {
136
+ webdrivers: ['chrome-mobile'],
137
+ webdriver_aliases: {
138
+ 'chrome-mobile': chromeMobileAlias,
139
+ },
140
+ },
141
+ () => {
142
+ is(settings.webdrivers, ['chrome-mobile'], 'validate known alias');
143
+ },
144
+ );
145
+
146
+ throws(
147
+ () =>
148
+ withSettings(
149
+ {
150
+ webdrivers: ['chrome-mobile'],
151
+ },
152
+ () => {},
153
+ ),
154
+ /unexpected webdriver 'chrome-mobile'/,
155
+ 'reject undefined alias in webdrivers',
156
+ );
157
+
158
+ throws(
159
+ () =>
160
+ withSettings(
161
+ {
162
+ webdriver_aliases: {
163
+ chrome: {
164
+ browser: 'chrome',
165
+ },
166
+ },
167
+ },
168
+ () => {},
169
+ ),
170
+ /conflicts with built-in browser/,
171
+ 'reject alias colliding with base browser',
172
+ );
173
+
174
+ throws(
175
+ () => settings.resolveWebdriver('unknown-alias'),
176
+ /unexpected webdriver 'unknown-alias'/,
177
+ 'reject unknown webdriver resolution',
178
+ );
179
+ }
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.3",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.3",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.3",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.3",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.3",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.1",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "../../../..": {
18
18
  "name": "@camperaid/watest",
19
- "version": "2.6.0",
19
+ "version": "2.6.1",
20
20
  "dev": true,
21
21
  "license": "MPL",
22
22
  "dependencies": {
package/tests/meta.js CHANGED
@@ -1 +1 @@
1
- export var folders = ['base', 'series', 'webdriver', 'e2e', 'deps'];
1
+ export var folders = ['base', 'core', 'series', 'webdriver', 'e2e', 'deps'];
@@ -24,18 +24,21 @@ import {
24
24
  import firefox from 'selenium-webdriver/firefox.js';
25
25
  import chrome from 'selenium-webdriver/chrome.js';
26
26
 
27
- function getChromeOptions() {
27
+ function getChromeOptions(resolved) {
28
+ const windowWidth =
29
+ resolved.windowWidth ?? settings.webdriver_window_width;
30
+ const windowHeight =
31
+ resolved.windowHeight ?? settings.webdriver_window_height;
32
+
28
33
  const chromeOptions = new chrome.Options().windowSize({
29
- width: settings.webdriver_window_width,
30
- height: settings.webdriver_window_height,
34
+ width: windowWidth,
35
+ height: windowHeight,
31
36
  });
32
37
  if (settings.webdriver_headless) {
33
38
  chromeOptions.addArguments('headless=new');
34
39
  }
35
- if (settings.webdriver == 'chrome-mobile') {
36
- chromeOptions.setMobileEmulation({
37
- deviceName: 'iPhone 7',
38
- });
40
+ if (resolved.chrome?.mobileEmulation) {
41
+ chromeOptions.setMobileEmulation(resolved.chrome.mobileEmulation);
39
42
  }
40
43
  // Accept self-signed certificates (for k3s testing)
41
44
  chromeOptions.addArguments('ignore-certificate-errors');
@@ -44,6 +47,11 @@ function getChromeOptions() {
44
47
  chromeOptions.addArguments('no-sandbox');
45
48
  chromeOptions.addArguments('disable-dev-shm-usage');
46
49
 
50
+ // Use pipe-based DevTools instead of a port file. Prevents
51
+ // "DevToolsActivePort file doesn't exist" when Chrome exits early or on some
52
+ // macOS setups (Chrome 111+).
53
+ chromeOptions.addArguments('remote-debugging-pipe');
54
+
47
55
  // Disable disk cache so stale resources from previous deploys are never
48
56
  // served from cache. Each test session fetches everything from the network.
49
57
  chromeOptions.addArguments('disk-cache-size=0');
@@ -58,6 +66,15 @@ function getFirefoxArgs() {
58
66
  return settings.webdriver_headless ? ['-headless'] : [];
59
67
  }
60
68
 
69
+ async function applyResolvedWindowSize(driver, resolved) {
70
+ if (resolved.windowWidth == null && resolved.windowHeight == null) {
71
+ return;
72
+ }
73
+ const width = resolved.windowWidth ?? settings.webdriver_window_width;
74
+ const height = resolved.windowHeight ?? settings.webdriver_window_height;
75
+ await driver.manage().window().setRect({ width, height, x: 0, y: 0 });
76
+ }
77
+
61
78
  const defaultTimeout = 10;
62
79
  const getTimeout = () => (testflow.core.getTimeout() || defaultTimeout) * 1000;
63
80
  const getTimeoutOnFailure = () => (testflow.core.getTimeout() || 0) * 1000; // use non zero values for failure debugging
@@ -102,9 +119,10 @@ class DriverBase {
102
119
  * Creates an instance of web driver.
103
120
  */
104
121
  static async build() {
105
- log(`Build WebDriver for '${settings.webdriver}'`);
122
+ const resolved = settings.resolveWebdriver(settings.webdriver);
123
+ log(`Build WebDriver for '${resolved.name}'`);
106
124
 
107
- switch (settings.webdriver) {
125
+ switch (resolved.browser) {
108
126
  case 'chrome': {
109
127
  const driver = await new Builder()
110
128
  .withCapabilities({
@@ -113,14 +131,13 @@ class DriverBase {
113
131
  browser: browserLogLevel,
114
132
  },
115
133
  })
116
- .setChromeOptions(getChromeOptions())
134
+ .setChromeOptions(getChromeOptions(resolved))
117
135
  .build();
118
136
 
119
137
  return [
120
138
  driver,
121
139
  {
122
- browserLoggingOn: true,
123
- chrome: true,
140
+ ...resolved.flags,
124
141
  },
125
142
  ];
126
143
  }
@@ -149,38 +166,18 @@ class DriverBase {
149
166
  return [
150
167
  driver,
151
168
  {
152
- firefox: true,
153
- },
154
- ];
155
- }
156
-
157
- case 'chrome-mobile': {
158
- const driver = await new Builder()
159
- .withCapabilities({
160
- 'browserName': Browser.CHROME,
161
- 'goog:loggingPrefs': {
162
- browser: browserLogLevel,
163
- },
164
- })
165
- .setChromeOptions(getChromeOptions())
166
- .build();
167
-
168
- return [
169
- driver,
170
- {
171
- isMobile: true,
172
- chrome: true,
173
- browserLoggingOn: true,
169
+ ...resolved.flags,
174
170
  },
175
171
  ];
176
172
  }
177
173
 
178
174
  case 'safari': {
179
175
  const driver = await new Builder().forBrowser(Browser.SAFARI).build();
176
+ await applyResolvedWindowSize(driver, resolved);
180
177
  return [
181
178
  driver,
182
179
  {
183
- safari: true,
180
+ ...resolved.flags,
184
181
  },
185
182
  ];
186
183
  }
@@ -190,18 +187,18 @@ class DriverBase {
190
187
  return [
191
188
  driver,
192
189
  {
193
- edge: true,
190
+ ...resolved.flags,
194
191
  },
195
192
  ];
196
193
  }
197
194
 
198
195
  default:
199
- throw new Error(`Unexpected '${settings.webdriver}' webdriver`);
196
+ throw new Error(`Unexpected '${resolved.browser}' webdriver browser`);
200
197
  }
201
198
  }
202
199
 
203
200
  static isStdOutLogging(webdriver) {
204
- return webdriver == 'firefox';
201
+ return settings.resolveWebdriver(webdriver).browser == 'firefox';
205
202
  }
206
203
 
207
204
  static get Errors() {