Skip to content

Commit 19f84eb

Browse files
pujaganititusfortner
authored andcommitted
[bidi][js] Add high-level logging API (SeleniumHQ#14135)
Related to SeleniumHQ#13992 Co-authored-by: Titus Fortner <[email protected]>
1 parent ea60388 commit 19f84eb

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

javascript/node/selenium-webdriver/bidi/logInspector.js

+6
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,17 @@ class LogInspector {
6666
}
6767

6868
removeCallback(id) {
69+
let hasId = false
6970
for (const [, callbacks] of this.listener) {
7071
if (callbacks.has(id)) {
7172
callbacks.delete(id)
73+
hasId = true
7274
}
7375
}
76+
77+
if (!hasId) {
78+
throw Error(`Callback with id ${id} not found`)
79+
}
7480
}
7581

7682
invokeCallbacks(eventType, data) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 logInspector = require('../bidi/logInspector')
19+
20+
class Script {
21+
#driver
22+
#logInspector
23+
24+
constructor(driver) {
25+
this.#driver = driver
26+
}
27+
28+
// This should be done in the constructor.
29+
// But since it needs to call async methods we cannot do that in the constructor.
30+
// We can have a separate async method that initialises the Script instance.
31+
// However, that pattern does not allow chaining the methods as we would like the user to use it.
32+
// Since it involves awaiting to get the instance and then another await to call the method.
33+
// Using this allows the user to do this "await driver.script().addJavaScriptErrorHandler(callback)"
34+
async #init() {
35+
if (this.#logInspector !== undefined) {
36+
return
37+
}
38+
this.#logInspector = await logInspector(this.#driver)
39+
}
40+
41+
async addJavaScriptErrorHandler(callback) {
42+
await this.#init()
43+
return await this.#logInspector.onJavascriptException(callback)
44+
}
45+
46+
async removeJavaScriptErrorHandler(id) {
47+
await this.#init()
48+
await this.#logInspector.removeCallback(id)
49+
}
50+
51+
async addConsoleMessageHandler(callback) {
52+
await this.#init()
53+
return this.#logInspector.onConsoleEntry(callback)
54+
}
55+
56+
async removeConsoleMessageHandler(id) {
57+
await this.#init()
58+
59+
await this.#logInspector.removeCallback(id)
60+
}
61+
}
62+
63+
module.exports = Script

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

+12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const { isObject } = require('./util')
4343
const BIDI = require('../bidi')
4444
const { PinnedScript } = require('./pinnedScript')
4545
const JSZip = require('jszip')
46+
const Script = require('./script')
4647

4748
// Capability names that are defined in the W3C spec.
4849
const W3C_CAPABILITY_NAMES = new Set([
@@ -654,6 +655,7 @@ function filterNonW3CCaps(capabilities) {
654655
* @implements {IWebDriver}
655656
*/
656657
class WebDriver {
658+
#script = undefined
657659
/**
658660
* @param {!(./session.Session|IThenable<!./session.Session>)} session Either
659661
* a known session or a promise that will be resolved to a session.
@@ -1104,6 +1106,16 @@ class WebDriver {
11041106
return new TargetLocator(this)
11051107
}
11061108

1109+
script() {
1110+
// The Script calls the LogInspector which maintains state of the callbacks.
1111+
// Returning a new instance of the same driver will not work while removing callbacks.
1112+
if (this.#script === undefined) {
1113+
this.#script = new Script(this)
1114+
}
1115+
1116+
return this.#script
1117+
}
1118+
11071119
validatePrintPageParams(keys, object) {
11081120
let page = {}
11091121
let margin = {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
'use strict'
19+
20+
const assert = require('node:assert')
21+
const { Browser } = require('../../')
22+
const { Pages, suite } = require('../../lib/test')
23+
const { until } = require('../../index')
24+
25+
suite(
26+
function (env) {
27+
let driver
28+
29+
beforeEach(async function () {
30+
driver = await env.builder().build()
31+
})
32+
33+
afterEach(async function () {
34+
await driver.quit()
35+
})
36+
37+
function delay(ms) {
38+
return new Promise((resolve) => setTimeout(resolve, ms))
39+
}
40+
41+
describe('script()', function () {
42+
it('can listen to console log', async function () {
43+
let log = null
44+
const handler = await driver.script().addConsoleMessageHandler((logEntry) => {
45+
log = logEntry
46+
})
47+
48+
await driver.get(Pages.logEntryAdded)
49+
await driver.findElement({ id: 'consoleLog' }).click()
50+
51+
await delay(3000)
52+
53+
assert.equal(log.text, 'Hello, world!')
54+
assert.equal(log.realm, null)
55+
assert.equal(log.type, 'console')
56+
assert.equal(log.level, 'info')
57+
assert.equal(log.method, 'log')
58+
assert.equal(log.args.length, 1)
59+
await driver.script().removeConsoleMessageHandler(handler)
60+
})
61+
62+
it('can listen to javascript error', async function () {
63+
let log = null
64+
const handler = await driver.script().addJavaScriptErrorHandler((logEntry) => {
65+
log = logEntry
66+
})
67+
68+
await driver.get(Pages.logEntryAdded)
69+
await driver.findElement({ id: 'jsException' }).click()
70+
71+
await delay(3000)
72+
73+
assert.equal(log.text, 'Error: Not working')
74+
assert.equal(log.type, 'javascript')
75+
assert.equal(log.level, 'error')
76+
77+
await driver.script().removeJavaScriptErrorHandler(handler)
78+
})
79+
80+
it('throws an error while removing a handler that does not exist', async function () {
81+
try {
82+
await driver.script().removeJavaScriptErrorHandler(10)
83+
assert.fail('Expected error not thrown. Non-existent handler cannot be removed')
84+
} catch (e) {
85+
assert.strictEqual(e.message, 'Callback with id 10 not found')
86+
}
87+
})
88+
})
89+
},
90+
{ browsers: [Browser.FIREFOX, Browser.CHROME, Browser.EDGE] },
91+
)

0 commit comments

Comments
 (0)