hof 23.0.2 → 23.0.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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 2026-03-18, Version 23.0.3 (Stable), @vinodhasamiyappan-ho
2
+
3
+ ### Security
4
+ - Upgraded WebdriverIO from v4 to v5, resolving security vulnerabilities caused by transitive dependencies (`minimist@0.0.10`, `form-data@~2.3.2`) in `webdriverio@4.14.4`.
5
+
6
+ ### Changed
7
+ - Migrated to WebdriverIO v5 (`webdriverio@5.23.0`), requiring the following compatibility updates:
8
+ - Refactored all functional tests to use async/await and the new WebdriverIO v5 command API.
9
+ - Updated test utilities and browser setup for async execution and modern WebdriverIO patterns.
10
+ - Removed deprecated synchronous and promise-chaining test code.
11
+ - Improved test isolation and reliability by ensuring each test runs with a fresh app and browser instance.
12
+ - Enhanced sessionDialog Jest tests with new and improved negative test scenarios
13
+
1
14
  ## 2026-03-17, Version 23.0.2 (Stable), @vivekkumar-ho
2
15
 
3
16
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hof",
3
3
  "description": "A bootstrap for HOF projects",
4
- "version": "23.0.2",
4
+ "version": "23.0.3",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
@@ -129,7 +129,7 @@
129
129
  "travis-conditions": "0.0.0",
130
130
  "vitest": "^4.0.8",
131
131
  "watchify": "^4.0.0",
132
- "webdriverio": "^4.14.4"
132
+ "webdriverio": "^5.0.0"
133
133
  },
