Skip to content

Commit 47a3af1

Browse files
committed
[js][bidi] Add request and response handler
1 parent 7b7ae95 commit 47a3af1

File tree

6 files changed

+433
-10
lines changed

6 files changed

+433
-10
lines changed

javascript/selenium-webdriver/bidi/network.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,26 @@ class Network {
154154

155155
this.ws = await this.bidi.socket
156156
this.ws.on('message', (event) => {
157-
const { params } = JSON.parse(Buffer.from(event.toString()))
157+
const { method, params } = JSON.parse(Buffer.from(event.toString()))
158158
if (params) {
159159
let response = null
160-
if ('initiator' in params) {
161-
response = new BeforeRequestSent(
160+
if ('request' in params && 'response' in params) {
161+
response = new ResponseStarted(
162162
params.context,
163163
params.navigation,
164164
params.redirectCount,
165165
params.request,
166166
params.timestamp,
167-
params.initiator,
167+
params.response,
168168
)
169-
} else if ('response' in params) {
170-
response = new ResponseStarted(
169+
} else if ('initiator' in params && !('response' in params)) {
170+
response = new BeforeRequestSent(
171171
params.context,
172172
params.navigation,
173173
params.redirectCount,
174174
params.request,
175175
params.timestamp,
176-
params.response,
176+
params.initiator,
177177
)
178178
} else if ('errorText' in params) {
179179
response = new FetchError(
@@ -185,7 +185,7 @@ class Network {
185185
params.errorText,
186186
)
187187
}
188-
this.invokeCallbacks(eventType, response)
188+
this.invokeCallbacks(method, response)
189189
}
190190
})
191191
return id

javascript/selenium-webdriver/bidi/networkTypes.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ class Header {
118118
get value() {
119119
return this._value
120120
}
121+
122+
/**
123+
* Converts the Header to a map.
124+
* @returns {Map<string, string>} A map representation of the Header.
125+
*/
126+
asMap() {
127+
const map = new Map()
128+
map.set('name', this._name)
129+
map.set('value', Object.fromEntries(this._value.asMap()))
130+
return map
131+
}
121132
}
122133

123134
/**

javascript/selenium-webdriver/lib/http.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,18 @@ class Request {
8888
* @param {string} method The HTTP method to use for the request.
8989
* @param {string} path The path on the server to send the request to.
9090
* @param {Object=} opt_data This request's non-serialized JSON payload data.
91+
* @param {Map<string, string>} [headers=new Map()] - The optional headers as a Map.
9192
*/
92-
constructor(method, path, opt_data) {
93+
constructor(method, path, opt_data, headers = new Map()) {
9394
this.method = /** string */ method
9495
this.path = /** string */ path
9596
this.data = /** Object */ opt_data
96-
this.headers = /** !Map<string, string> */ new Map([['Accept', 'application/json; charset=utf-8']])
97+
98+
if (headers.size > 0) {
99+
this.headers = headers
100+
} else {
101+
this.headers = /** !Map<string, string> */ new Map([['Accept', 'application/json; charset=utf-8']])
102+
}
97103
}
98104

99105
/** @override */

javascript/selenium-webdriver/lib/network.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818
const { Network: getNetwork } = require('../bidi/network')
1919
const { InterceptPhase } = require('../bidi/interceptPhase')
2020
const { AddInterceptParameters } = require('../bidi/addInterceptParameters')
21+
const { ContinueRequestParameters } = require('../bidi/continueRequestParameters')
22+
const { ProvideResponseParameters } = require('../bidi/provideResponseParameters')
23+
const { Request } = require('./http')
24+
const { BytesValue, Header } = require('../bidi/networkTypes')
2125

2226
class Network {
2327
#callbackId = 0
2428
#driver
2529
#network
2630
#authHandlers = new Map()
31+
#requestHandlers = new Map()
32+
#responseHandlers = new Map()
2733

2834
constructor(driver) {
2935
this.#driver = driver
@@ -43,6 +49,8 @@ class Network {
4349

4450
await this.#network.addIntercept(new AddInterceptParameters(InterceptPhase.AUTH_REQUIRED))
4551

52+
await this.#network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT))
53+
4654
await this.#network.authRequired(async (event) => {
4755
const requestId = event.request.request
4856
const uri = event.request.url
@@ -54,6 +62,76 @@ class Network {
5462

5563
await this.#network.continueWithAuthNoCredentials(requestId)
5664
})
65+
66+
await this.#network.beforeRequestSent(async (event) => {
67+
const requestId = event.request.request
68+
const requestData = event.request
69+
70+
// Build the original request from the intercepted request details.
71+
const originalRequest = new Request(requestData.method, requestData.url, null, new Map(requestData.headers))
72+
73+
let requestHandler = this.getRequestHandler(originalRequest)
74+
let responseHandler = this.getResponseHandler(originalRequest)
75+
76+
// Populate the headers of the original request.
77+
// Body is not available as part of WebDriver Spec, hence we cannot add that or use that.
78+
79+
const continueRequestParams = new ContinueRequestParameters(requestId)
80+
81+
// If a response handler exists, we mock the response instead of modifying the outgoing request
82+
if (responseHandler !== null) {
83+
const modifiedResponse = await responseHandler()
84+
85+
const provideResponseParams = new ProvideResponseParameters(requestId)
86+
provideResponseParams.statusCode(modifiedResponse.status)
87+
88+
// Convert headers
89+
if (modifiedResponse.headers.size > 0) {
90+
const headers = []
91+
92+
modifiedResponse.headers.forEach((value, key) => {
93+
headers.push(new Header(key, new BytesValue('string', value)))
94+
})
95+
provideResponseParams.headers(headers)
96+
}
97+
98+
// Convert body if available
99+
if (modifiedResponse.body && modifiedResponse.body.length > 0) {
100+
provideResponseParams.body(new BytesValue('string', modifiedResponse.body))
101+
}
102+
103+
await this.#network.provideResponse(provideResponseParams)
104+
return
105+
}
106+
107+
// If request handler exists, modify the request
108+
if (requestHandler !== null) {
109+
const modifiedRequest = requestHandler(originalRequest)
110+
111+
continueRequestParams.method(modifiedRequest.method)
112+
113+
if (originalRequest.path !== modifiedRequest.path) {
114+
continueRequestParams.url(modifiedRequest.path)
115+
}
116+
117+
// Convert headers
118+
if (modifiedRequest.headers.size > 0) {
119+
const headers = []
120+
121+
modifiedRequest.headers.forEach((value, key) => {
122+
headers.push(new Header(key, new BytesValue('string', value)))
123+
})
124+
continueRequestParams.headers(headers)
125+
}
126+
127+
if (modifiedRequest.data && modifiedRequest.data.length > 0) {
128+
continueRequestParams.body(new BytesValue('string', modifiedRequest.data))
129+
}
130+
}
131+
132+
// Continue with the modified or original request
133+
await this.#network.continueRequest(continueRequestParams)
134+
})
57135
}
58136

59137
getAuthCredentials(uri) {
@@ -64,6 +142,27 @@ class Network {
64142
}
65143
return null
66144
}
145+
146+
getRequestHandler(req) {
147+
for (let [, value] of this.#requestHandlers) {
148+
const filter = value.filter
149+
if (filter(req)) {
150+
return value.handler
151+
}
152+
}
153+
return null
154+
}
155+
156+
getResponseHandler(req) {
157+
for (let [, value] of this.#responseHandlers) {
158+
const filter = value.filter
159+
if (filter(req)) {
160+
return value.handler
161+
}
162+
}
163+
return null
164+
}
165+
67166
async addAuthenticationHandler(username, password, uri = '//') {
68167
await this.#init()
69168

@@ -86,6 +185,82 @@ class Network {
86185
async clearAuthenticationHandlers() {
87186
this.#authHandlers.clear()
88187
}
188+
189+
/**
190+
* Adds a request handler that filters requests based on a predicate function.
191+
* @param {Function} filter - A function that takes an HTTP request and returns true or false.
192+
* @param {Function} handler - A function that takes an HTTP request and returns a modified request.
193+
* @returns {number} - A unique handler ID.
194+
* @throws {Error} - If filter is not a function or handler does not return a request.
195+
*/
196+
async addRequestHandler(filter, handler) {
197+
if (typeof filter !== 'function') {
198+
throw new Error('Filter must be a function.')
199+
}
200+
201+
if (typeof handler !== 'function') {
202+
throw new Error('Handler must be a function.')
203+
}
204+
205+
await this.#init()
206+
207+
const id = this.#callbackId++
208+
209+
this.#requestHandlers.set(id, { filter, handler })
210+
return id
211+
}
212+
213+
async removeRequestHandler(id) {
214+
await this.#init()
215+
216+
if (this.#requestHandlers.has(id)) {
217+
this.#requestHandlers.delete(id)
218+
} else {
219+
throw Error(`Callback with id ${id} not found`)
220+
}
221+
}
222+
223+
async clearRequestHandlers() {
224+
this.#requestHandlers.clear()
225+
}
226+
227+
/**
228+
* Adds a response handler that mocks responses.
229+
* @param {Function} filter - A function that takes an HTTP request, returning a boolean.
230+
* @param {Function} handler - A function that returns a mocked HTTP response.
231+
* @returns {number} - A unique handler ID.
232+
* @throws {Error} - If filter is not a function or handler is not an async function.
233+
*/
234+
async addResponseHandler(filter, handler) {
235+
if (typeof filter !== 'function') {
236+
throw new Error('Filter must be a function.')
237+
}
238+
239+
if (typeof handler !== 'function') {
240+
throw new Error('Handler must be a function.')
241+
}
242+
243+
await this.#init()
244+
245+
const id = this.#callbackId++
246+
247+
this.#responseHandlers.set(id, { filter, handler })
248+
return id
249+
}
250+
251+
async removeResponseHandler(id) {
252+
await this.#init()
253+
254+
if (this.#responseHandlers.has(id)) {
255+
this.#responseHandlers.delete(id)
256+
} else {
257+
throw Error(`Callback with id ${id} not found`)
258+
}
259+
}
260+
261+
async clearResponseHandlers() {
262+
this.#responseHandlers.clear()
263+
}
89264
}
90265

91266
module.exports = Network

javascript/selenium-webdriver/lib/test/fileserver.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const Pages = (function () {
4545
})
4646
}
4747

48+
addPage('addRequestBody', 'addRequestBody')
4849
addPage('ajaxyPage', 'ajaxy_page.html')
4950
addPage('alertsPage', 'alerts.html')
5051
addPage('basicAuth', 'basicAuth')
@@ -131,6 +132,7 @@ const Path = {
131132
PAGE: WEB_ROOT + '/page',
132133
SLEEP: WEB_ROOT + '/sleep',
133134
UPLOAD: WEB_ROOT + '/upload',
135+
ADD_REQUEST_BODY: WEB_ROOT + '/addRequestBody',
134136
}
135137

136138
var app = express()
@@ -143,6 +145,7 @@ app
143145
})
144146
.use(JS_ROOT, serveIndex(jsDirectory), express.static(jsDirectory))
145147
.post(Path.UPLOAD, handleUpload)
148+
.post(Path.ADD_REQUEST_BODY, addRequestBody)
146149
.use(WEB_ROOT, serveIndex(baseDirectory), express.static(baseDirectory))
147150
.use(DATA_ROOT, serveIndex(dataDirectory), express.static(dataDirectory))
148151
.get(Path.ECHO, sendEcho)
@@ -187,6 +190,32 @@ function sendInifinitePage(request, response) {
187190
response.end(body)
188191
}
189192

193+
function addRequestBody(request, response) {
194+
let requestBody = ''
195+
196+
request.on('data', (chunk) => {
197+
requestBody += chunk
198+
})
199+
200+
request.on('end', () => {
201+
let body = [
202+
'<!DOCTYPE html>',
203+
'<html>',
204+
'<head><title>Page</title></head>',
205+
'<body>',
206+
`<p>Request Body:</p><pre>${requestBody}</pre>`,
207+
'</body>',
208+
'</html>',
209+
].join('')
210+
211+
response.writeHead(200, {
212+
'Content-Length': Buffer.byteLength(body, 'utf8'),
213+
'Content-Type': 'text/html; charset=utf-8',
214+
})
215+
response.end(body)
216+
})
217+
}
218+
190219
function sendBasicAuth(request, response) {
191220
const denyAccess = function () {
192221
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="test"' })

0 commit comments

Comments
 (0)