Skip to content

Commit b6abf90

Browse files
committed
Improved webDriver waits
1 parent d823e6a commit b6abf90

File tree

2 files changed

+78
-89
lines changed

2 files changed

+78
-89
lines changed

src/clients/browser.js

Lines changed: 78 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -14,135 +14,126 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this PurpleTeam project. If not, see <https://www.gnu.org/licenses/>.
1616

17-
const { By } = require('selenium-webdriver');
18-
19-
let log;
20-
let publisher;
21-
let driver;
22-
let knownZapErrorsWithHelpMessageForBuildUser;
23-
24-
25-
const authenticated = async (expectedPageSourceSuccess) => {
26-
const page = await driver.getPageSource();
27-
return page.includes(expectedPageSourceSuccess);
17+
const { By, until } = require('selenium-webdriver');
18+
19+
// The three types of wiats in Selenium are:
20+
// Implicit Wait Wait for a measure of time before throwing exception (this is a blunt tool).
21+
// Explicit Wait Wiat for a condition to occur, but no longer than the specified timeout (most commonly used).
22+
// Fluent Waits Maximum wait time (Advanced version of an explicit wait).
23+
// Docs:
24+
// https://www.testim.io/blog/how-to-wait-for-a-page-to-load-in-selenium/
25+
// https://www.selenium.dev/documentation/webdriver/waits/#tabs-0-4
26+
// https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/until.html
27+
const internals = {
28+
log: undefined,
29+
publisher: undefined,
30+
driver: undefined,
31+
knownZapErrorsWithHelpMessageForBuildUser: undefined,
32+
explicitTimeout: 10000, // 10 seconds. This is a maximum.
33+
authenticated: async (expectedPageSourceSuccess) => {
34+
const result = await internals.driver.wait(async () => {
35+
const page = await internals.driver.getPageSource();
36+
return page.includes(expectedPageSourceSuccess);
37+
}, internals.explicitTimeout);
38+
return result;
39+
},
40+
// Doc:
41+
// By.js: https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_By.html
42+
// executeScript: https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#executeScript
43+
// The search function can not be object shorthand.
44+
search: function () { // eslint-disable-line
45+
// Selenium requires the use of arguments.
46+
const searchText = arguments[0]; // eslint-disable-line prefer-rest-params
47+
const elementById = document.getElementById(searchText); // eslint-disable-line no-undef
48+
const elementByClassName = document.getElementsByClassName(searchText)[0] || null; // eslint-disable-line no-undef
49+
const elementByName = document.getElementsByName(searchText)[0] || null; // eslint-disable-line no-undef
50+
// This could be extended with the likes of querySelector. It would mean that the attackFields in the Job file may need an additional search string.
51+
return elementById || elementByClassName || elementByName || null;
52+
}
2853
};
2954

30-
3155
const findElementThenClick = async (searchText, testSessionId, expectedPageSourceSuccess) => {
56+
const { publisher, driver, explicitTimeout, authenticated, search } = internals;
3257
const authenticatedFeedback = async () => (expectedPageSourceSuccess ? `. User was ${await authenticated(expectedPageSourceSuccess) ? 'authenticated' : '***not*** authenticated, check the login credentials you supplied in the Job'}` : '');
3358
try {
34-
await driver.findElement(By.id(searchText)).click();
35-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using id="${searchText}", and clicked it${await authenticatedFeedback()} for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
59+
await driver.wait(until.elementLocated(By.js(search, searchText)), explicitTimeout).click();
60+
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element for Test Session with id: "${testSessionId}" using searchText: "${searchText}", and clicked it${await authenticatedFeedback()}.`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
3661
} catch (e) {
37-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using id="${searchText}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
38-
}
39-
try {
40-
await driver.findElement(By.className(searchText)).click();
41-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using className="${searchText}", and clicked it${await authenticatedFeedback()} for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
42-
} catch (e) {
43-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using className="${searchText}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
44-
}
45-
try {
46-
await driver.findElement(By.name(searchText)).click();
47-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using name="${searchText}", and clicked it${await authenticatedFeedback()} for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
48-
} catch (e) {
49-
const logText = `Unable to locate element using id, className, or name of "${searchText}" for Test Session with id: "${testSessionId}".`;
50-
publisher.pubLog({ testSessionId, logLevel: 'crit', textData: `Unable to locate element using id, className, or name of "${searchText}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
51-
throw new Error(logText);
62+
const textData = `Unable to locate element using searchText: "${searchText}" to click for Test Session with id: "${testSessionId}" using the following document methods: getElementById, getElementsByClassName, getElementsByName.`;
63+
publisher.pubLog({ testSessionId, logLevel: 'notice', textData, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
64+
throw new Error(textData);
5265
}
5366
};
5467

55-
5668
const findElementThenClear = async (attackField, testSessionId) => {
69+
const { publisher, driver, explicitTimeout, search } = internals;
5770
try {
5871
if (attackField && attackField.visible) {
59-
await driver.findElement(By.id(attackField.name)).clear();
60-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using id="${attackField.name}" and cleared it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
61-
}
62-
} catch (e) {
63-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using id="${attackField.name}" to clear it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
64-
}
65-
try {
66-
if (attackField && attackField.visible) {
67-
await driver.findElement(By.className(attackField.name)).clear();
68-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using className="${attackField.name}" and cleared it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
72+
await driver.wait(until.elementLocated(By.js(search, attackField.name)), explicitTimeout).clear();
73+
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element for Test Session with id: "${testSessionId}" using attackField.name: "${attackField.name}" and cleared it's value.`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
6974
}
7075
} catch (e) {
71-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using className="${attackField.name}" to clear it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
72-
}
73-
try {
74-
if (attackField && attackField.visible) {
75-
await driver.findElement(By.name(attackField.name)).clear();
76-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using name="${attackField.name}" and cleared it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
77-
}
78-
} catch (e) {
79-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using name="${attackField.name}" to clear it's value for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
80-
throw new Error(`Unable to locate element using id, className, or name of "${attackField.name}". For Test Session with id: "${testSessionId}".`);
76+
const textData = `Unable to locate element using attackField.name: "${attackField.name}" to clear for Test Session with id: "${testSessionId}" using the following document methods: getElementById, getElementsByClassName, getElementsByName.`;
77+
publisher.pubLog({ testSessionId, logLevel: 'notice', textData, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
78+
throw new Error(textData);
8179
}
8280
return ''; // Keep eslint happy
8381
};
8482

85-
8683
const findElementThenSendKeys = async (attackField, testSessionId) => {
84+
const { publisher, driver, explicitTimeout, search } = internals;
8785
try {
8886
if (attackField && attackField.visible) {
89-
await driver.findElement(By.id(attackField.name)).sendKeys(attackField.value);
90-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using id="${attackField.name}" and sent keys for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
91-
}
92-
} catch (e) {
93-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using id="${attackField.name}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
94-
}
95-
try {
96-
if (attackField && attackField.visible) {
97-
await driver.findElement(By.className(attackField.name)).sendKeys(attackField.value);
98-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using className="${attackField.name}" and sent keys for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
99-
}
100-
} catch (e) {
101-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using className="${attackField.name}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
102-
}
103-
try {
104-
if (attackField && attackField.visible) {
105-
await driver.findElement(By.name(attackField.name)).sendKeys(attackField.value);
106-
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element using name="${attackField.name}" and sent keys for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
87+
await driver.wait(until.elementLocated(By.js(search, attackField.name)), explicitTimeout).sendKeys(attackField.value);
88+
return publisher.pubLog({ testSessionId, logLevel: 'info', textData: `Located element for Test Session with id: "${testSessionId}" using attackField.name: "${attackField.name}" and sent keys.`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
10789
}
10890
} catch (e) {
109-
publisher.pubLog({ testSessionId, logLevel: 'notice', textData: `Unable to locate element using name="${attackField.name}" for Test Session with id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
110-
throw new Error(`Unable to locate element using id, className, or name of "${attackField.name}". For Test Session with id: "${testSessionId}".`);
91+
const textData = `Unable to locate element using attackField.name: "${attackField.name}" to send keys for Test Session with id: "${testSessionId}" using the following document methods: getElementById, getElementsByClassName, getElementsByName.`;
92+
publisher.pubLog({ testSessionId, logLevel: 'notice', textData, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
93+
throw new Error(textData);
11194
}
11295
return ''; // Keep eslint happy
11396
};
11497

115-
11698
const checkAndNotifyBuildUserIfAnyKnownBrowserErrors = async (testSessionId) => {
117-
const pageSource = await driver.getPageSource();
118-
119-
if (pageSource.includes('ZAP Error')) {
120-
const knownZapErrorWithHelpMessageForBuildUser = knownZapErrorsWithHelpMessageForBuildUser.find((k) => pageSource.includes(k.zapMessage));
121-
122-
if (knownZapErrorWithHelpMessageForBuildUser) {
123-
publisher.pubLog({ testSessionId, logLevel: 'error', textData: `${knownZapErrorWithHelpMessageForBuildUser.helpMessageForBuildUser} The message received in the browser was: "${knownZapErrorWithHelpMessageForBuildUser.zapMessage}" ... for Test Session id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
124-
} else {
125-
const messageForUnknownZapError = `An unknown Zap Error was received in the browser for Test Session id: "${testSessionId}".`;
126-
log.error(`${messageForUnknownZapError} The page source was: ${pageSource}`, { tags: [`pid-${process.pid}`, 'browser'] });
127-
publisher.publish(testSessionId, `${messageForUnknownZapError} If running local: inspect the app-scanner log, if running in cloud: Ask @binarymist for further details.`);
99+
const { log, publisher, driver, knownZapErrorsWithHelpMessageForBuildUser } = internals;
100+
try {
101+
let page;
102+
const zapError = await driver.wait(async () => {
103+
page = await driver.getPageSource();
104+
return page.includes('ZAP Error');
105+
}, 5000); // 5 seconds.
106+
if (zapError) {
107+
const knownZapErrorWithHelpMessageForBuildUser = knownZapErrorsWithHelpMessageForBuildUser.find((k) => page.includes(k.zapMessage));
108+
109+
if (knownZapErrorWithHelpMessageForBuildUser) {
110+
publisher.pubLog({ testSessionId, logLevel: 'error', textData: `${knownZapErrorWithHelpMessageForBuildUser.helpMessageForBuildUser} The message received in the browser was: "${knownZapErrorWithHelpMessageForBuildUser.zapMessage}" ... for Test Session id: "${testSessionId}".`, tagObj: { tags: [`pid-${process.pid}`, 'browser'] } });
111+
} else {
112+
const messageForUnknownZapError = `An unknown Zap Error was received in the browser for Test Session id: "${testSessionId}".`;
113+
log.error(`${messageForUnknownZapError} The page source was: ${page}`, { tags: [`pid-${process.pid}`, 'browser'] });
114+
publisher.publish(testSessionId, `${messageForUnknownZapError} If running local: inspect the app-scanner log, if running in cloud: Ask @binarymist for further details.`);
115+
}
128116
}
117+
} catch (e) {
118+
if (e.name !== 'TimeoutError') throw new Error(`Error occurred in browser.js while looking for ZAP Error. The error was: ${e}`);
129119
}
130120
};
131121

132-
133122
const percentEncode = (str) => str.split('').map((char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).reduce((accum, cV) => `${accum}${cV}`, '');
134123

135-
136124
module.exports = {
137125
findElementThenClick,
138126
findElementThenClear,
139127
findElementThenSendKeys,
140128
checkAndNotifyBuildUserIfAnyKnownBrowserErrors,
141129
init(options) {
142-
({ log, publisher, knownZapErrorsWithHelpMessageForBuildUser, webDriver: driver } = options);
130+
internals.log = options.log;
131+
internals.publisher = options.publisher;
132+
internals.knownZapErrorsWithHelpMessageForBuildUser = options.knownZapErrorsWithHelpMessageForBuildUser;
133+
internals.driver = options.webDriver;
143134
},
144135
getWebDriver() {
145-
return driver;
136+
return internals.driver;
146137
},
147138
percentEncode
148139
};

src/steps/app_scan_steps.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ Given('a new Test Session based on each Build User supplied appScanner resourceO
4242
await webDriver.getWindowHandle();
4343
await webDriver.get(`${sutBaseUrl}${loginRoute}`);
4444
await checkAndNotifyBuildUserIfAnyKnownBrowserErrors(id);
45-
await webDriver.sleep(5000);
4645
await findElementThenSendKeys({ name: usernameFieldLocater, value: username, visible: true }, id);
4746
await findElementThenSendKeys({ name: passwordFieldLocater, value: password, visible: true }, id);
48-
await webDriver.sleep(5000);
4947
await findElementThenClick(submit, id, expectedPageSourceSuccess);
5048
});
5149

0 commit comments

Comments
 (0)