134
134
  "jest": {
135
135
  "testEnvironment": "jsdom"
@@ -3,187 +3,211 @@
3
3
 
4
4
  const url = require('url');
5
5
  const Inputs = require('./inputs');
6
- const Promise = require('bluebird');
7
6
 
8
7
  const debug = require('debug')('hof:util:autofill');
9
8
 
10
9
  const MAX_LOOPS = 3;
11
10
 
12
- module.exports = browser => (target, input, opts) => {
11
+ function getRemoteFilePath(uploadResult) {
12
+ if (uploadResult && typeof uploadResult === 'object' && uploadResult.value) {
13
+ return uploadResult.value;
14
+ }
15
+
16
+ return uploadResult;
17
+ }
18
+
19
+ module.exports = browser => async (target, input, opts) => {
13
20
  const options = opts || {};
14
21
  options.maxLoops = options.maxLoops || MAX_LOOPS;
15
-
16
22
  const getValue = Inputs(input);
17
-
18
23
  let last;
19
24
  let count = 0;
20
25
 
21
- function completeTextField(element, name) {
26
+ async function completeTextField(element, name) {
22
27
  const value = getValue(name, 'text');
23
28
  debug(`Filling field: ${name} with value: ${value}`);
24
- return browser
25
- .elementIdClear(element)
26
- .elementIdValue(element, value)
27
- .catch(() => {
28
- // any error here is *probably* because the field is hidden
29
- // ignore and hope for the best
30
- });
29
+ try {
30
+ await element.clearValue();
31
+ await element.setValue(value);
32
+ } catch (e) {
33
+ // any error here is *probably* because the field is hidden
34
+ // ignore and hope for the best
35
+ }
31
36
  }
32
37
 
33
- function completeFileField(element, name) {
38
+ async function completeFileField(element, name) {
34
39
  const value = getValue(name, 'file');
35
40
  if (value) {
36
41
  debug(`Uploading file: ${value}`);
37
- return browser.uploadFile(value)
38
- .then(response => {
39
- debug(`Uploaded file: ${value} - remote path ${response.value}`);
40
- return browser
41
- .addValue(`input[name="${name}"]`, response.value);
42
- });
42
+ const remotePath = getRemoteFilePath(await browser.uploadFile(value));
43
+ debug(`Uploaded file: ${value} - remote path ${remotePath}`);
44
+ await element.setValue(remotePath);
45
+ } else {
46
+ debug(`No file specified for input ${name} - ignoring`);
43
47
  }
44
- debug(`No file specified for input ${name} - ignoring`);
45
48
  }
46
49
 
47
- function completeRadio(element, name) {
50
+ async function completeRadioGroup(name) {
48
51
  const value = getValue(name, 'radio');
49
- if (!value) {
50
- return browser.elements(`input[type="radio"][name="${name}"]`)
51
- .then(radios => {
52
- debug(`Checking random radio: ${name}`);
53
- const index = 1 + Math.floor(Math.random() * (radios.value.length - 1));
54
- return browser.elementIdClick(radios.value[index].ELEMENT);
55
- });
52
+ const radios = await browser.$$(`input[type="radio"][name="${name}"]`);
53
+
54
+ if (!radios.length) {
55
+ debug(`No radio inputs found for ${name}`);
56
+ return;
56
57
  }
57
- return browser.elementIdAttribute(element, 'value')
58
- .then(val => {
59
- if (val.value === value) {
60
- debug(`Checking radio: ${name} with value: ${val.value}`);
61
- browser.elementIdClick(element);
58
+
59
+ if (!value) {
60
+ debug(`Checking random radio: ${name}`);
61
+ const index = Math.floor(Math.random() * radios.length);
62
+ if (!await radios[index].isSelected()) {
63
+ await radios[index].click();
64
+ }
65
+ } else {
66
+ for (const radio of radios) {
67
+ const val = await radio.getAttribute('value');
68
+ if (val === value) {
69
+ debug(`Checking radio: ${name} with value: ${val}`);
70
+ if (!await radio.isSelected()) {
71
+ await radio.click();
72
+ }
73
+ return;
62
74
  }
63
- });
75
+ }
76
+
77
+ debug(`Ignoring radio group: ${name} - no option matches ${value}`);
78
+ }
64
79
  }
65
80
 
66
- function completeCheckbox(element, name) {
81
+ async function completeCheckbox(element, name) {
67
82
  const value = getValue(name, 'checkbox');
68
- return browser.elementIdAttribute(element, 'value')
69
- .then(val => browser.elementIdAttribute(element, 'checked')
70
- .then(checked => {
71
- if (value === null) {
72
- if (!checked.value) {
73
- debug(`Leaving checkbox: ${name} blank`);
74
- return;
75
- }
76
- debug(`Unchecking checkbox: ${name}`);
77
- return browser.elementIdClick(element);
78
- }
79
- if (!value && !checked.value) {
80
- debug(`Checking checkbox: ${name} with value: ${val.value}`);
81
- return browser.elementIdClick(element);
82
- } else if (value && value.indexOf(val.value) > -1 && !checked.value) {
83
- debug(`Checking checkbox: ${name} with value: ${val.value}`);
84
- return browser.elementIdClick(element);
85
- } else if (value && value.indexOf(val.value) === -1 && checked.value) {
86
- debug(`Unchecking checkbox: ${name} with value: ${val.value}`);
87
- return browser.elementIdClick(element);
88
- }
89
- debug(`Ignoring checkbox: ${name} with value: ${val.value} - looking for ${value}`);
90
- }));
83
+ const val = await element.getAttribute('value');
84
+ const checked = await element.isSelected();
85
+ if (value === null) {
86
+ if (!checked) {
87
+ debug(`Leaving checkbox: ${name} blank`);
88
+ return;
89
+ }
90
+ debug(`Unchecking checkbox: ${name}`);
91
+ await element.click();
92
+ } else if (!value && !checked) {
93
+ debug(`Checking checkbox: ${name} with value: ${val}`);
94
+ await element.click();
95
+ } else if (value && value.indexOf(val) > -1 && !checked) {
96
+ debug(`Checking checkbox: ${name} with value: ${val}`);
97
+ await element.click();
98
+ } else if (value && value.indexOf(val) === -1 && checked) {
99
+ debug(`Unchecking checkbox: ${name} with value: ${val}`);
100
+ await element.click();
101
+ } else {
102
+ debug(`Ignoring checkbox: ${name} with value: ${val} - looking for ${value}`);
103
+ }
91
104
  }
92
105
 
93
- function completeSelectElement(element, name) {
106
+ async function completeSelectElement(element, name) {
94
107
  const value = getValue(name, 'select');
95
108
  if (!value) {
96
- return browser.elementIdElements(element, 'option')
97
- .then(o => {
98
- const index = 1 + Math.floor(Math.random() * (o.value.length - 1));
99
- debug(`Selecting option: ${index} from select box: ${name}`);
100
- return browser.selectByIndex(`select[name="${name}"]`, index);
101
- });
109
+ const selectOptions = await element.$$('option');
110
+ if (selectOptions.length > 1) {
111
+ const index = 1 + Math.floor(Math.random() * (selectOptions.length - 1));
112
+ debug(`Selecting option: ${index} from select box: ${name}`);
113
+ await element.selectByIndex(index);
114
+ }
115
+ } else {
116
+ debug(`Selecting options: ${value} from select box: ${name}`);
117
+ await element.selectByAttribute('value', value);
102
118
  }
103
- debug(`Selecting options: ${value} from select box: ${name}`);
104
- return browser.selectByValue(`select[name="${name}"]`, value);
105
119
  }
106
120
 
107
- function completeStep(path) {
108
- return browser
109
- .elements('input')
110
- .then(fields => {
111
- debug(`Found ${fields.value.length} <input> elements`);
112
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'type')
113
- .then(type => browser.elementIdAttribute(field.ELEMENT, 'name')
114
- .then(name => {
115
- if (type.value === 'radio') {
116
- return completeRadio(field.ELEMENT, name.value);
117
- } else if (type.value === 'checkbox') {
118
- return completeCheckbox(field.ELEMENT, name.value);
119
- } else if (type.value === 'file') {
120
- return completeFileField(field.ELEMENT, name.value);
121
- } else if (type.value === 'text') {
122
- return completeTextField(field.ELEMENT, name.value);
123
- }
124
- debug(`Ignoring field of type ${type.value}`);
125
- })), {concurrency: 1});
126
- })
127
- .elements('select')
128
- .then(fields => {
129
- debug(`Found ${fields.value.length} <select> elements`);
130
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'name')
131
- .then(name => completeSelectElement(field.ELEMENT, name.value)));
132
- })
133
- .elements('textarea')
134
- .then(fields => {
135
- debug(`Found ${fields.value.length} <textarea> elements`);
136
- return Promise.map(fields.value, field => browser.elementIdAttribute(field.ELEMENT, 'name')
137
- .then(name => completeTextField(field.ELEMENT, name.value)));
138
- })
139
- .then(() => {
140
- if (options.screenshots) {
141
- const screenshot = require('path').resolve(options.screenshots, 'hof-autofill.pre-submit.png');
142
- return browser.saveScreenshot(screenshot);
121
+ async function completeStep(path) {
122
+ const completedRadioGroups = new Set();
123
+
124
+ // Fill inputs
125
+ const inputs = await browser.$$('input');
126
+ debug(`Found ${inputs.length} <input> elements`);
127
+ for (const element of inputs) {
128
+ const type = await element.getAttribute('type');
129
+ const name = await element.getAttribute('name');
130
+ if (type === 'radio') {
131
+ if (!completedRadioGroups.has(name)) {
132
+ completedRadioGroups.add(name);
133
+ await completeRadioGroup(name);
143
134
  }
144
- })
145
- .then(() => {
146
- debug('Submitting form');
147
- return browser.$('input[type="submit"]').click();
148
- })
149
- .then(() => browser.getUrl()
150
- .then(p => {
151
- const u = url.parse(p);
152
- debug(`New page is: ${u.path}`);
153
- if (u.path !== path) {
154
- debug(`Checking current path ${u.path} against last path ${last}`);
155
- if (last === u.path) {
156
- count++;
157
- debug(`Stuck on path ${u.path} for ${count} iterations`);
158
- if (count === options.maxLoops) {
159
- if (options.screenshots) {
160
- const screenshot = require('path').resolve(options.screenshots, 'hof-autofill.debug.png');
161
- return browser.saveScreenshot(screenshot)
162
- .then(() => {
163
- throw new Error(`Progress stuck at ${u.path} - screenshot saved to ${screenshot}`);
164
- });
165
- }
166
- throw new Error(`Progress stuck at ${u.path}`);
167
- }
168
- } else {
169
- count = 0;
170
- }
171
- last = u.path;
172
- return completeStep(path);
135
+ } else if (type === 'checkbox') {
136
+ await completeCheckbox(element, name);
137
+ } else if (type === 'file') {
138
+ await completeFileField(element, name);
139
+ } else if (type === 'text') {
140
+ await completeTextField(element, name);
141
+ } else {
142
+ debug(`Ignoring field of type ${type}`);
143
+ }
144
+ }
145
+
146
+ // Fill selects
147
+ const selects = await browser.$$('select');
148
+ debug(`Found ${selects.length} <select> elements`);
149
+ for (const element of selects) {
150
+ const name = await element.getAttribute('name');
151
+ await completeSelectElement(element, name);
152
+ }
153
+
154
+ // Fill textareas
155
+ const textareas = await browser.$$('textarea');
156
+ debug(`Found ${textareas.length} <textarea> elements`);
157
+ for (const element of textareas) {
158
+ const name = await element.getAttribute('name');
159
+ await completeTextField(element, name);
160
+ }
161
+
162
+ if (options.screenshots) {
163
+ const screenshot = path.resolve(options.screenshots, 'hof-autofill.pre-submit.png');
164
+ await browser.saveScreenshot(screenshot);
165
+ }
166
+
167
+ debug('Submitting form');
168
+ const submitBtn = await browser.$('input[type="submit"], button[type="submit"]');
169
+ if (!await submitBtn.isExisting()) {
170
+ throw new Error('No submit control found on page');
171
+ }
172
+ await submitBtn.click();
173
+
174
+ const p = await browser.getUrl();
175
+ const u = url.parse(p);
176
+ debug(`New page is: ${u.path}`);
177
+ if (u.path !== path) {
178
+ debug(`Checking current path ${u.path} against last path ${last}`);
179
+ if (last === u.path) {
180
+ count++;
181
+ debug(`Stuck on path ${u.path} for ${count} iterations`);
182
+ if (count === options.maxLoops) {
183
+ if (options.screenshots) {
184
+ const screenshot = path.resolve(options.screenshots, 'hof-autofill.debug.png');
185
+ await browser.saveScreenshot(screenshot);
186
+ throw new Error(`Progress stuck at ${u.path} - screenshot saved to ${screenshot}`);
173
187
  }
174
- debug(`Arrived at ${path}. Done.`);
175
- }))
176
- .catch(e => browser.getText('#content')
177
- .then(text => {
178
- debug('PAGE CONTENT >>>>>>');
179
- debug(text);
180
- debug('END PAGE CONTENT >>>>>>');
181
- })
182
- .catch(() => null)
183
- .then(() => {
184
- throw e;
185
- }));
188
+ throw new Error(`Progress stuck at ${u.path}`);
189
+ }
190
+ } else {
191
+ count = 0;
192
+ }
193
+ last = u.path;
194
+ return completeStep(path);
195
+ }
196
+ debug(`Arrived at ${path}. Done.`);
186
197
  }
187
198
 
188
- return completeStep(target);
199
+ try {
200
+ await completeStep(target);
201
+ } catch (e) {
202
+ try {
203
+ const content = await browser.$('#content');
204
+ const text = await content.getText();
205
+ debug('PAGE CONTENT >>>>>>');
206
+ debug(text);
207
+ debug('END PAGE CONTENT >>>>>>');
208
+ } catch (err) {
209
+ // ignore error
210
+ }
211
+ throw e;
212
+ }
189
213
  };