Skip to content

Commit

Permalink
[bidi][js] Add high-level logging API
Browse files Browse the repository at this point in the history
Related to SeleniumHQ#13992
  • Loading branch information
pujagani committed Jun 14, 2024
1 parent 59a25c7 commit c9f4b60
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 0 deletions.
6 changes: 6 additions & 0 deletions javascript/node/selenium-webdriver/bidi/logInspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,17 @@ class LogInspector {
}

removeCallback(id) {
let hasId = false
for (const [, callbacks] of this.listener) {
if (callbacks.has(id)) {
callbacks.delete(id)
hasId = true
}
}

if (!hasId) {
throw Error(`Callback with id ${id} not found`)
}
}

invokeCallbacks(eventType, data) {
Expand Down
63 changes: 63 additions & 0 deletions javascript/node/selenium-webdriver/lib/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const logInspector = require('../bidi/logInspector')

class Script {
#driver
#logInspector

constructor(driver) {
this.#driver = driver
}

// This should be done in the constructor.
// But since it needs to call async methods we cannot do that in the constructor.
// We can have a separate async method that initialises the Script instance.
// However, that pattern does not allow chaining the methods as we would like the user to use it.
// Since it involves awaiting to get the instance and then another await to call the method.
// Using this allows the user to do this "await driver.script().addJavaScriptErrorHandler(callback)"
async #init() {
if (this.#logInspector !== undefined) {
return
}
this.#logInspector = await logInspector(this.#driver)
}

async addJavaScriptErrorHandler(callback) {
await this.#init()
return await this.#logInspector.onJavascriptException(callback)
}

async removeJavaScriptErrorHandler(id) {
await this.#init()
await this.#logInspector.removeCallback(id)
}

async addConsoleMessageHandler(callback) {
await this.#init()
return this.#logInspector.onConsoleEntry(callback)
}

async removeConsoleMessageHandler(id) {
await this.#init()

await this.#logInspector.removeCallback(id)
}
}

module.exports = Script
12 changes: 12 additions & 0 deletions javascript/node/selenium-webdriver/lib/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const { isObject } = require('./util')
const BIDI = require('../bidi')
const { PinnedScript } = require('./pinnedScript')
const JSZip = require('jszip')
const Script = require('./script')

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

script() {
// The Script calls the LogInspector which maintains state of the callbacks.
// Returning a new instance of the same driver will not work while removing callbacks.
if (this.#script === undefined) {
this.#script = new Script(this)
}

return this.#script
}

validatePrintPageParams(keys, object) {
let page = {}
let margin = {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict'

const assert = require('node:assert')
const { Browser } = require('../../')
const { Pages, suite } = require('../../lib/test')
const { until } = require('../../index')

suite(
function (env) {
let driver

beforeEach(async function () {
driver = await env.builder().build()
})

afterEach(async function () {
await driver.quit()
})

function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

describe('script()', function () {
it('can listen to console log', async function () {
let log = null
const handler = await driver.script().addConsoleMessageHandler((logEntry) => {
log = logEntry
})

await driver.get(Pages.logEntryAdded)
await driver.findElement({ id: 'consoleLog' }).click()

await delay(3000)

assert.equal(log.text, 'Hello, world!')
assert.equal(log.realm, null)
assert.equal(log.type, 'console')
assert.equal(log.level, 'info')
assert.equal(log.method, 'log')
assert.equal(log.args.length, 1)
await driver.script().removeConsoleMessageHandler(handler)
})

it('can listen to javascript error', async function () {
let log = null
const handler = await driver.script().addJavaScriptErrorHandler((logEntry) => {
log = logEntry
})

await driver.get(Pages.logEntryAdded)
await driver.findElement({ id: 'jsException' }).click()

await delay(3000)

assert.equal(log.text, 'Error: Not working')
assert.equal(log.type, 'javascript')
assert.equal(log.level, 'error')

await driver.script().removeJavaScriptErrorHandler(handler)
})

it('throws an error while removing a handler that does not exist', async function () {
try {
await driver.script().removeJavaScriptErrorHandler(10)
assert.fail('Expected error not thrown. Non-existent handler cannot be removed')
} catch (e) {
assert.strictEqual(e.message, 'Callback with id 10 not found')
}
})
})
},
{ browsers: [Browser.FIREFOX, Browser.CHROME, Browser.EDGE] },
)

0 comments on commit c9f4b60

Please sign in to comment.