diff --git a/javascript/node/selenium-webdriver/bidi/logInspector.js b/javascript/node/selenium-webdriver/bidi/logInspector.js index 13f7a8c78dbbf..8146a0c53d1e6 100644 --- a/javascript/node/selenium-webdriver/bidi/logInspector.js +++ b/javascript/node/selenium-webdriver/bidi/logInspector.js @@ -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) { diff --git a/javascript/node/selenium-webdriver/lib/script.js b/javascript/node/selenium-webdriver/lib/script.js new file mode 100644 index 0000000000000..cf23c525bd68b --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/script.js @@ -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 diff --git a/javascript/node/selenium-webdriver/lib/webdriver.js b/javascript/node/selenium-webdriver/lib/webdriver.js index 012e02a0db7f4..add28497d0211 100644 --- a/javascript/node/selenium-webdriver/lib/webdriver.js +++ b/javascript/node/selenium-webdriver/lib/webdriver.js @@ -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([ @@ -654,6 +655,7 @@ function filterNonW3CCaps(capabilities) { * @implements {IWebDriver} */ class WebDriver { + #script = undefined /** * @param {!(./session.Session|IThenable)} session Either * a known session or a promise that will be resolved to a session. @@ -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 = {} diff --git a/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js b/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js new file mode 100644 index 0000000000000..dcf27600e5759 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/lib/webdriver_script_test.js @@ -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] }, +)