Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Track and allow querying in-flight requests #186

Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e53e296
test(pending): add tests for pending requests
tehhowch Nov 2, 2021
4d5a49b
chore(types): update shape of `InterceptedRequest`
tehhowch Nov 4, 2021
cb6c4d2
feat(types): update type signatures for `getRequest`, `getRequests`
tehhowch Nov 4, 2021
e5c8158
feat: add `hasPendingRequests` test method
tehhowch Nov 15, 2021
06bec12
refactor(types): switch from property declaration to function declara…
tehhowch Nov 17, 2021
d543255
refactor(sessionStorage): add helper method for support checking, ext…
tehhowch Nov 19, 2021
2ac7125
feat(getRequest): attach pending property to incomplete retrieved req…
tehhowch Nov 19, 2021
46ff17e
fix(tests): add missed awaits to `assert.rejects` usage
tehhowch Nov 19, 2021
0eb905a
feat(fetch-api): add basic support for pending request queries
tehhowch Nov 22, 2021
37ff5c8
feat(xhr-api): add basic support for pending request queries
tehhowch Nov 22, 2021
d39a8cb
refactor(xhr-api): simplify parsePayload method
tehhowch Dec 3, 2021
5a7222a
feat(logs): prefix any emitted errors with `[wdio-intercept-service]`
tehhowch Dec 3, 2021
e8971c5
test(getRequest): add test for includePending: false (default case)
tehhowch Dec 3, 2021
87d3d01
feat(getRequest): wire up the `includePending` request option
tehhowch Dec 3, 2021
0399024
test(request-order): add tests for the order of retrieved requests
tehhowch Dec 3, 2021
98b87b5
feat(request-order): wire up `orderBy` parameter in interceptor lib
tehhowch Dec 4, 2021
7cefc9d
refactor: Avoid fetching full request list from browser to check if a…
tehhowch Dec 4, 2021
5074b90
feat(assert-api): allow passing `orderBy` options to `assertRequests`…
tehhowch Dec 4, 2021
d23bfab
test(firefox): bump slow delay to 1000 ms to account for slower FF dr…
tehhowch Dec 4, 2021
caabd77
docs: update README with latest API changes
tehhowch Dec 4, 2021
c3bfb5e
docs: update comment, formatting
tehhowch Dec 5, 2021
ded6f2b
refactor: add package prefix constant for logs, errors
tehhowch Dec 6, 2021
36ca0dc
docs: Update README to better describe configuration options for seve…
tehhowch Dec 6, 2021
03874f5
docs: update changelog with release summaries
tehhowch Dec 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
errorShots
node_modules
test/logs
test/**/logs
test/**/dist
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ There's one catch though: you can't intercept HTTP calls that are initiated on p

## Prerequisites

* webdriver.io **v5.x**.
* webdriver.io **v5.x** or newer.

**Heads up! If you're still using webdriver.io v4, please use the v2.x branch of this plugin!**

Expand Down Expand Up @@ -132,33 +132,37 @@ Helper method. Returns all the expectations you've made up until that point

Helper method. Resets all the expectations you've made up until that point

### browser.assertRequests()
### browser.assertRequests({ orderBy?: 'START' | 'END' })

Call this method when all expected ajax requests are finished. It compares the expectations to the actual requests made and asserts the following:

- Count of the requests that were made
- The order of the requests
- The order of the requests (defaults to `orderBy: 'END'`, i.e. when the requests were completed)
- The method, the URL and the statusCode should match for every request made

### browser.assertExpectedRequestsOnly(inOrder?: boolean)
### browser.assertExpectedRequestsOnly({ inOrder?: boolean, orderBy?: 'START' | 'END' })

Similar to `browser.assertRequests`, but validates only the requests you specify in your `expectRequest` directives, without having to map out all the network requests that might happen around that. If `inOrder` equals to `true` (default), the requests are expected to be made in the same order as they were setup with `expectRequest`.
Similar to `browser.assertRequests`, but validates only the requests you specify in your `expectRequest` directives, without having to map out all the network requests that might happen around that. If `inOrder` option is `true` (default), the requests are expected to be found in the same order as they were setup with `expectRequest`.

