// 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'), sinon = require('sinon') const Capabilities = require('selenium-webdriver/lib/capabilities').Capabilities, Command = require('selenium-webdriver/lib/command').Command, CommandName = require('selenium-webdriver/lib/command').Name, error = require('selenium-webdriver/lib/error'), http = require('selenium-webdriver/lib/http'), Session = require('selenium-webdriver/lib/session').Session, WebElement = require('selenium-webdriver/lib/webdriver').WebElement describe('http', function () { describe('buildPath', function () { it('properly replaces path segments with command parameters', function () { const parameters = { sessionId: 'foo', url: 'http://www.google.com' } const finalPath = http.buildPath('/session/:sessionId/url', parameters) assert.strictEqual(finalPath, '/session/foo/url') assert.deepStrictEqual(parameters, { url: 'http://www.google.com' }) }) it('handles web element references', function () { const parameters = { sessionId: 'foo', id: WebElement.buildId('bar') } const finalPath = http.buildPath('/session/:sessionId/element/:id/click', parameters) assert.strictEqual(finalPath, '/session/foo/element/bar/click') assert.deepStrictEqual(parameters, {}) }) it('throws if missing a parameter', function () { assert.throws( () => http.buildPath('/session/:sessionId', {}), function (err) { return err instanceof error.InvalidArgumentError && 'Missing required parameter: sessionId' === err.message }, ) assert.throws( () => http.buildPath('/session/:sessionId/element/:id', { sessionId: 'foo', }), function (err) { return err instanceof error.InvalidArgumentError && 'Missing required parameter: id' === err.message }, ) }) it('does not match on segments that do not start with a colon', function () { assert.strictEqual(http.buildPath('/session/foo:bar/baz', {}), '/session/foo:bar/baz') }) }) describe('Executor', function () { let executor let client let send beforeEach(function setUp() { client = new http.Client() send = sinon.stub(client, 'send') executor = new http.Executor(client) }) describe('command routing', function () { it('rejects unrecognized commands', function () { return executor.execute(new Command('fake-name')).then(assert.fail, (err) => { if (err instanceof error.UnknownCommandError && 'Unrecognized command: fake-name' === err.message) { return } throw err }) }) it('rejects promise if client fails to send request', function () { let error = new Error('boom') send.returns(Promise.reject(error)) return assertFailsToSend(new Command(CommandName.NEW_SESSION)).then(function (e) { assert.strictEqual(error, e) assertSent('POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]) }) }) it('can execute commands with no URL parameters', function () { const resp = JSON.stringify({ sessionId: 'abc123' }) send.returns(Promise.resolve(new http.Response(200, {}, resp))) let command = new Command(CommandName.NEW_SESSION) return assertSendsSuccessfully(command).then(function (_response) { assertSent('POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]) }) }) it('rejects commands missing URL parameters', async function () { let command = new Command(CommandName.FIND_CHILD_ELEMENT) .setParameter('sessionId', 's123') // Let this be missing: setParameter('id', {'ELEMENT': 'e456'}). .setParameter('using', 'id') .setParameter('value', 'foo') try { await executor.execute(command) return Promise.reject(Error('should have thrown')) } catch (err) { assert.strictEqual(err.constructor, error.InvalidArgumentError) assert.strictEqual(err.message, 'Missing required parameter: id') } assert.ok(!send.called) }) it('replaces URL parameters with command parameters', function () { const command = new Command(CommandName.GET) .setParameter('sessionId', 's123') .setParameter('url', 'http://www.google.com') send.returns(Promise.resolve(new http.Response(200, {}, ''))) return assertSendsSuccessfully(command).then(function (_response) { assertSent('POST', '/session/s123/url', { url: 'http://www.google.com' }, [ ['Accept', 'application/json; charset=utf-8'], ]) }) }) describe('uses correct URL', function () { beforeEach(() => (executor = new http.Executor(client))) describe('in W3C mode', function () { test(CommandName.MAXIMIZE_WINDOW, { sessionId: 's123' }, true, 'POST', '/session/s123/window/maximize') // This is consistent b/w legacy and W3C, just making sure. test( CommandName.GET, { sessionId: 's123', url: 'http://www.example.com' }, true, 'POST', '/session/s123/url', { url: 'http://www.example.com' }, ) }) function test(command, parameters, w3c, expectedMethod, expectedUrl, opt_expectedParams) { it(`command=${command}`, function () { const resp = JSON.stringify({ sessionId: 'abc123' }) send.returns(Promise.resolve(new http.Response(200, {}, resp))) let cmd = new Command(command).setParameters(parameters) executor.w3c = w3c return executor.execute(cmd).then(function () { assertSent(expectedMethod, expectedUrl, opt_expectedParams || {}, [ ['Accept', 'application/json; charset=utf-8'], ]) }) }) } }) }) describe('response parsing', function () { it('extracts value from JSON response', function () { const responseObj = { status: error.ErrorCode.SUCCESS, value: 'http://www.google.com', } const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify(responseObj)))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, 'http://www.google.com') }) }) describe('extracts Session from NEW_SESSION response', function () { beforeEach(() => (executor = new http.Executor(client))) const command = new Command(CommandName.NEW_SESSION) describe('fails if server returns invalid response', function () { describe('(empty response)', function () { test(true) test(false) function test(w3c) { it('w3c === ' + w3c, function () { send.returns(Promise.resolve(new http.Response(200, {}, ''))) executor.w3c = w3c return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e } }, ) }) } }) describe('(no session ID)', function () { test(true) test(false) function test(w3c) { it('w3c === ' + w3c, function () { let resp = { value: { name: 'Bob' } } send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify(resp)))) executor.w3c = w3c return executor.execute(command).then( () => assert.fail('expected to fail'), (e) => { if (!e.message.startsWith('Unable to parse')) { throw e } }, ) }) } }) }) it('handles legacy response', function () { const rawResponse = { sessionId: 's123', status: 0, value: { name: 'Bob' }, } send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify(rawResponse)))) assert.ok(!executor.w3c) return executor.execute(command).then(function (response) { assert.ok(response instanceof Session) assert.strictEqual(response.getId(), 's123') let caps = response.getCapabilities() assert.ok(caps instanceof Capabilities) assert.strictEqual(caps.get('name'), 'Bob') assert.ok(!executor.w3c) }) }) it('auto-upgrades on W3C response', function () { let rawResponse = { value: { sessionId: 's123', value: { name: 'Bob', }, }, } send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify(rawResponse)))) assert.ok(!executor.w3c) return executor.execute(command).then(function (response) { assert.ok(response instanceof Session) assert.strictEqual(response.getId(), 's123') let caps = response.getCapabilities() assert.ok(caps instanceof Capabilities) assert.strictEqual(caps.get('name'), 'Bob') assert.ok(executor.w3c) }) }) it('if w3c, does not downgrade on legacy response', function () { const rawResponse = { sessionId: 's123', status: 0, value: null } send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify(rawResponse)))) executor.w3c = true return executor.execute(command).then(function (response) { assert.ok(response instanceof Session) assert.strictEqual(response.getId(), 's123') assert.strictEqual(response.getCapabilities().size, 0) assert.ok(executor.w3c, 'should never downgrade') }) }) it('handles legacy new session failures', function () { let rawResponse = { status: error.ErrorCode.NO_SUCH_ELEMENT, value: { message: 'hi' }, } send.returns(Promise.resolve(new http.Response(500, {}, JSON.stringify(rawResponse)))) return executor.execute(command).then( () => assert.fail('should have failed'), (e) => { assert.ok(e instanceof error.NoSuchElementError) assert.strictEqual(e.message, 'hi') }, ) }) it('handles w3c new session failures', function () { let rawResponse = { value: { error: 'no such element', message: 'oops' }, } send.returns(Promise.resolve(new http.Response(500, {}, JSON.stringify(rawResponse)))) return executor.execute(command).then( () => assert.fail('should have failed'), (e) => { assert.ok(e instanceof error.NoSuchElementError) assert.strictEqual(e.message, 'oops') }, ) }) }) it('handles JSON null', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, 'null'))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, null) }) }) describe('falsy values', function () { test(0) test(false) test('') function test(value) { it(`value=${value}`, function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, JSON.stringify({ status: 0, value: value })))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, value) }) }) } }) it('handles non-object JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, '123'))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, 123) }) }) it('returns body text when 2xx but not JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, 'hello, world\r\ngoodbye, world!'))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, 'hello, world\ngoodbye, world!') }) }) it('returns body text when 2xx but invalid JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, '['))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, '[') }) }) it('returns null if no body text and 2xx', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, ''))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, null) }) }) it('returns normalized body text when 2xx but not JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(200, {}, '\r\n\n\n\r\n'))) return executor.execute(command).then(function (response) { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) assert.strictEqual(response, '\n\n\n\n') }) }) it('throws UnsupportedOperationError for 404 and body not JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(404, {}, 'hello, world\r\ngoodbye, world!'))) return executor .execute(command) .then( () => assert.fail('should have failed'), checkError(error.UnsupportedOperationError, 'getCurrentUrl: hello, world\ngoodbye, world!'), ) }) it('throws WebDriverError for generic 4xx when body not JSON', function () { const command = new Command(CommandName.GET_CURRENT_URL).setParameter('sessionId', 's123') send.returns(Promise.resolve(new http.Response(500, {}, 'hello, world\r\ngoodbye, world!'))) return executor .execute(command) .then( () => assert.fail('should have failed'), checkError(error.WebDriverError, 'hello, world\ngoodbye, world!'), ) .then(function () { assertSent('GET', '/session/s123/url', {}, [['Accept', 'application/json; charset=utf-8']]) }) }) }) it('canDefineNewCommands', function () { executor.defineCommand('greet', 'GET', '/person/:name') const command = new Command('greet').setParameter('name', 'Bob') send.returns(Promise.resolve(new http.Response(200, {}, ''))) return assertSendsSuccessfully(command).then(function (_response) { assertSent('GET', '/person/Bob', {}, [['Accept', 'application/json; charset=utf-8']]) }) }) it('canRedefineStandardCommands', function () { executor.defineCommand(CommandName.GO_BACK, 'POST', '/custom/back') const command = new Command(CommandName.GO_BACK).setParameter('times', 3) send.returns(Promise.resolve(new http.Response(200, {}, ''))) return assertSendsSuccessfully(command).then(function (_response) { assertSent('POST', '/custom/back', { times: 3 }, [['Accept', 'application/json; charset=utf-8']]) }) }) it('accepts promised http clients', function () { executor = new http.Executor(Promise.resolve(client)) const resp = JSON.stringify({ sessionId: 'abc123' }) send.returns(Promise.resolve(new http.Response(200, {}, resp))) let command = new Command(CommandName.NEW_SESSION) return executor.execute(command).then((_response) => { assertSent('POST', '/session', {}, [['Accept', 'application/json; charset=utf-8']]) }) }) function entries(map) { let entries = [] for (let e of map.entries()) { entries.push(e) } return entries } function checkError(type, message) { return function (e) { if (e instanceof type) { assert.strictEqual(e.message, message) } else { throw e } } } function assertSent(method, path, data, headers) { assert.ok( send.calledWith( sinon.match(function (value) { assert.strictEqual(value.method, method) assert.strictEqual(value.path, path) assert.deepStrictEqual(value.data, data) assert.deepStrictEqual(entries(value.headers), headers) return true }), ), ) } function assertSendsSuccessfully(command) { return executor.execute(command).then(function (response) { return response }) } function assertFailsToSend(command, _opt_onError) { return executor.execute(command).then( () => { throw Error('should have failed') }, (e) => { return e }, ) } }) })