codeceptjs 3.5.2 → 3.5.4-beta.1
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/README.md +4 -5
- package/docs/basics.md +1 -2
- package/docs/build/Playwright.js +461 -0
- package/docs/changelog.md +0 -7
- package/docs/custom-helpers.md +2 -2
- package/docs/data.md +6 -2
- package/docs/docker.md +2 -3
- package/docs/helpers/Playwright.md +305 -153
- package/docs/installation.md +1 -1
- package/docs/mobile.md +0 -2
- package/docs/plugins.md +0 -2
- package/docs/quickstart.md +0 -1
- package/docs/testcafe.md +1 -1
- package/docs/webdriver.md +0 -2
- package/lib/command/init.js +1 -1
- package/lib/helper/Playwright.js +562 -136
- package/lib/helper/extras/PlaywrightReact.js +9 -0
- package/lib/plugin/standardActingHelpers.js +0 -2
- package/lib/plugin/wdio.js +0 -1
- package/lib/recorder.js +2 -1
- package/package.json +5 -7
- package/typings/index.d.ts +0 -1
- package/typings/promiseBasedTypes.d.ts +127 -0
- package/typings/types.d.ts +127 -25
- package/docs/helpers/Nightmare.md +0 -1258
- package/docs/nightmare.md +0 -223
- package/lib/helper/Nightmare.js +0 -1410
- package/lib/helper/Protractor.js +0 -1832
- package/lib/helper/clientscripts/nightmare.js +0 -213
package/lib/helper/Playwright.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
|
|
4
4
|
const Helper = require('@codeceptjs/helper');
|
|
5
5
|
const { v4: uuidv4 } = require('uuid');
|
|
6
|
+
const assert = require('assert');
|
|
6
7
|
const Locator = require('../locator');
|
|
7
8
|
const store = require('../store');
|
|
8
9
|
const recorder = require('../recorder');
|
|
@@ -311,6 +312,11 @@ class Playwright extends Helper {
|
|
|
311
312
|
this.electronSessions = [];
|
|
312
313
|
this.storageState = null;
|
|
313
314
|
|
|
315
|
+
// for network stuff
|
|
316
|
+
this.requests = [];
|
|
317
|
+
this.recording = false;
|
|
318
|
+
this.recordedAtLeastOnce = false;
|
|
319
|
+
|
|
314
320
|
// override defaults with config
|
|
315
321
|
this._setConfig(config);
|
|
316
322
|
}
|
|
@@ -818,9 +824,9 @@ class Playwright extends Helper {
|
|
|
818
824
|
return;
|
|
819
825
|
}
|
|
820
826
|
|
|
821
|
-
const
|
|
822
|
-
assertElementExists(
|
|
823
|
-
this.context =
|
|
827
|
+
const el = await this._locateElement(locator);
|
|
828
|
+
assertElementExists(el, locator);
|
|
829
|
+
this.context = el;
|
|
824
830
|
this.contextLocator = locator;
|
|
825
831
|
|
|
826
832
|
this.withinLocator = new Locator(locator);
|
|
@@ -923,11 +929,11 @@ class Playwright extends Helper {
|
|
|
923
929
|
*
|
|
924
930
|
*/
|
|
925
931
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
926
|
-
const
|
|
927
|
-
assertElementExists(
|
|
932
|
+
const el = await this._locateElement(locator);
|
|
933
|
+
assertElementExists(el, locator);
|
|
928
934
|
|
|
929
935
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
930
|
-
const { x, y } = await clickablePoint(
|
|
936
|
+
const { x, y } = await clickablePoint(el);
|
|
931
937
|
await this.page.mouse.move(x + offsetX, y + offsetY);
|
|
932
938
|
return this._waitForAction();
|
|
933
939
|
}
|
|
@@ -937,9 +943,8 @@ class Playwright extends Helper {
|
|
|
937
943
|
*
|
|
938
944
|
*/
|
|
939
945
|
async focus(locator, options = {}) {
|
|
940
|
-
const
|
|
941
|
-
assertElementExists(
|
|
942
|
-
const el = els[0];
|
|
946
|
+
const el = await this._locateElement(locator);
|
|
947
|
+
assertElementExists(el, locator, 'Element to focus');
|
|
943
948
|
|
|
944
949
|
await el.focus(options);
|
|
945
950
|
return this._waitForAction();
|
|
@@ -950,12 +955,10 @@ class Playwright extends Helper {
|
|
|
950
955
|
*
|
|
951
956
|
*/
|
|
952
957
|
async blur(locator, options = {}) {
|
|
953
|
-
const
|
|
954
|
-
assertElementExists(
|
|
955
|
-
// TODO: locator change required after #3677 implementation
|
|
956
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
958
|
+
const el = await this._locateElement(locator);
|
|
959
|
+
assertElementExists(el, locator, 'Element to blur');
|
|
957
960
|
|
|
958
|
-
await
|
|
961
|
+
await el.blur(options);
|
|
959
962
|
return this._waitForAction();
|
|
960
963
|
}
|
|
961
964
|
|
|
@@ -1052,10 +1055,10 @@ class Playwright extends Helper {
|
|
|
1052
1055
|
}
|
|
1053
1056
|
|
|
1054
1057
|
if (locator) {
|
|
1055
|
-
const
|
|
1056
|
-
assertElementExists(
|
|
1057
|
-
await
|
|
1058
|
-
const elementCoordinates = await clickablePoint(
|
|
1058
|
+
const el = await this._locateElement(locator);
|
|
1059
|
+
assertElementExists(el, locator, 'Element');
|
|
1060
|
+
await el.scrollIntoViewIfNeeded();
|
|
1061
|
+
const elementCoordinates = await clickablePoint(el);
|
|
1059
1062
|
await this.executeScript((offsetX, offsetY) => window.scrollBy(offsetX, offsetY), { offsetX: elementCoordinates.x + offsetX, offsetY: elementCoordinates.y + offsetY });
|
|
1060
1063
|
} else {
|
|
1061
1064
|
await this.executeScript(({ offsetX, offsetY }) => window.scrollTo(offsetX, offsetY), { offsetX, offsetY });
|
|
@@ -1123,7 +1126,20 @@ class Playwright extends Helper {
|
|
|
1123
1126
|
}
|
|
1124
1127
|
|
|
1125
1128
|
/**
|
|
1126
|
-
*
|
|
1129
|
+
* Get the first element by different locator types, including strict locator
|
|
1130
|
+
* Should be used in custom helpers:
|
|
1131
|
+
*
|
|
1132
|
+
* ```js
|
|
1133
|
+
* const element = await this.helpers['Playwright']._locateElement({name: 'password'});
|
|
1134
|
+
* ```
|
|
1135
|
+
*/
|
|
1136
|
+
async _locateElement(locator) {
|
|
1137
|
+
const context = await this.context || await this._getContext();
|
|
1138
|
+
return findElement(context, locator);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Find a checkbox by providing human-readable text:
|
|
1127
1143
|
* NOTE: Assumes the checkable element exists
|
|
1128
1144
|
*
|
|
1129
1145
|
* ```js
|
|
@@ -1138,7 +1154,7 @@ class Playwright extends Helper {
|
|
|
1138
1154
|
}
|
|
1139
1155
|
|
|
1140
1156
|
/**
|
|
1141
|
-
* Find a clickable element by providing human
|
|
1157
|
+
* Find a clickable element by providing human-readable text:
|
|
1142
1158
|
*
|
|
1143
1159
|
* ```js
|
|
1144
1160
|
* this.helpers['Playwright']._locateClickable('Next page').then // ...
|
|
@@ -1150,7 +1166,7 @@ class Playwright extends Helper {
|
|
|
1150
1166
|
}
|
|
1151
1167
|
|
|
1152
1168
|
/**
|
|
1153
|
-
* Find field elements by providing human
|
|
1169
|
+
* Find field elements by providing human-readable text:
|
|
1154
1170
|
*
|
|
1155
1171
|
* ```js
|
|
1156
1172
|
* this.helpers['Playwright']._locateFields('Your email').then // ...
|
|
@@ -1526,13 +1542,8 @@ class Playwright extends Helper {
|
|
|
1526
1542
|
const els = await findFields.call(this, field);
|
|
1527
1543
|
assertElementExists(els, field, 'Field');
|
|
1528
1544
|
const el = els[0];
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if (tag === 'INPUT' || tag === 'TEXTAREA') {
|
|
1532
|
-
await this._evaluateHandeInContext(el => el.value = '', el);
|
|
1533
|
-
} else if (editable) {
|
|
1534
|
-
await this._evaluateHandeInContext(el => el.innerHTML = '', el);
|
|
1535
|
-
}
|
|
1545
|
+
|
|
1546
|
+
await el.clear();
|
|
1536
1547
|
|
|
1537
1548
|
highlightActiveElement.call(this, el, this.page);
|
|
1538
1549
|
|
|
@@ -1559,21 +1570,16 @@ class Playwright extends Helper {
|
|
|
1559
1570
|
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
1560
1571
|
*/
|
|
1561
1572
|
async clearField(locator, options = {}) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1573
|
+
const els = await findFields.call(this, locator);
|
|
1574
|
+
assertElementExists(els, locator, 'Field to clear');
|
|
1575
|
+
|
|
1576
|
+
const el = els[0];
|
|
1564
1577
|
|
|
1565
|
-
|
|
1566
|
-
const els = await findFields.call(this, locator);
|
|
1567
|
-
assertElementExists(els, locator, 'Field to clear');
|
|
1568
|
-
// TODO: locator change required after #3677 implementation
|
|
1569
|
-
const elXpath = await getXPathForElement(els[0]);
|
|
1578
|
+
highlightActiveElement.call(this, el, this.page);
|
|
1570
1579
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
result = await this.fillField(locator, '');
|
|
1575
|
-
}
|
|
1576
|
-
return result;
|
|
1580
|
+
await el.clear();
|
|
1581
|
+
|
|
1582
|
+
return this._waitForAction();
|
|
1577
1583
|
}
|
|
1578
1584
|
|
|
1579
1585
|
/**
|
|
@@ -1627,29 +1633,11 @@ class Playwright extends Helper {
|
|
|
1627
1633
|
const els = await findFields.call(this, select);
|
|
1628
1634
|
assertElementExists(els, select, 'Selectable field');
|
|
1629
1635
|
const el = els[0];
|
|
1630
|
-
|
|
1631
|
-
throw new Error('Element is not <select>');
|
|
1632
|
-
}
|
|
1636
|
+
|
|
1633
1637
|
highlightActiveElement.call(this, el, this.page);
|
|
1634
1638
|
if (!Array.isArray(option)) option = [option];
|
|
1635
1639
|
|
|
1636
|
-
|
|
1637
|
-
const opt = xpathLocator.literal(option[key]);
|
|
1638
|
-
let optEl = await findElements.call(this, el, { xpath: Locator.select.byVisibleText(opt) });
|
|
1639
|
-
if (optEl.length) {
|
|
1640
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1643
|
-
optEl = await findElements.call(this, el, { xpath: Locator.select.byValue(opt) });
|
|
1644
|
-
if (optEl.length) {
|
|
1645
|
-
this._evaluateHandeInContext(el => el.selected = true, optEl[0]);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
await this._evaluateHandeInContext((element) => {
|
|
1649
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1650
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1651
|
-
}, el);
|
|
1652
|
-
|
|
1640
|
+
await el.selectOption(option);
|
|
1653
1641
|
return this._waitForAction();
|
|
1654
1642
|
}
|
|
1655
1643
|
|
|
@@ -1900,7 +1888,7 @@ class Playwright extends Helper {
|
|
|
1900
1888
|
const els = await this._locate(locator);
|
|
1901
1889
|
const texts = [];
|
|
1902
1890
|
for (const el of els) {
|
|
1903
|
-
texts.push(await (await el.
|
|
1891
|
+
texts.push(await (await el.innerText()));
|
|
1904
1892
|
}
|
|
1905
1893
|
this.debug(`Matched ${els.length} elements`);
|
|
1906
1894
|
return texts;
|
|
@@ -1922,7 +1910,7 @@ class Playwright extends Helper {
|
|
|
1922
1910
|
async grabValueFromAll(locator) {
|
|
1923
1911
|
const els = await findFields.call(this, locator);
|
|
1924
1912
|
this.debug(`Matched ${els.length} elements`);
|
|
1925
|
-
return Promise.all(els.map(el => el.
|
|
1913
|
+
return Promise.all(els.map(el => el.inputValue()));
|
|
1926
1914
|
}
|
|
1927
1915
|
|
|
1928
1916
|
/**
|
|
@@ -1941,7 +1929,7 @@ class Playwright extends Helper {
|
|
|
1941
1929
|
async grabHTMLFromAll(locator) {
|
|
1942
1930
|
const els = await this._locate(locator);
|
|
1943
1931
|
this.debug(`Matched ${els.length} elements`);
|
|
1944
|
-
return Promise.all(els.map(el => el
|
|
1932
|
+
return Promise.all(els.map(el => el.innerHTML()));
|
|
1945
1933
|
}
|
|
1946
1934
|
|
|
1947
1935
|
/**
|
|
@@ -1962,7 +1950,7 @@ class Playwright extends Helper {
|
|
|
1962
1950
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
1963
1951
|
const els = await this._locate(locator);
|
|
1964
1952
|
this.debug(`Matched ${els.length} elements`);
|
|
1965
|
-
const cssValues = await Promise.all(els.map(el => el
|
|
1953
|
+
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)));
|
|
1966
1954
|
|
|
1967
1955
|
return cssValues;
|
|
1968
1956
|
}
|
|
@@ -1978,21 +1966,20 @@ class Playwright extends Helper {
|
|
|
1978
1966
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
1979
1967
|
const elemAmount = res.length;
|
|
1980
1968
|
const commands = [];
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
}));
|
|
1969
|
+
let props = [];
|
|
1970
|
+
|
|
1971
|
+
for (const element of res) {
|
|
1972
|
+
const cssProperties = await element.evaluate((el) => getComputedStyle(el));
|
|
1973
|
+
|
|
1974
|
+
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
1975
|
+
if (isColorProperty(prop)) {
|
|
1976
|
+
props.push(convertColorToRGBA(cssProperties[prop]));
|
|
1977
|
+
} else {
|
|
1978
|
+
props.push(cssProperties[prop]);
|
|
1979
|
+
}
|
|
1993
1980
|
});
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1996
1983
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
1997
1984
|
if (!Array.isArray(props)) props = [props];
|
|
1998
1985
|
let chunked = chunkArray(props, values.length);
|
|
@@ -2018,7 +2005,7 @@ class Playwright extends Helper {
|
|
|
2018
2005
|
res.forEach((el) => {
|
|
2019
2006
|
Object.keys(attributes).forEach((prop) => {
|
|
2020
2007
|
commands.push(el
|
|
2021
|
-
|
|
2008
|
+
.evaluate((el, attr) => el[attr] || el.getAttribute(attr), prop));
|
|
2022
2009
|
});
|
|
2023
2010
|
});
|
|
2024
2011
|
let attrs = await Promise.all(commands);
|
|
@@ -2039,11 +2026,11 @@ class Playwright extends Helper {
|
|
|
2039
2026
|
*
|
|
2040
2027
|
*/
|
|
2041
2028
|
async dragSlider(locator, offsetX = 0) {
|
|
2042
|
-
const src = await this.
|
|
2029
|
+
const src = await this._locateElement(locator);
|
|
2043
2030
|
assertElementExists(src, locator, 'Slider Element');
|
|
2044
2031
|
|
|
2045
2032
|
// Note: Using clickablePoint private api because the .BoundingBox does not take into account iframe offsets!
|
|
2046
|
-
const sliderSource = await clickablePoint(src
|
|
2033
|
+
const sliderSource = await clickablePoint(src);
|
|
2047
2034
|
|
|
2048
2035
|
// Drag start point
|
|
2049
2036
|
await this.page.mouse.move(sliderSource.x, sliderSource.y, { steps: 5 });
|
|
@@ -2077,8 +2064,7 @@ class Playwright extends Helper {
|
|
|
2077
2064
|
const array = [];
|
|
2078
2065
|
|
|
2079
2066
|
for (let index = 0; index < els.length; index++) {
|
|
2080
|
-
|
|
2081
|
-
array.push(await a.jsonValue());
|
|
2067
|
+
array.push(await els[index].getAttribute(attr));
|
|
2082
2068
|
}
|
|
2083
2069
|
|
|
2084
2070
|
return array;
|
|
@@ -2091,10 +2077,9 @@ class Playwright extends Helper {
|
|
|
2091
2077
|
async saveElementScreenshot(locator, fileName) {
|
|
2092
2078
|
const outputFile = screenshotOutputFolder(fileName);
|
|
2093
2079
|
|
|
2094
|
-
const res = await this.
|
|
2080
|
+
const res = await this._locateElement(locator);
|
|
2095
2081
|
assertElementExists(res, locator);
|
|
2096
|
-
|
|
2097
|
-
const elem = res[0];
|
|
2082
|
+
const elem = res;
|
|
2098
2083
|
this.debug(`Screenshot of ${(new Locator(locator))} element has been saved to ${outputFile}`);
|
|
2099
2084
|
return elem.screenshot({ path: outputFile, type: 'png' });
|
|
2100
2085
|
}
|
|
@@ -2520,16 +2505,26 @@ class Playwright extends Helper {
|
|
|
2520
2505
|
}
|
|
2521
2506
|
return;
|
|
2522
2507
|
}
|
|
2508
|
+
let contentFrame;
|
|
2509
|
+
|
|
2523
2510
|
if (!locator) {
|
|
2524
|
-
this.context = this.page;
|
|
2511
|
+
this.context = await this.page.frames()[0];
|
|
2525
2512
|
this.contextLocator = null;
|
|
2526
2513
|
return;
|
|
2527
2514
|
}
|
|
2528
2515
|
|
|
2529
2516
|
// iframe by selector
|
|
2530
2517
|
const els = await this._locate(locator);
|
|
2531
|
-
assertElementExists(els, locator);
|
|
2532
|
-
|
|
2518
|
+
// assertElementExists(els, locator);
|
|
2519
|
+
|
|
2520
|
+
// get content of the first iframe
|
|
2521
|
+
if ((locator.frame && locator.frame === 'iframe') || locator.toLowerCase() === 'iframe') {
|
|
2522
|
+
contentFrame = await this.page.frames()[1];
|
|
2523
|
+
// get content of the iframe using its name
|
|
2524
|
+
} else if (locator.toLowerCase().includes('name=')) {
|
|
2525
|
+
const frameName = locator.split('=')[1].replace(/"/g, '').replaceAll(/]/g, '');
|
|
2526
|
+
contentFrame = await this.page.frame(frameName);
|
|
2527
|
+
}
|
|
2533
2528
|
|
|
2534
2529
|
if (contentFrame) {
|
|
2535
2530
|
this.context = contentFrame;
|
|
@@ -2618,9 +2613,9 @@ class Playwright extends Helper {
|
|
|
2618
2613
|
* {{> grabElementBoundingRect }}
|
|
2619
2614
|
*/
|
|
2620
2615
|
async grabElementBoundingRect(locator, prop) {
|
|
2621
|
-
const
|
|
2622
|
-
assertElementExists(
|
|
2623
|
-
const rect = await
|
|
2616
|
+
const el = await this._locateElement(locator);
|
|
2617
|
+
assertElementExists(el, locator);
|
|
2618
|
+
const rect = await el.boundingBox();
|
|
2624
2619
|
if (prop) return rect[prop];
|
|
2625
2620
|
return rect;
|
|
2626
2621
|
}
|
|
@@ -2655,6 +2650,330 @@ class Playwright extends Helper {
|
|
|
2655
2650
|
async stopMockingRoute(url, handler) {
|
|
2656
2651
|
return this.browserContext.unroute(...arguments);
|
|
2657
2652
|
}
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Starts recording of network traffic.
|
|
2656
|
+
* This also resets recorded network requests.
|
|
2657
|
+
*
|
|
2658
|
+
* ```js
|
|
2659
|
+
* I.startRecordingTraffic();
|
|
2660
|
+
* ```
|
|
2661
|
+
*
|
|
2662
|
+
* @return {Promise<void>}
|
|
2663
|
+
*/
|
|
2664
|
+
async startRecordingTraffic() {
|
|
2665
|
+
this.flushNetworkTraffics();
|
|
2666
|
+
this.recording = true;
|
|
2667
|
+
this.recordedAtLeastOnce = true;
|
|
2668
|
+
|
|
2669
|
+
this.page.on('requestfinished', async (request) => {
|
|
2670
|
+
const information = {
|
|
2671
|
+
url: request.url(),
|
|
2672
|
+
method: request.method(),
|
|
2673
|
+
requestHeaders: request.headers(),
|
|
2674
|
+
requestPostData: request.postData(),
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2678
|
+
|
|
2679
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2680
|
+
this.requests.push(information);
|
|
2681
|
+
return this._waitForAction();
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/**
|
|
2686
|
+
* Grab the recording network traffics
|
|
2687
|
+
*
|
|
2688
|
+
* @return { Array<any> }
|
|
2689
|
+
*
|
|
2690
|
+
*/
|
|
2691
|
+
grabRecordedNetworkTraffics() {
|
|
2692
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2693
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2694
|
+
}
|
|
2695
|
+
return this.requests;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
/**
|
|
2699
|
+
* Blocks traffic for URL.
|
|
2700
|
+
*
|
|
2701
|
+
* Examples:
|
|
2702
|
+
*
|
|
2703
|
+
* ```js
|
|
2704
|
+
* I.blockTraffic('http://example.com/css/style.css');
|
|
2705
|
+
* I.blockTraffic('http://example.com/css/*.css');
|
|
2706
|
+
* I.blockTraffic('http://example.com/**');
|
|
2707
|
+
* I.blockTraffic(/\.css$/);
|
|
2708
|
+
* ```
|
|
2709
|
+
*
|
|
2710
|
+
* @param url URL to block . URL can contain * for wildcards. Example: https://www.example.com** to block all traffic for that domain. Regexp are also supported.
|
|
2711
|
+
*/
|
|
2712
|
+
async blockTraffic(url) {
|
|
2713
|
+
this.page.route(url, (route) => {
|
|
2714
|
+
route
|
|
2715
|
+
.abort()
|
|
2716
|
+
// Sometimes it happens that browser has been closed in the meantime. It is ok to ignore error then.
|
|
2717
|
+
.catch((e) => {});
|
|
2718
|
+
});
|
|
2719
|
+
return this._waitForAction();
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* Mocks traffic for URL(s).
|
|
2724
|
+
* This is a powerful feature to manipulate network traffic. Can be used e.g. to stabilize your tests, speed up your tests or as a last resort to make some test scenarios even possible.
|
|
2725
|
+
*
|
|
2726
|
+
* Examples:
|
|
2727
|
+
*
|
|
2728
|
+
* ```js
|
|
2729
|
+
* I.mockTraffic('/api/users/1', '{ id: 1, name: 'John Doe' }');
|
|
2730
|
+
* I.mockTraffic('/api/users/*', JSON.stringify({ id: 1, name: 'John Doe' }));
|
|
2731
|
+
* I.mockTraffic([/^https://api.example.com/v1/, 'https://api.example.com/v2/**'], 'Internal Server Error', 'text/html');
|
|
2732
|
+
* ```
|
|
2733
|
+
*
|
|
2734
|
+
* @param urls string|Array These are the URL(s) to mock, e.g. "/fooapi/*" or "['/fooapi_1/*', '/barapi_2/*']". Regular expressions are also supported.
|
|
2735
|
+
* @param responseString string The string to return in fake response's body.
|
|
2736
|
+
* @param contentType Content type of fake response. If not specified default value 'application/json' is used.
|
|
2737
|
+
*/
|
|
2738
|
+
async mockTraffic(urls, responseString, contentType = 'application/json') {
|
|
2739
|
+
// Required to mock cross-domain requests
|
|
2740
|
+
const headers = { 'access-control-allow-origin': '*' };
|
|
2741
|
+
|
|
2742
|
+
if (typeof urls === 'string') {
|
|
2743
|
+
urls = [urls];
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
urls.forEach((url) => {
|
|
2747
|
+
this.page.route(url, (route) => {
|
|
2748
|
+
if (this.page.isClosed()) {
|
|
2749
|
+
// Sometimes it happens that browser has been closed in the meantime.
|
|
2750
|
+
// In this case we just don't fulfill to prevent error in test scenario.
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
route.fulfill({
|
|
2754
|
+
contentType,
|
|
2755
|
+
headers,
|
|
2756
|
+
body: responseString,
|
|
2757
|
+
});
|
|
2758
|
+
});
|
|
2759
|
+
});
|
|
2760
|
+
return this._waitForAction();
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* Resets all recorded network requests.
|
|
2765
|
+
*/
|
|
2766
|
+
flushNetworkTraffics() {
|
|
2767
|
+
this.requests = [];
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/**
|
|
2771
|
+
* Stops recording of network traffic. Recorded traffic is not flashed.
|
|
2772
|
+
*
|
|
2773
|
+
* ```js
|
|
2774
|
+
* I.stopRecordingTraffic();
|
|
2775
|
+
* ```
|
|
2776
|
+
*/
|
|
2777
|
+
stopRecordingTraffic() {
|
|
2778
|
+
this.page.removeAllListeners('request');
|
|
2779
|
+
this.recording = false;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
* Verifies that a certain request is part of network traffic.
|
|
2784
|
+
*
|
|
2785
|
+
* ```js
|
|
2786
|
+
* // checking the request url contains certain query strings
|
|
2787
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
2788
|
+
* I.startRecordingTraffic();
|
|
2789
|
+
* await I.seeTraffic({
|
|
2790
|
+
* name: 'sentry event',
|
|
2791
|
+
* url: 'https://images.openai.com/blob/cf717bdb-0c8c-428a-b82b-3c3add87a600',
|
|
2792
|
+
* parameters: {
|
|
2793
|
+
* width: '1919',
|
|
2794
|
+
* height: '1138',
|
|
2795
|
+
* },
|
|
2796
|
+
* });
|
|
2797
|
+
* ```
|
|
2798
|
+
*
|
|
2799
|
+
* ```js
|
|
2800
|
+
* // checking the request url contains certain post data
|
|
2801
|
+
* I.amOnPage('https://openai.com/blog/chatgpt');
|
|
2802
|
+
* I.startRecordingTraffic();
|
|
2803
|
+
* await I.seeTraffic({
|
|
2804
|
+
* name: 'event',
|
|
2805
|
+
* url: 'https://cloudflareinsights.com/cdn-cgi/rum',
|
|
2806
|
+
* requestPostData: {
|
|
2807
|
+
* st: 2,
|
|
2808
|
+
* },
|
|
2809
|
+
* });
|
|
2810
|
+
* ```
|
|
2811
|
+
*
|
|
2812
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
2813
|
+
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
2814
|
+
* @param {string} opts.url Expected URL of request in network traffic
|
|
2815
|
+
* @param {Object} [opts.parameters] Expected parameters of that request in network traffic
|
|
2816
|
+
* @param {Object} [opts.requestPostData] Expected that request contains post data in network traffic
|
|
2817
|
+
* @param {number} [opts.timeout] Timeout to wait for request in seconds. Default is 10 seconds.
|
|
2818
|
+
* @return { Promise<*> }
|
|
2819
|
+
*/
|
|
2820
|
+
async seeTraffic({
|
|
2821
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
2822
|
+
}) {
|
|
2823
|
+
if (!name) {
|
|
2824
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
if (!url) {
|
|
2828
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2832
|
+
throw new Error('Failure in test automation. You use "I.seeInTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
2836
|
+
const found = this._isInTraffic(url, parameters);
|
|
2837
|
+
if (found) {
|
|
2838
|
+
return true;
|
|
2839
|
+
}
|
|
2840
|
+
await new Promise((done) => setTimeout(done, 1000));
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// check request post data
|
|
2844
|
+
if (requestPostData && this._isInTraffic(url)) {
|
|
2845
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
2846
|
+
|
|
2847
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
2848
|
+
} else if (parameters && this._isInTraffic(url)) {
|
|
2849
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
2850
|
+
|
|
2851
|
+
assert.fail(
|
|
2852
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
2853
|
+
+ `${advancedTestResults}`,
|
|
2854
|
+
);
|
|
2855
|
+
} else {
|
|
2856
|
+
assert.fail(
|
|
2857
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
2858
|
+
+ `Expected url: ${url}.\n`
|
|
2859
|
+
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
/**
|
|
2865
|
+
* Returns full URL of request matching parameter "urlMatch".
|
|
2866
|
+
*
|
|
2867
|
+
* @param {string|RegExp} urlMatch Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
2868
|
+
*
|
|
2869
|
+
* Examples:
|
|
2870
|
+
*
|
|
2871
|
+
* ```js
|
|
2872
|
+
* I.grabTrafficUrl('https://api.example.com/session');
|
|
2873
|
+
* I.grabTrafficUrl(/session.*start/);
|
|
2874
|
+
* ```
|
|
2875
|
+
*
|
|
2876
|
+
* @return {Promise<*>}
|
|
2877
|
+
*/
|
|
2878
|
+
grabTrafficUrl(urlMatch) {
|
|
2879
|
+
if (!this.recordedAtLeastOnce) {
|
|
2880
|
+
throw new Error('Failure in test automation. You use "I.grabTrafficUrl", but "I.startRecordingTraffic" was never called before.');
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
for (const i in this.requests) {
|
|
2884
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
2885
|
+
if (this.requests.hasOwnProperty(i)) {
|
|
2886
|
+
const request = this.requests[i];
|
|
2887
|
+
|
|
2888
|
+
if (request.url && request.url.match(new RegExp(urlMatch))) {
|
|
2889
|
+
return request.url;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
assert.fail(`Method "getTrafficUrl" failed: No request found in traffic that matches ${urlMatch}`);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
/**
|
|
2898
|
+
* Verifies that a certain request is not part of network traffic.
|
|
2899
|
+
*
|
|
2900
|
+
* Examples:
|
|
2901
|
+
*
|
|
2902
|
+
* ```js
|
|
2903
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call', url: 'https://api.example.com' });
|
|
2904
|
+
* I.dontSeeTraffic({ name: 'Unexpected API Call of "user" endpoint', url: /api.example.com.*user/ });
|
|
2905
|
+
* ```
|
|
2906
|
+
*
|
|
2907
|
+
* @param {Object} opts - options when checking the traffic network.
|
|
2908
|
+
* @param {string} opts.name A name of that request. Can be any value. Only relevant to have a more meaningful error message in case of fail.
|
|
2909
|
+
* @param {string|RegExp} opts.url Expected URL of request in network traffic. Can be a string or a regular expression.
|
|
2910
|
+
*
|
|
2911
|
+
*/
|
|
2912
|
+
dontSeeTraffic({ name, url }) {
|
|
2913
|
+
if (!this.recordedAtLeastOnce) {
|
|
2914
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
if (!name) {
|
|
2918
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
if (!url) {
|
|
2922
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
if (this._isInTraffic(url)) {
|
|
2926
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
/**
|
|
2931
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
2932
|
+
*
|
|
2933
|
+
* @param url URL to look for.
|
|
2934
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
2935
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
2936
|
+
* @private
|
|
2937
|
+
*/
|
|
2938
|
+
_isInTraffic(url, parameters) {
|
|
2939
|
+
let isInTraffic = false;
|
|
2940
|
+
this.requests.forEach((request) => {
|
|
2941
|
+
if (isInTraffic) {
|
|
2942
|
+
return; // We already found traffic. Continue with next request
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
if (!request.url.match(new RegExp(url))) {
|
|
2946
|
+
return; // url not found in this request. continue with next request
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// URL has matched. Now we check the parameters
|
|
2950
|
+
|
|
2951
|
+
if (parameters) {
|
|
2952
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
2953
|
+
if (advancedReport === true) {
|
|
2954
|
+
isInTraffic = true;
|
|
2955
|
+
}
|
|
2956
|
+
} else {
|
|
2957
|
+
isInTraffic = true;
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
return isInTraffic;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
2966
|
+
*
|
|
2967
|
+
* @return {string} List of URLs recorded as a string, seperaeted by new lines after each URL
|
|
2968
|
+
* @private
|
|
2969
|
+
*/
|
|
2970
|
+
_getTrafficDump() {
|
|
2971
|
+
let dumpedTraffic = '';
|
|
2972
|
+
this.requests.forEach((request) => {
|
|
2973
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
2974
|
+
});
|
|
2975
|
+
return dumpedTraffic;
|
|
2976
|
+
}
|
|
2658
2977
|
}
|
|
2659
2978
|
|
|
2660
2979
|
module.exports = Playwright;
|
|
@@ -2667,42 +2986,19 @@ function buildLocatorString(locator) {
|
|
|
2667
2986
|
}
|
|
2668
2987
|
return locator.simplify();
|
|
2669
2988
|
}
|
|
2670
|
-
// TODO: locator change required after #3677 implementation. Temporary solution before migration. Should be deleted after #3677 implementation
|
|
2671
|
-
async function getXPathForElement(elementHandle) {
|
|
2672
|
-
function calculateIndex(node) {
|
|
2673
|
-
let index = 1;
|
|
2674
|
-
let sibling = node.previousElementSibling;
|
|
2675
|
-
while (sibling) {
|
|
2676
|
-
if (sibling.tagName === node.tagName) {
|
|
2677
|
-
index++;
|
|
2678
|
-
}
|
|
2679
|
-
sibling = sibling.previousElementSibling;
|
|
2680
|
-
}
|
|
2681
|
-
return index;
|
|
2682
|
-
}
|
|
2683
2989
|
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
if (node.hasAttribute('id')) {
|
|
2688
|
-
segments.unshift(`*[@id="${node.getAttribute('id')}"]`);
|
|
2689
|
-
break;
|
|
2690
|
-
} else {
|
|
2691
|
-
const index = calculateIndex(node);
|
|
2692
|
-
segments.unshift(`${node.localName}[${index}]`);
|
|
2693
|
-
node = node.parentNode;
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
return `//${segments.join('/')}`;
|
|
2697
|
-
}
|
|
2990
|
+
async function findElements(matcher, locator) {
|
|
2991
|
+
if (locator.react) return findReact(matcher, locator);
|
|
2992
|
+
locator = new Locator(locator, 'css');
|
|
2698
2993
|
|
|
2699
|
-
return
|
|
2994
|
+
return matcher.locator(buildLocatorString(locator)).all();
|
|
2700
2995
|
}
|
|
2701
2996
|
|
|
2702
|
-
async function
|
|
2997
|
+
async function findElement(matcher, locator) {
|
|
2703
2998
|
if (locator.react) return findReact(matcher, locator);
|
|
2704
2999
|
locator = new Locator(locator, 'css');
|
|
2705
|
-
|
|
3000
|
+
|
|
3001
|
+
return matcher.locator(buildLocatorString(locator));
|
|
2706
3002
|
}
|
|
2707
3003
|
|
|
2708
3004
|
async function getVisibleElements(elements) {
|
|
@@ -2732,7 +3028,6 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
2732
3028
|
assertElementExists(els, locator, 'Clickable element');
|
|
2733
3029
|
}
|
|
2734
3030
|
|
|
2735
|
-
const element = els[0];
|
|
2736
3031
|
highlightActiveElement.call(this, els[0], this.page);
|
|
2737
3032
|
|
|
2738
3033
|
/*
|
|
@@ -2781,22 +3076,22 @@ async function findClickable(matcher, locator) {
|
|
|
2781
3076
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
2782
3077
|
let description;
|
|
2783
3078
|
let allText;
|
|
3079
|
+
|
|
2784
3080
|
if (!context) {
|
|
2785
3081
|
let el = await this.context;
|
|
2786
|
-
|
|
2787
3082
|
if (el && !el.getProperty) {
|
|
2788
3083
|
// Fallback to body
|
|
2789
|
-
el = await this.
|
|
3084
|
+
el = await this.page.$('body');
|
|
2790
3085
|
}
|
|
2791
3086
|
|
|
2792
|
-
allText = [await el.
|
|
3087
|
+
allText = [await el.innerText()];
|
|
2793
3088
|
description = 'web application';
|
|
2794
3089
|
} else {
|
|
2795
3090
|
const locator = new Locator(context, 'css');
|
|
2796
3091
|
description = `element ${locator.toString()}`;
|
|
2797
3092
|
const els = await this._locate(locator);
|
|
2798
3093
|
assertElementExists(els, locator.toString());
|
|
2799
|
-
allText = await Promise.all(els.map(el => el.
|
|
3094
|
+
allText = await Promise.all(els.map(el => el.innerText()));
|
|
2800
3095
|
}
|
|
2801
3096
|
|
|
2802
3097
|
if (strict) {
|
|
@@ -2864,15 +3159,15 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2864
3159
|
const els = await findFields.call(this, field);
|
|
2865
3160
|
assertElementExists(els, field, 'Field');
|
|
2866
3161
|
const el = els[0];
|
|
2867
|
-
const tag = await el.
|
|
2868
|
-
const fieldType = await el.
|
|
3162
|
+
const tag = await el.evaluate(e => e.tagName);
|
|
3163
|
+
const fieldType = await el.getAttribute('type');
|
|
2869
3164
|
|
|
2870
3165
|
const proceedMultiple = async (elements) => {
|
|
2871
3166
|
const fields = Array.isArray(elements) ? elements : [elements];
|
|
2872
3167
|
|
|
2873
3168
|
const elementValues = [];
|
|
2874
3169
|
for (const element of fields) {
|
|
2875
|
-
elementValues.push(await element.
|
|
3170
|
+
elementValues.push(await element.inputValue());
|
|
2876
3171
|
}
|
|
2877
3172
|
|
|
2878
3173
|
if (typeof value === 'boolean') {
|
|
@@ -2886,8 +3181,8 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2886
3181
|
};
|
|
2887
3182
|
|
|
2888
3183
|
if (tag === 'SELECT') {
|
|
2889
|
-
if (await el.
|
|
2890
|
-
const selectedOptions = await el
|
|
3184
|
+
if (await el.getAttribute('multiple')) {
|
|
3185
|
+
const selectedOptions = await el.all('option:checked');
|
|
2891
3186
|
if (!selectedOptions.length) return null;
|
|
2892
3187
|
|
|
2893
3188
|
const options = await filterFieldsByValue(selectedOptions, value, true);
|
|
@@ -2918,7 +3213,7 @@ async function proceedSeeInField(assertType, field, value) {
|
|
|
2918
3213
|
async function filterFieldsByValue(elements, value, onlySelected) {
|
|
2919
3214
|
const matches = [];
|
|
2920
3215
|
for (const element of elements) {
|
|
2921
|
-
const val = await element.
|
|
3216
|
+
const val = await element.getAttribute('value');
|
|
2922
3217
|
let isSelected = true;
|
|
2923
3218
|
if (onlySelected) {
|
|
2924
3219
|
isSelected = await elementSelected(element);
|
|
@@ -2942,12 +3237,12 @@ async function filterFieldsBySelectionState(elements, state) {
|
|
|
2942
3237
|
}
|
|
2943
3238
|
|
|
2944
3239
|
async function elementSelected(element) {
|
|
2945
|
-
const type = await element.
|
|
3240
|
+
const type = await element.getAttribute('type');
|
|
2946
3241
|
|
|
2947
3242
|
if (type === 'checkbox' || type === 'radio') {
|
|
2948
3243
|
return element.isChecked();
|
|
2949
3244
|
}
|
|
2950
|
-
return element.
|
|
3245
|
+
return element.getAttribute('selected');
|
|
2951
3246
|
}
|
|
2952
3247
|
|
|
2953
3248
|
function isFrameLocator(locator) {
|
|
@@ -3152,3 +3447,134 @@ function highlightActiveElement(element, context) {
|
|
|
3152
3447
|
|
|
3153
3448
|
highlightElement(element, context);
|
|
3154
3449
|
}
|
|
3450
|
+
|
|
3451
|
+
const createAdvancedTestResults = (url, dataToCheck, requests) => {
|
|
3452
|
+
// Creates advanced test results for a network traffic check.
|
|
3453
|
+
// Advanced test results only applies when expected parameters are set
|
|
3454
|
+
if (!dataToCheck) return '';
|
|
3455
|
+
|
|
3456
|
+
let urlFound = false;
|
|
3457
|
+
let advancedResults;
|
|
3458
|
+
requests.forEach((request) => {
|
|
3459
|
+
// url not found in this request. continue with next request
|
|
3460
|
+
if (urlFound || !request.url.match(new RegExp(url))) return;
|
|
3461
|
+
urlFound = true;
|
|
3462
|
+
|
|
3463
|
+
// Url found. Now we create advanced test report for that URL and show which parameters failed
|
|
3464
|
+
if (!request.requestPostData) {
|
|
3465
|
+
advancedResults = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), dataToCheck);
|
|
3466
|
+
} else if (request.requestPostData) {
|
|
3467
|
+
advancedResults = allRequestPostDataValuePairsMatchExtreme(request.requestPostData, dataToCheck);
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
return advancedResults;
|
|
3471
|
+
};
|
|
3472
|
+
|
|
3473
|
+
const extractQueryObjects = (queryString) => {
|
|
3474
|
+
// Converts a string of GET parameters into an array of parameter objects. Each parameter object contains the properties "name" and "value".
|
|
3475
|
+
if (queryString.indexOf('?') === -1) {
|
|
3476
|
+
return [];
|
|
3477
|
+
}
|
|
3478
|
+
const queryObjects = [];
|
|
3479
|
+
|
|
3480
|
+
const queryPart = queryString.split('?')[1];
|
|
3481
|
+
|
|
3482
|
+
const queryParameters = queryPart.split('&');
|
|
3483
|
+
|
|
3484
|
+
queryParameters.forEach((queryParameter) => {
|
|
3485
|
+
const keyValue = queryParameter.split('=');
|
|
3486
|
+
const queryObject = {};
|
|
3487
|
+
// eslint-disable-next-line prefer-destructuring
|
|
3488
|
+
queryObject.name = keyValue[0];
|
|
3489
|
+
queryObject.value = decodeURIComponent(keyValue[1]);
|
|
3490
|
+
queryObjects.push(queryObject);
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
return queryObjects;
|
|
3494
|
+
};
|
|
3495
|
+
|
|
3496
|
+
const allParameterValuePairsMatchExtreme = (queryStringObject, advancedExpectedParameterValuePairs) => {
|
|
3497
|
+
// More advanced check if all request parameters match with the expectations
|
|
3498
|
+
let littleReport = '\nQuery parameters:\n';
|
|
3499
|
+
let success = true;
|
|
3500
|
+
|
|
3501
|
+
for (const expectedKey in advancedExpectedParameterValuePairs) {
|
|
3502
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedParameterValuePairs, expectedKey)) {
|
|
3503
|
+
continue;
|
|
3504
|
+
}
|
|
3505
|
+
let parameterFound = false;
|
|
3506
|
+
const expectedValue = advancedExpectedParameterValuePairs[expectedKey];
|
|
3507
|
+
|
|
3508
|
+
for (const queryParameter of queryStringObject) {
|
|
3509
|
+
if (queryParameter.name === expectedKey) {
|
|
3510
|
+
parameterFound = true;
|
|
3511
|
+
if (expectedValue === undefined) {
|
|
3512
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
3513
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
3514
|
+
const decodedActualValue = Buffer.from(queryParameter.value, 'base64').toString('utf8');
|
|
3515
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
3516
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
3517
|
+
} else {
|
|
3518
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
3519
|
+
success = false;
|
|
3520
|
+
}
|
|
3521
|
+
} else if (queryParameter.value === expectedValue) {
|
|
3522
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
3523
|
+
} else {
|
|
3524
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${queryParameter.value}"\n`;
|
|
3525
|
+
success = false;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
if (parameterFound === false) {
|
|
3531
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> parameter not found in request\n`;
|
|
3532
|
+
success = false;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
return success ? true : littleReport;
|
|
3537
|
+
};
|
|
3538
|
+
|
|
3539
|
+
const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advancedExpectedRequestPostValuePairs) => {
|
|
3540
|
+
// More advanced check if all request post data match with the expectations
|
|
3541
|
+
let littleReport = '\nRequest Post Data:\n';
|
|
3542
|
+
let success = true;
|
|
3543
|
+
|
|
3544
|
+
for (const expectedKey in advancedExpectedRequestPostValuePairs) {
|
|
3545
|
+
if (!Object.prototype.hasOwnProperty.call(advancedExpectedRequestPostValuePairs, expectedKey)) {
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
let keyFound = false;
|
|
3549
|
+
const expectedValue = advancedExpectedRequestPostValuePairs[expectedKey];
|
|
3550
|
+
|
|
3551
|
+
for (const [key, value] of Object.entries(RequestPostDataObject)) {
|
|
3552
|
+
if (key === expectedKey) {
|
|
3553
|
+
keyFound = true;
|
|
3554
|
+
if (expectedValue === undefined) {
|
|
3555
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')}\n`;
|
|
3556
|
+
} else if (typeof expectedValue === 'object' && expectedValue.base64) {
|
|
3557
|
+
const decodedActualValue = Buffer.from(value, 'base64').toString('utf8');
|
|
3558
|
+
if (decodedActualValue === expectedValue.base64) {
|
|
3559
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64})\n`;
|
|
3560
|
+
} else {
|
|
3561
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = base64(${expectedValue.base64}) -> actual value: "base64(${decodedActualValue})"\n`;
|
|
3562
|
+
success = false;
|
|
3563
|
+
}
|
|
3564
|
+
} else if (value === expectedValue) {
|
|
3565
|
+
littleReport += ` ${expectedKey.padStart(10, ' ')} = ${expectedValue}\n`;
|
|
3566
|
+
} else {
|
|
3567
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')} = ${expectedValue} -> actual value: "${value}"\n`;
|
|
3568
|
+
success = false;
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
if (keyFound === false) {
|
|
3574
|
+
littleReport += ` ✖ ${expectedKey.padStart(10, ' ')}${expectedValue ? ` = ${JSON.stringify(expectedValue)}` : ''} -> key not found in request\n`;
|
|
3575
|
+
success = false;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
return success ? true : littleReport;
|
|
3580
|
+
};
|