### browser.getRequest(index: number)
### browser.getRequest(index: number, { includePending?: boolean, orderBy?: 'START' | 'END' })

To make more sophisticated assertions about a specific request you can get details for a specific request after it is finished. You have to provide the index of the request you want to access in the order the requests were initiated (starting with 0).
To make more sophisticated assertions about a specific request you can get details for a specific request. You have to provide the 0-based index of the request you want to access, in the order the requests were completed (default), or initiated (by passing the `orderBy: 'START'` option).

* `index` (`Number`): number of the request you want to access
* `options` (`object`): Request options
* `options.includePending` (`boolean`): Whether not-yet-completed requests should be returned. By default, this is false, to match the behavior of the library in v4.1.10 and earlier.
* `options.orderBy` (`'START' | 'END'`): How the requests should be ordered. By default, this is `'END'`, to match the behavior of the library in v4.1.10 and earlier. If `'START'`, the requests will be ordered by the time of initiation, rather than the time of request completion. (Since a pending request has not yet completed, when ordering by `'END'` all pending requests will come after all completed requests.)

**Returns** `request` object:

* `request.url`: requested URL
* `request.method`: used HTTP method
* `request.body`: payload/body data used in request
* `request.headers`: request http headers as JS object
* `request.response.headers`: response http headers as JS object
* `request.response.body`: response body (will be parsed as JSON if possible)
* `request.response.statusCode`: response status code
* `request.pending`: boolean flag for whether this request is complete (i.e. has a `response` property), or in-flight.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* `request.response?.headers`: response http headers as JS object
* `request.response?.body`: response body (will be parsed as JSON if possible)
* `request.response?.statusCode`: response status code

**A note on `request.body`:** wdio-intercept-service will try to parse the request body as follows:

Expand All @@ -170,12 +174,18 @@ To make more sophisticated assertions about a specific request you can get detai

**For the `fetch` API, we only support string and JSON data!**

### browser.getRequests()
### browser.getRequests(options)
tehhowch marked this conversation as resolved.
Show resolved Hide resolved

Get all captured requests as an array.
Get all captured requests as an array, supporting the same options as `getRequest`.

**Returns** array of `request` objects.


### browser.hasPendingRequests()

A utility method that checks whether any HTTP requests are still pending. Can be used by tests to ensure all requests have completed within a reasonable amount of time, or to verify that a call to `getRequests()` or `assertRequests()` will include all of the desired HTTP requests.

**Returns** boolean
## TypeScript support

