Skip to content

Commit

Permalink
Merge branch 'main' into handle-uncaught
Browse files Browse the repository at this point in the history
  • Loading branch information
garg3133 authored Jan 2, 2025
2 parents 92a24ee + 8ee74c4 commit bd42235
Show file tree
Hide file tree
Showing 66 changed files with 1,424 additions and 209 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/missing-types-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ jobs:
github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Download artifact'
uses: actions/github-script@v3.1.0
uses: actions/github-script@v6
with:
script: |
var artifacts = await github.actions.listWorkflowRunArtifacts({
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{github.event.workflow_run.id }},
});
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr"
})[0];
var download = await github.actions.downloadArtifact({
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/missing-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
echo $(git diff --name-only origin/$GITHUB_BASE_REF $GITHUB_SHA types/ | tr '\n' ',') > ./pr/files_changed
echo ${{ github.event.number }} > ./pr/NR
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: pr
path: pr/
22 changes: 22 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Publish to NPM
on:
push:
tags:
- vv* # to enable this action, change this tag back to v*
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm install
- name: Publish package on NPM 📦
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35 changes: 35 additions & 0 deletions examples/tests/element/dragAndDrop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
describe('Element Drag & Drop Demo', function () {
before(browser => {
browser.navigateTo(
'https://mdn.github.io/dom-examples/drag-and-drop/copy-move-DataTransfer.html'
);
});

it('move element demo', async function (browser) {
const srcMoveElem = browser.element('#src_move'); // returns a Nightwatch Element Wrapper with loads of element commands.

// pause to see the initial state
browser.pause(1000);

// drag src element 80 pixels below.
srcMoveElem.dragAndDrop({x: 0, y: 80});
});

it('copy element demo', async function (browser) {
const srcCopyElem = browser.element('#src_copy'); // returns a Nightwatch Element Wrapper with loads of element commands.
const destCopyWebElem = await browser.element('#dest_copy'); // awaiting the browser.element command returns a WebElement object (actual result).

// pause to see the initial state
browser.pause(1000);

// drag src element to dest element.
srcCopyElem.dragAndDrop(destCopyWebElem);
});

after((browser) => {
// pause to see the final state
browser.pause(2000);

browser.end();
});
});
30 changes: 30 additions & 0 deletions examples/tests/element/elementScreenshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
describe('Take Screenshot Demo', function () {
before((browser) => {
browser.navigateTo('https://nightwatchjs.org/');
});

it('takes screenshot without async-await', function (browser) {
browser.waitForElementVisible('body');

const heading = browser.element('.hero__heading');
const screenshot = heading.takeScreenshot();
screenshot.then((screenshotData) => {
require('fs').writeFile('heading.png', screenshotData, 'base64', (err) => {
browser.assert.strictEqual(err, null);
});
});
});

it('takes screenshot with async-await', async function (browser) {
browser.waitForElementVisible('body');

const heading = browser.element('.hero__heading');
const screenshotData = await heading.takeScreenshot();

require('fs').writeFile('heading1.png', screenshotData, 'base64', (err) => {
browser.assert.strictEqual(err, null);
});
});

after((browser) => browser.end());
});
35 changes: 35 additions & 0 deletions examples/tests/element/isCommands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
describe('Element "is" commands Demo', function () {
before((browser) => {
browser.navigateTo('https://www.ecosia.org/settings');
});

it('Demo', async function (browser) {
// accepting cookies to remove modal
browser.element('.cookie-consent__actions').getLastElementChild().click();

const saveButton = browser.element('.settings-form__buttons > .base-button--variant-solid-green');
const cancelButton = browser.element('.settings-form__buttons > .base-button--variant-outline');

saveButton.isVisible().assert.equals(true);
cancelButton.isVisible().assert.equals(true);

saveButton.isEnabled().assert.equals(false);
cancelButton.isEnabled().assert.equals(true);

const newTabCheckbox = browser.element('#e-field-newTab');

newTabCheckbox.isSelected().assert.equals(false);

// Clicking the checkbox selects it.
// Also our save button becomes enabled.
newTabCheckbox.click();

newTabCheckbox.isSelected().assert.equals(true);
saveButton.isEnabled().assert.equals(true);

// click the cancel button
cancelButton.click();
});

after((browser) => browser.end());
});
16 changes: 0 additions & 16 deletions examples/tests/staleElementReferenceError.js

This file was deleted.

7 changes: 6 additions & 1 deletion lib/api/_loaders/_base-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,12 @@ class BaseLoader extends EventEmitter {

//prevent unhandled rejection.
node.deferred.promise.catch(err => {
BaseLoader.lastDeferred?.reject(err);
if (BaseLoader.lastDeferred) {
// TODO: check in what cases BaseLoader.lastDeferred could be set to null
// in between the execution of the test.
// issue: #4265; hint: could have something to do with custom commands.
BaseLoader.lastDeferred.reject(err);
}
});

if (!this.module?.returnFn) {
Expand Down
5 changes: 4 additions & 1 deletion lib/api/_loaders/assertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ class AssertionLoader extends BaseCommandLoader {

// prevent unhandledRejection errors
node.deferred.promise.catch(err => {
return AssertionLoader.lastDeferred?.reject(err);
// null check, as done for BaseLoader.lastDeferred as well.
if (AssertionLoader.lastDeferred) {
return AssertionLoader.lastDeferred.reject(err);
}
});

return node.deferred.promise;
Expand Down
23 changes: 22 additions & 1 deletion lib/api/_loaders/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ class CommandLoader extends BaseCommandLoader {

createWrapper() {
if (this.module) {
// this place is only reached by client-commands, protocol commands and custom-commands (no assertions or element-commands).
if (this.isUserDefined) {
// only custom-commands will reach here.
// later extend this to client-commands and protocol commands as well.
Object.defineProperty(this.module, 'rejectNodeOnAbortFailure', {
configurable: true,
get() {
return true;
}
});
}

this.commandFn = function commandFn({args, stackTrace}) {
const instance = CommandLoader.createInstance(this.nightwatchInstance, this.module, {
stackTrace,
Expand Down Expand Up @@ -183,7 +195,16 @@ class CommandLoader extends BaseCommandLoader {
})
.catch(err => {
if (instance instanceof EventEmitter) {
instance.emit('error', err);
if (instance.needsPromise) {
// if the instance has `needsPromise` set to `true`, the `error` event is listened
// on the `context` object, not on the `instance` object (in `treenode.js`).
this.emit('error', err);
} else {
// for class-based commands that inherit from EventEmitter.
// Since the `needsPromise` is set to `false` in this case, the `complete` and `error`
// events are listened on the `instance` object.
instance.emit('error', err);
}

return;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/api/protocol/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const findElement = function(selector) {
};

module.exports = class Session extends ProtocolAction {
static get avoidPrematureParentNodeResolution() {
return true;
}

async command(frameId, callback) {
if (arguments.length === 1 && typeof frameId === 'function') {
callback = frameId;
Expand Down
31 changes: 26 additions & 5 deletions lib/api/web-element/assert/element-assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,33 @@ class ScopedElementAssertions {
this.nightwatchInstance = nightwatchInstance;
}

async assert(callback) {
const assert = this.nightwatchInstance.api.assert;
assert(callback) {
// The below promise is returned to the test case by the assertion, so
// it should fail in case the actual assertion (callback) fails. In case
// of a sync test, the actual assertion would never fail, so we always
// resolve the promise.
const assertPromise = new Promise((resolve, reject) => {
const assert = this.nightwatchInstance.api.assert;

await callback(this.negated ? assert.not : assert, this.scopedElement.webElement);
const callbackResult = callback(this.negated ? assert.not : assert, this.scopedElement.webElement);
if (callbackResult instanceof Promise) {
callbackResult
.then(() => {
resolve(this.scopedElement);
})
.catch(err => {
reject(err);
});
} else {
resolve(this.scopedElement);
}
});

return this.scopedElement;
// prevent unhandledRejection errors, while also making sure
// that the exception/failure passes to the actual test case.
assertPromise.catch(() => {});

return assertPromise;
}

async executeScript(scriptFn, args) {
Expand Down Expand Up @@ -69,7 +90,7 @@ module.exports.create = function createAssertions(scopedElement, {negated = fals
value(message) {
return instance.assert(async (assertApi, element) => {
const el = await element;
const id = await el.getId();
const id = await el?.getId();

return assertApi.ok(el instanceof WebElement, message || `Testing if the element <WebElement: ${id}> is present.`);
});
Expand Down
29 changes: 25 additions & 4 deletions lib/api/web-element/assert/value-assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,32 @@ module.exports.create = function createAssertions(scopedValue, {negated, nightwa
this.scopedValue = scopedValue;
}

async _assert(callback) {
const assert = nightwatchInstance.api.assert;
await callback(this.negated ? assert.not : assert, this.scopedValue.value);
_assert(callback) {
// The below promise is returned to the test case by the assertion, so
// it should fail in case the actual assertion (callback) fails. In case
// of a sync test, the actual assertion would never fail, so we always
// resolve the promise.
const assertPromise = new Promise((resolve, reject) => {
const assert = nightwatchInstance.api.assert;
const callbackResult = callback(this.negated ? assert.not : assert, this.scopedValue.value);
if (callbackResult instanceof Promise) {
callbackResult
.then(() => {
resolve(this.scopedValue);
})
.catch(err => {
reject(err);
});
} else {
resolve(this.scopedValue);
}
});

// prevent unhandledRejection errors, while also making sure
// that the exception/failure passes to the actual test case.
assertPromise.catch((err) => {});

return this.scopedValue;
return assertPromise;
}

get not() {
Expand Down
4 changes: 3 additions & 1 deletion lib/api/web-element/commands/getAccessibleName.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
* @method getAccessibleName
* @memberof ScopedWebElement
* @instance
* @syntax browser.element(selector).getAccessibleName()
* @syntax browser.element.find(selector).getAccessibleName()
* @syntax browser.element.find(selector).getComputedLabel()
* @link /#get-computed-label
* @returns {ScopedValue<string>} A container with accessible name of an element.
* @alias getComputedLabel
*/
module.exports.command = function() {
return this.runQueuedCommandScoped('getElementAccessibleName');
Expand Down
4 changes: 3 additions & 1 deletion lib/api/web-element/commands/getAriaRole.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
* @method getAriaRole
* @memberof ScopedWebElement
* @instance
* @syntax browser.element(selector).getAriaRole()
* @syntax browser.element.find(selector).getAriaRole()
* @syntax browser.element.find(selector).getComputedRole()
* @link /#get-computed-role
* @returns {ScopedValue<string>} The container with computed WAI-ARIA role of an element.
* @alias getComputedRole
*/
module.exports.command = function() {
return this.runQueuedCommandScoped('getElementAriaRole');
Expand Down
31 changes: 31 additions & 0 deletions lib/api/web-element/commands/isActive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Determines if an element is currently active/focused in the DOM.
*
* For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
* For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
*
* @example
* describe('isActive Demo', function() {
* it('test isActive', function(browser) {
* browser.element.find('#search')
* .isActive()
* .assert.equals(true);
* });
*
* it('test async isActive', async function(browser) {
* const result = await browser.element.find('#search').isActive();
* browser.assert.equal(result, true);
* });
* });
*
* @since 3.9.0
* @method isActive
* @memberof ScopedWebElement
* @instance
* @syntax browser.element.find(selector).isActive()
* @link /#get-active-element
* @returns {ScopedValue<boolean>}
*/
module.exports.command = function () {
return this.runQueuedCommandScoped('isElementActive');
};
Loading

0 comments on commit bd42235

Please sign in to comment.