Skip to content

Commit 8cfe8a4

Browse files
authored
[js] Add script pinning (#11584)
1 parent 4e02ef4 commit 8cfe8a4

File tree

4 files changed

+281
-1
lines changed

4 files changed

+281
-1
lines changed

javascript/node/selenium-webdriver/devtools/CDPConnection.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
const RESPONSE_TIMEOUT = 1000 * 30
1819
class CDPConnection {
1920
constructor(wsConnection) {
2021
this._wsConnection = wsConnection
@@ -35,6 +36,42 @@ class CDPConnection {
3536
const mergedMessage = Object.assign({ params: params }, message)
3637
this._wsConnection.send(JSON.stringify(mergedMessage), callback)
3738
}
39+
40+
async send(method, params) {
41+
let cdp_id = this.cmd_id++
42+
let message = {
43+
method,
44+
id: cdp_id,
45+
}
46+
if (this.sessionId) {
47+
message['sessionId'] = this.sessionId
48+
}
49+
50+
const mergedMessage = Object.assign({ params: params }, message)
51+
this._wsConnection.send(JSON.stringify(mergedMessage))
52+
53+
return new Promise((resolve, reject) => {
54+
const timeoutId = setTimeout(() => {
55+
reject(new Error(`Request with id ${cdp_id} timed out`))
56+
handler.off('message', listener)
57+
}, RESPONSE_TIMEOUT)
58+
59+
const listener = (data) => {
60+
try {
61+
const payload = JSON.parse(data.toString())
62+
if (payload.id === cdp_id) {
63+
clearTimeout(timeoutId)
64+
handler.off('message', listener)
65+
resolve(payload)
66+
}
67+
} catch (err) {
68+
console.error(`Failed parse message: ${err.message}`)
69+
}
70+
}
71+
72+
const handler = this._wsConnection.on('message', listener)
73+
})
74+
}
3875
}
3976

4077
exports.CdpConnection = CDPConnection
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
const crypto = require('crypto')
19+
20+
class PinnedScript {
21+
constructor(script) {
22+
this.scriptSource_ = script
23+
this.scriptHandle_ = crypto.randomUUID().replace(/-/gi, '')
24+
}
25+
26+
get handle() {
27+
return this.scriptHandle_
28+
}
29+
30+
get source() {
31+
return this.scriptSource_
32+
}
33+
34+
get scriptId() {
35+
return this.scriptId_
36+
}
37+
38+
set scriptId(id) {
39+
this.scriptId_ = id
40+
}
41+
42+
creationScript() {
43+
return `function __webdriver_${this.scriptHandle_}(arguments) { ${this.scriptSource_} }`
44+
}
45+
46+
executionScript() {
47+
return `return __webdriver_${this.scriptHandle_}(arguments)`
48+
}
49+
50+
removalScript() {
51+
return `__webdriver_${this.scriptHandle_} = undefined`
52+
}
53+
}
54+
55+
// PUBLIC API
56+
57+
module.exports = {
58+
PinnedScript,
59+
}

javascript/node/selenium-webdriver/lib/webdriver.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const { Credential } = require('./virtual_authenticator')
4141
const webElement = require('./webelement')
4242
const { isObject } = require('./util')
4343
const BIDI = require('../bidi')
44+
const { PinnedScript } = require('./pinnedScript')
4445

4546
// Capability names that are defined in the W3C spec.
4647
const W3C_CAPABILITY_NAMES = new Set([
@@ -686,6 +687,8 @@ class WebDriver {
686687

687688
/** @private {./virtual_authenticator}*/
688689
this.authenticatorId_ = null
690+
691+
this.pinnedScripts_ = {}
689692
}
690693

691694
/**
@@ -795,6 +798,15 @@ class WebDriver {
795798
if (typeof script === 'function') {
796799
script = 'return (' + script + ').apply(null, arguments);'
797800
}
801+
802+
if (script && script instanceof PinnedScript) {
803+
return this.execute(
804+
new command.Command(command.Name.EXECUTE_SCRIPT)
805+
.setParameter('script', script.executionScript())
806+
.setParameter('args', args)
807+
)
808+
}
809+
798810
return this.execute(
799811
new command.Command(command.Name.EXECUTE_SCRIPT)
800812
.setParameter('script', script)
@@ -807,6 +819,15 @@ class WebDriver {
807819
if (typeof script === 'function') {
808820
script = 'return (' + script + ').apply(null, arguments);'
809821
}
822+
823+
if (script && script instanceof PinnedScript) {
824+
return this.execute(
825+
new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT)
826+
.setParameter('script', script.executionScript())
827+
.setParameter('args', args)
828+
)
829+
}
830+
810831
return this.execute(
811832
new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT)
812833
.setParameter('script', script)
@@ -1226,7 +1247,9 @@ class WebDriver {
12261247
this._wsUrl = await this.getWsUrl(debuggerUrl, target, caps)
12271248
return new Promise((resolve, reject) => {
12281249
try {
1229-
this._wsConnection = new WebSocket(this._wsUrl.replace('localhost', '127.0.0.1'))
1250+
this._wsConnection = new WebSocket(
1251+
this._wsUrl.replace('localhost', '127.0.0.1')
1252+
)
12301253
this._cdpConnection = new cdp.CdpConnection(this._wsConnection)
12311254
} catch (err) {
12321255
reject(err)
@@ -1532,6 +1555,74 @@ class WebDriver {
15321555
})
15331556
}
15341557

1558+
async pinScript(script) {
1559+
let pinnedScript = new PinnedScript(script)
1560+
let connection
1561+
if (Object.is(this._cdpConnection, undefined)) {
1562+
connection = await this.createCDPConnection('page')
1563+
} else {
1564+
connection = this._cdpConnection
1565+
}
1566+
1567+
await connection.execute('Page.enable', {}, null)
1568+
1569+
await connection.execute(
1570+
'Runtime.evaluate',
1571+
{
1572+
expression: pinnedScript.creationScript(),
1573+
},
1574+
null
1575+
)
1576+
1577+
let result = await connection.send(
1578+
'Page.addScriptToEvaluateOnNewDocument',
1579+
{
1580+
source: pinnedScript.creationScript(),
1581+
}
1582+
)
1583+
1584+
pinnedScript.scriptId = result['result']['identifier']
1585+
1586+
this.pinnedScripts_[pinnedScript.handle] = pinnedScript
1587+
1588+
return pinnedScript
1589+
}
1590+
1591+
async unpinScript(script) {
1592+
if (script && !(script instanceof PinnedScript)) {
1593+
throw Error(`Pass valid PinnedScript object. Received: ${script}`)
1594+
}
1595+
1596+
if (script.handle in this.pinnedScripts_) {
1597+
let connection
1598+
if (Object.is(this._cdpConnection, undefined)) {
1599+
connection = this.createCDPConnection('page')
1600+
} else {
1601+
connection = this._cdpConnection
1602+
}
1603+
1604+
await connection.execute('Page.enable', {}, null)
1605+
1606+
await connection.execute(
1607+
'Runtime.evaluate',
1608+
{
1609+
expression: script.removalScript(),
1610+
},
1611+
null
1612+
)
1613+
1614+
await connection.execute(
1615+
'Page.removeScriptToEvaluateOnLoad',
1616+
{
1617+
identifier: script.scriptId,
1618+
},
1619+
null
1620+
)
1621+
1622+
delete this.pinnedScripts_[script.handle]
1623+
}
1624+
}
1625+
15351626
/**
15361627
*
15371628
* @returns The value of authenticator ID added

javascript/node/selenium-webdriver/test/chrome/devtools_test.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const fs = require('fs')
2222
const path = require('path')
2323

2424
const chrome = require('../../chrome')
25+
const by = require('../../lib/by')
2526
const error = require('../../lib/error')
2627
const fileServer = require('../../lib/test/fileserver')
2728
const io = require('../../io')
@@ -205,6 +206,98 @@ test.suite(
205206
}
206207
})
207208
})
209+
210+
describe('Script pinning', function () {
211+
it('allows to pin script', async function () {
212+
await driver.get(fileServer.Pages.xhtmlTestPage)
213+
214+
let script = await driver.pinScript('return document.title;')
215+
216+
const result = await driver.executeScript(script)
217+
218+
assert.strictEqual(result, 'XHTML Test Page')
219+
})
220+
221+
it('ensures pinned script is available on new pages', async function () {
222+
await driver.get(fileServer.Pages.xhtmlTestPage)
223+
await driver.createCDPConnection('page')
224+
225+
let script = await driver.pinScript('return document.title;')
226+
await driver.get(fileServer.Pages.formPage)
227+
228+
const result = await driver.executeScript(script)
229+
230+
assert.strictEqual(result, 'We Leave From Here')
231+
})
232+
233+
it('allows to unpin script', async function () {
234+
let script = await driver.pinScript('return document.title;')
235+
await driver.unpinScript(script)
236+
237+
await assertJSError(() => driver.executeScript(script))
238+
239+
async function assertJSError(fn) {
240+
try {
241+
await fn()
242+
return Promise.reject(Error('should have failed'))
243+
} catch (err) {
244+
if (err instanceof error.JavascriptError) {
245+
return
246+
}
247+
throw err
248+
}
249+
}
250+
})
251+
252+
it('ensures unpinned scripts are not available on new pages', async function () {
253+
await driver.createCDPConnection('page')
254+
255+
let script = await driver.pinScript('return document.title;')
256+
await driver.unpinScript(script)
257+
258+
await driver.get(fileServer.Pages.formPage)
259+
260+
await assertJSError(() => driver.executeScript(script))
261+
262+
async function assertJSError(fn) {
263+
try {
264+
await fn()
265+
return Promise.reject(Error('should have failed'))
266+
} catch (err) {
267+
if (err instanceof error.JavascriptError) {
268+
return
269+
}
270+
throw err
271+
}
272+
}
273+
})
274+
275+
it('handles arguments in pinned script', async function () {
276+
await driver.get(fileServer.Pages.xhtmlTestPage)
277+
await driver.createCDPConnection('page')
278+
279+
let script = await driver.pinScript('return arguments;')
280+
let element = await driver.findElement(by.By.id('id1'))
281+
282+
const result = await driver.executeScript(script, 1, true, element)
283+
284+
assert.deepEqual(result, [1, true, element])
285+
})
286+
287+
it('supports async pinned scripts', async function () {
288+
let script = await driver.pinScript('arguments[0]()')
289+
await assertAsyncScriptPinned(() => driver.executeAsyncScript(script))
290+
291+
async function assertAsyncScriptPinned(fn) {
292+
try {
293+
await fn()
294+
return
295+
} catch (err) {
296+
throw err
297+
}
298+
}
299+
})
300+
})
208301
},
209302
{ browsers: ['chrome'] }
210303
)

0 commit comments

Comments
 (0)