This plugin provides its own TS types. Just point your tsconfig to the type extensions like mentioned [here](https://webdriver.io/docs/typescript.html#framework-types):
Expand All @@ -189,7 +199,7 @@ This plugin provides its own TS types. Just point your tsconfig to the type exte

## Running the tests

A recent version of Chrome is required to run the tests locally. You may need to update the `chromedriver` dependency to match the version installed on your system.
Recent versions of Chrome and Firefox are required to run the tests locally. You may need to update the `chromedriver` and `geckodriver` dependencies to match the version installed on your system.

```shell
npm test
Expand Down
94 changes: 74 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

const interceptor = require('./lib/interceptor');

const issueDeprecation = (map, key, what) => {
if (!map[key]) {
console.warn(
`[wdio-intercept-service]: ${what} is deprecated and will no longer work in v5`
);
map[key] = true;
}
};

class WebdriverAjax {
constructor() {
this._wdajaxExpectations = null;
this._deprecations = {};
}

beforeTest() {
Expand Down Expand Up @@ -34,8 +44,9 @@ class WebdriverAjax {
'assertExpectedRequestsOnly',
assertExpectedRequestsOnly.bind(this)
);
browser.addCommand('hasPendingRequests', hasPendingRequests);
browser.addCommand('getRequest', getRequest);
browser.addCommand('getRequests', getRequest);
browser.addCommand('getRequests', getRequests);

function setup() {
return browser.executeAsync(interceptor.setup);
Expand All @@ -50,15 +61,22 @@ class WebdriverAjax {
return browser;
}

function assertRequests() {
function assertRequests(options = {}) {
const expectations = this._wdajaxExpectations;

if (!expectations.length) {
return Promise.reject(
new Error('No expectations found. Call .expectRequest() first')
);
}
return getRequest().then((requests) => {

// Don't let users request pending requests:
if (options.includePending) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice catch here, providing a less frustrating experience :)

throw new Error(
'[wdio-intercept-service]: passing `includePending` option to `assertRequests` is not supported!'
);
}
return getRequests(options).then((requests) => {
if (expectations.length !== requests.length) {
return Promise.reject(
new Error(
Expand Down Expand Up @@ -135,13 +153,33 @@ class WebdriverAjax {
});
}

function assertExpectedRequestsOnly(inOrder = true) {
function assertExpectedRequestsOnly(orderOrOptions) {
const expectations = this._wdajaxExpectations;
let inOrder = true;
let options = {};
if (typeof orderOrOptions === 'boolean') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I think I would be OK with changing the API (like in the readme) and bumping the major version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vastly prefer the approach of Ember.JS, where the framework's policy is that users should be able to upgrade to the next major versions without their app breaking so as long as their app runs the previous version without encountering deprecation warnings. That is, a semver major bump should never add functionality, it should only remove already-deprecated functionality.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah okay, I see your point. The downside here is that the actual API is not necessarily 100% consistent with the actual code but that might only cause confusion for people who actually work on the code and not for the ones using it. All in all I'd be fine with this.

issueDeprecation(
this._deprecations,
'inOrder',
'Calling `assertExpectedRequestsOnly` with a boolean parameter'
);
inOrder = orderOrOptions;
} else if (orderOrOptions && typeof orderOrOptions === 'object') {
options = orderOrOptions;
inOrder = 'inOrder' in orderOrOptions ? orderOrOptions.inOrder : true;
delete options.inOrder;
}

return getRequest().then((requests) => {
const clonedRequests = [...requests];
// Don't let users request pending requests:
if (options.includePending) {
throw new Error(
'[wdio-intercept-service]: passing `includePending` option to `assertExpectedRequestsOnly` is not supported!'
);
}
return getRequests(options).then((requests) => {
const clonedRequests = requests.slice();

let matchedRequestIndexes = [];
const matchedRequestIndexes = [];
for (let i = 0; i < expectations.length; i++) {
const ex = expectations[i];

Expand Down Expand Up @@ -192,7 +230,7 @@ class WebdriverAjax {
} else if (
inOrder &&
JSON.stringify(matchedRequestIndexes) !==
JSON.stringify(matchedRequestIndexes.concat().sort())
JSON.stringify(matchedRequestIndexes.slice().sort())
) {
return Promise.reject(
new Error('Requests not received in the expected order')
Expand All @@ -214,13 +252,16 @@ class WebdriverAjax {
return this._wdajaxExpectations;
}

async function getRequest(index) {
let request;
if (index > -1) {
request = await browser.execute(interceptor.getRequest, index);
} else {
request = await browser.execute(interceptor.getRequest);
}
function getRequests(options = {}) {
return getRequest(undefined, options);
}

async function getRequest(index, options = {}) {
const request = await browser.execute(
interceptor.getRequest,
index > -1 ? index : undefined,
options
);
if (!request) {
if (index != null) {
return Promise.reject(
Expand All @@ -239,22 +280,35 @@ class WebdriverAjax {
return transformRequest(request);
}

function hasPendingRequests() {
return browser.execute(interceptor.hasPending);
}

function transformRequest(req) {
if (!req) {
return;
}

return {
const transformed = {
url: req.url,
method: req.method && req.method.toUpperCase(),
body: parseBody(req.requestBody),
headers: normalizeRequestHeaders(req.requestHeaders),
response: {
body: parseBody(req.requestBody),
pending: true,
};
// Check for a '__fulfilled' property on the retrieved request, which is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

// set by the interceptor only when the response completes. Before this
// flag is set, the request is still being processed (e.g. a large response
// body is downloading) and therefore is pending.
if (req.__fulfilled) {
transformed.pending = false;
transformed.response = {
headers: parseResponseHeaders(req.headers),
body: parseBody(req.body),
statusCode: req.statusCode,
},
};
};
}
return transformed;
}

function normalizeRequestHeaders(headers) {
Expand Down
Loading