From c31582ebd01bb4ab2b978b5c6aff61cae0ee1ea6 Mon Sep 17 00:00:00 2001 From: David Mello Date: Wed, 17 Jul 2024 06:29:55 -0400 Subject: [PATCH] Add .check and .uncheck commands for immutable checkbox operations (#4232) --- lib/api/element-commands/check.js | 64 ++++++ lib/api/element-commands/uncheck.js | 64 ++++++ lib/api/web-element/commands/check.js | 28 +++ lib/api/web-element/commands/uncheck.js | 26 +++ .../selenium-webdriver/method-mappings.js | 36 ++++ test/src/api/commands/element/testCheck.js | 137 ++++++++++++ test/src/api/commands/element/testUncheck.js | 109 ++++++++++ .../src/api/commands/web-element/testCheck.js | 201 ++++++++++++++++++ .../api/commands/web-element/testUncheck.js | 201 ++++++++++++++++++ types/index.d.ts | 105 +++++++++ types/tests/elementCommands.test-d.ts | 60 ++++++ types/tests/webElement.test-d.ts | 2 + types/web-element.d.ts | 3 + 13 files changed, 1036 insertions(+) create mode 100644 lib/api/element-commands/check.js create mode 100644 lib/api/element-commands/uncheck.js create mode 100644 lib/api/web-element/commands/check.js create mode 100644 lib/api/web-element/commands/uncheck.js create mode 100644 test/src/api/commands/element/testCheck.js create mode 100644 test/src/api/commands/element/testUncheck.js create mode 100644 test/src/api/commands/web-element/testCheck.js create mode 100644 test/src/api/commands/web-element/testUncheck.js diff --git a/lib/api/element-commands/check.js b/lib/api/element-commands/check.js new file mode 100644 index 0000000000..e985f74f6d --- /dev/null +++ b/lib/api/element-commands/check.js @@ -0,0 +1,64 @@ +const BaseElementCommand = require('./_baseElementCommand.js'); + +/** + * Will check, by clicking, on a checkbox or radio input if it is not already checked. + * + * @example + * module.exports = { + * demoTest(browser) { + * browser.check('input[type=checkbox]:not(:checked)'); + * + * browser.check('input[type=checkbox]:not(:checked)', function(result) { + * console.log('Check result', result); + * }); + * + * // with explicit locate strategy + * browser.check('css selector', 'input[type=checkbox]:not(:checked)'); + * + * // with selector object - see https://nightwatchjs.org/guide#element-properties + * browser.check({ + * selector: 'input[type=checkbox]:not(:checked)', + * index: 1, + * suppressNotFoundErrors: true + * }); + * + * browser.check({ + * selector: 'input[type=checkbox]:not(:checked)', + * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present + * }); + * }, + * + * demoTestAsync: async function(browser) { + * const result = await browser.check('input[type=checkbox]:not(:checked)'); + * console.log('Check result', result); + * } + * } + * + * @method check + * @syntax .check(selector, [callback]) + * @syntax .check(using, selector, [callback]) + * @syntax browser.element(selector).check() + * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) + * @param {string} selector The CSS/Xpath selector used to locate the element. + * @param {function} [callback] Optional callback function to be called when the command finishes. + * @api protocol.elementinteraction + */ +class CheckElement extends BaseElementCommand { + get extraArgsCount() { + return 0; + } + + get elementProtocolAction() { + return 'checkElement'; + } + + static get isTraceable() { + return true; + } + + async protocolAction() { + return this.executeProtocolAction(this.elementProtocolAction); + } +} + +module.exports = CheckElement; diff --git a/lib/api/element-commands/uncheck.js b/lib/api/element-commands/uncheck.js new file mode 100644 index 0000000000..888f455a3d --- /dev/null +++ b/lib/api/element-commands/uncheck.js @@ -0,0 +1,64 @@ +const BaseElementCommand = require('./_baseElementCommand.js'); + +/** + * Will uncheck, by clicking, on a checkbox or radio input if it is not already unchecked. + * + * @example + * module.exports = { + * demoTest(browser) { + * browser.uncheck('input[type=checkbox]:checked)'); + * + * browser.uncheck('input[type=checkbox]:checked)', function(result) { + * console.log('Check result', result); + * }); + * + * // with explicit locate strategy + * browser.uncheck('css selector', 'input[type=checkbox]:checked)'); + * + * // with selector object - see https://nightwatchjs.org/guide#element-properties + * browser.uncheck({ + * selector: 'input[type=checkbox]:checked)', + * index: 1, + * suppressNotFoundErrors: true + * }); + * + * browser.uncheck({ + * selector: 'input[type=checkbox]:checked)', + * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present + * }); + * }, + * + * demoTestAsync: async function(browser) { + * const result = await browser.uncheck('input[type=checkbox]:checked)'); + * console.log('Check result', result); + * } + * } + * + * @method check + * @syntax .uncheck(selector, [callback]) + * @syntax .uncheck(using, selector, [callback]) + * @syntax browser.element(selector).uncheck() + * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) + * @param {string} selector The CSS/Xpath selector used to locate the element. + * @param {function} [callback] Optional callback function to be called when the command finishes. + * @api protocol.elementinteraction + */ +class UncheckElement extends BaseElementCommand { + get extraArgsCount() { + return 0; + } + + get elementProtocolAction() { + return 'uncheckElement'; + } + + static get isTraceable() { + return true; + } + + async protocolAction() { + return this.executeProtocolAction(this.elementProtocolAction); + } +} + +module.exports = UncheckElement; diff --git a/lib/api/web-element/commands/check.js b/lib/api/web-element/commands/check.js new file mode 100644 index 0000000000..cb91ffc467 --- /dev/null +++ b/lib/api/web-element/commands/check.js @@ -0,0 +1,28 @@ +/** + * Will check, by clicking, on a checkbox or radio input if it is not already checked. + * The element is scrolled into view if it is not already pointer-interactable. See the WebDriver specification for element interactability. + * + * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. + * + * @example + * export default { + * demoTest(browser: NightwatchAPI): void { + * browser.element('input[type=checkbox]:not(:checked)').check(); + * browser.element('input[type=radio]:not(:checked)').check(); + * }, + * async demoTestAsync(browser: NightwatchAPI): Promise { + * await browser.element('input[type=checkbox]:not(:checked)').check(); + * await browser.element('input[type=radio]:not(:checked)').check(); + * }, + * } + * + * @since 3.6.4 + * @method check + * @memberof ScopedWebElement + * @instance + * @syntax browser.element(selector).check() + * @returns {ScopedWebElement} + */ +module.exports.command = function () { + return this.runQueuedCommand('checkElement'); +}; diff --git a/lib/api/web-element/commands/uncheck.js b/lib/api/web-element/commands/uncheck.js new file mode 100644 index 0000000000..5bab1354d8 --- /dev/null +++ b/lib/api/web-element/commands/uncheck.js @@ -0,0 +1,26 @@ +/** + * Will uncheck, by clicking, on a checkbox or radio input if it is not already unchecked. + * The element is scrolled into view if it is not already pointer-interactable. See the WebDriver specification for element interactability. + * + * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. + * + * @example + * export default { + * demoTest(browser: NightwatchAPI): void { + * browser.element('input[type=checkbox]:checked)').check(); + * }, + * async demoTestAsync(browser: NightwatchAPI): Promise { + * await browser.element('input[type=checkbox]:checked)').check(); + * }, + * } + * + * @since 3.6.4 + * @method uncheck + * @memberof ScopedWebElement + * @instance + * @syntax browser.element(selector).check() + * @returns {ScopedWebElement} + */ +module.exports.command = function () { + return this.runQueuedCommand('uncheckElement'); +}; diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js index 0f10c8f72e..5843598a3a 100644 --- a/lib/transport/selenium-webdriver/method-mappings.js +++ b/lib/transport/selenium-webdriver/method-mappings.js @@ -606,6 +606,42 @@ module.exports = class MethodMappings { return this.methods.session.setElementValue.call(this, webElementOrId, modifiedValue); }, + async checkElement(webElementOrId) { + const element = await this.getWebElement(webElementOrId); + const elementType = await element.getAttribute('type'); + const checkableTypes = ['checkbox', 'radio']; + + if (!checkableTypes.includes(elementType)) { + throw new Error('must be an input element with type attribute \'checkbox\' or \'radio\''); + } + + const value = await element.isSelected(); + + if (!value) { + await element.click(); + } + + return null; + }, + + async uncheckElement(webElementOrId) { + const element = await this.getWebElement(webElementOrId); + const elementType = await element.getAttribute('type'); + const checkableTypes = ['checkbox', 'radio']; + + if (!checkableTypes.includes(elementType)) { + throw new Error('must be an input element with type attribute \'checkbox\' or \'radio\''); + } + + const value = await element.isSelected(); + + if (value) { + await element.click(); + } + + return null; + }, + async setElementValue(webElementOrId, value) { if (Array.isArray(value)) { value = value.join(''); diff --git a/test/src/api/commands/element/testCheck.js b/test/src/api/commands/element/testCheck.js new file mode 100644 index 0000000000..8d9d925f50 --- /dev/null +++ b/test/src/api/commands/element/testCheck.js @@ -0,0 +1,137 @@ +const assert = require('assert'); +const MockServer = require('../../../../lib/mockserver.js'); +const CommandGlobals = require('../../../../lib/globals/commands.js'); + +describe('.check()', function () { + beforeEach(function(done) { + CommandGlobals.beforeEach.call(this, done); + }); + + afterEach(function(done) { + CommandGlobals.afterEach.call(this, done); + }); + + it('client.check() will click unselected checkbox', function (done) { + MockServer.addMock({ + 'url': '/wd/hub/session/1352110219202/element/0/click', + 'response': { + sessionId: '1352110219202', + status: 0 + } + }).addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: false + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'checkbox' + }) + }); + + this.client.api.check('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).check('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); + + it('client.check() will click unselected radio input', function (done) { + MockServer.addMock({ + 'url': '/wd/hub/session/1352110219202/element/0/click', + 'response': { + sessionId: '1352110219202', + status: 0 + } + }).addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: false + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'radio' + }) + }); + + this.client.api.check('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).check('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); + + it('client.check() will not click selected checkbox', function (done) { + MockServer.addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: true + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'checkbox' + }) + }); + + this.client.api.check('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).check('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); + + it('client.check() will not click selected radio input', function (done) { + MockServer.addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: true + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'checkbox' + }) + }); + + this.client.api.check('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).check('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); +}); diff --git a/test/src/api/commands/element/testUncheck.js b/test/src/api/commands/element/testUncheck.js new file mode 100644 index 0000000000..82374e6e1a --- /dev/null +++ b/test/src/api/commands/element/testUncheck.js @@ -0,0 +1,109 @@ +const assert = require('assert'); +const MockServer = require('../../../../lib/mockserver.js'); +const CommandGlobals = require('../../../../lib/globals/commands.js'); + +describe('.uncheck()', function () { + beforeEach(function(done) { + CommandGlobals.beforeEach.call(this, done); + }); + + afterEach(function(done) { + CommandGlobals.afterEach.call(this, done); + }); + + it('client.uncheck() will uncheck selected checkbox input', function (done) { + MockServer.addMock({ + 'url': '/wd/hub/session/1352110219202/element/0/click', + 'response': { + sessionId: '1352110219202', + status: 0 + } + }).addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: true + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'checkbox' + }) + }); + + this.client.api.uncheck('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).uncheck('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); + + it('client.uncheck() will uncheck selected radio input', function (done) { + MockServer.addMock({ + 'url': '/wd/hub/session/1352110219202/element/0/click', + 'response': { + sessionId: '1352110219202', + status: 0 + } + }).addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: true + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'radio' + }) + }); + + this.client.api.uncheck('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).uncheck('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); + + it('client.uncheck() will not click unselected checkbox element', function (done) { + MockServer.addMock({ + url: '/wd/hub/session/1352110219202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: false + }) + }).addMock({ + url: '/wd/hub/session/1352110219202/execute/sync', + method: 'POST', + response: JSON.stringify({ + sessionId: '1352110219202', + status: 0, + value: 'checkbox' + }) + }); + + this.client.api.uncheck('css selector', '#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }).uncheck('#weblogin', function callback(result) { + assert.strictEqual(result.status, 0); + }); + + this.client.start(done); + }); +}); diff --git a/test/src/api/commands/web-element/testCheck.js b/test/src/api/commands/web-element/testCheck.js new file mode 100644 index 0000000000..941632f977 --- /dev/null +++ b/test/src/api/commands/web-element/testCheck.js @@ -0,0 +1,201 @@ +const assert = require('assert'); +const {WebElement} = require('selenium-webdriver'); +const MockServer = require('../../../../lib/mockserver.js'); +const CommandGlobals = require('../../../../lib/globals/commands-w3c.js'); +const common = require('../../../../common.js'); +const Element = common.require('element/index.js'); + +describe('element().check() command', function() { + before(function (done) { + CommandGlobals.beforeEach.call(this, done); + }); + + after(function (done) { + CommandGlobals.afterEach.call(this, done); + }); + + it('test .element().check() will check checkbox if not selected', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: false + }) + }, true); + + // For returning 'checkbox' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'checkbox' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').check(); + + // Click command should have been used one time to uncheck + assert.strictEqual(nCallsToClick, 1); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().check() will check radio input if not selected', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: false + }) + }, true); + + // For returning 'radio' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'radio' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').check(); + + // Click command should have been used one time to uncheck + assert.strictEqual(nCallsToClick, 1); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().check() will not check radio input if type not a checkbox or radio input', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: false + }) + }, true); + + // For returning 'submit' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'submit' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').check(); + + // Click command should have been used one time to uncheck + assert.strictEqual(nCallsToClick, 0); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().check() will not click if checked already', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }, true); + + // For returning 'radio' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'radio' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').check(); + + // Click command should not have been executed since element is unchecked already + assert.strictEqual(nCallsToClick, 0); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); +}); + diff --git a/test/src/api/commands/web-element/testUncheck.js b/test/src/api/commands/web-element/testUncheck.js new file mode 100644 index 0000000000..23942a7775 --- /dev/null +++ b/test/src/api/commands/web-element/testUncheck.js @@ -0,0 +1,201 @@ +const assert = require('assert'); +const {WebElement} = require('selenium-webdriver'); +const MockServer = require('../../../../lib/mockserver.js'); +const CommandGlobals = require('../../../../lib/globals/commands-w3c.js'); +const common = require('../../../../common.js'); +const Element = common.require('element/index.js'); + +describe('element().uncheck() command', function() { + before(function (done) { + CommandGlobals.beforeEach.call(this, done); + }); + + after(function (done) { + CommandGlobals.afterEach.call(this, done); + }); + + it('test .element().uncheck() will uncheck checkbox if selected', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }, true); + + // For returning 'checkbox' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'checkbox' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').uncheck(); + + // Click command should have been used one time to uncheck + assert.strictEqual(nCallsToClick, 1); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().uncheck() will uncheck radio input if selected', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }, true); + + // For returning 'radio' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'radio' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').uncheck(); + + // Click command should have been used one time to uncheck + assert.strictEqual(nCallsToClick, 1); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().uncheck() will not uncheck if not a checkbox', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: true + }) + }, true); + + // For returning 'submit' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'submit' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').uncheck(); + + // Click command should not have been fired + assert.strictEqual(nCallsToClick, 0); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); + + it('test .element().uncheck() will not click if unchecked already', async function () { + let nCallsToClick = 0; + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/click', + method: 'POST', + response: JSON.stringify({ + value: null + }), + onRequest(_) { + nCallsToClick++; + } + }, true); + + MockServer.addMock({ + url: '/session/13521-10219-202/element/0/selected', + method: 'GET', + response: JSON.stringify({ + value: false + }) + }, true); + + // For returning 'checkbox' from getAttribute for type attribute + MockServer.addMock({ + url: '/session/13521-10219-202/execute/sync', + method: 'POST', + response: JSON.stringify({ + value: 'checkbox' + }) + }, true); + + const resultPromise = await this.client.api.element('#signupSection').uncheck(); + + // Click command should not have been executed since element is unchecked already + assert.strictEqual(nCallsToClick, 0); + + // neither an instance of Element or Promise, but an instance of ScopedWebElement. + assert.strictEqual(resultPromise instanceof Element, false); + assert.strictEqual(typeof resultPromise.find, 'undefined'); + assert.strictEqual(resultPromise instanceof Promise, false); + + const result = await resultPromise; + assert.strictEqual(result instanceof WebElement, true); + assert.strictEqual(await result.getId(), '0'); + }); +}); + diff --git a/types/index.d.ts b/types/index.d.ts index 566162646c..64b5c4fe3a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2587,6 +2587,57 @@ export interface ClientCommands extends ChromiumClientCommands { } export interface ElementCommands { + /** + * Will check, click, on an unchecked checkbox or radio input if not already checked. + * + * @example + * module.exports = { + * demoTest(browser) { + * browser.check('input[type=checkbox]:not(:checked)'); + * + * browser.check('input[type=checkbox]:not(:checked)', function(result) { + * console.log('Check result', result); + * }); + * + * // with explicit locate strategy + * browser.check('css selector', 'input[type=checkbox]:not(:checked)'); + * + * // with selector object - see https://nightwatchjs.org/guide#element-properties + * browser.check({ + * selector: 'input[type=checkbox]:not(:checked)', + * index: 1, + * suppressNotFoundErrors: true + * }); + * + * browser.check({ + * selector: 'input[type=checkbox]:not(:checked)', + * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present + * }); + * }, + * + * demoTestAsync: async function(browser) { + * const result = await browser.check('input[type=checkbox]:not(:checked)'); + * console.log('Check result', result); + * } + * } + * + * @see https://nightwatchjs.org/api/check.html + */ + check( + selector: Definition, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable; + check( + using: LocateStrategy, + selector: Definition, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable; /** * Clear a textarea or a text input element's value. * Starting with v1.1 `clearValue()` will wait for the element to be present (until the specified timeout). @@ -4521,6 +4572,60 @@ export interface ElementCommands { result: NightwatchCallbackResult ) => void ): Awaitable; + + /** + * Will uncheck, click, on a checked checkbox or radio input if not already unchecked. + * + * @example + * module.exports = { + * demoTest(browser) { + * browser.uncheck('input[type=checkbox]:checked)'); + * + * browser.uncheck('input[type=checkbox]:checked)', function(result) { + * console.log('Check result', result); + * }); + * + * // with explicit locate strategy + * browser.uncheck('css selector', 'input[type=checkbox]:checked)'); + * + * // with selector object - see https://nightwatchjs.org/guide#element-properties + * browser.uncheck({ + * selector: 'input[type=checkbox]:checked)', + * index: 1, + * suppressNotFoundErrors: true + * }); + * + * browser.uncheck({ + * selector: 'input[type=checkbox]:checked)', + * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present + * }); + * }, + * + * demoTestAsync: async function(browser) { + * const result = await browser.uncheck('input[type=checkbox]:checked)'); + * console.log('Check result', result); + * } + * } + * + * Will uncheck, click, on a checked checkbox or radio input if not already unchecked. + * + * @see https://nightwatchjs.org/api/uncheck.html + */ + uncheck( + selector: Definition, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable; + uncheck( + using: LocateStrategy, + selector: Definition, + callback?: ( + this: NightwatchAPI, + result: NightwatchCallbackResult + ) => void + ): Awaitable; } export interface AppiumCommands { diff --git a/types/tests/elementCommands.test-d.ts b/types/tests/elementCommands.test-d.ts index 581b76022a..e20b8b6a40 100644 --- a/types/tests/elementCommands.test-d.ts +++ b/types/tests/elementCommands.test-d.ts @@ -1,6 +1,66 @@ import { expectType } from 'tsd'; import { JSON_WEB_OBJECT, NightwatchSizeAndPosition, ElementResult, NightwatchAPI, NightwatchCallbackResult, ElementGlobal } from '..'; +// +// .check +// +describe('check command demo', function () { + test('demo test', function () { + browser + .url('https://www.selenium.dev/selenium/web/formPage.html') + .waitForElementVisible('#checkbox-with-label') + .check('#checkbox-with-label', function (result) { + expectType(this); + expectType>(result); + }) + .expect.element('#checkbox-with-label').to.be.selected; + + // Should not uncheck the checkbox if .check is rerun on the same element + browser + .check('#checkbox-with-label') + .expect.element('#checkbox-with-label').to.be.selected + }); + + test('async demo test', async function (browser) { + const result = await browser + .url('https://www.selenium.dev/selenium/web/formPage.html') + .waitForElementVisible('#checkbox-with-label') + .check('#checkbox-with-label'); + expectType(result); + }); +}); + +// +// .uncheck +// +describe('uncheck command demo', function () { + test('demo test', function () { + browser + .url('https://www.selenium.dev/selenium/web/formPage.html') + .waitForElementVisible('#checkbox-with-label') + .click('#checkbox-with-label') + .assert.selected('#checkbox-with-label') + .uncheck('#checkbox-with-label', function (result) { + expectType(this); + expectType>(result); + }) + .expect.element('#checkbox-with-label').to.not.be.selected; + + // Should not check the checkbox if .check is rerun on the same element + browser + .uncheck('#checkbox-with-label') + .expect.element('#checkbox-with-label').to.not.be.selected + }); + + test('async demo test', async function (browser) { + const result = await browser + .url('https://www.selenium.dev/selenium/web/formPage.html') + .waitForElementVisible('#checkbox-with-label') + .uncheck('#checkbox-with-label'); + expectType(result); + }); +}); + // // .clearValue // diff --git a/types/tests/webElement.test-d.ts b/types/tests/webElement.test-d.ts index e7a3d0ff69..b530c42d1e 100644 --- a/types/tests/webElement.test-d.ts +++ b/types/tests/webElement.test-d.ts @@ -175,6 +175,8 @@ describe('new element() api', function () { expectType>(elem.click()); expectType>(elem.clear()); + expectType>(elem.check()); + expectType>(elem.uncheck()); expectType>(elem.sendKeys('something', 1)); expectType>(elem.update('something', 1)); expectType>(elem.setValue('something', 1)); diff --git a/types/web-element.d.ts b/types/web-element.d.ts index a5e2c77866..bde0cc4c12 100644 --- a/types/web-element.d.ts +++ b/types/web-element.d.ts @@ -157,6 +157,9 @@ export interface ScopedElement extends Element, PromiseLike { clear(): Promise; + check(): Promise; + uncheck(): Promise; + sendKeys(...keys: E): Promise; submit(): Promise;