Repository: nightwatchjs/nightwatch Branch: main Commit: 86d8086cc0f1 Files: 1247 Total size: 4.7 MB Directory structure: gitextract_4_y53lij/ ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature-request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── label-commenter-config.yml │ ├── stale.yml │ └── workflows/ │ ├── build-node.yaml │ ├── component-tests.yaml │ ├── coverage.yml │ ├── label-commenter.yml │ ├── missing-types-comment.yml │ ├── missing-types.yml │ ├── nightly.yml │ ├── publish.yml │ └── type-declaration-tests.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── api/ │ ├── README.md │ └── index.js ├── bin/ │ ├── .gitignore │ ├── nightwatch │ ├── runner.js │ └── show_survey.js ├── codecov.yml ├── examples/ │ ├── .gitignore │ ├── cucumber-js/ │ │ ├── README.md │ │ └── features/ │ │ ├── nightwatch.feature │ │ └── step_definitions/ │ │ └── nightwatch.js │ ├── custom-assertions/ │ │ └── testCustomAssertion.js │ ├── custom-commands/ │ │ ├── angular/ │ │ │ └── getElementsInList.js │ │ └── strictClick.js │ ├── globals.json │ ├── globalsModule.js │ ├── pages/ │ │ ├── google/ │ │ │ ├── consent.js │ │ │ ├── search.js │ │ │ └── searchResults.js │ │ └── nightwatchFeatures.js │ ├── test-app/ │ │ ├── globals.js │ │ ├── index.html │ │ └── page2.html │ ├── tests/ │ │ ├── README.md │ │ ├── angularTodoTest.js │ │ ├── bstackdemo/ │ │ │ ├── auth.js │ │ │ └── checkout.js │ │ ├── chromeCDP_example.js │ │ ├── duckDuckGo.js │ │ ├── ecosia.js │ │ ├── element/ │ │ │ ├── dragAndDrop.js │ │ │ ├── elementScreenshot.js │ │ │ ├── elementapi-tests.js │ │ │ └── isCommands.js │ │ ├── google.js │ │ ├── googlePageObject.js │ │ ├── sample-with-relative-locators.js │ │ ├── selectElement.js │ │ ├── shadowRootExample.js │ │ └── vueTodoList.js │ ├── tsconfig.json │ └── unittests/ │ ├── demoTestAsync.js │ ├── testUtils.js │ └── testUtilsWithChai.js ├── index.js ├── lib/ │ ├── api/ │ │ ├── _loaders/ │ │ │ ├── _base-loader.js │ │ │ ├── _command-loader.js │ │ │ ├── assertion-scheduler.js │ │ │ ├── assertion.js │ │ │ ├── chrome.js │ │ │ ├── command.js │ │ │ ├── element-api.js │ │ │ ├── element-command.js │ │ │ ├── element-global.js │ │ │ ├── ensure.js │ │ │ ├── expect-assertion.js │ │ │ ├── expect.js │ │ │ ├── firefox.js │ │ │ ├── page-object.js │ │ │ ├── plugin.js │ │ │ ├── static.js │ │ │ └── within-context.js │ │ ├── assertions/ │ │ │ ├── _assertionInstance.js │ │ │ ├── attributeContains.js │ │ │ ├── attributeEquals.js │ │ │ ├── attributeMatches.js │ │ │ ├── contains.js │ │ │ ├── containsText.js │ │ │ ├── cssClassNotPresent.js │ │ │ ├── cssClassPresent.js │ │ │ ├── cssProperty.js │ │ │ ├── domPropertyContains.js │ │ │ ├── domPropertyEquals.js │ │ │ ├── domPropertyMatches.js │ │ │ ├── elementNotPresent.js │ │ │ ├── elementPresent.js │ │ │ ├── elementsCount.js │ │ │ ├── enabled.js │ │ │ ├── hasAttribute.js │ │ │ ├── hasClass.js │ │ │ ├── hasDescendants.js │ │ │ ├── hidden.js │ │ │ ├── promisedValue.js │ │ │ ├── selected.js │ │ │ ├── textContains.js │ │ │ ├── textEquals.js │ │ │ ├── textMatches.js │ │ │ ├── title.js │ │ │ ├── titleContains.js │ │ │ ├── titleEquals.js │ │ │ ├── titleMatches.js │ │ │ ├── urlContains.js │ │ │ ├── urlEquals.js │ │ │ ├── urlMatches.js │ │ │ ├── value.js │ │ │ ├── valueContains.js │ │ │ ├── valueEquals.js │ │ │ └── visible.js │ │ ├── client-commands/ │ │ │ ├── _base-command.js │ │ │ ├── _locateStrategy.js │ │ │ ├── alerts/ │ │ │ │ ├── accept.js │ │ │ │ ├── dismiss.js │ │ │ │ ├── getText.js │ │ │ │ └── setText.js │ │ │ ├── axeInject.js │ │ │ ├── axeRun.js │ │ │ ├── cookies/ │ │ │ │ ├── delete.js │ │ │ │ ├── deleteAll.js │ │ │ │ ├── get.js │ │ │ │ ├── getAll.js │ │ │ │ └── set.js │ │ │ ├── debug.js │ │ │ ├── deleteCookie.js │ │ │ ├── deleteCookies.js │ │ │ ├── document/ │ │ │ │ ├── executeAsyncScript.js │ │ │ │ ├── executeScript.js │ │ │ │ ├── injectScript.js │ │ │ │ └── source.js │ │ │ ├── enablePerformanceMetrics.js │ │ │ ├── end.js │ │ │ ├── getCookie.js │ │ │ ├── getCookies.js │ │ │ ├── getLog.js │ │ │ ├── getLogTypes.js │ │ │ ├── getPerformanceMetrics.js │ │ │ ├── getTitle.js │ │ │ ├── getWindowPosition.js │ │ │ ├── getWindowRect.js │ │ │ ├── getWindowSize.js │ │ │ ├── init.js │ │ │ ├── injectScript.js │ │ │ ├── isLogAvailable.js │ │ │ ├── logs/ │ │ │ │ ├── captureBrowserConsoleLogs.js │ │ │ │ ├── captureBrowserExceptions.js │ │ │ │ ├── getSessionLog.js │ │ │ │ ├── getSessionLogTypes.js │ │ │ │ └── isSessionLogAvailable.js │ │ │ ├── maximizeWindow.js │ │ │ ├── network/ │ │ │ │ ├── captureRequests.js │ │ │ │ ├── mockResponse.js │ │ │ │ └── setConditions.js │ │ │ ├── pageSource.js │ │ │ ├── pause.js │ │ │ ├── perform.js │ │ │ ├── registerBasicAuth.js │ │ │ ├── resizeWindow.js │ │ │ ├── saveScreenshot.js │ │ │ ├── saveSnapshot.js │ │ │ ├── setCookie.js │ │ │ ├── setDeviceDimensions.js │ │ │ ├── setGeolocation.js │ │ │ ├── setWindowPosition.js │ │ │ ├── setWindowRect.js │ │ │ ├── setWindowSize.js │ │ │ ├── takeHeapSnapshot.js │ │ │ ├── urlHash.js │ │ │ ├── useCss.js │ │ │ ├── useXpath.js │ │ │ ├── window/ │ │ │ │ ├── close.js │ │ │ │ ├── fullscreen.js │ │ │ │ ├── getAllHandles.js │ │ │ │ ├── getHandle.js │ │ │ │ ├── getPosition.js │ │ │ │ ├── getRect.js │ │ │ │ ├── getSize.js │ │ │ │ ├── maximize.js │ │ │ │ ├── minimize.js │ │ │ │ ├── open.js │ │ │ │ ├── setPosition.js │ │ │ │ ├── setRect.js │ │ │ │ ├── setSize.js │ │ │ │ └── switchTo.js │ │ │ └── within.js │ │ ├── element-commands/ │ │ │ ├── _baseElementCommand.js │ │ │ ├── _waitFor.js │ │ │ ├── _waitForDisplayed.js │ │ │ ├── check.js │ │ │ ├── clearValue.js │ │ │ ├── click.js │ │ │ ├── clickAndHold.js │ │ │ ├── doubleClick.js │ │ │ ├── dragAndDrop.js │ │ │ ├── findElement.js │ │ │ ├── findElements.js │ │ │ ├── getAccessibleName.js │ │ │ ├── getAriaRole.js │ │ │ ├── getAttribute.js │ │ │ ├── getCssProperty.js │ │ │ ├── getElementProperty.js │ │ │ ├── getElementRect.js │ │ │ ├── getElementSize.js │ │ │ ├── getFirstElementChild.js │ │ │ ├── getLastElementChild.js │ │ │ ├── getLocation.js │ │ │ ├── getLocationInView.js │ │ │ ├── getNextSibling.js │ │ │ ├── getPreviousSibling.js │ │ │ ├── getShadowRoot.js │ │ │ ├── getTagName.js │ │ │ ├── getText.js │ │ │ ├── getValue.js │ │ │ ├── hasDescendants.js │ │ │ ├── isEnabled.js │ │ │ ├── isPresent.js │ │ │ ├── isSelected.js │ │ │ ├── isVisible.js │ │ │ ├── moveToElement.js │ │ │ ├── rightClick.js │ │ │ ├── sendKeys.js │ │ │ ├── setAttribute.js │ │ │ ├── setPassword.js │ │ │ ├── setValue.js │ │ │ ├── submitForm.js │ │ │ ├── takeElementScreenshot.js │ │ │ ├── uncheck.js │ │ │ ├── updateValue.js │ │ │ ├── uploadFile.js │ │ │ ├── waitForElementNotPresent.js │ │ │ ├── waitForElementNotVisible.js │ │ │ ├── waitForElementPresent.js │ │ │ └── waitForElementVisible.js │ │ ├── expect/ │ │ │ ├── _baseExpect.js │ │ │ ├── assertions/ │ │ │ │ ├── _baseAssertion.js │ │ │ │ ├── element/ │ │ │ │ │ ├── _element-assertion.js │ │ │ │ │ ├── active.js │ │ │ │ │ ├── attribute.js │ │ │ │ │ ├── css.js │ │ │ │ │ ├── enabled.js │ │ │ │ │ ├── present.js │ │ │ │ │ ├── property.js │ │ │ │ │ ├── selected.js │ │ │ │ │ ├── text.js │ │ │ │ │ ├── type.js │ │ │ │ │ ├── value.js │ │ │ │ │ └── visible.js │ │ │ │ └── elements/ │ │ │ │ └── count.js │ │ │ ├── component.js │ │ │ ├── cookie.js │ │ │ ├── element.js │ │ │ ├── elements.js │ │ │ ├── title.js │ │ │ └── url.js │ │ ├── index.js │ │ ├── protocol/ │ │ │ ├── _base-action.js │ │ │ ├── acceptAlert.js │ │ │ ├── appium/ │ │ │ │ ├── getContext.js │ │ │ │ ├── getContexts.js │ │ │ │ ├── getCurrentActivity.js │ │ │ │ ├── getCurrentPackage.js │ │ │ │ ├── getGeolocation.js │ │ │ │ ├── getOrientation.js │ │ │ │ ├── hideKeyboard.js │ │ │ │ ├── isKeyboardShown.js │ │ │ │ ├── longPressKeyCode.js │ │ │ │ ├── pressKeyCode.js │ │ │ │ ├── resetApp.js │ │ │ │ ├── setContext.js │ │ │ │ ├── setGeolocation.js │ │ │ │ ├── setOrientation.js │ │ │ │ └── startActivity.js │ │ │ ├── back.js │ │ │ ├── closeWindow.js │ │ │ ├── contexts.js │ │ │ ├── cookie.js │ │ │ ├── currentContext.js │ │ │ ├── dismissAlert.js │ │ │ ├── element.js │ │ │ ├── elementActive.js │ │ │ ├── elementIdAttribute.js │ │ │ ├── elementIdClear.js │ │ │ ├── elementIdClick.js │ │ │ ├── elementIdCssProperty.js │ │ │ ├── elementIdDisplayed.js │ │ │ ├── elementIdDoubleClick.js │ │ │ ├── elementIdElement.js │ │ │ ├── elementIdElements.js │ │ │ ├── elementIdEnabled.js │ │ │ ├── elementIdEquals.js │ │ │ ├── elementIdLocation.js │ │ │ ├── elementIdLocationInView.js │ │ │ ├── elementIdName.js │ │ │ ├── elementIdProperty.js │ │ │ ├── elementIdSelected.js │ │ │ ├── elementIdSize.js │ │ │ ├── elementIdText.js │ │ │ ├── elementIdValue.js │ │ │ ├── elements.js │ │ │ ├── forward.js │ │ │ ├── frame.js │ │ │ ├── frameParent.js │ │ │ ├── fullscreenWindow.js │ │ │ ├── getAlertText.js │ │ │ ├── getCurrentUrl.js │ │ │ ├── getOrientation.js │ │ │ ├── keys.js │ │ │ ├── minimizeWindow.js │ │ │ ├── mouseButtonClick.js │ │ │ ├── mouseButtonDown.js │ │ │ ├── mouseButtonUp.js │ │ │ ├── moveTo.js │ │ │ ├── navigateTo.js │ │ │ ├── openNewWindow.js │ │ │ ├── quit.js │ │ │ ├── refresh.js │ │ │ ├── releaseMouseButton.js │ │ │ ├── screenshot.js │ │ │ ├── session.js │ │ │ ├── sessionLog.js │ │ │ ├── sessionLogTypes.js │ │ │ ├── sessions.js │ │ │ ├── setAlertText.js │ │ │ ├── setContext.js │ │ │ ├── setOrientation.js │ │ │ ├── source.js │ │ │ ├── status.js │ │ │ ├── submit.js │ │ │ ├── switchToWindow.js │ │ │ ├── timeouts.js │ │ │ ├── timeoutsAsyncScript.js │ │ │ ├── timeoutsImplicitWait.js │ │ │ ├── title.js │ │ │ ├── url.js │ │ │ ├── waitUntil.js │ │ │ ├── windowHandle.js │ │ │ ├── windowHandles.js │ │ │ ├── windowMaximize.js │ │ │ ├── windowPosition.js │ │ │ ├── windowRect.js │ │ │ └── windowSize.js │ │ └── web-element/ │ │ ├── assert/ │ │ │ ├── element-assertions.js │ │ │ ├── elements-assertions.js │ │ │ └── value-assertions.js │ │ ├── commands/ │ │ │ ├── check.js │ │ │ ├── clear.js │ │ │ ├── click.js │ │ │ ├── clickAndHold.js │ │ │ ├── doubleClick.js │ │ │ ├── dragAndDrop.js │ │ │ ├── find.js │ │ │ ├── findAll.js │ │ │ ├── findAllByAltText.js │ │ │ ├── findAllByPlaceholderText.js │ │ │ ├── findAllByRole.js │ │ │ ├── findAllByText.js │ │ │ ├── findByAltText.js │ │ │ ├── findByLabelText.js │ │ │ ├── findByPlaceholderText.js │ │ │ ├── findByRole.js │ │ │ ├── findByText.js │ │ │ ├── getAccessibleName.js │ │ │ ├── getAriaRole.js │ │ │ ├── getAttribute.js │ │ │ ├── getCssProperty.js │ │ │ ├── getFirstElementChild.js │ │ │ ├── getId.js │ │ │ ├── getLastElementChild.js │ │ │ ├── getNextElementSibling.js │ │ │ ├── getParentElement.js │ │ │ ├── getPreviousElementSibling.js │ │ │ ├── getProperty.js │ │ │ ├── getRect.js │ │ │ ├── getShadowRoot.js │ │ │ ├── getTagName.js │ │ │ ├── getText.js │ │ │ ├── getValue.js │ │ │ ├── inspectInDevTools.js │ │ │ ├── isActive.js │ │ │ ├── isEnabled.js │ │ │ ├── isPresent.js │ │ │ ├── isSelected.js │ │ │ ├── isVisible.js │ │ │ ├── moveTo.js │ │ │ ├── rightClick.js │ │ │ ├── sendKeys.js │ │ │ ├── setAttribute.js │ │ │ ├── setProperty.js │ │ │ ├── setValue.js │ │ │ ├── submit.js │ │ │ ├── takeScreenshot.js │ │ │ ├── uncheck.js │ │ │ ├── update.js │ │ │ └── upload.js │ │ ├── element-locator.js │ │ ├── element-value.js │ │ ├── factory.js │ │ ├── index.js │ │ ├── scoped-element.js │ │ ├── scoped-elements.js │ │ └── waitUntil.js │ ├── assertion/ │ │ ├── assertion-error.js │ │ ├── assertion-runner.js │ │ ├── assertion.js │ │ └── index.js │ ├── core/ │ │ ├── asynctree.js │ │ ├── client.js │ │ ├── namespaced-api.js │ │ ├── queue.js │ │ └── treenode.js │ ├── element/ │ │ ├── appium-locator.js │ │ ├── command.js │ │ ├── index.js │ │ ├── locate/ │ │ │ ├── elements-by-recursion.js │ │ │ ├── recursive-lookup.js │ │ │ └── single-element-by-recursion.js │ │ ├── locator-factory.js │ │ ├── locator.js │ │ └── strategy.js │ ├── http/ │ │ ├── auth.js │ │ ├── formatter.js │ │ ├── http.js │ │ ├── options.js │ │ ├── request.js │ │ └── response.js │ ├── index.js │ ├── page-object/ │ │ ├── base-object.js │ │ ├── command-wrapper.js │ │ ├── index.js │ │ └── section.js │ ├── reporter/ │ │ ├── axe-report.js │ │ ├── base-reporter.js │ │ ├── global-reporter.js │ │ ├── index.js │ │ ├── reporters/ │ │ │ ├── html.js │ │ │ ├── json.js │ │ │ ├── junit.js │ │ │ ├── junit.xml.ejs │ │ │ └── minimalJson.js │ │ ├── results.js │ │ ├── simplified.js │ │ └── summary.js │ ├── runner/ │ │ ├── androidEmulator.js │ │ ├── cli/ │ │ │ ├── argv-setup.js │ │ │ ├── cli.js │ │ │ └── nightwatch.conf.ejs │ │ ├── concurrency/ │ │ │ ├── child-process.js │ │ │ ├── index.js │ │ │ ├── task.js │ │ │ ├── worker-process.js │ │ │ └── worker-task.js │ │ ├── eventHub.js │ │ ├── folder-walk.js │ │ ├── matchers/ │ │ │ ├── filename.js │ │ │ └── tags.js │ │ ├── process-listener.js │ │ ├── rerunUtil.js │ │ ├── runner.js │ │ ├── test-runners/ │ │ │ ├── cucumber/ │ │ │ │ ├── README.md │ │ │ │ ├── _setup_cucumber_runner.js │ │ │ │ └── nightwatch-format.js │ │ │ ├── cucumber.js │ │ │ ├── default.js │ │ │ ├── mocha/ │ │ │ │ ├── custom-runnable.js │ │ │ │ ├── custom-runner.js │ │ │ │ ├── extensions.js │ │ │ │ └── nightwatchSuite.js │ │ │ └── mocha.js │ │ └── test-source.js │ ├── settings/ │ │ ├── defaults.js │ │ └── settings.js │ ├── testsuite/ │ │ ├── context.js │ │ ├── globals.js │ │ ├── hooks/ │ │ │ ├── _basehook.js │ │ │ ├── afterAll.js │ │ │ ├── afterChildProcess.js │ │ │ ├── afterEach.js │ │ │ ├── beforeAll.js │ │ │ ├── beforeChildProcess.js │ │ │ └── beforeEach.js │ │ ├── hooks.js │ │ ├── index.js │ │ ├── interfaces/ │ │ │ ├── common.js │ │ │ ├── describe.js │ │ │ └── exports.js │ │ ├── nightwatch-inspector/ │ │ │ ├── index.js │ │ │ └── websocket-server.js │ │ ├── repl.js │ │ ├── retries.js │ │ ├── runnable.js │ │ └── testcase.js │ ├── transport/ │ │ ├── errors/ │ │ │ └── index.js │ │ ├── factory.js │ │ ├── index.js │ │ └── selenium-webdriver/ │ │ ├── actions.js │ │ ├── appium.js │ │ ├── appiumBase.js │ │ ├── browserstack/ │ │ │ ├── appAutomate.js │ │ │ ├── automate.js │ │ │ ├── automateTurboScale.js │ │ │ └── browserstack.js │ │ ├── cdp.js │ │ ├── chrome.js │ │ ├── edge.js │ │ ├── firefox.js │ │ ├── httpclient.js │ │ ├── index.js │ │ ├── method-mappings.js │ │ ├── options.js │ │ ├── safari.js │ │ ├── selenium.js │ │ ├── service-builders/ │ │ │ ├── appium.js │ │ │ ├── base-service.js │ │ │ ├── chrome.js │ │ │ ├── edge.js │ │ │ ├── firefox.js │ │ │ ├── safari.js │ │ │ └── selenium.js │ │ └── session.js │ └── utils/ │ ├── addDetailedError.js │ ├── alwaysDisplayError.js │ ├── analytics.js │ ├── beautifyStackTrace.js │ ├── browsername.js │ ├── chalkColors.js │ ├── createPromise.js │ ├── debuggability.js │ ├── getAllClassMethodNames.js │ ├── getFreePort.js │ ├── index.js │ ├── isErrorObject.js │ ├── locatestrategy.js │ ├── logger/ │ │ ├── index.js │ │ └── log_settings.js │ ├── mobile.js │ ├── periodic-promise.js │ ├── printVersionInfo.js │ ├── requireModule.js │ ├── safeStringify.js │ ├── screenshots.js │ ├── seleniumAtoms.js │ ├── snapshots.js │ ├── stackTrace.js │ ├── timed-callback.js │ └── version.js ├── package.json ├── test/ │ ├── .eslintrc │ ├── apidemos/ │ │ ├── actions-api/ │ │ │ ├── actionsApi.js │ │ │ └── asyncActionsApi.js │ │ ├── angular-test/ │ │ │ ├── angularTodoListWithClassicApis.js │ │ │ ├── angularTodoListWithElementGlobal.js │ │ │ ├── angularTodoListWithElementGlobalAndError.js │ │ │ └── angularTodoListWithElementGlobalAsync.js │ │ ├── appium/ │ │ │ └── appiumTest.js │ │ ├── cdp/ │ │ │ ├── registerAuth.js │ │ │ └── registerAuth2.js │ │ ├── chrome/ │ │ │ └── chromeTest.js │ │ ├── cookies/ │ │ │ ├── cookieTests.js │ │ │ └── cookieTestsWithError.js │ │ ├── custom-commands/ │ │ │ ├── testNamespacedAliases.js │ │ │ ├── testUsingAsyncCustomAssert.js │ │ │ ├── testUsingAutoInvokeCommand.js │ │ │ ├── testUsingCommandReturnFn.js │ │ │ ├── testUsingCustomExecute.js │ │ │ ├── testUsingCustomGetEmail.js │ │ │ └── testUsingES6AsyncCustomCommands.js │ │ ├── custom-commands-parallel/ │ │ │ └── testUsingES6AsyncCustomCommands.js │ │ ├── elements/ │ │ │ ├── elementGlobalTest.js │ │ │ ├── findElementsCustomCommand.js │ │ │ ├── findElementsPageCommands.js │ │ │ └── testGlobalLocateStrategy.js │ │ ├── ensure/ │ │ │ ├── ensureTestError.js │ │ │ ├── ensureTestNotSelected.js │ │ │ └── ensureTestSelected.js │ │ ├── expect-global/ │ │ │ ├── deepEqual.js │ │ │ └── expect.js │ │ ├── firefox/ │ │ │ └── firefoxTest.js │ │ ├── namespaced-api/ │ │ │ └── namespacedApiTest.js │ │ ├── navigation/ │ │ │ └── navigateTest.js │ │ ├── page-objects/ │ │ │ └── commandsReturnTypeTest.js │ │ ├── relative-locators/ │ │ │ └── sample-with-relative-locators.js │ │ └── web-elements/ │ │ ├── assertionTest.js │ │ ├── elementApiWithPageObjects.js │ │ ├── waitUntilElementNotPresent.js │ │ ├── waitUntilFailureTest.js │ │ └── waitUntilTest.js │ ├── asynchookstests/ │ │ ├── afterEach-timeout/ │ │ │ └── sampleAsyncHooks.js │ │ ├── async-provide-error/ │ │ │ ├── after.js │ │ │ ├── afterAsync.js │ │ │ ├── afterEach.js │ │ │ ├── afterEachAsync.js │ │ │ ├── afterEachWithClient.js │ │ │ ├── afterWithClient.js │ │ │ ├── before.js │ │ │ ├── beforeAsync.js │ │ │ ├── beforeEach.js │ │ │ ├── beforeEachAsync.js │ │ │ ├── beforeEachAsyncWithClient.js │ │ │ ├── beforeEachAsyncWithClientMultiple.js │ │ │ ├── beforeEachWithClient.js │ │ │ └── beforeWithClient.js │ │ ├── before-timeout/ │ │ │ └── sampleAsyncHooks.js │ │ ├── sampleWithAssertionFailedInAfter.js │ │ ├── sampleWithAssertionFailedInBefore.js │ │ ├── sampleWithErrorInTestcaseAndAfter.js │ │ ├── sampleWithFailureInBeforeAndAfter.js │ │ ├── sampleWithFailureInTestcaseAndAfter.js │ │ ├── unittest-async-timeout.js │ │ ├── unittest-error.js │ │ └── unittest-failure/ │ │ └── unittest-failure.js │ ├── common.js │ ├── component-tests/ │ │ ├── samples/ │ │ │ ├── nightwatch.conf-noplugins.js │ │ │ ├── react/ │ │ │ │ ├── nightwatch.conf.js │ │ │ │ ├── src/ │ │ │ │ │ └── Form.jsx │ │ │ │ └── tests/ │ │ │ │ ├── Form.spec--_with-$hooks.jsx │ │ │ │ ├── formTest--notFound.js │ │ │ │ └── formTest.js │ │ │ └── vue/ │ │ │ ├── nightwatch.conf.js │ │ │ ├── src/ │ │ │ │ └── Form.vue │ │ │ └── tests/ │ │ │ ├── formTest--notFound.js │ │ │ └── formTest.js │ │ └── src/ │ │ ├── testRunBasicReact.js │ │ ├── testRunBasicVue.js │ │ └── testRunnJSXReact.js │ ├── cucumber-integration-tests/ │ │ ├── sample_cucumber_tests/ │ │ │ ├── chainingCommands/ │ │ │ │ └── testCommands.js │ │ │ ├── customCommands/ │ │ │ │ └── testCucumberWithCustomWait.js │ │ │ ├── integration/ │ │ │ │ ├── sample.feature │ │ │ │ ├── testSample.js │ │ │ │ └── testWithFailures.js │ │ │ ├── parallel/ │ │ │ │ ├── sample.feature │ │ │ │ ├── testSample.js │ │ │ │ └── testWithFailures.js │ │ │ ├── parallel-formatters/ │ │ │ │ ├── sample.feature │ │ │ │ ├── testSample.js │ │ │ │ └── testWithFailures.js │ │ │ ├── parallelWithMultipleDefinition/ │ │ │ │ ├── testExpect.js │ │ │ │ └── testSample.js │ │ │ ├── with-extra-setup/ │ │ │ │ ├── _extra_setup.js │ │ │ │ ├── sample.feature │ │ │ │ ├── testSample.js │ │ │ │ └── testWithFailures.js │ │ │ ├── with-modified-settings/ │ │ │ │ ├── plugin/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── nightwatch/ │ │ │ │ │ └── globals.js │ │ │ │ └── testSample.js │ │ │ ├── withexpect/ │ │ │ │ ├── sample.feature │ │ │ │ ├── testSample.js │ │ │ │ └── testWithFailures.js │ │ │ └── withwaitFor/ │ │ │ ├── sample.feature │ │ │ └── testWithFailures.js │ │ ├── testCucumberTestsInParallel.js │ │ ├── testCucumberTestsParallelFormatters.js │ │ ├── testCucumberTestsWithExpect.js │ │ ├── testCucumberTestsWithExtras.js │ │ ├── testCucumberTestsWithModifiedSettings.js │ │ └── testCucumberTestsWithWaitFor.js │ ├── extra/ │ │ ├── assertions/ │ │ │ ├── .dotrc │ │ │ ├── customAssertion.js │ │ │ └── customAssertionWithSelector.js │ │ ├── commands/ │ │ │ ├── _otherPerform.js │ │ │ ├── autoInvoke/ │ │ │ │ └── customCommandInvoke.js │ │ │ ├── customClearValue.js │ │ │ ├── customCommand.js │ │ │ ├── customCommandAppendResults.js │ │ │ ├── customCommandConstructor.js │ │ │ ├── customCommandWithFailure.js │ │ │ ├── customCommandWithFailureClass.js │ │ │ ├── customCommandWithSelector.js │ │ │ ├── customElementPresent.js │ │ │ ├── customExecute.js │ │ │ ├── customPauseWithNamespacedAlias.js │ │ │ ├── customPerform.js │ │ │ ├── customQuit.js │ │ │ ├── customWaitForPresent.js │ │ │ ├── es6async/ │ │ │ │ ├── basicPerform.js │ │ │ │ ├── basicPerformWithFluentApi.js │ │ │ │ ├── customExecuteAsync.js │ │ │ │ ├── customFindElements.js │ │ │ │ ├── customFindElementsES6.js │ │ │ │ ├── customPerform.js │ │ │ │ ├── customPerformClass.js │ │ │ │ ├── customVisible.js │ │ │ │ └── getEmail.js │ │ │ ├── other/ │ │ │ │ └── otherCommand.js │ │ │ ├── returnFn/ │ │ │ │ └── customCommandReturnFn.js │ │ │ └── typescript/ │ │ │ ├── tsWait.js │ │ │ └── tsWait.ts │ │ ├── cucumber-config-events.js │ │ ├── cucumber-config-expect.js │ │ ├── cucumber-config-noautostart.js │ │ ├── cucumber-config-parallel-formatters.js │ │ ├── cucumber-config-parallel.js │ │ ├── cucumber-config-waitFor.js │ │ ├── cucumber-config.js │ │ ├── existing-custom-commands/ │ │ │ └── click.js │ │ ├── external-globals-async.js │ │ ├── external-globals.js │ │ ├── globals-err.js │ │ ├── globals.js │ │ ├── minimalJsonReporter.json │ │ ├── mock-errors/ │ │ │ └── sample-error.js │ │ ├── nightwatch.json │ │ ├── otherPageobjects/ │ │ │ └── otherPage.js │ │ ├── package.json │ │ ├── pageobjects/ │ │ │ ├── commands/ │ │ │ │ ├── commandsClassWithName.js │ │ │ │ ├── common-commands.js │ │ │ │ ├── helperCommandsClass.js │ │ │ │ └── workingCommandsClass.js │ │ │ └── pages/ │ │ │ ├── addl/ │ │ │ │ └── simplePageObject.js │ │ │ ├── api/ │ │ │ │ └── method/ │ │ │ │ ├── bar.js │ │ │ │ └── foo.js │ │ │ ├── invalidPageObj.js │ │ │ ├── pageObjElementsArray.js │ │ │ ├── simplePageObj.js │ │ │ ├── simplePageObjDefaultXpath.js │ │ │ ├── simplePageObjWithCommandsClass.js │ │ │ ├── simplePageObjWithCommandsClassThrowsError.js │ │ │ ├── simplePageObjWithCommandsObject.js │ │ │ ├── simplePageObjWithError.js │ │ │ ├── waitForElementNotPresentPageObj.js │ │ │ └── workingPageObjPlain.js │ │ ├── parallelism-auto.json │ │ ├── parallelism-count.json │ │ ├── parallelism-customCommandsSync.json │ │ ├── parallelism-disabled.json │ │ ├── parallelism-envs.json │ │ ├── parallelism-execArgv-selected.json │ │ ├── parallelism-execArgv.json │ │ ├── parallelism-selenium-server.json │ │ ├── parallelism-workers.conf.js │ │ ├── parallelism.json │ │ ├── plugin/ │ │ │ ├── index.js │ │ │ └── nightwatch/ │ │ │ ├── assertions/ │ │ │ │ └── customAssertion.js │ │ │ ├── commands/ │ │ │ │ └── customCommand.js │ │ │ └── globals.js │ │ ├── reportObject.js │ │ ├── reporter/ │ │ │ ├── custom.js │ │ │ └── notvalid.js │ │ ├── withgeckodriver-concurrent.json │ │ ├── withmocha.json │ │ └── withsafari-concurrent.json │ ├── lib/ │ │ ├── command-mocks.js │ │ ├── fakedriver.js │ │ ├── globals/ │ │ │ ├── commands-w3c.js │ │ │ ├── commands.js │ │ │ └── expect.js │ │ ├── globals.js │ │ ├── mochatest.js │ │ ├── mockclient.js │ │ ├── mocks/ │ │ │ ├── api-loader/ │ │ │ │ ├── assertion-loader.js │ │ │ │ └── assertion.js │ │ │ ├── assertionLoader.js │ │ │ ├── core/ │ │ │ │ ├── assertion.js │ │ │ │ └── session.js │ │ │ ├── mocks-jsonwire.yaml │ │ │ └── mocks-w3c.yaml │ │ ├── mocks.json │ │ ├── mockserver.js │ │ ├── nightwatch.js │ │ ├── nocks.js │ │ ├── nockselements.js │ │ └── utils.js │ ├── mocha.opts │ ├── mochatests/ │ │ ├── async/ │ │ │ ├── sampleWithAsyncWithFailures.js │ │ │ └── sampleWithBeforeAndAfter.js │ │ └── integration/ │ │ ├── sampleWithAsyncTestcase.js │ │ ├── sampleWithBeforeAndAfter.js │ │ └── sampleWithFailures.js │ ├── nightwatch.json │ ├── sampletests/ │ │ ├── appendtestresult/ │ │ │ └── sampleWithAppendResults.js │ │ ├── async/ │ │ │ └── test/ │ │ │ └── sample.js │ │ ├── asyncwithexpectfailures/ │ │ │ └── sample.js │ │ ├── asyncwithfailures/ │ │ │ ├── sampleWithAsyncWithFailures.js │ │ │ ├── sampleWithAsyncWithSingleTestFailure.js │ │ │ └── sampleWithBeforeAndAfter.js │ │ ├── before-after/ │ │ │ ├── sampleSingleTest.js │ │ │ ├── sampleWithBeforeAndAfter.js │ │ │ ├── sampleWithBeforeAndAfterNoCallback.js │ │ │ └── syncBeforeAndAfter.js │ │ ├── beforewithcommand/ │ │ │ ├── commandInsideBefore.js │ │ │ └── commandInsideBeforeWithNoParams.js │ │ ├── empty/ │ │ │ └── .gitignore │ │ ├── es6await/ │ │ │ ├── basicSampleTest.js │ │ │ ├── incorrect-element-args/ │ │ │ │ └── sampleTestProperty.js │ │ │ ├── selenium/ │ │ │ │ ├── basicSampleTestSelenium.js │ │ │ │ ├── failures/ │ │ │ │ │ └── sampleWithFailures.js │ │ │ │ └── getlog/ │ │ │ │ └── getLogTestES6.js │ │ │ └── webdriver/ │ │ │ └── getText/ │ │ │ └── getTextES6.js │ │ ├── isPresent/ │ │ │ └── elementNotPresent.js │ │ ├── mixed/ │ │ │ └── sample.js │ │ ├── mixed-files/ │ │ │ ├── sampleJs.js │ │ │ └── sampleTs.ts │ │ ├── passwordValueRedacted/ │ │ │ └── passwordValueRedacted.js │ │ ├── reusebrowser/ │ │ │ ├── firstTest.js │ │ │ └── secondTest.js │ │ ├── sampleforreport/ │ │ │ ├── sample.js │ │ │ └── sampleWithFailure.js │ │ ├── simple/ │ │ │ └── test/ │ │ │ └── sample.js │ │ ├── srcfolders/ │ │ │ └── other_sample.js │ │ ├── suiteretries/ │ │ │ ├── locate-strategy/ │ │ │ │ └── suiteRetriesLocateStrategy.js │ │ │ └── sample/ │ │ │ └── suiteRetriesSample.js │ │ ├── syncnames/ │ │ │ └── sampleTest.js │ │ ├── tags/ │ │ │ └── sample.js │ │ ├── tagswithbrowserobject/ │ │ │ └── sample.js │ │ ├── trace/ │ │ │ └── sample.js │ │ ├── typescript/ │ │ │ └── sample.ts │ │ ├── unittests/ │ │ │ ├── sample.js │ │ │ └── sampleAnnotation.js │ │ ├── unknown-method/ │ │ │ └── UnknownMethod.js │ │ ├── usexpath/ │ │ │ └── sample.js │ │ ├── usingwithin/ │ │ │ ├── testNoWithin.js │ │ │ └── testWithin.js │ │ ├── webelement/ │ │ │ └── findWithSuppressNotFoundErrors.js │ │ ├── withaftereach/ │ │ │ └── sampleSingleTest.js │ │ ├── withasynchooks/ │ │ │ └── sampleAsyncHooks.js │ │ ├── withchaiexpect/ │ │ │ ├── sampleWithChai.js │ │ │ └── sampleWithGlobalExpect.js │ │ ├── withcommanderrors/ │ │ │ └── demoTest.js │ │ ├── withcustomcommands/ │ │ │ ├── element/ │ │ │ │ └── withCustomElementCommand.js │ │ │ ├── sampleWithAsyncFailures.js │ │ │ └── sampleWithFailures.js │ │ ├── withdescribe/ │ │ │ ├── basic/ │ │ │ │ ├── sample.js │ │ │ │ └── sampleWithOnly.js │ │ │ ├── basic2/ │ │ │ │ └── sample.js │ │ │ ├── failures/ │ │ │ │ ├── sampleSkipTestcases.js │ │ │ │ ├── sampleTest.js │ │ │ │ └── sampleTestWithAttribute.js │ │ │ ├── skipped/ │ │ │ │ ├── duplicateTestcases.js │ │ │ │ ├── skipBeforeAfterEach.js │ │ │ │ ├── skipTestcase.js │ │ │ │ └── skipTestsuite.js │ │ │ ├── suite-retries/ │ │ │ │ ├── sample.js │ │ │ │ └── sampleWithAttribute.js │ │ │ └── unittests/ │ │ │ └── sampleAnnotationWithDescribe.js │ │ ├── withelementerrors/ │ │ │ └── sampleTestWithElementError.js │ │ ├── witherrors/ │ │ │ └── sampleWithError.js │ │ ├── withes6asynccommands/ │ │ │ └── sampleWithAsyncCommands.js │ │ ├── withexclude/ │ │ │ ├── excluded/ │ │ │ │ ├── excluded-module.js │ │ │ │ └── not-excluded.js │ │ │ └── simple/ │ │ │ └── sample.js │ │ ├── withfailures/ │ │ │ └── sample.js │ │ ├── withpageobjects/ │ │ │ └── useXpathWithPageObjects.js │ │ ├── withservererrors/ │ │ │ └── sampleTestWithServerError.js │ │ ├── withsubfolders/ │ │ │ ├── simple/ │ │ │ │ └── sample.js │ │ │ └── tags/ │ │ │ └── sampleTags.js │ │ ├── withuknownRequire/ │ │ │ └── sample.js │ │ ├── withuncaughterrors/ │ │ │ ├── demoTestFine.js │ │ │ └── demoTestWithError.js │ │ ├── withverify/ │ │ │ ├── verifySampleFailures.js │ │ │ └── verifyWithinPerform.js │ │ ├── withwait/ │ │ │ └── elementNotPresent.js │ │ └── withwebdriver/ │ │ └── sampleTestUsingSeleniumWebdriver.js │ ├── src/ │ │ ├── analytics/ │ │ │ └── testAnalytics.js │ │ ├── api/ │ │ │ ├── assertions/ │ │ │ │ ├── testAttributeContains.js │ │ │ │ ├── testAttributeEquals.js │ │ │ │ ├── testAttributeMatches.js │ │ │ │ ├── testContainsText.js │ │ │ │ ├── testCssClassPresent.js │ │ │ │ ├── testCssProperty.js │ │ │ │ ├── testDomPropertyContains.js │ │ │ │ ├── testDomPropertyEquals.js │ │ │ │ ├── testDomPropertyMatches.js │ │ │ │ ├── testElementPresent.js │ │ │ │ ├── testElementsCount.js │ │ │ │ ├── testEnabled.js │ │ │ │ ├── testHasAttribute.js │ │ │ │ ├── testHasClass.js │ │ │ │ ├── testHidden.js │ │ │ │ ├── testSelected.js │ │ │ │ ├── testTextEquals.js │ │ │ │ ├── testTextMatches.js │ │ │ │ ├── testTitle.js │ │ │ │ ├── testTitleContains.js │ │ │ │ ├── testTitleEquals.js │ │ │ │ ├── testTitleMatches.js │ │ │ │ ├── testUrlContains.js │ │ │ │ ├── testUrlEquals.js │ │ │ │ ├── testUrlMatches.js │ │ │ │ ├── testValue.js │ │ │ │ ├── testValueContains.js │ │ │ │ ├── testValueEquals.js │ │ │ │ └── testVisible.js │ │ │ ├── commands/ │ │ │ │ ├── client/ │ │ │ │ │ ├── testAxeCommands.js │ │ │ │ │ ├── testCaptureNetworkRequests.js │ │ │ │ │ ├── testEnablePerformanceMetrics.js │ │ │ │ │ ├── testEnd.js │ │ │ │ │ ├── testGetPerformanceMetrics.js │ │ │ │ │ ├── testInit.js │ │ │ │ │ ├── testLocateStrategy.js │ │ │ │ │ ├── testMockNetworkResponse.js │ │ │ │ │ ├── testNavigateTo.js │ │ │ │ │ ├── testNightwatchRepl.js │ │ │ │ │ ├── testPause.js │ │ │ │ │ ├── testPerform.js │ │ │ │ │ ├── testRegisterBasicAuth.js │ │ │ │ │ ├── testSaveScreenshot.js │ │ │ │ │ ├── testSaveSnapshot.js │ │ │ │ │ ├── testSetDeviceDimensions.js │ │ │ │ │ ├── testSetGeolocation.js │ │ │ │ │ ├── testSetNetworkConditions.js │ │ │ │ │ ├── testTakeHeapSnapshot.js │ │ │ │ │ └── testWaitUntil.js │ │ │ │ ├── cookies/ │ │ │ │ │ ├── testCookie.js │ │ │ │ │ ├── testCookies.js │ │ │ │ │ └── testCookiesErrors.js │ │ │ │ ├── document/ │ │ │ │ │ ├── testExecute.js │ │ │ │ │ ├── testInjectScript.js │ │ │ │ │ └── testSource.js │ │ │ │ ├── element/ │ │ │ │ │ ├── testCheck.js │ │ │ │ │ ├── testClearValue.js │ │ │ │ │ ├── testClick.js │ │ │ │ │ ├── testClickAndHold.js │ │ │ │ │ ├── testClickMobile.js │ │ │ │ │ ├── testDoubleClick.js │ │ │ │ │ ├── testDragAndDrop.js │ │ │ │ │ ├── testGetAccessibleName.js │ │ │ │ │ ├── testGetAriaRole.js │ │ │ │ │ ├── testGetAttribute.js │ │ │ │ │ ├── testGetAttributeW3c.js │ │ │ │ │ ├── testGetCssProperty.js │ │ │ │ │ ├── testGetElementProperty.js │ │ │ │ │ ├── testGetElementRect.js │ │ │ │ │ ├── testGetElementSize.js │ │ │ │ │ ├── testGetFirstElementChild.js │ │ │ │ │ ├── testGetLastElementChild.js │ │ │ │ │ ├── testGetLocation.js │ │ │ │ │ ├── testGetLocationInView.js │ │ │ │ │ ├── testGetNextSibling.js │ │ │ │ │ ├── testGetPreviousSibling.js │ │ │ │ │ ├── testGetTagName.js │ │ │ │ │ ├── testGetText.js │ │ │ │ │ ├── testGetTitle.js │ │ │ │ │ ├── testGetValue.js │ │ │ │ │ ├── testHasDescendants.js │ │ │ │ │ ├── testIsEnabled.js │ │ │ │ │ ├── testIsPresent.js │ │ │ │ │ ├── testIsSelected.js │ │ │ │ │ ├── testIsVisible.js │ │ │ │ │ ├── testMoveToElement.js │ │ │ │ │ ├── testRightClick.js │ │ │ │ │ ├── testSendKeys.js │ │ │ │ │ ├── testSetAttribute.js │ │ │ │ │ ├── testSetPassword.js │ │ │ │ │ ├── testSetPasswordReport.js │ │ │ │ │ ├── testSetValue.js │ │ │ │ │ ├── testSubmitForm.js │ │ │ │ │ ├── testTakeElementScreenshot.js │ │ │ │ │ ├── testUncheck.js │ │ │ │ │ ├── testUploadFile.js │ │ │ │ │ ├── testWaitForElementNotPresent.js │ │ │ │ │ ├── testWaitForElementNotVisible.js │ │ │ │ │ ├── testWaitForElementPresent.js │ │ │ │ │ └── testWaitForElementVisible.js │ │ │ │ ├── logs/ │ │ │ │ │ ├── testCaptureBrowserConsoleLogs.js │ │ │ │ │ ├── testCaptureBrowserExceptions.js │ │ │ │ │ ├── testGetLog.js │ │ │ │ │ ├── testGetLogTypes.js │ │ │ │ │ └── testIsLogAvailable.js │ │ │ │ ├── testPageSource.js │ │ │ │ ├── testSendKeys.js │ │ │ │ ├── testUpdateValue.js │ │ │ │ ├── web-element/ │ │ │ │ │ ├── assert/ │ │ │ │ │ │ └── testElementAssertions.js │ │ │ │ │ ├── testCheck.js │ │ │ │ │ ├── testClear.js │ │ │ │ │ ├── testClick.js │ │ │ │ │ ├── testClickAndHold.js │ │ │ │ │ ├── testDoubleClick.js │ │ │ │ │ ├── testDragAndDrop.js │ │ │ │ │ ├── testElement.js │ │ │ │ │ ├── testFind.js │ │ │ │ │ ├── testFindAll.js │ │ │ │ │ ├── testFindAllByAltText.js │ │ │ │ │ ├── testFindAllByPlaceholderText.js │ │ │ │ │ ├── testFindAllByRole.js │ │ │ │ │ ├── testFindAllByText.js │ │ │ │ │ ├── testFindByAltText.js │ │ │ │ │ ├── testFindByLabelText.js │ │ │ │ │ ├── testFindByPlaceholderText.js │ │ │ │ │ ├── testFindByRole.js │ │ │ │ │ ├── testFindByText.js │ │ │ │ │ ├── testFindOnErrors.js │ │ │ │ │ ├── testGetAccessibleName.js │ │ │ │ │ ├── testGetAriaRole.js │ │ │ │ │ ├── testGetAttribute.js │ │ │ │ │ ├── testGetCssProperty.js │ │ │ │ │ ├── testGetFirstElementChild.js │ │ │ │ │ ├── testGetId.js │ │ │ │ │ ├── testGetLastElementChild.js │ │ │ │ │ ├── testGetNextElementSibling.js │ │ │ │ │ ├── testGetPreviousElementSibling.js │ │ │ │ │ ├── testGetProperty.js │ │ │ │ │ ├── testGetRect.js │ │ │ │ │ ├── testGetShadowRoot.js │ │ │ │ │ ├── testGetTagName.js │ │ │ │ │ ├── testGetText.js │ │ │ │ │ ├── testGetValue.js │ │ │ │ │ ├── testIsActive.js │ │ │ │ │ ├── testIsEnabled.js │ │ │ │ │ ├── testIsPresent.js │ │ │ │ │ ├── testIsSelected.js │ │ │ │ │ ├── testIsVisible.js │ │ │ │ │ ├── testMoveTo.js │ │ │ │ │ ├── testRightClick.js │ │ │ │ │ ├── testSendKeys.js │ │ │ │ │ ├── testSetAttribute.js │ │ │ │ │ ├── testSetProperty.js │ │ │ │ │ ├── testSetValue.js │ │ │ │ │ ├── testShadowFind.js │ │ │ │ │ ├── testShadowFindAll.js │ │ │ │ │ ├── testSubmit.js │ │ │ │ │ ├── testTakeScreenshot.js │ │ │ │ │ ├── testUncheck.js │ │ │ │ │ ├── testUpdate.js │ │ │ │ │ └── testUpload.js │ │ │ │ └── window/ │ │ │ │ ├── testCloseWindow.js │ │ │ │ ├── testMaximizeWindow.js │ │ │ │ ├── testWindowCommands.js │ │ │ │ ├── testWindowCommandsW3C.js │ │ │ │ └── testWindowNamespaceCommands.js │ │ │ ├── expect/ │ │ │ │ ├── testExpectActive.js │ │ │ │ ├── testExpectAttribute.js │ │ │ │ ├── testExpectCookie.js │ │ │ │ ├── testExpectCount.js │ │ │ │ ├── testExpectCss.js │ │ │ │ ├── testExpectEnabled.js │ │ │ │ ├── testExpectPresent.js │ │ │ │ ├── testExpectProperty.js │ │ │ │ ├── testExpectSelected.js │ │ │ │ ├── testExpectText.js │ │ │ │ ├── testExpectTitle.js │ │ │ │ ├── testExpectType.js │ │ │ │ ├── testExpectUrl.js │ │ │ │ ├── testExpectValue.js │ │ │ │ └── testExpectVisible.js │ │ │ ├── expect-global/ │ │ │ │ ├── testElementAttribute.js │ │ │ │ ├── testElementCss.js │ │ │ │ ├── testElementIsDisplayed.js │ │ │ │ ├── testElementIsEnabled.js │ │ │ │ ├── testElementIsSelected.js │ │ │ │ ├── testElementProperty.js │ │ │ │ └── testElementText.js │ │ │ └── protocol/ │ │ │ ├── testActivity.js │ │ │ ├── testAlerts.js │ │ │ ├── testBrowserCommands.js │ │ │ ├── testChrome.js │ │ │ ├── testContext.js │ │ │ ├── testCookies.js │ │ │ ├── testDoubleClick.js │ │ │ ├── testElementCommands.js │ │ │ ├── testEnsure.js │ │ │ ├── testFirefox.js │ │ │ ├── testFrame.js │ │ │ ├── testFrameCommand.js │ │ │ ├── testGeolocation.js │ │ │ ├── testKeyboardInteraction.js │ │ │ ├── testKeys.js │ │ │ ├── testLog.js │ │ │ ├── testMouseButtonClick.js │ │ │ ├── testMoveTo.js │ │ │ ├── testOrientation.js │ │ │ ├── testReleaseMouseButton.js │ │ │ ├── testResetApp.js │ │ │ ├── testScreenshot.js │ │ │ ├── testSendKeys.js │ │ │ ├── testSession.js │ │ │ ├── testSetValue.js │ │ │ ├── testSource.js │ │ │ ├── testStatus.js │ │ │ ├── testSubmit.js │ │ │ ├── testTimeouts.js │ │ │ ├── testTitle.js │ │ │ ├── testUploadFile.js │ │ │ ├── testUrl.js │ │ │ ├── testWindowCommands.js │ │ │ ├── testWindowPosition.js │ │ │ ├── testWindowRect.js │ │ │ └── testWindowSize.js │ │ ├── apidemos/ │ │ │ ├── actions-api/ │ │ │ │ ├── asyncPerformActionsTest.js │ │ │ │ └── performActionsTest.js │ │ │ ├── angular-test/ │ │ │ │ └── runAngularDemo.js │ │ │ ├── appium/ │ │ │ │ └── testAppiumAPI.js │ │ │ ├── cdp/ │ │ │ │ └── testCdpCommands.js │ │ │ ├── chrome/ │ │ │ │ └── testChromeAPI.js │ │ │ ├── cookies/ │ │ │ │ └── testCookieCommands.js │ │ │ ├── custom-commands/ │ │ │ │ ├── testCustomCommandAutoInvoke.js │ │ │ │ ├── testCustomCommandSync.js │ │ │ │ ├── testCustomCommandsAsync.js │ │ │ │ ├── testCustomExecuteAsync.js │ │ │ │ └── testCustomPauseWithNamespacedAlias.js │ │ │ ├── elements/ │ │ │ │ ├── testElementGlobal.js │ │ │ │ ├── testFindElementsEs6CustomCommand.js │ │ │ │ └── testFindElementsWithPageCommand.js │ │ │ ├── ensure/ │ │ │ │ └── testEnsure.js │ │ │ ├── expect-global/ │ │ │ │ └── testExpect.js │ │ │ ├── firefox/ │ │ │ │ └── testFirefoxAPI.js │ │ │ ├── namespaced-api/ │ │ │ │ └── testNamespacedApi.js │ │ │ ├── navigation/ │ │ │ │ └── testNavigationCommands.js │ │ │ ├── page-objects/ │ │ │ │ └── testCommandsReturnType.js │ │ │ ├── relative-locators/ │ │ │ │ └── testWithRelativeLocators.js │ │ │ └── web-elements/ │ │ │ ├── testAssert.js │ │ │ ├── testElementApiWithPageObjects.js │ │ │ └── testWaitUntil.js │ │ ├── cli/ │ │ │ ├── testCliRunner.js │ │ │ ├── testCliRunnerGenerate.js │ │ │ ├── testCliRunnerMocha.js │ │ │ ├── testDemoTypescriptFile.js │ │ │ ├── testParallelExecution.js │ │ │ ├── testParallelExecutionExitCode.js │ │ │ └── testServiceCreationFromCli.js │ │ ├── core/ │ │ │ ├── testAsyncTree.js │ │ │ ├── testCreateSession.js │ │ │ ├── testPageObjectApi.js │ │ │ ├── testPageObjectCommands.js │ │ │ ├── testPageObjectCustomCommands.js │ │ │ ├── testPageObjectWaitForElementNotPresent.js │ │ │ ├── testPlugins.js │ │ │ ├── testQueue.js │ │ │ ├── testRequestWithCredentials.js │ │ │ └── testTreeNode.js │ │ ├── element/ │ │ │ ├── testCommandsElementSelectors.js │ │ │ ├── testElementCommands.js │ │ │ ├── testExpectElementSelectors.js │ │ │ ├── testIndexedElementSelectors.js │ │ │ ├── testPageObjectElementSelectors-use_xpath.js │ │ │ ├── testPageObjectElementSelectors.js │ │ │ └── testProtocolElementSelectors.js │ │ ├── globals.js │ │ ├── index/ │ │ │ ├── testDefaultPathPrefixOption.js │ │ │ ├── testMobileComponent.js │ │ │ ├── testNightwatchIndex.js │ │ │ ├── testProgrammaticApis.js │ │ │ ├── testRequest.js │ │ │ ├── testRequestTimeout.js │ │ │ ├── testRunProtocolAction.js │ │ │ ├── testSettings.js │ │ │ └── transport/ │ │ │ ├── testAppiumTransport.js │ │ │ ├── testBrowserstackTransport.js │ │ │ ├── testChromeOptions.js │ │ │ ├── testCreateTransport.js │ │ │ ├── testEdgeOptions.js │ │ │ ├── testFirefoxOptions.js │ │ │ ├── testIeOptions.js │ │ │ ├── testMobileOptions.js │ │ │ ├── testMobileWebSupport.js │ │ │ ├── testSafariOptions.js │ │ │ └── testSeleniumTransport.js │ │ ├── runner/ │ │ │ ├── cli/ │ │ │ │ └── testCliRunnerParallel.js │ │ │ ├── cucumber-integration/ │ │ │ │ ├── testCliArgs.js │ │ │ │ ├── testCucumberSampleTests.js │ │ │ │ └── testCucumberWithBrowserstack.js │ │ │ ├── mocha/ │ │ │ │ ├── testMochaRunnerAsync.js │ │ │ │ └── testMochaRunnerIntegration.js │ │ │ ├── testEventReporter.js │ │ │ ├── testInspectorExtension.js │ │ │ ├── testReporter.js │ │ │ ├── testRerunFunctionality.js │ │ │ ├── testRunBrowserstackTransport.js │ │ │ ├── testRunTestcase.js │ │ │ ├── testRunTestsuite.js │ │ │ ├── testRunTestsuiteWithSuiteRetries.js │ │ │ ├── testRunWithCommandErrors.js │ │ │ ├── testRunWithCustomCommands.js │ │ │ ├── testRunWithExclude.js │ │ │ ├── testRunWithExistingCommands.js │ │ │ ├── testRunWithExternalGlobals.js │ │ │ ├── testRunWithGlobalHooks.js │ │ │ ├── testRunWithGlobalReporter.js │ │ │ ├── testRunWithHooks.js │ │ │ ├── testRunWithMultipleSources.js │ │ │ ├── testRunWithServerErrors.js │ │ │ ├── testRunWithTags.js │ │ │ ├── testRunWithVerify.js │ │ │ ├── testRunner.js │ │ │ ├── testRunnerChaiExpect.js │ │ │ ├── testRunnerEndSessionOnFail.js │ │ │ ├── testRunnerEs6Async.js │ │ │ ├── testRunnerHtmlOutput.js │ │ │ ├── testRunnerJsonOutput.js │ │ │ ├── testRunnerJunitOutput.js │ │ │ ├── testRunnerMixedFiles.js │ │ │ ├── testRunnerRerunJsonOutput.js │ │ │ ├── testRunnerScreenshotsOutput.js │ │ │ ├── testRunnerSessionCreate.js │ │ │ ├── testRunnerTypeScript.js │ │ │ ├── testRunnerUncaughtErrors.js │ │ │ ├── testRunnerUnitTests.js │ │ │ ├── testRunnerUsingWithin.js │ │ │ ├── testRunnerWithAsyncAssertionFailure.js │ │ │ ├── testRunnerWithPageObjects.js │ │ │ ├── testRunnerWithTrace.js │ │ │ └── testTagsMatcher.js │ │ ├── service-builders/ │ │ │ ├── testAppiumServer.js │ │ │ ├── testChromeDriver.js │ │ │ ├── testEdgeDriver.js │ │ │ ├── testFirefoxDriver.js │ │ │ ├── testSafariDriver.js │ │ │ └── testSeleniumServer.js │ │ └── utils/ │ │ ├── __data__/ │ │ │ ├── meaning-of-life.js │ │ │ ├── meaning-of-life.mjs │ │ │ ├── mixed-exports.mjs │ │ │ └── named-exports.mjs │ │ ├── testRequireModule.js │ │ ├── testStackTrace.js │ │ └── testUtils.js │ ├── tsconfig.json │ └── typescript-tests/ │ ├── demo.ts │ └── tsconfig.nightwatch.json └── types/ ├── assertions.d.ts ├── chrome-options.d.ts ├── custom-assertion.d.ts ├── custom-command.d.ts ├── desired-capabilities.d.ts ├── expect.d.ts ├── globals.d.ts ├── index.d.ts ├── nightwatch-options.d.ts ├── page-object.d.ts ├── tests/ │ ├── actions.test-d.ts │ ├── appiumCommands.test-d.ts │ ├── chromiumClientCommands.test-d.ts │ ├── clientCommands.test-d.ts │ ├── component.test-d.ts │ ├── customCommands.test-d.ts │ ├── describe.test-d.ts │ ├── elementCommands.test-d.ts │ ├── expect.test-d.ts │ ├── globalElementApi.test-d.ts │ ├── index.test-d.ts │ ├── namespaceCommands.test-d.ts │ ├── page-object.test-d.ts │ ├── programmaticApi.test-d.ts │ ├── tsconfig.json │ ├── webElement.test-d.ts │ └── webdriverProtocolCommands.test-d.ts ├── utils.d.ts └── web-element.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules package.json *.json lib/utils/requireModule.js lib/reporter/reporters/html dist ================================================ FILE: .eslintrc ================================================ { "root": true, "extends": [ "eslint:recommended" ], "parserOptions": { "ecmaVersion": 13, "sourceType": "module", "ecmaFeatures": { "jsx": false } }, "env": { "mocha": true, "node": true }, "overrides": [ { "files": [ "**/*.ts" ], "excludedFiles": [ "test/**/*.ts" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "parserOptions": { "ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": { "jsx": false }, "project": "./tsconfig.json" } } ], "rules": { "eqeqeq": [ "error", "smart" ], "no-extra-boolean-cast": 0, "quotes": [ "error", "single" ], "curly": [ "error", "all" ], "no-console": [ "error", { "allow": [ "error" ] } ], "no-debugger": 1, "semi": [ "error", "always", { "omitLastInOneLineBlock": true } ], "no-trailing-spaces": 1, "no-else-return": 2, "no-extra-bind": 0, "no-implicit-coercion": 0, "no-useless-call": 0, "no-return-assign": 0, "eol-last": 1, "no-unused-vars": 0, "no-extra-semi": 0, "comma-dangle": 2, "no-underscore-dangle": 0, "no-lone-blocks": 0, "array-bracket-spacing": 2, "object-curly-spacing": 2, "brace-style": [ 2, "1tbs", { "allowSingleLine": true } ], "comma-spacing": 2, "comma-style": 2, "key-spacing": 2, "one-var": [ "error", "never" ], "semi-style": [ "error", "last" ], "space-in-parens": [ "error", "never" ], "keyword-spacing": [ 2, { "before": true, "after": true } ], "space-infix-ops": 1, "padding-line-between-statements": [ "error", { "blankLine": "always", "prev": "*", "next": "return" } ], "indent": [ "error", 2, { "SwitchCase": 1 } ], "prefer-const": [ "warn" ] }, "globals": { "Promise": true, "Proxy": true, "Set": true, "Reflect": true, "element": "readonly", "by": "readonly", "expect": "readonly", "browser": "readonly", "app": "readonly", "Key": "readonly" } } ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: nightwatch ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: "Bug Report" description: "File a bug report to help us improve Nightwatch." body: - type: "markdown" attributes: value: | Thanks in advance for your contribution. Before submitting a new issue, try searching for a similar one here: https://github.com/nightwatchjs/nightwatch/search?type=Issues. - type: "textarea" id: "description" attributes: label: "Description of the bug/issue" description: "A brief description of the issue, how to reproduce it, and the expected behavior." placeholder: | When I ____, I expected ____ to happen but ____ happened instead. validations: required: true - type: "textarea" id: "steps" attributes: label: "Steps to reproduce" description: | Explain how to cause the issue in the provided reproduction. value: | 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error - type: textarea id: "sample-test" attributes: label: "Sample test" description: Include a sample test that reproduces the problem you're experiencing. If possible, the test should be against a public URL. Please add the test and other info inline, not as attachments or screenshots. placeholder: | // Please add the sample test here module.exports = { sampleTest: function() { browser .navigateTo('') .click('') .end(); } } render: node - type: textarea id: "run-with-command" attributes: label: "Command to run" description: "Include the command used to run the test." placeholder: | nightwatch test/sampleTest.js --your-other-arguments-here render: bash - type: textarea id: "verbose-output" attributes: label: "Verbose Output" description: | "Extended nightwatch command logging during the session. Please use nightwatch --verbose and paste the output here" placeholder: | [Sample Test] Test Suite ──────────────────────────────────────────────────────── ⠋ Starting ChromeDriver on port 9515... Request POST /session { desiredCapabilities: { browserName: 'chrome', 'goog:chromeOptions': { w3c: true, args: [] }, name: 'Sample Test' }, capabilities: { alwaysMatch: { browserName: 'chrome', 'goog:chromeOptions': { w3c: true, args: [] } } } render: fundamental - type: textarea id: "configuration" attributes: label: "Nightwatch Configuration" description: "Add your nightwatch.json or nightwatch.conf.js here; Make sure to leave out any sensitive details." placeholder: | module.exports = { src_folders: [], page_objects_path: [], custom_commands_path: [], custom_assertions_path: '', plugins: [], globals_path: '', webdriver: {}, test_settings: {} } render: node - type: "markdown" attributes: value: | ## Your Environment Include the relevant details related to your environment. - type: "input" id: "nightwatch-version" attributes: label: "Nightwatch.js Version" description: "The version of Nightwatch you are using. Run: nightwatch --version" placeholder: "2.3.0" validations: required: true - type: "input" id: "node-version" attributes: label: "Node Version" description: "The version of Node you are using." placeholder: "14.0.0" - type: "input" id: "browser" attributes: label: "Browser" description: "The browser(s) this issue occurred with." placeholder: "Firefox 102.0.1; Chrome 104.0.0" - type: "input" id: "operating-system" attributes: label: "Operating System" description: "The operating system(s) this issue occurred with." placeholder: "MacOs Monterey 12.5" - type: "textarea" id: "additional-information" attributes: label: "Additional Information" description: | Use this section to provide any additional information you might have like screenshots, notes, or links to ideas. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/nightwatchjs/nightwatch/discussions about: Ask questions and discuss topics with other community members - name: Documentation Request url: https://github.com/nightwatchjs/nightwatch-docs/issues/new about: Request for documentation to be added/altered - name: Chat with other community members. Ask support questions, discuss news and feature requests. url: https://discord.gg/x5RuXc6Z about: The official Nightwatchjs Discord community ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: "Feature Request" description: "Request a feature or enhancement for Nightwatch" labels: ["enhancement"] body: - type: "markdown" attributes: value: | Thanks in advance for your contribution. Before requesting a new feature, try searching for a similar issue here: https://github.com/nightwatchjs/nightwatch/search?type=Issues. - type: "textarea" id: "description" attributes: label: "Description" description: "Please describe your request in one or two sentences." validations: required: true - type: "textarea" id: "proposed-solution" attributes: label: "Suggested solution" description: | Please provide code snippets, gists, or links to the ideal design or API. - type: "textarea" id: "alternatives" attributes: label: "Alternatives / Workarounds" description: | What alternative solutions have you considered before making this request? - type: "textarea" id: "additional-information" attributes: label: "Additional Information" description: | What resources (links, screenshots, etc.) do you have to assist this effort? ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Thanks in advance for your contribution. Please follow the below steps in submitting a pull request, as it will help us with reviewing it quicker. - [ ] Before marking your PR for review, please test and verify your changes by making appropriate modifications to any of the Nightwatch example tests (present in `examples/tests` directory of the project) and running them. `ecosia.js` and `duckDuckGo.js` are good examples to work with. - [ ] Create a new branch from master (e.g. `features/my-new-feature` or `issue/123-my-bugfix`); - [ ] If you're fixing a bug also create an issue if one doesn't exist yet; - [ ] If it's a new feature explain why do you think it's necessary. Please check with the maintainers beforehand to make sure it is something that we will accept. Usually we only accept new features if we feel that they will benefit the entire community; - [ ] Please avoid sending PRs which contain drastic or low level changes. If you are certain that the changes are needed, please discuss them beforehand and indicate what the impact will be; - [ ] If your change is based on existing functionality please consider refactoring first. Pull requests that duplicate code will most likely be ignored; - [ ] Do not include changes that are not related to the issue at hand; - [ ] Follow the same coding style with regards to spaces, semicolons, variable naming etc.; - [ ] Always add unit tests - PRs without tests are most of the times ignored. ================================================ FILE: .github/label-commenter-config.yml ================================================ # Configuration for Label Commenter - https://github.com/peaceiris/actions-label-commenter labels: - name: needs-triaging labeled: issue: body: | @{{ issue.user.login }}, thank you for creating this issue. We will troubleshoot it as soon as we can. ---
Info for maintainers

Triage this issue by using labels.

If information is missing, add a helpful comment and add the I-issue-template label.

Thank you!

- name: I-issue-template labeled: issue: body: | Hi, @{{ issue.user.login }}. Please follow the issue template, we need more information to reproduce the issue. Either a complete code snippet (if more than one file is needed, provide a GitHub repo and instructions to run the code), the specific versions used, or a more detailed description to help us understand the issue. Reply to this issue when all information is provided, thank you. ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 90 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - META - pinned - enhancement # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had any recent activity. If possible, please retry using the latest Nightwatch version and update the issue with any relevant details. If no further activity occurs, it will be closed. Thank you for your contribution. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/build-node.yaml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: tests on: push: branches: [ main] pull_request: branches: [ main] jobs: linux: runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: [18.x, 20.x, 22.x, 24.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run eslint - run: npm test windows: runs-on: windows-latest strategy: fail-fast: false matrix: node-version: [18.x, 20.x, 22.x, 24.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run eslint - run: npm test ================================================ FILE: .github/workflows/component-tests.yaml ================================================ name: component-tests on: push: branches: [ main] pull_request: branches: [ main] jobs: linux: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - run: npm ci # - run: npm i @nightwatch/react @nightwatch/vue @testing-library/dom vite-plugin-nightwatch vue react react-dom # - run: npm run component-tests ================================================ FILE: .github/workflows/coverage.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Coverage Job on: push: branches: [ ] pull_request: branches: [ ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run eslint - run: npm run mocha-coverage - run: npm run coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_ACCESS_TOKEN }} files: ./coverage/mocha_coverage.lcov flags: unittests name: codecov-umbrella fail_ci_if_error: true path_to_write_report: codecov_report.txt verbose: false ================================================ FILE: .github/workflows/label-commenter.yml ================================================ # Configuration for Label Commenter - https://github.com/peaceiris/actions-label-commenter name: Label Commenter on: issues: types: [ labeled ] permissions: contents: read issues: write jobs: comment: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Label Commenter uses: peaceiris/actions-label-commenter@v1 ================================================ FILE: .github/workflows/missing-types-comment.yml ================================================ name: Add comment for missing types on: workflow_run: workflows: ["Check missing types"] types: - completed jobs: add_comment: runs-on: ubuntu-latest if: > github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - name: 'Download artifact' uses: actions/github-script@v6 with: script: | 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.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: matchArtifact.id, archive_format: 'zip', }); var fs = require('fs'); fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data)); - name: 'Read artifact' id: modified-files run: | unzip pr.zip cat files_changed echo "modified=$(cat files_changed)" >> "$GITHUB_OUTPUT" - name: Comment on PR if: steps.modified-files.outputs.modified == '' id: comment-on-pr uses: actions/github-script@v6 with: script: | const fs = require('fs'); const issue_number = Number(fs.readFileSync('./NR')); const comment = ` ## Status * ❌ No modified files found in the **types** directory. Please make sure to include types for any changes you have made. Thank you!.`; const pullRequest = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: issue_number }); const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number }); const existingComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.includes('No modified files found in the **types** director')); if (existingComment) { console.log('Comment already exists'); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, body: comment }); } - name: Edit comment on PR if: steps.modified-files.outputs.modified != '' uses: actions/github-script@v6 with: script: | const fs = require('fs'); const issue_number = Number(fs.readFileSync('./NR')); const comment = ` ## Status * :white_check_mark: Type files updated!`; const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number }); const existingComment = comments.data.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.includes('No modified files found in the **types** director')); if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, body: comment }); } ================================================ FILE: .github/workflows/missing-types.yml ================================================ name: Check missing types on: pull_request jobs: detect-modified-files: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 with: fetch-depth: 0 - name: Get modified files id: modified-files run: | echo $(git diff --name-only origin/$GITHUB_BASE_REF $GITHUB_SHA types/ | tr '\n' ',') mkdir -p ./pr 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@v4 with: name: pr path: pr/ ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Nightly on: workflow_dispatch: inputs: version: description: Nightly version required: false type: string default: '' ref: description: Branch, tag, or SHA to release required: false type: string default: '' jobs: nightly: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: ref: ${{ inputs.ref }} - name: Setup Node uses: actions/setup-node@v3 with: node-version: '18.x' - name: Install dependencies run: npm install - name: Build Nightwatch run: npm run build - name: Create publishable package 📦 run: npm pack - name: Release Nightly uses: marvinpinto/action-automatic-releases@latest with: repo_token: "${{ secrets.GITHUB_TOKEN }}" automatic_release_tag: "nightly-${{ inputs.version }}" title: "Nightly ${{ inputs.version }}" prerelease: true files: '*.tgz' ================================================ FILE: .github/workflows/publish.yml ================================================ 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 }} ================================================ FILE: .github/workflows/type-declaration-tests.yml ================================================ name: Type Declaration Tests on: push: branches: [main, feat/types-test] pull_request: branches: [main] jobs: linux: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x] typescript-version: [3.9, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5.0] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Install TypeScript run: npm install -g typescript@${{ matrix.typescript-version }} - name: Install Dependencies run: npm ci - run: npm run test:types ================================================ FILE: .gitignore ================================================ .project .DS_Store *.vim *.idea *.vscode *.log *.env /nightwatch.json /nightwatch.conf.js node_modules dist npm-debug.log lib-cov coverage.html selenium-debug.log phantomjsdriver.log tests_output *~ \#* \.#* output /test/hooks_output /bin/geckodriver* /coverage/ /.nyc_output/ screenshot.png /sample /screens /logs html-report/ test-results.xml chromedriver-mobile/ *.sw[po] snapshot.html ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx lint-staged ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, gender, gender identity and expression, sex characteristics, disability, ethnicity, level of experience, education, socio-economic status, nationality, personal appearance, body size, race, religion (or lack thereof), or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language - please read the [inclusive language guidance](https://www.uua.org/lgbtq/welcoming/ways/200008.shtml) from the UUA * Being respectful of differing viewpoints and experiences * Showing empathy towards other community members * Gracefully accepting constructive criticism * Exercising consideration and respect in your speech and actions * Attempting collaboration before conflict or criticism Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, spamming, intentionally disrupting conversations, making insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing or threatening to publish others' private information, such as a physical or electronic address, without explicit permission * Advocating for, or encouraging, any of the above behavior * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@pineview.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. It's inspired among other things by: - [Citizen Code of Conduct](http://citizencodeofconduct.org/) - [Appium](https://github.com/appium/appium/blob/master/CONDUCT.md) [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Nightwatch Contributions to Nightwatch are always welcome but please try to follow these guidelines when sending in something as it will help addressing the issue quicker and more smoothly. __Please do not ask for assistance in the Issues list.__ Join our [Discord server](https://discord.com/invite/SN8Da2X) to ask questions and seek help. Assistance requests posted in the Issues list are usually closed right away. ## Code of Conduct Contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). ## Submit an issue Before submitting a new issues, try searching for a similar one here: https://github.com/nightwatchjs/nightwatch/search?type=Issues and add your scenario there and anything else which you think will help with fixing it; If you are filing a bug report, regression issue or what it appears to be strange behaviour, please follow the steps below: 1. Please write an elaborate title which explains the problem as accurate as possible For instance: - __not helpful__: "Issue with tags" - __much better__: "Tags don't work when combined with --skiptags option" 2. Include a sample test which reproduces the problem you're experiencing. The test should be against a __public url__. The test and other info should be posted inline, attachments will be ignored; 3. Include the verbose output, if possible (run nightwatch with `--verbose` argument); 4. Include your configuration (try to leave out the irrelevant bits); 5. Also include: Nightwatch version, Node.js version, OS version and Webdriver/Selenium Server version; 6. Please try not to report issues you have with individual browser drivers which cannot or should not be solved in Nightwatch. ## Requesting a feature Feature requests are welcome. 1. Indicate in the issue title that it is a feature/enhancement request; 2. Explain the use case and include a sample test case and/or usage, if possible; 3. Try to submit enhancements that you cannot build with custom commands/assertions and something that will benefit the community; 4. Same as for issues, add your comments/vote to an existing feature request if you'd like to see it implemented. ## Submitting a pull request Thanks in advance for your contribution. 1. Follow the usual git workflow for submitting a pull request: - Fork and clone the project. - Create a new branch from main (e.g. `features/my-new-feature` or `issue/123-my-bugfix`). - Add your changes. - Try to run some Nightwatch example tests locally to test the functionality after making your changes: `node ./bin/nightwatch examples/tests/ecosia.js --env chrome`. - Run Nightwatch unit tests locally: `npm test`; Or, run individual tests: `npx mocha test/src/api/commands/client/testWaitUntil.js`. - Commit your changes and create a pull request. __Note:__ When running Nightwatch example test for the first time, if you encounter _"Cannot read source"_ error, go to `nightwatch.conf.js` file and set 'page_objects_path' and 'custom_commands_path' configs to `[]`. 2. If you're fixing a bug, also create an issue if one doesn't exist yet. 3. If it's a new feature/enhancement, explain why do you think it's necessary. 4. If your change include drastic or low level changes please discuss them to make sure they will be accepted and what the impact will be. 5. If your change is based on existing functionality, please consider refactoring first. Pull requests that duplicate code will not make it in very quick, if at all. 6. Do not include changes that are not related to the issue at hand. 7. Follow the same coding style with regards to spaces, semicolons, variable naming etc. 8. Always add tests - after all this _is_ a testing framework. ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2014-2021, Pine View Software AS. https://pineview.io Copyright (c) 2022, BrowserStack Limited. https://www.browserstack.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Nightwatch.js [![npm](https://img.shields.io/npm/v/nightwatch.svg)](https://www.npmjs.com/package/nightwatch) [![Node.js CI](https://github.com/nightwatchjs/nightwatch/actions/workflows/build-node.yaml/badge.svg?branch=main)](https://github.com/nightwatchjs/nightwatch/actions/workflows/build-node.yaml) [![codecov](https://codecov.io/gh/nightwatchjs/nightwatch/branch/main/graph/badge.svg?token=MSObyfECEh)](https://codecov.io/gh/nightwatchjs/nightwatch) [![Discord][discord-badge]][discord]

Nightwatch.js Logo

#### [Homepage](https://nightwatchjs.org) • [Developer Guide](https://nightwatchjs.org/guide) • [API Reference](https://nightwatchjs.org/api) • [About](https://nightwatchjs.org/about) • [Blog](https://nightwatchjs.org/blog) Nightwatch is an integrated testing framework powered by Node.js and using the [W3C Webdriver API](https://www.w3.org/TR/webdriver/). It is a complete testing solution developed at [BrowserStack](https://www.browserstack.com/) and which can be used for: ☑️ end-to-end testing of web applications and websites ☑️ component testing in isolation (React / Vue / Storybook / Angular) ☑️ Node.js unit, visual regression testing, accessibility testing & API testing ☑️ Native mobile app testing on Android & iOS ## 🚀 Nightwatch v3 [What's New](https://nightwatchjs.org/guide/overview/whats-new-in-v3.html) | [Release Notes](https://github.com/nightwatchjs/nightwatch/releases/tag/v3.0.1) | [Discussions](https://github.com/nightwatchjs/nightwatch/discussions) Nightwatch v3 is an all new generation that has been built around these three pillars: * **Developer Experience** : The entire experience from getting started, to writing and debugging tests, has been redesigned for speed, stability, and consistent non-flaky results. * **Mobile first**: Test your web or native, iOS and Android, mobile applications on simulators, real mobile devices or a cloud grid like BrowserStack. * **One test automation framework**: Run all types of tests from unit, component, and E2E to API, visual, and accessibility with a single framework. The [Nightwatch v3](https://github.com/nightwatchjs/nightwatch/releases/tag/v3.0.1) is not just a new version, it’s the result of months of engineering effort to reimagine test automation for the future. Try it out in 60 seconds and see it in action. ## ⚙️ Get started in 60 seconds #### 1. Install Nightwatch from NPM From your existing project's root dir: ```sh npm init nightwatch@latest ``` or, if you want to initialize a new project: ```sh npm init nightwatch@latest ./path/to/new/project ``` ![nightwatch-cli-gif](https://user-images.githubusercontent.com/39924567/174841680-59664ff6-da2d-44a3-a1df-52d22c69b1e2.gif) #### 2. Answer a few questions about your preferred setup: - What is your Language - Test Runner setup? - Where do you want to run your e2e tests? - Where you'll be testing on? - Where do you plan to keep your end-to-end tests? - What is the base_url of your project? Nightwatch will do the entire setup for you based on your answers. #### 3. Run a Demo Test: Nightwatch comes with a few examples, which are automatically copied to your Nightwatch project during the setup and can also be used as boilerplate to write your own tests on top of them. You can follow the instructions given at the end of the setup to run your first test with Nightwatch. image --- ## Nightwatch mobile app testing Nightwatch enables automation testing of native mobile applications via Appium. It combines the robustness of Appium with the enhanced developer experience provided by Nightwatch. It enables end-to-end functional testing of native mobile apps on Android and iOS devices. Try it now ## Go beyond E2E ### [Component testing](https://nightwatchjs.org/guide/component-testing/introduction.html) With Nightwatch you can test components in isolation by mounting them in the browser. Nightwatch 2 added support for component testing for popular web frameworks such as 1. [React](https://nightwatchjs.org/guide/component-testing/testing-react-components.html) 2. [VueJS](https://nightwatchjs.org/guide/component-testing/vite-plugin.html) 3. [Angular](https://nightwatchjs.org/guide/component-testing/angular-component-testing.html) 4. [Storybook](https://nightwatchjs.org/guide/component-testing/storybook-component-testing.html) ### Nightwatch unit tests The tests for Nightwatch are written using Mocha. 1. **Clone the project** ```bash git clone https://github.com/nightwatchjs/nightwatch.git # change directory cd nightwatch # install the dependencies npm install ``` 2. **Run tests** To run the complete test suite: ```bash npm test ``` To check test coverage, run the command: ```bash npm run mocha-coverage ``` and then open the generated coverage/index.html file in your browser. See [Unit testing guide](https://nightwatchjs.org/guide/writing-tests/write-nodejs-unit-integration-tests.html) for more details. ### Other types of testing #### [Visual Regression Testing](https://nightwatchjs.org/guide/writing-tests/visual-regression-testing.html) Nightwatch v3 introduces visual regression testing as an in-house plugin. The plugin takes care of 1. Capturing screenshots 2. Comparison with baseline to highlight visual differences 3. Report to review the differences 4. Approve the changes VRT can be done on real desktop & mobile browsers. Also, VRT can be run on components as part of component testing as well. #### [API Testing](https://nightwatchjs.org/guide/writing-tests/api-testing.html) API testing is now available with Nightwatch v3. The following functionality can be achieved with API testing 1. Request assertions 2. Response assertions 3. Viewing API tests in the HTML report 4. Mock server #### [Accessibility Testing](https://nightwatchjs.org/guide/using-nightwatch/accessibility-testing.html) Nightwatch v3 packages the aXe-core package developed by Deque Systems as a plugin. It enables 90 different types of accessibility tests for WCAG compliance. ## 🦉 About Nightwatch Nightwatch was initially built by [@pineviewlabs](https://github.com/pineviewlabs/) - an independent software consultancy based in Oslo, Norway, with help from [contributors](https://github.com/nightwatchjs/nightwatch/graphs/contributors). In mid 2021, Nightwatch has become a part of the [@BrowserStack](https://github.com/browserstack) family and it is being developed further at the BrowserStack Open-source Program Office. Read more on [our blog](https://nightwatchjs.org/blog/nightwatch-has-joined-the-browserstack-family.html). ## Contributing We welcome any and all contributions from the community which can help improve Nightwatch. Please check out [CONTRIBUTING.md](CONTRIBUTING.md) for more extensive contributing guidelines. ## Licence [MIT](https://github.com/nightwatchjs/nightwatch/blob/main/LICENSE.md) [discord-badge]: https://img.shields.io/discord/618399631038218240.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square&label=discord [discord]: https://discord.gg/SN8Da2X ================================================ FILE: api/README.md ================================================ # Nightwatch API Commands Public commands api exported by Nightwatch in order to be extended upon from outside. **Example:** ```js const {Quit: quit} = require('nightwatch/api'); module.exports = CustomQuit extends Quit { async command(cb = function(r) {return r}) { console.log('from custom quit command'); await super.command(cb); } } ``` **This is a work in progress.** ## Element commands: - ### Finding elements: - [findElement](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/findElement.js) - [findElements](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/findElements.js) - [element](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/protocol/element.js) - [elements](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/protocol/elements.js) - [elementIdElement](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/protocol/elementIdElement.js) - [elementIdElements](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/protocol/elementIdElements.js) - [waitForElementPresent](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/waitForElementPresent.js) - [waitForElementNotPresent](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/waitForElementNotPresent.js) - [waitForElementVisible](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/waitForElementVisible.js) - [waitForElementNotVisible](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/waitForElementNotVisible.js) - ### Element interaction: - [clearValue](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/clearValue.js) - [click](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/click.js) - [moveToElement](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/moveToElement.js) - [setValue](https://github.com/nightwatchjs/nightwatch/blob/main/lib/api/element-commands/setValue.js) - ### Element state: - ### Element location: - ### Element location: ## Document handling: ## Session related: ## Navigation: ## Window related: ## Frames: ## User actions: ## User prompts: ## Mobile related: ## Utilities & Debugging: ================================================ FILE: api/index.js ================================================ const path = require('path'); const exportedCommands = [ 'element-commands/findElement.js', 'element-commands/findElements.js', 'element-commands/element.js', 'element-commands/elements.js', 'element-commands/elementIdElement.js', 'element-commands/elementIdElements.js', 'element-commands/waitForElementPresent.js', 'element-commands/waitForElementNotPresent.js', 'element-commands/waitForElementVisible.js', 'element-commands/waitForElementNotVisible.js', 'protocol/quit.js' ]; const basePath = '../lib/api'; const Commands = {}; const props = exportedCommands.reduce((prev, fileName) => { const commandName = fileName.substring(fileName.lastIndexOf('/') + 1).replace('.js', ''); prev[commandName] = { configurable: true, get: function() { return require(path.join(basePath, fileName)); } }; return prev; }, {}); Object.defineProperties(Commands, props); module.exports = Commands; ================================================ FILE: bin/.gitignore ================================================ selenium-server-standalone-* chromedriver chromedriver-* phantomjs IEDriverServer_* ================================================ FILE: bin/nightwatch ================================================ #!/usr/bin/env node const semver = require('semver'); const {Logger} = require('../lib/utils'); const requiredVersion = require('../package.json').engines.node; function checkNodeVersion (wanted, id) { if (!semver.satisfies(process.version, wanted)) { Logger.error('You are using Node ' + process.version + ', but this version of ' + id + ' requires Node ' + wanted + '.\nPlease upgrade your Node version.' ); process.exit(1); } } checkNodeVersion(requiredVersion, 'nightwatch'); require('./runner.js'); ================================================ FILE: bin/runner.js ================================================ /** * Module dependencies */ const Nightwatch = require('../lib/index.js'); const {Logger, shouldReplaceStack, alwaysDisplayError} = require('../lib/utils'); try { Nightwatch.cli(function (argv) { argv._source = argv['_'].slice(0); const runner = Nightwatch.CliRunner(argv); return runner .setupAsync() .catch((err) => { if (err.code === 'ERR_REQUIRE_ESM') { err.showTrace = false; } throw err; }) .then(() => runner.runTests()) .catch((err) => { if (!err.displayed || (alwaysDisplayError(err) && !err.displayed)) { Logger.error(err); } runner.processListener.setExitCode(10).exit(); }); }); } catch (err) { const {message} = err; err.message = 'An error occurred while trying to start the Nightwatch Runner:'; err.showTrace = !shouldReplaceStack(err); err.detailedErr = message; Logger.error(err); process.exit(2); } ================================================ FILE: bin/show_survey.js ================================================ #!/usr/bin/env node // eslint-disable-next-line console.log('\t 👋 Hey there! Thanks for installing Nightwatch. \n\n\tPlease let us know what features you\'d like to see \n\tin Nightwatch v2.0 by taking this quick survey: \n\n\t\thttps://forms.gle/zBhbjdsDE77hTHSB7\n'); ================================================ FILE: codecov.yml ================================================ codecov: require_ci_to_pass: yes coverage: precision: 2 round: down range: "70...100" parsers: gcov: branch_detection: conditional: yes loop: yes method: no macro: no comment: false ================================================ FILE: examples/.gitignore ================================================ reports screens ================================================ FILE: examples/cucumber-js/README.md ================================================ # Using Cucumber.js with Nightwatch 2 Nightwatch 2 brings integrated support for using [Cucumber.js](https://cucumber.io/) directly as an alternative test runner. No other plugins are necessary, other than the [Cucumber library](https://www.npmjs.com/package/@cucumber/cucumber) itself (version 7.3 or higher). Simply run the following in the same project where Nightwatch is also installed: ```sh $ npm i @cucumber/cucumber --save-dev ``` ## Configuration ```js { test_runner: { // set cucumber as the runner type: 'cucumber', // define cucumber specific options options: { //set the feature path feature_path: 'examples/cucumber-js/*/*.feature', // start the webdriver session automatically (enabled by default) auto_start_session: true, // use parallel execution in Cucumber parallel: 2 // set number of workers to use (can also be defined in the cli as --parallel 2 } }, src_folders: ['examples/cucumber-js/features/step_definitions'] } ``` ## CLI Options | option | description | |------------------------|------------------------------| | `--dry-run` | Do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/dry_run.md) | `--name` | Specify a scenario by its name matching a regular expression. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#running-specific-features) | `--tags` | Use tags to run specific scenario or features. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#tags) | `--require` | Use `--require ` to explicitly require support files before executing the features. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#requiring-support-files) | `--format` | Use `--format ` to specify the format of the output. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#formats) | `--format-options` | Many formatters, including the built-in ones, support some configurability via options. You can provide this data as a JSON literal via the --format-options CLI option. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/formatters.md#options) | `--fail-fast` | Abort the run on first failure (default: false). [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#--fail-fast) | `--retries` | Use `--retries ` to have Cucumber attempt it multiple times until either it passes or the maximum number of attempts is reached. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/retry.md#retry) | `--retry-tag-filter` | Use `--retry-tag-filter` to retry failed scenarios based on tags. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/retry.md#targeting-scenarios) | `--require-module` | [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/cli.md#transpilation) | `--no-strict` | By default, cucumber runner works in strict mode, meaning it will fail if there are pending steps. | `--parallel` | Use `--parallel ` to run your scenarios in parallel. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/parallel.md#parallel) | `--profile` | As of now only `cucumber.js` is considered while picking up profiles. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/profiles.md) | `--world-parameters` | Provide this data as a JSON literal via the `--world-parameters`. [read more](https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/world.md#world-parameters) ## Running Cucumber spec files/step definition files can be provided in `src_folders` in Nightwatch config or as a CLI argument. With `src_folders` defined: ```sh $ npx nightwatch ``` Without `src_folders` defined: ```sh $ npx nightwatch examples/cucumber-js/features/step_definitions ``` Parallel running using 2 workers: ```sh $ nightwatch examples/cucumber-js/features/step_definitions --parallel 2 ``` Use other [test runner options](https://nightwatchjs.org/guide/running-tests/command-line-options.html) as usual: ```sh $ npx nightwatch examples/cucumber-js/features/step_definitions --headless ``` ## Disable the automatic session start You might need sometimes to not start the Webdriver session automatically after Nightwatch is instantiated. For this purpose, Nightwatch provides the instance available as `this.client`, which contains an `launchBrowser()` method. ### Configuration: ```js test_runner: { type: 'cucumber', options: { feature_path: 'examples/cucumber-js/*/*.feature', auto_start_session: false } } ``` You can then use an extra setup file that you can pass as an extra `--require` to Nightwatch, which will be forwarded to Cucumber. In the extra setup file, you can add other operations needed to be executed before the session is started. #### Example _extra_setup.js: Remember to set the `browser` on `this` so it can be closed automatically by Nightwatch. Otherwise, remember to call `.quit()` in your own Cucumber `After()` hooks. ```js const {Before} = require('@cucumber/cucumber'); Before(async function(testCase) { if (!this.client) { console.error('Nightwatch instance was not created.'); return; } this.client.updateCapabilities({ testCap: 'testing' }); this.browser = await this.client.launchBrowser(); }); ``` #### Nightwatch setup file for Cucumber You might also want to inspect the built-in setup file that Nightwatch uses for initializing the Cucumber runner. It is available in our project root folder at [/cucumber-js/_setup_cucumber_runner.js](https://github.com/nightwatchjs/nightwatch/blob/v2/cucumber-js/_setup_cucumber_runner.js). #### Run with extra setup: ```sh $ nightwatch examples/cucumber-js/features/step_definitions --require {/full/path/to/_extra_setup.js} ``` ## Reporting When using the integrated Cucumber test runner, you need to use the Cucumber [formatters](https://github.com/cucumber/cucumber-js/blob/main/docs/formatters.md) for generating output. Nightwatch reporters (like JUnit XML reports or the [global custom reporter](https://nightwatchjs.org/guide/extending-nightwatch/custom-reporter.html)) are not available. The main reason is that reporting is delegated to the Cucumber CLI. You can also [write your own](https://github.com/cucumber/cucumber-js/blob/main/docs/custom_formatters.md) Cucumber formatter. Nightwatch will forward `--format` and `--format-options` CLI arguments, if present, to Cucumber. By default, the `progress` formatter is used. Here's how the output looks like when running the example tests in Firefox: ```sh ℹ Connected to GeckoDriver on port 4444 (1740ms). Using: firefox (92.0.1) on MAC (20.6.0). .. ✔ Testing if the page title equals 'Rijksmuseum Amsterdam, home of the Dutch masters' (4ms) . ✔ Element <#rijksmuseum-app> was visible after 46 milliseconds. . ✔ Testing if element <.search-results> contains text 'Operation Night Watch' (1994ms) ... ✔ Testing if the page title equals 'Rijksmuseum Amsterdam, home of the Dutch masters' (8ms) . ✔ Element <#rijksmuseum-app> was visible after 49 milliseconds. . ✔ Testing if element <.search-results> contains text 'The Night Watch, Rembrandt van Rijn, 1642' (1427ms) . 2 scenarios (2 passed) 10 steps (10 passed) 0m13.024s (executing steps: 0m12.998s) ``` ================================================ FILE: examples/cucumber-js/features/nightwatch.feature ================================================ Feature: Google Search Background: Background name Given I open the Rijksmuseum page And I dismiss the cookie dialog Then the title is "Rijksmuseum Amsterdam, home of the Dutch masters" @a @b Scenario: Searching the Rijksmuseum Given I search "night watch" Then Body contains "Operation Night Watch" @a @b Scenario: Searching the Rijksmuseum-1 Given I search "night watch" Then Body contains "The Night Watch, Rembrandt van Rijn, 1642" ================================================ FILE: examples/cucumber-js/features/step_definitions/nightwatch.js ================================================ const {Given, Then, When, Before} = require('@cucumber/cucumber'); Given(/^I open the Rijksmuseum page$/, function() { return browser.navigateTo('https://www.rijksmuseum.nl/en'); }); Given(/^I dismiss the cookie dialog$/, async function() { const cookieDialogVisible = await browser.isVisible({ selector: '.cookie-consent-bar-wrap', suppressNotFoundErrors: true }); if (cookieDialogVisible) { return browser.click('.cookie-consent-bar-wrap button.link'); } }); Given(/^I search "([^"]*)"$/, async function(searchTerm) { // FIXME: chaining the click command to the rest of the commands causes an uncaughtRejection in case of an element locate error await browser.pause(1000).click('a[aria-label="Search"]'); return browser.waitForElementVisible('#rijksmuseum-app') .setValue('input.search-bar-input[type=text]', [searchTerm, browser.Keys.ENTER]) .pause(1000); }); Then(/^the title is "([^"]*)"$/, function(title) { return browser.assert.titleEquals(title); }); Then(/^Body contains "([^"]*)"$/, function(contains) { return browser.assert.textContains('.search-results', contains); }); ================================================ FILE: examples/custom-assertions/testCustomAssertion.js ================================================ const util = require('util'); exports.assertion = function(selector, attribute, expected, msg) { let DEFAULT_MSG = 'Testing if attribute %s of <%s> contains "%s".'; let MSG_ELEMENT_NOT_FOUND = `${DEFAULT_MSG} Element could not be located.`; let MSG_ATTR_NOT_FOUND = `${DEFAULT_MSG} Element does not have a ${attribute} attribute.`; this.message = msg || util.format(DEFAULT_MSG, attribute, selector, expected); this.expected = function() { return expected; }; this.pass = function(value) { return value === expected; }; this.failure = function(result) { let failed = (result === false) || // no such element result && (result.status === -1 || result.value === null); if (failed) { let defaultMsg = MSG_ELEMENT_NOT_FOUND; if (result && result.value === null) { defaultMsg = MSG_ATTR_NOT_FOUND; } this.message = msg || util.format(defaultMsg, attribute, selector, expected); } return failed; }; this.value = function(result) { return result.value; }; this.command = function(callback) { return this.api.getAttribute(selector, attribute, callback); }; }; ================================================ FILE: examples/custom-commands/angular/getElementsInList.js ================================================ module.exports = class AngularCommand { async command(listName, cb = function(r) {return r}) { return this.api.executeScript(function(listName) { // executed in the browser context // eslint-disable-next-line var elements = document.querySelectorAll('*[ng-repeat$="'+listName+'"]'); if (elements) {return elements} // eslint-disable-next-line return null; }, [listName], async function(result) { const cbResult = await cb(result); if (cbResult.value) { return cbResult.value; } return cbResult; }); } }; ================================================ FILE: examples/custom-commands/strictClick.js ================================================ module.exports = { command: function(selector){ return this.waitForElementVisible(selector) .click(selector); } }; ================================================ FILE: examples/globals.json ================================================ { "default" : { "myGlobal" : 1 }, "test_env" : { "myGlobal" : 2 } } ================================================ FILE: examples/globalsModule.js ================================================ module.exports = { // this controls whether to abort the test execution when an assertion failed and skip the rest // it's being used in waitFor commands and expect assertions abortOnAssertionFailure: true, // this will overwrite the default polling interval (currently 500ms) for waitFor commands // and expect assertions that use retry waitForConditionPollInterval: 500, // default timeout value in milliseconds for waitFor commands and implicit waitFor value for // expect assertions waitForConditionTimeout: 5000, // this will cause waitFor commands on elements to throw an error if multiple // elements are found using the given locate strategy and selector throwOnMultipleElementsReturned: false, // controls the timeout value for async hooks. Expects the done() callback to be invoked within this time // or an error is thrown asyncHookTimeout: 10000, // controls the timeout value for when running async unit tests. Expects the done() callback to be invoked within this time // or an error is thrown unitTestsTimeout: 2000, // controls the timeout value for when executing the global async reporter. Expects the done() callback to be invoked within this time // or an error is thrown customReporterCallbackTimeout: 20000, // Automatically retrying failed assertions - You can tell Nightwatch to automatically retry failed assertions until a given timeout is reached, before the test runner gives up and fails the test. retryAssertionTimeout: 1000, 'default': { myGlobal: function() { return 'I\'m a method'; } }, 'test_env': { myGlobal: 'test_global', beforeEach: function() { } }, before(cb) { //console.log('GLOBAL BEFORE') cb(); }, beforeEach(browser, cb) { //console.log('GLOBAL beforeEach') cb(); }, after(cb) { //console.log('GLOBAL AFTER') cb(); }, afterEach(browser, cb) { browser.perform(function() { //console.log('GLOBAL afterEach') cb(); }); }, reporter(results, cb) { cb(); } }; ================================================ FILE: examples/pages/google/consent.js ================================================ class ConsentCommand { turnOffSearchCustomization() { this.page.section.customizeSearch.click('@turnOffButton'); return this; } turnOffYoutubeHistory() { this.page.section.youtubeHistory.click('@turnOffButton'); return this; } turnOffAdPersonalization() { this.page.section.adPersonalization.click('@turnOffButton'); return this; } confirm() { this.page.section.consentForm.click('@submitButton'); return this; } turnOffEverything() { return this.turnOffSearchCustomization() .turnOffYoutubeHistory() .turnOffAdPersonalization() .confirm(); } } const createSectionFor = (text) => { return Object.assign({ selector: `//div[contains(.,"${text}")]`, locateStrategy: 'xpath' }, { elements: { turnOffButton: 'button[aria-label^="Turn off"]' } }); }; module.exports = { url: 'http://google.com', commands: ConsentCommand, elements: { consentModal: 'form[action^="https://consent.google"]' }, sections: { customizeSearch: createSectionFor('Search customization'), youtubeHistory: createSectionFor('YouTube History'), adPersonalization: createSectionFor('Ad personalization'), consentForm: { selector: 'form[action^="https://consent.google"]', elements: { submitButton: 'button' } } } }; ================================================ FILE: examples/pages/google/search.js ================================================ const searchCommands = { submit() { this.waitForElementVisible('@submitButton', 1000) .click('@submitButton'); this.pause(1000); return this; // Return page object for chaining } }; const consentModal = '[aria-modal="true"]'; module.exports = { url: 'https://google.no', commands: [ searchCommands ], sections: { consentModal: { selector: consentModal, elements: { rejectAllButton: '.GzLjMd button:nth-child(1)' } } }, elements: { consentModal, searchBar: { selector: 'textarea[name=q]' }, submitButton: { selector: 'input[value="Google Search"]' } } }; ================================================ FILE: examples/pages/google/searchResults.js ================================================ const util = require('util'); // starting xpath with './/' tells browser to begin search from the current element, // while starting with '//' tells browser to begin search from the start of html document. const menuXpath = './/span[contains(text(), "%s")]'; const menuCommands = { productIsSelected: function (product, callback) { var self = this; return this.getAttribute(product, 'class', function (result) { const isSelected = result.value.indexOf('hdtb-msel') > -1; callback.call(self, isSelected); }); } }; module.exports = { elements: { results: {selector: '#rso'} }, sections: { menu: { selector: 'div[role="navigation"] div[data-st-cnt="mode"]', commands: [menuCommands], elements: { maps: { selector: util.format(menuXpath, 'Maps'), locateStrategy: 'xpath', index: 0 }, videos: { selector: util.format(menuXpath, 'Videos'), locateStrategy: 'xpath', index: 0 }, images: { selector: util.format(menuXpath, 'Images'), locateStrategy: 'xpath', index: 0 }, news: { selector: util.format(menuXpath, 'News'), locateStrategy: 'xpath', index: 0 } } } } }; ================================================ FILE: examples/pages/nightwatchFeatures.js ================================================ const featuresCommands = { getFeatureCount: function(callback) { this.api.elements(null, this.elements.features, function (result) { if (result.status === 0) { var countResult = { status: 0, value: result.value.length }; callback.call(this, countResult); } else { callback.call(this, result); } }.bind(this)); return this; } }; module.exports = { commands: [featuresCommands], elements: { featuresHeading: { selector: '#index-container h2', index: 1 }, features: '#index-container .features h3' } }; ================================================ FILE: examples/test-app/globals.js ================================================ const waitOn = require('wait-on'); const {spawn} = require('child_process'); const path = require('path'); let serverPid = null; const serverPort = '13370'; module.exports = { before(done) { // serve --listen 13370 ./test-app serverPid = spawn(path.resolve('node_modules/.bin/serve'), ['--listen', serverPort, '--no-request-logging', __dirname], { cwd: process.cwd(), stdio: 'inherit' }).pid; waitOn({ resources: [`http://localhost:${serverPort}`] }).then(() => { setTimeout(done, 500); }); }, after() { if (serverPid) { process.kill(serverPid); } } }; ================================================ FILE: examples/test-app/index.html ================================================ nightwatch-testing-library

getByPlaceholderText

getByText

getByText within

getByText within

text only in 2nd nested another thing only in 2nd nested

getByLabelText

getByAltText

Image Alt Text

getByTestId

configure

configure standalone

getAllByText

navigate

Go to Page 2
================================================ FILE: examples/test-app/page2.html ================================================

second page

configure

================================================ FILE: examples/tests/README.md ================================================ # Using Mocha as a test runner in Nightwatch 2 Nightwatch 2 brings support for Mocha v9 as an alternative test runner which contains most of the existing functionality from the Nightwatch default runner. We are still in the process of updating the examples. Please check back here soon. ================================================ FILE: examples/tests/angularTodoTest.js ================================================ describe('angularjs homepage todo list', function() { it('should add a todo using custom commands', async function(browser) { // adding a new task to the list const elements = await browser .navigateTo('https://angularjs.org') .sendKeys('[ng-model="todoList.todoText"]', 'what is nightwatch?') .click('[value="add"]') .angular.getElementsInList('todoList.todos'); const taskEl = element(elements[2]); // verifying if the third task is the one we have just added // browser.assert.textEquals(taskEl, 'what is nightwatch?'); await expect(taskEl).text.toEqual('what is nightwatch?'); // find our task in the list and mark it as done const inputElement = await element(elements[2]).findElement('input'); await browser.click(inputElement); // verify if there are 2 tasks which are marked as done in the list await expect.elements('*[module=todoApp] li .done-true').count.to.equal(2); }); }); ================================================ FILE: examples/tests/bstackdemo/auth.js ================================================ describe('Authentication Tests', function () { beforeEach((browser) => browser.navigateTo('https://www.bstackdemo.com')); it('Login test', function () { browser .click('#signin') .setValue('#username input', ['demouser', browser.Keys.ENTER]) .setValue('#password input', ['testingisfun99', browser.Keys.ENTER]) .click('#login-btn') .assert.textEquals('.username', 'demouser', 'demouser had logged in successfuly.'); }); it('Locked account test', function () { browser .click('#signin') .setValue('#username input', ['locked_user', browser.Keys.ENTER]) .setValue('#password input', ['testingisfun99', browser.Keys.ENTER]) .click('#login-btn') .assert.textContains('.api-error', 'Your account has been locked.'); }); afterEach((browser) => browser.end()); }); ================================================ FILE: examples/tests/bstackdemo/checkout.js ================================================ describe('Checkout Test', function () { before((browser) => browser.navigateTo('https://www.bstackdemo.com/')); it('checkout products on bstackdemo.com', function (browser) { browser .waitForElementVisible('body') .assert.titleContains('StackDemo') // Filter Google from the avaialble filters .click('input[value=\'Google\'] + span') .assert.selected('input[value=\'Google\']') // Add different phones to cart .click('[id="17"] .shelf-item__buy-btn') .click('[id="18"] .shelf-item__buy-btn') .assert.elementsCount('.float-cart__shelf-container .shelf-item', 2) // Click checkout .click('.buy-btn') // Loging with given credentials .setValue('#username input', ['demouser', browser.Keys.ENTER]) .setValue('#password input', ['testingisfun99', browser.Keys.ENTER]) .click('#login-btn') // Fill shipping details .setValue('#firstNameInput', 'John') .setValue('#lastNameInput', 'Doe') .setValue('#addressLine1Input', 'localhost') .setValue('#provinceInput', 'local') .setValue('#postCodeInput', '127001') .click('#checkout-shipping-continue') // Check order successfully placed .assert.textEquals('#confirmation-message', 'Your Order has been successfully placed.'); }); after((browser) => browser.end()); }); ================================================ FILE: examples/tests/chromeCDP_example.js ================================================ describe('Chrome DevTools Example', function() { this.disabled = this.argv.env !== 'chrome'; it ('using CDP DOM Snapshot', async function(browser) { await browser.navigateTo('https://nightwatchjs.org'); const dom = await browser.chrome.sendAndGetDevToolsCommand('DOMSnapshot.captureSnapshot', { computedStyles: [] }); browser.assert.deepStrictEqual(Object.keys(dom), ['documents', 'strings']); }); }); ================================================ FILE: examples/tests/duckDuckGo.js ================================================ describe('duckduckgo example', function() { this.tags = ['end-to-end']; it('Search Nightwatch.js and check results', function(browser) { browser .navigateTo('https://duckduckgo.com') .waitForElementVisible('body') .assert.visible('input[name="q"]') .sendKeys('input[name="q"]', ['Nightwatch.js']) .assert.visible('button[type=submit]') .click('button[type=submit]') .assert.textContains('.react-results--main', 'Nightwatch.js'); }); }); ================================================ FILE: examples/tests/ecosia.js ================================================ describe('Ecosia.org Demo', function() { before(browser => { browser .navigateTo('https://www.ecosia.org/'); }); it('Demo test ecosia.org', function(browser) { browser .waitForElementVisible('body') .assert.titleContains('Ecosia') .assert.visible('input[type=search]') .setValue('input[type=search]', 'nightwatch') .assert.visible('button[type=submit]') .click('button[type=submit]') .assert.textContains('.layout__content', 'Nightwatch.js'); }); after(browser => browser.end()); }); ================================================ FILE: examples/tests/element/dragAndDrop.js ================================================ 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(); }); }); ================================================ FILE: examples/tests/element/elementScreenshot.js ================================================ 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()); }); ================================================ FILE: examples/tests/element/elementapi-tests.js ================================================ describe('queries tests', function() { beforeEach(function(browser) { browser.navigateTo('http://localhost:13370'); }); it('getFirstElementChild', async function({element}) { const firstChild = await element.find('#nested').getFirstElementChild().inspectInDevTools('firstChild'); await expect(firstChild).to.be.an('h3'); const lastChild = await element('#nested').getLastElementChild().inspectInDevTools('lastChild'); const nextElementSibling = await element('#nested').getNextElementSibling().inspectInDevTools('nextElementSibling'); const previousElementSibling = await element('#nested').inspectInDevTools('previousElementSibling'); }); it('assert.present', async function({element}) { await element.findAll('section').nth(1).assert.present(); }); it('assert.hasAttribute', async function({element}) { await element.findAll('section').nth(1).find('button').assert.hasAttribute('role'); }); it('custom element assertion', async function({element}) { await element.findAll('section').nth(1).find('button').assert.customScript(function(element, content) { return element.innerHTML === content; }, ['Unique Button Text'], 'Custom assertion message: button has correct text'); }); it('findByRole', async function({element}) { const button = await element.findAll('section').nth(1).findByRole('button'); await expect(button).to.be.an('button'); await browser.click(button); await expect(button).text.to.equal('Button Clicked'); }); it('findAll', async function({element}) { await element.findAll('section').count().assert.equals(9); }); it('Button click works', async function(browser) { const button = await browser.element.findByText('Unique Button Text'); await expect(button).text.not.to.equal('Button Clicked'); await browser.click(button); await expect(button).text.to.equal('Button Clicked'); }); it('findByPlaceholderText', async function ({element, actions}) { const input = await element.findByPlaceholderText('Placeholder Text').assert.visible('Input is visible'); await expect(input).property('value').not.to.equal('Hello Placeholder'); // // Uses the User Actions API to type into the input await actions().sendKeys(input, 'Hello Placeholder').perform(); await expect(input).property('value').to.equal('Hello Placeholder'); }); it('findByLabelText', async function ({element}) { const input = await element.findByLabelText('Label For Input Labelled By Id').sendKeys('Hello Input Labelled by Id'); expect(input).value.toEqual('Hello Input Labelled by Id'); }); it('findByAltText', async function ({element}) { const image = await element.findByAltText('Image Alt Text').click(); expect(image).css('border').toEqual('5px solid rgb(255, 0, 0)'); }); it('findAllByText', async function ({element}) { const philosophers = await element.findAllByText('of Miletus', {exact: false}); expect(philosophers).to.have.length(2); }); }); ================================================ FILE: examples/tests/element/isCommands.js ================================================ 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()); }); ================================================ FILE: examples/tests/google.js ================================================ describe('sample google search', function() { this.tags = ['google']; it('demo test using expect apis', async function(browser) { await browser.navigateTo('http://google.no'); const consentPresent = await browser.isPresent('[aria-modal="true"][title="Before you continue to Google Search"]'); // Wait until we are on consent page if (consentPresent === true) { browser .waitForElementVisible('[aria-modal="true"][title="Before you continue to Google Search"]') .click('[aria-modal="true"] div.VDity button:nth-child(1)') // Wait until we are on consent page .expect.url().toContain('https://consent.google.no') // Turn everything off .click('button[aria-label="Turn off Search customization"]') .click('button[aria-label="Turn off YouTube History"]') .click('button[aria-label="Turn off Ad personalization"]') // click on confirm button .click('form[action^="https://consent.google.no"] button') // saving the consent form takes some time, no need to check for anything else .pause(1000); } let locator; if (browser.isMobile()) { locator = 'form[action="/search"] input[type=search]'; } else { locator = 'form[action="/search"] input[type=text]'; } await browser .waitForElementVisible(locator) .sendKeys(locator, ['Nightwatch.js', browser.Keys.ENTER]) .assert.textContains('#rso>:first-child', 'Nightwatch.js') .end(); }); }); ================================================ FILE: examples/tests/googlePageObject.js ================================================ describe('google search with consent form - page objects', function() { const homePage = browser.page.google.search(); before(async () => homePage.navigate()); after(async (browser) => browser.quit()); it('should find nightwatch.js in results', function (browser) { homePage.setValue('@searchBar', 'Nightwatch.js'); homePage.submit(); const resultsPage = browser.page.google.searchResults(); resultsPage.expect.element('@results').to.be.present; resultsPage.expect.element('@results').text.to.contain('Nightwatch.js'); resultsPage.expect.section('@menu').to.be.visible; const menuSection = resultsPage.section.menu; menuSection.expect.element('@videos').to.be.visible; }); }); ================================================ FILE: examples/tests/sample-with-relative-locators.js ================================================ /* eslint-disable no-undef */ describe('sample with relative locators', function () { before(browser => browser.navigateTo('https://archive.org/account/login')); it('locate password input', function (browser) { const passwordElement = locateWith(By.tagName('input')).below(By.css('input[type=email]')); browser .waitForElementVisible(passwordElement) .expect.element(passwordElement).to.be.an('input'); browser.expect.element(passwordElement).attribute('type').equal('password'); }); it('fill in password input', function (browser) { const passwordElement = locateWith(By.tagName('input')).below(By.css('input[type=email]')); browser .waitForElementVisible('form.login-form') .setValue(passwordElement, 'password') .assert.valueEquals('input[type=password]', 'password'); }); after(browser => browser.end()); }); ================================================ FILE: examples/tests/selectElement.js ================================================ const {Select} = require('selenium-webdriver'); module.exports = { before(browser) { browser.url('https://www.selenium.dev/selenium/web/formPage.html'); }, async demoTest(browser) { const selectElement = browser.element('select[name=selectomatic]'); await browser .perform(async function() { const selectWebElement = await selectElement; // `Select` class expects a WebElement const select = new Select(selectWebElement); await select.selectByVisibleText('Four'); }) .assert.selected(await selectElement.findElement('option[value=four]'), 'Forth option is selected'); } }; ================================================ FILE: examples/tests/shadowRootExample.js ================================================ describe('Shadow Root example test', function() { it('retrieve the shadowRoot', async function(browser) { browser .navigateTo('https://mdn.github.io/web-components-examples/popup-info-box-web-component/') .waitForElementVisible('form'); // const shadowId = browser.element('popup-info').getId().map(val => { // return val + 'shadow'; // }).assert.contains('shadow'); // console.log('!!! shadowId', shadowId) // const elForm = await browser.element.findAllByText('Pop-up info widget', {exact: false}).nth(0); // expect(elForm).to.be.a('h1'); // const elForm = await browser.element.findAll('form').nth(0); // expect(elForm).to.be.a('form'); // const formCount = await browser.element.findAll('form').count(); // expect(formCount).to.equal(1); // // const elInput = await browser.element.find('form').find('input'); // console.log('!!! elInput', elInput); //expect(elInput).to.be.an('input'); // const els = await browser.element.findAll('form').count(); // console.log('!!! els', els); // // //await expect(shadowId).to.be.a('string').and.to.include('shadow') // // no `await` used since `shadowRootEl` is going to be chained further const shadowRootEl = browser.element('popup-info').getShadowRoot(); const infoElement = shadowRootEl.find('.info').property('innerHTML'); // // //console.log('!!! infoElement', infoElement.assert) // // expect(infoElement) // .to.be.a('string') // .and.to.include('card validation code'); // // const iconElement = await shadowRootEl.find('.icon'); // console.log('!!! iconElement', iconElement) // // const firstElement = await browser.getFirstElementChild(iconElement); // // await expect.element(firstElement).to.be.an('img'); }); }); ================================================ FILE: examples/tests/vueTodoList.js ================================================ /** * End-to-end test for the sample Vue3+Vite todo app located at * https://github.com/nightwatchjs-community/todo-vue */ describe('To-Do List End-to-End Test', function() { // using the new element() global utility in Nightwatch 2 to init elements // before tests and use them later const todoElement = element('#new-todo-input'); const addButtonEl = element('form button[type="submit"]'); it('should add a todo using global element()', async function() { /////////////////////////////////////////////////// // browser can now also be accessed as a global | /////////////////////////////////////////////////// // adding a new task to the list await browser .navigateTo('https://todo-vue3-vite.netlify.app/') .sendKeys(todoElement, 'what is nightwatch?') .click(addButtonEl); /////////////////////////////////////////////////// // global expect is equivalent to browser.expect | /////////////////////////////////////////////////// // verifying if there are 5 tasks in the list await expect.elements('#todo-list ul li').count.toEqual(5); // verifying if the 4th task if the one we have just added const lastElementTask = element({ selector: '#todo-list ul li', index: 4 }); await expect(lastElementTask).text.toContain('what is nightwatch?'); // find our task in the list and mark it as done const inputElement = await lastElementTask.findElement('input[type="checkbox"]'); await browser.click(inputElement); // verify if there are 3 tasks which are marked as done in the list await expect.elements('#todo-list ul li input:checked').count.toEqual(3); }); }); ================================================ FILE: examples/tsconfig.json ================================================ { "extends": "../tsconfig", "include": ["."] } ================================================ FILE: examples/unittests/demoTestAsync.js ================================================ const assert = require('assert'); module.exports = { '@unitTest': true, 'demo UnitTest': function (done) { assert.strictEqual('TEST', 'TEST'); setTimeout(function() { done(); }, 10); } }; ================================================ FILE: examples/unittests/testUtils.js ================================================ const assert = require('assert'); const Utils = require('../../dist/utils/'); module.exports = { testFormatElapsedTime: function() { var resultMs = Utils.formatElapsedTime(999); assert.strictEqual(resultMs, '999ms'); var resultSec = Utils.formatElapsedTime(1999); assert.strictEqual(resultSec, '1.999s'); var resultMin = Utils.formatElapsedTime(122299, true); assert.strictEqual(resultMin, '2m 2s / 122299ms'); }, testGetTestSuiteName: function() { assert.strictEqual(Utils.getTestSuiteName('test-case-one'), 'Test Case One'); assert.strictEqual(Utils.getTestSuiteName('test_case_two'), 'Test Case Two'); assert.strictEqual(Utils.getTestSuiteName('test.case.one'), 'Test Case One'); assert.strictEqual(Utils.getTestSuiteName('testCaseOne'), 'Test Case One'); } }; ================================================ FILE: examples/unittests/testUtilsWithChai.js ================================================ const Utils = require('../../dist/utils/'); const expect = require('chai').expect; module.exports = { testFormatElapsedTime: function() { var resultMs = Utils.formatElapsedTime(999); var resultSec = Utils.formatElapsedTime(1999); var resultMin = Utils.formatElapsedTime(122299, true); expect(resultMs).to.equal('999ms'); expect(resultSec).to.equal('1.999s'); expect(resultMin).to.equal('2m 2s / 122299ms'); }, testFormatElapsedTimeMore: function() { var resultMs = Utils.formatElapsedTime(999); expect(resultMs).to.equal('999ms'); } }; ================================================ FILE: index.js ================================================ module.exports = process.env.NIGHTWATCH_COV ? require('./lib-cov/index') : require('./lib/index'); ================================================ FILE: lib/api/_loaders/_base-loader.js ================================================ const path = require('path'); const fs = require('fs'); const EventEmitter = require('events'); const Utils = require('../../utils'); const namespacedApi = require('../../core/namespaced-api.js'); const ScopedElementAPILoader = require('./element-api.js'); let __last_deferred__ = null; class BaseLoader extends EventEmitter { static get commandOverrides () { return { element(nightwatchInstance, ...args) { if (nightwatchInstance.settings.backwards_compatibility_mode) { return args[0]; } return new ScopedElementAPILoader(nightwatchInstance).createElementMethod(); } }; } static handleModuleError(err, fullPath) { const {message} = err; const showStackTrace = ['SyntaxError', 'TypeError'].includes(err.name); const error = new Error(`There was an error while trying to load the file ${fullPath}:`); error.detailedErr = `[${err.code || err.name}] ${message};`; error.extraDetail = `\n Current working directory is: ${process.cwd()}`; error.showTrace = showStackTrace; error.displayed = false; if (showStackTrace) { error.stack = err.stack; } throw error; } static unflattenNamespace(target, namespace, value) { const key = namespace.shift(); if (key) { target[key] = target[key] || {}; value = target[key]; return BaseLoader.unflattenNamespace(target[key], namespace, value); } return value; } static createDriverCommand(nightwatchInstance, commandName) { return function commandFn({args}) { return nightwatchInstance.transport.driver[commandName](...args).catch((error) => { if (error.remoteStacktrace) { delete error.remoteStacktrace; } return { value: null, error }; }); }; } static get lastDeferred() { return __last_deferred__; } static set lastDeferred(value) { __last_deferred__ = value; } get commandQueue() { return this.nightwatchInstance.queue; } get loadSubDirectories() { return false; } get reporter() { return this.nightwatchInstance.reporter; } get api() { return this.nightwatchInstance.api; } get elementLocator() { return this.nightwatchInstance.elementLocator; } get transport() { return this.nightwatchInstance.transport; } get namespace() { return this.__namespace; } get module() { return this.__module; } set module(val) { this.__module = val; } get commandName() { return this.__commandName; } get commandFn() { return this.__commandFn; } set commandFn(val) { this.__commandFn = val; } set commandName(val) { this.__commandName = val; } set stackTrace(val) { this.__stackTrace = val; } get stackTrace() { return this.__stackTrace; } get instance() { return this.__instance; } get isUserDefined() { return this.__isUserDefined; } set isUserDefined(val) { this.__isUserDefined = val; } get addedInsideCallback() { return this.__addedInsideCallback; } set addedInsideCallback(val) { this.__addedInsideCallback = val; } set ignoreUnderscoreLeadingNames(val) { this.__ignoreUnderscoreNames = val; } get ignoreUnderscoreLeadingNames() { return this.__ignoreUnderscoreNames && !this.isUserDefined; } constructor(nightwatchInstance) { super(); BaseLoader.lastDeferred = null; this.nightwatchInstance = nightwatchInstance; } isTypescriptDisabled() { return this.nightwatchInstance.settings.disable_typescript; } get settings() { return this.nightwatchInstance.settings; } getTargetNamespace() { return null; } isFileNameValid(fileName) { if (fileName.startsWith('_') && this.ignoreUnderscoreLeadingNames) { return false; } return Utils.isFileNameValid(fileName); } setNamespace(val) { this.__namespace = val; return this; } loadModule(dirPath, fileName) { const fullPath = path.join(dirPath, fileName); if (!this.loadSubDirectories && fs.lstatSync(fullPath).isDirectory()) { return this; } this.requireModule(fullPath, fileName); return this; } requireModule(fullPath, fileName) { if (!this.isFileNameValid(fileName)) { return this; } if (this.isTypescriptDisabled() && Utils.isTsFile(fileName)) { return this; } this.commandName = path.parse(fullPath).name; this.fileName = fullPath; try { this.module = Utils.requireModule(fullPath); } catch (err) { BaseLoader.handleModuleError(err, fullPath); } } async loadModuleAsync(dirPath, fileName) { const fullPath = path.join(dirPath, fileName); if (!this.loadSubDirectories && fs.lstatSync(fullPath).isDirectory()) { return this; } await this.requireModuleAsync(fullPath, fileName); } async requireModuleAsync(fullPath, fileName) { if (!this.isFileNameValid(fileName)) { return this; } if (this.isTypescriptDisabled() && Utils.isTsFile(fileName)) { return this; } this.commandName = path.parse(fullPath).name; this.fileName = fullPath; try { this.module = await Utils.requireModule(fullPath); } catch (err) { BaseLoader.handleModuleError(err, fullPath); } } validateMethod() {} defineArgs(parent, namespacedApi) { const {commandName} = this; const commandFn = this.commandFn.bind(this); let stringifiedNamespace; let mainNamespace; let apiToReturn; if (this.namespace && Utils.isString(this.namespace)) { stringifiedNamespace = this.namespace; mainNamespace = this.namespace; } else if (Array.isArray(this.namespace) && this.namespace.length > 0) { stringifiedNamespace = this.namespace.join('.'); mainNamespace = this.namespace[0]; } if (namespacedApi && mainNamespace) { namespacedApi[mainNamespace] = namespacedApi[mainNamespace] || {}; apiToReturn = new Proxy(namespacedApi[mainNamespace], { get(_, name) { // namespacedApi[mainNamespace] may change after API loading is finished. return namespacedApi[mainNamespace][name]; } }); } else if (parent && parent.__is_page_object_cache) { // commands loaded onto a page object should return the page object only. apiToReturn = parent; } const args = [ this.createQueuedCommandFn({ parent, commandName, namespace: stringifiedNamespace, commandFn, apiToReturn, context: this }) ]; // Define the element method as a namespace. if (commandName in BaseLoader.commandOverrides) { args[0] = BaseLoader.commandOverrides[commandName](this.nightwatchInstance, ...args); } const namespace = this.getTargetNamespace(parent, namespacedApi); if (namespace) { args.unshift(namespace); } return args; } getNamespacedAliases() { const nsAliases = this.module && this.module.namespacedAliases; if (nsAliases && Utils.isString(nsAliases)) { return [nsAliases.split('.')]; } if (Array.isArray(nsAliases)) { return nsAliases .filter((nsAlias) => nsAlias && Utils.isString(nsAlias)) .map((nsAlias) => nsAlias.split('.')); } return []; } define(parent = null) { if (!this.commandFn) { return this; } const {commandName, namespace, nightwatchInstance} = this; this.validateMethod(parent); const args = this.defineArgs(parent); nightwatchInstance.setApiMethod(commandName, ...args); // check for namespaced aliases const nsAliases = this.getNamespacedAliases(); nsAliases.forEach((ns) => { this.commandName = ns.pop(); this.setNamespace(ns); this.validateMethod(parent); const args = this.defineArgs(parent); nightwatchInstance.setApiMethod(this.commandName, ...args); }); this.commandName = commandName; this.setNamespace(namespace); return this.defineNamespacedApi(parent, namespacedApi); } defineNamespacedApi(parent) { // namespaced api would have already been loaded as named exports (without parent), // do not load commands again for parent (page-objects or within-context). if (parent || !this.commandFn) { return this; } const {commandName, namespace, nightwatchInstance} = this; if (this.namespace && this.namespace.length) { const args = this.defineArgs(parent, namespacedApi); nightwatchInstance.setNamespacedApiMethod(commandName, ...args); } // check for namespaced aliases const nsAliases = this.getNamespacedAliases(); nsAliases.forEach((ns) => { this.commandName = ns.pop(); if (ns.length) { this.setNamespace(ns); const args = this.defineArgs(parent, namespacedApi); nightwatchInstance.setNamespacedApiMethod(this.commandName, ...args); } }); this.commandName = commandName; this.setNamespace(namespace); return this; } getCommandOptions() { return {}; } createQueuedCommandFn({parent, commandName, namespace, commandFn, context, apiToReturn}) { const {commandQueue, api, nightwatchInstance} = this; return function queuedCommandFn(...args) { const stackTrace = Utils.getOriginalStackTrace(queuedCommandFn); const deferred = Utils.createPromise(); deferred.commandName = commandName; // if this command was called from another async (custom) command const isAsyncCommand = this.isES6Async; const options = this.getCommandOptions(); if (args && args.length > 0 && Utils.isFunction(args[args.length - 1])) { const callback = args.pop(); const userCallbackWrapper = (function(context) { const proxyFn = new Proxy(callback, { apply: function(target, thisArg, argumentsList) { context.addedInsideCallback = true; return target.apply(thisArg, argumentsList); } }); proxyFn.originalTarget = callback; return proxyFn; })(this); args.push(userCallbackWrapper); } // if this command was called from an async test case let isES6Async = options.alwaysAsync || nightwatchInstance.settings.always_async_commands; if (!isES6Async) { isES6Async = Utils.isUndefined(this.isES6Async) ? ( nightwatchInstance.isES6AsyncTestcase || nightwatchInstance.isES6AsyncCommand ) : isAsyncCommand; } if (!Utils.isUndefined(nightwatchInstance.isES6AsyncTestHook)) { isES6Async = nightwatchInstance.isES6AsyncTestHook; } const node = commandQueue.add({ commandName, commandFn, context: this, args, stackTrace, namespace, options, deferred, isES6Async }); if (this.module && this.module.autoInvoke) { const result = node.execute(true); return result; } if (isES6Async || options.alwaysAsync || node.isES6Async) { BaseLoader.lastDeferred = node.deferred; if (parent && parent.__pageObjectItem__) { // never seem to reach here, because page-object commands are loaded // in the starting itself before the actual page-objects are loaded, // so `parent` here represents `__page_object_cache` and not the actual // page on which command is being run. Object.assign(node.deferred.promise, parent.__pageObjectItem__); } else { Object.assign(node.deferred.promise, apiToReturn || api); } //prevent unhandled rejection. node.deferred.promise.catch(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) { return node.deferred.promise; } } if (this.module?.returnFn) { return this.module.returnFn(node, apiToReturn || api); } return apiToReturn || api; }.bind(context); } loadDriverCommands({commands, namespace}) { commands.forEach(propertyName => { const commandFn = BaseLoader.createDriverCommand(this, propertyName); this.nightwatchInstance.setApiMethod(propertyName, namespace, (function (commandName) { return this.createQueuedCommandFn({ commandName, commandFn, context: this, namespace }); }.bind(this))(propertyName)); // Add command to namespaced-api as well namespacedApi[namespace] = namespacedApi[namespace] || {}; this.nightwatchInstance.setNamespacedApiMethod(propertyName, namespace, (function (commandName) { return this.createQueuedCommandFn({ commandName, commandFn, context: this, namespace, apiToReturn: namespacedApi[namespace] }); }.bind(this))(propertyName)); }); } } module.exports = BaseLoader; ================================================ FILE: lib/api/_loaders/_command-loader.js ================================================ const Element = require('../../element'); const BaseLoader = require('./_base-loader.js'); class BaseCommandLoader extends BaseLoader { constructor(nightwatchInstance) { super(nightwatchInstance); this.type = 'command'; this.__namespace = null; this.__module = null; this.__commandName = null; this.__commandFn = null; this.__instance = null; this.__isUserDefined = false; this.ignoreUnderscoreLeadingNames = true; } static isTypeImplemented(instance, method, type) { const methodTypes = method.split('|'); if (type === '*') { return instance[method] !== undefined; } return methodTypes.some(method => (typeof instance[method] == type)); } get sessionId() { return this.nightwatchInstance.session.sessionId; } getCommandOptions() { const redact = this.module.RedactParams || false; const alwaysAsync = this.module.alwaysAsync || false; const isTraceable = this.module.isTraceable; const avoidPrematureParentNodeResolution = !!this.module.avoidPrematureParentNodeResolution; const rejectNodeOnAbortFailure = !!this.module.rejectNodeOnAbortFailure; return {redact, alwaysAsync, isTraceable, avoidPrematureParentNodeResolution, rejectNodeOnAbortFailure}; } validateMethod(parent) { let namespace = this.getTargetNamespace(parent); if (Array.isArray(namespace) && namespace.length > 0) { namespace = BaseLoader.unflattenNamespace(this.api, namespace.slice()); } if (this.module.allowOverride) { this.nightwatchInstance.overridableCommands.add(this.commandName); } if (this.nightwatchInstance.isApiMethodDefined(this.commandName, namespace) && !this.nightwatchInstance.overridableCommands.has(this.commandName)) { const err = new TypeError(`Error while loading the API commands: the ${this.type} ${this.namespace || ''}.${this.commandName}() is already defined.`); err.displayed = false; err.detailedErr = `Source: ${this.fileName}`; err.showTrace = false; throw err; } return this; } resolveElementSelector(args) { if ((args[0] instanceof Element) && this.isUserDefined) { const element = args[0]; if (element.usingRecursion) { return this.elementLocator.resolveElementRecursively({element}); } return Promise.resolve(element); } return Promise.resolve(); } getTargetNamespace(parent) { const namespace = this.namespace; if (!parent) { return namespace; } if (!namespace || namespace.length === 0) { return parent; } // would give unexpected results if command with nested namespaces // is loaded onto say page-object (but wouldn't throw an error) parent[namespace] = parent[namespace] || {}; return parent[namespace]; } } module.exports = BaseCommandLoader; ================================================ FILE: lib/api/_loaders/assertion-scheduler.js ================================================ const Utils = require('../../utils'); const Element = require('../../element'); const {AssertionRunner} = require('../../assertion'); const {Logger} = Utils; class AssertionScheduler { get instance() { return this.__instance; } get opts() { return this.__opts; } /** * @param {AssertionInstance} instance * @param {{rescheduleInterval, abortOnFailure, stackTrace, timeout, reporter}} opts */ constructor(instance, opts = {}) { this.__instance = instance; this.__opts = opts; this.retries = 0; this.deferred = Utils.createPromise(); this.shouldRetry = this.opts.timeout > 0; this.setOptsFromSelector(); } reschedule() { setTimeout(() => { this.retries++; this.schedule(); }, this.opts.rescheduleInterval); } setOptsFromSelector() { if (this.instance.element instanceof Element) { const {timeout, retryInterval} = this.instance.element; if (!Utils.isUndefined(timeout)) { this.__opts.timeout = timeout; } if (!Utils.isUndefined(retryInterval)) { this.__opts.rescheduleInterval = retryInterval; } } } createAssertionRunner({passed, actual, commandCallback, message, elapsedTime}) { const expected = Utils.isFunction(this.instance.expected) ? this.instance.expected() : this.instance.expected; const {abortOnFailure, stackTrace, reporter} = this.opts; this.runner = new AssertionRunner({ passed, err: { expected, actual }, calleeFn: commandCallback, message, abortOnFailure, stackTrace, reporter, elapsedTime }); } verifyCommandArgs() { if (this.instance.command.length === 0) { throw new Error(`Assertion "command" methods require one "callback" parameter. Check the command method in: ${this.instance.fileName}.`); } return this; } start() { this.startTime = new Date().getTime(); this.schedule(); return this.deferred.promise; } schedule() { this.instance.command(function commandCallback(commandResult, commandInstance) { const {instance, startTime} = this; const elapsedTime = new Date().getTime() - startTime; if (commandInstance) { //this.__opts.abortOnFailure = commandInstance.abortOnFailure; } let passed; let value; instance.elapsedTime = elapsedTime; instance.setResult(commandResult); if (instance.hasFailure()) { passed = false; value = null; } else { value = instance.getValue(); passed = instance.isOk(value); } if (!passed && elapsedTime < this.opts.timeout) { this.reschedule(); return null; } if (instance.refineFormattedMessage) { // For new generic assertions we have to create message after the actual // value is resolved. Unfortunately the main formatMessage method cannot be // asynchronous. // I have no idea why assertion message is created before the assertion happens // or at least actual value is resolved O_o instance.refineFormattedMessage(value); } const message = this.getFullMessage(passed, elapsedTime); const actual = instance.getActual(); this.createAssertionRunner({passed, actual, commandCallback, message, elapsedTime}); // FIXME: Returning the promise here in the event the it's needed in the assertion callback, // throws an unhandledRejection when using es6 async because there's no catch in the assertion callback // - adding the catch should be done when breaking changes are going to be introduced. let resolveValue = {}; let testError; if (commandResult && commandResult.error instanceof Error) { testError = commandResult.error; } return this.runner .run(commandResult) .then(result => { resolveValue = Object.assign(resolveValue, result); resolveValue.passed = true; }) .catch(err => { resolveValue.passed = false; resolveValue.err = err; return err; }) .then(() => { Utils.makePromise(this.instance.doneCallback, this, [resolveValue, this.instance]); }) .then(_ => { if (testError && testError.name !== 'NoSuchElementError') { this.opts.reporter.registerTestError(testError); } setTimeout(()=> { if (resolveValue.passed) { this.deferred.resolve(resolveValue); } else { this.deferred.reject(resolveValue.err); } }); }); }.bind(this), this.instance); } getFullMessage(passed, elapsedTime) { if (!this.shouldRetry) { return this.instance.message; } let timeLogged = passed ? elapsedTime : this.opts.timeout; if (this.instance.message.endsWith('.')) { this.instance.message = this.instance.message.substr(0, this.instance.message.length - 1); } return `${this.instance.message} ${(passed ? Logger.colors.stack_trace(`(${timeLogged}ms)`) : `in ${timeLogged}ms`)}`; } } module.exports.create = function(instance, opts) { const scheduler = new AssertionScheduler(instance, opts); scheduler.verifyCommandArgs(); return scheduler.start(); }; ================================================ FILE: lib/api/_loaders/assertion.js ================================================ const {WebElementPromise} = require('selenium-webdriver'); const BaseCommandLoader = require('./_command-loader.js'); const Utils = require('../../utils'); const AssertionInstance = require('../assertions/_assertionInstance.js'); const Scheduler = require('./assertion-scheduler.js'); class AssertionLoader extends BaseCommandLoader { static get interfaceMethods() { return { expected: '*', 'pass|evaluate': 'function', command: 'function' }; } static validateAssertClass(instance) { Object.keys(AssertionLoader.interfaceMethods).forEach(method => { const type = AssertionLoader.interfaceMethods[method]; if (!BaseCommandLoader.isTypeImplemented(instance, method, type)) { const methodTypes = method.split('|').map(name => `"${name}"`); throw new Error(`Assertion class must implement method/property ${methodTypes.join(' or ')}`); } }); } constructor(nightwatchInstance) { super(nightwatchInstance); AssertionLoader.lastDeferred = null; this.type = 'assertion'; this.abortOnFailure = this.globals.abortOnAssertionFailure; } get timeout() { return this.globals.retryAssertionTimeout; } get rescheduleInterval() { return this.globals.waitForConditionPollInterval; } get globals() { return this.api.globals; } containsES6Class() { return this.module.prototype && this.module.prototype.constructor.toString().startsWith('class'); } validateModuleDefinition() { if (this.containsES6Class()) { return this.validateES6Class(); } return this.validateCJSModule(); } validateCJSModule() { if (!(Utils.isObject(this.module) && this.module.assertion)) { throw new Error('The assertion module needs to contain an .assertion() method'); } } validateES6Class() { if (!this.module.prototype.command) { throw new Error('Assertion class must implement an command() method'); } } createQueuedCommandFn({parent, apiToReturn}) { const commandFn = this.commandFn.bind(this); const {commandQueue, commandName, namespace, api, nightwatchInstance} = this; return function queuedCommandFn({negate, args}) { const stackTrace = Utils.getOriginalStackTrace(queuedCommandFn); // we only should return a Promise if not a nightwatch element, otherwise we risk breaking changes const element = args[0]; const shouldReturnPromise = element && !(element instanceof WebElementPromise) && (element.sessionId && element.elementId || element.driver_ && element.id_); const deferred = Utils.createPromise(); const isES6Async = shouldReturnPromise || nightwatchInstance.settings.always_async_commands || (Utils.isUndefined(this.isES6Async) ? nightwatchInstance.isES6AsyncTestcase : this.isES6Async); const node = commandQueue.add({ commandName, commandFn, context: this, args, options: { negate }, stackTrace, namespace, deferred, isES6Async }); if (isES6Async) { AssertionLoader.lastDeferred = deferred; commandQueue.tree.once('asynctree:finished', (err) => { node.deferred.resolve(node.resolveValue); }); if (parent && parent.__pageObjectItem__) { Object.assign(node.deferred.promise, parent.__pageObjectItem__); } else { Object.assign(node.deferred.promise, apiToReturn || api); } // prevent unhandledRejection errors node.deferred.promise.catch(err => { // null check, as done for BaseLoader.lastDeferred as well. if (AssertionLoader.lastDeferred) { return AssertionLoader.lastDeferred.reject(err); } }); return node.deferred.promise; } return apiToReturn || api; }.bind(this); } createWrapper(abortOnFailure) { if (this.module) { this.validateModuleDefinition(); this.abortOnFailure = abortOnFailure; this.commandFn = function commandFn({args, stackTrace, options}) { return this.resolveElementSelector(args) .then(elementResult => { if (elementResult) { args[0] = elementResult; } const {nightwatchInstance, reporter, abortOnFailure, module, fileName} = this; const instance = AssertionInstance.create({ nightwatchInstance, assertionModule: module, fileName, args, options }); AssertionLoader.validateAssertClass(instance); this.__instance = instance; return Scheduler.create(instance, { stackTrace, rescheduleInterval: Utils.isNumber(instance.rescheduleInterval) ? instance.rescheduleInterval : this.rescheduleInterval, timeout: Utils.isNumber(instance.retryAssertionTimeout) ? instance.retryAssertionTimeout : this.timeout, abortOnFailure, reporter }); }); }; } return this; } } module.exports = AssertionLoader; ================================================ FILE: lib/api/_loaders/chrome.js ================================================ const BaseLoader = require('./_base-loader.js'); class ChromeCommandLoader extends BaseLoader { static get chromeCommands() { return [ 'launchApp', 'getNetworkConditions', 'setNetworkConditions', 'deleteNetworkConditions', 'sendDevToolsCommand', 'sendAndGetDevToolsCommand', 'setPermission', 'setDownloadPath', 'getCastSinks', 'setCastSinkToUse', 'startCastTabMirroring', 'getCastIssueMessage', 'stopCasting' ]; } loadCommands() { const commands = ChromeCommandLoader.chromeCommands; this.loadDriverCommands({ commands, namespace: 'chrome' }); return this; } } module.exports = ChromeCommandLoader; ================================================ FILE: lib/api/_loaders/command.js ================================================ const EventEmitter = require('events'); const BaseCommandLoader = require('./_command-loader.js'); const {Logger, isES6AsyncFn, isFunction, isObject, makePromise} = require('../../utils'); class CommandLoader extends BaseCommandLoader { static get interfaceMethods() { return { command: 'function' }; } static isDeprecatedCommandStyle(CommandModule) { return isObject(CommandModule) && isFunction(CommandModule.command); } /** * This is to support backwards-compatibility for commands defined as objects, * with a command() property * * @param CommandModule */ static createFromObject(CommandModule) { return class CommandClass extends EventEmitter { command(...args) { if (isES6AsyncFn(CommandModule.command)) { return CommandModule.command.apply(this.api, args); } setImmediate(() => { CommandModule.command.apply(this.api, args); }); return this.api; } }; } static transportActions({actions, api}) { return new Proxy(actions, { get(target, name) { return function(...args) { let callback; let method; const isLastArgFunction = isFunction(args[args.length - 1]); if (isLastArgFunction) { callback = args.pop(); } else if (args.length === 0 || !isLastArgFunction) { callback = function(result) {return result}; } const definition = { args }; if (name in target.session) { // actions that require the current session method = target.session[name]; definition.sessionId = api.sessionId; } else { method = target[name]; } return method(definition).then((result) => makePromise(callback, api, [result])); }; } }); } static createInstance(nightwatchInstance, CommandModule, opts) { const CommandClass = CommandLoader.isDeprecatedCommandStyle(CommandModule) ? CommandLoader.createFromObject(CommandModule) : CommandModule; class CommandInstance extends CommandClass { reportProtocolErrors(result) { if (opts.isUserDefined) { return true; } return super.reportProtocolErrors(result); } get api() { return nightwatchInstance.api; } get reuseBrowser() { return nightwatchInstance.argv['reuse-browser'] || (nightwatchInstance.settings.globals && nightwatchInstance.settings.globals.reuseBrowserSession); } get isES6AsyncCommand() { return isES6AsyncFn( CommandLoader.isDeprecatedCommandStyle(CommandModule) ? CommandModule.command : this.command ); } get client() { return this.__nightwatchInstance || nightwatchInstance; } get commandFileName() { return opts.commandName; } get commandArgs() { return opts.args; } get transportActions() { return this.client.transportActions; } get driver() { return this.client.transport.driver; } httpRequest(requestOptions) { return this.client.transport.runProtocolAction(requestOptions); } toString() { return `${this.constructor.name} [name=${opts.commandName}]`; } complete(...args) { if (isFunction(super.complete)) { return super.complete(...args); } this.emit('complete', ...args); } } const instance = new CommandInstance(); Object.keys(CommandLoader.interfaceMethods).forEach(method => { const type = CommandLoader.interfaceMethods[method]; if (!BaseCommandLoader.isTypeImplemented(instance, method, type)) { throw new Error(`Command class must implement method .${method}()`); } }); instance.stackTrace = opts.stackTrace; instance.needsPromise = CommandLoader.isDeprecatedCommandStyle(CommandModule); return instance; } get loadSubDirectories() { return true; } 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, args, commandName: this.commandName, isUserDefined: this.isUserDefined }); if (this.module.autoInvoke) { this.nightwatchInstance.isES6AsyncCommand = instance.isES6AsyncCommand && this.isUserDefined; return instance.command(...args); } if (instance.w3c_deprecated) { const extraMessage = instance.deprecationNotice ? `\n ${instance.deprecationNotice}` : ''; // eslint-disable-next-line no-console console.warn(`This command has been deprecated and is removed from the W3C Webdriver standard. It is only working with legacy Selenium JSONWire protocol.${extraMessage}`); } const result = this.resolveElementSelector(args) .then(elementResult => { if (elementResult) { args[0] = elementResult; } this.nightwatchInstance.isES6AsyncCommand = instance.isES6AsyncCommand && this.isUserDefined; return instance.command(...args); }) .catch(err => { if (instance instanceof EventEmitter) { 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; } if (!['NightwatchAssertError', 'NightwatchMountError', 'TestingLibraryError'].includes(err.name)) { Logger.error(err); instance.client.reporter.registerTestError(err); } return err; }) .then(result => { let reportErrors = instance.client.settings.report_command_errors; const reportNetworkErrors = instance.client.settings.report_network_errors; if (result && result.error && result.error.code && result.status === -1 && reportNetworkErrors) { // node.js errors, e.g. ECONNRESET reportErrors = true; } if (result && result.status === -1 && instance.reportProtocolErrors(result) && reportErrors) { const err = new Error(`Error while running .${this.commandName}(): ${result.error}`); if (result.stack) { err.stack = result.stack; } if (result.error instanceof Error) { result.error.registered = true; } else { err.registered = true; } Logger.error(err); instance.client.reporter.registerTestError(err); } return result; }); if (instance instanceof EventEmitter) { return instance; } if (result instanceof Promise) { return result; } return result; }; } return this; } getTargetNamespace(parent, namespacedApi) { let namespace; if (parent) { namespace = super.getTargetNamespace(parent); } else if (Array.isArray(this.namespace) && this.namespace.length > 0) { namespace = BaseCommandLoader.unflattenNamespace(namespacedApi || this.api, this.namespace.slice()); } return namespace; } } module.exports = CommandLoader; ================================================ FILE: lib/api/_loaders/element-api.js ================================================ const {Locator} = require('../../element'); const Utils = require('../../utils'); const ScopedWebElement = require('../web-element'); const {By} = require('selenium-webdriver'); class ScopedElementAPILoader { static get availableElementCommands() { // commands directly available on `browser.element` return [ ['findElement', 'find', 'get'], ['findAll', 'findElements', 'getAll'], ['findByText'], ['findAllByText'], ['findByRole'], ['findAllByRole'], ['findByAltText'], ['findAllByAltText'], ['findByLabelText'], ['findByPlaceholderText'], ['findAllByPlaceholderText'] ]; } static isScopedElementCommand(commandName) { return ScopedElementAPILoader.availableElementCommands.some( commands => commands.includes(commandName) ); } constructor(nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; } createLocatingCommand(name) { return Utils.setFunctionName((...args) => { const rootElement = ScopedWebElement.root(this.nightwatchInstance, args); return rootElement.createRootElementCommand(name, rootElement, args); }, name); } loadElementMethods(target, element) { const findActive = () => ScopedWebElement.active(this.nightwatchInstance); const exportedElement = Object.assign(target, { findActive, getActive: findActive }); ScopedElementAPILoader.availableElementCommands.forEach(commandName => { let names = commandName; if (!Array.isArray(names)) { names = [names]; } names.forEach(commandName => { Object.defineProperty(exportedElement, commandName, { value: this.createLocatingCommand.call(this, commandName, element), writable: false }); }); }); return exportedElement; } createElementMethod() { const nightwatchInstance = this.nightwatchInstance; return this.loadElementMethods(function(...args) { let selector = args[0]; if (args.length >= 2 && Utils.isString(args[0]) && Utils.isString(args[1])) { selector = By[Locator.AVAILABLE_LOCATORS[args[0]]](args[1]); } return ScopedWebElement.create(selector, null, nightwatchInstance); }); } } module.exports = ScopedElementAPILoader; ================================================ FILE: lib/api/_loaders/element-command.js ================================================ const CommandLoader = require('./command.js'); const ElementCommand = require('../../element').Command; class ElementCommandLoader extends CommandLoader { static createInstance(CommandModule, opts) { let ClassName = CommandModule || ElementCommand; return new ClassName(opts); } createWrapper() { if (this.module) { this.commandFn = function commandFn({args, stackTrace}) { const instance = ElementCommandLoader.createInstance(this.module, { args, commandName: this.commandName, nightwatchInstance: this.nightwatchInstance }); instance.stackTrace = stackTrace; return instance.command(); }; } return this; } } module.exports = ElementCommandLoader; ================================================ FILE: lib/api/_loaders/element-global.js ================================================ const {until, WebElement} = require('selenium-webdriver'); const Utils = require('../../utils'); const {Logger, isUndefined, isObject, isString, isFunction} = Utils; const isDefined = function(val) { return !isUndefined(val); }; const Element = require('../../element'); const {Locator} = Element; class ElementGlobal { static get availableElementCommands() { return [ 'getId', ['findElement', 'element', 'find', 'get'], ['findElements', 'findAll'], 'click', 'sendKeys', ['getTagName', 'tagName'], ['getCssValue', 'css'], ['getAttribute', 'attr', 'attribute'], ['getProperty', 'property', 'prop'], ['getText', 'text'], ['getAriaRole', 'ariaRole'], ['getAccessibleName', 'accessibleName'], ['getRect', 'rect'], 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', ['takeScreenshot', 'screenshot'] ]; } static element({locator, testSuite, client, options}) { const instance = new ElementGlobal({testSuite, client, options}); instance.setLocator(locator); return instance.exported(); } get api() { return this.nightwatchInstance.api; } get reporter() { return this.nightwatchInstance.reporter; } get commandQueue() { return this.nightwatchInstance.queue; } get transport() { return this.nightwatchInstance.transport; } get settings() { return this.client ? this.client.settings : this.testSuite.settings; } constructor({testSuite, client, options = {}}) { this.testSuite = testSuite; this.client = client; this.isComponent = options.isComponent || false; this.componentType = this.isComponent && options.type; this.suppressNotFoundErrors = false; this.abortOnFailure = this.settings.globals.abortOnElementLocateError; this.timeout = this.settings.globals.waitForConditionTimeout; this.retryInterval = this.settings.globals.waitForConditionPollInterval; this.init(); } init() { this.nightwatchInstance = this.testSuite && this.testSuite.client ? this.testSuite.client : this.client; } async findElement() { if (this.element) { return; } const {locator} = this; if ((locator instanceof Element) && locator.resolvedElement) { this.element = this.createWebElement(locator.resolvedElement); return; } this.element = await this.transport.driver.wait(until.elementLocated(locator), this.timeout, null, this.retryInterval); } static isElementObject(element) { if (!isObject(element)) { return false; } if (!isString(element.selector)) { return false; } const { abortOnFailure, retryInterval, timeout, suppressNotFoundErrors, index } = element; return isDefined(abortOnFailure) || isDefined(retryInterval) || isDefined(timeout) || isDefined(suppressNotFoundErrors) || isDefined(index); } setPropertiesFromElement(element) { const { abortOnFailure, retryInterval, timeout, suppressNotFoundErrors, index } = element; if (isDefined(index)) { this.index = index; } if (isDefined(abortOnFailure)) { this.abortOnFailure = abortOnFailure; } if (isDefined(suppressNotFoundErrors)) { this.suppressNotFoundErrors = suppressNotFoundErrors; } if (isDefined(retryInterval)) { this.retryInterval = retryInterval; } if (isDefined(timeout)) { this.timeout = timeout; } } createWebElement(id) { return new WebElement(this.transport.driver, id); } setLocator(locator) { if (WebElement.isId(locator)) { this.element = this.createWebElement(WebElement.extractId(locator)); return; } if (locator instanceof WebElement) { this.element = locator; return; } if (ElementGlobal.isElementObject(locator)) { locator = Element.createFromSelector(locator); } if (locator instanceof Element) { this.locator = locator; } else { let value; if (isString(locator) && this.client && this.client.locateStrategy) { value = { value: locator, using: this.client.locateStrategy }; } else { value = locator; } this.locator = Locator.create(value); } } exported() { const exportedElement = Element.createFromSelector(this.locator || this.element); this.loadCommandsOntoObject(exportedElement); return exportedElement; } computeArguments(args, commandName) { if (args.length === 0) { return args; } if (['findElements', 'findElement'].includes(commandName)) { if (isString(args[0])) { args[0] = { value: args[0], using: 'css selector' }; } else if (isObject(args[0]) && args[0].selector) { if (args[0] instanceof Element || ElementGlobal.isElementObject(args[0])) { this.setPropertiesFromElement(args[0]); } args[0] = Locator.create(args[0]); } } return args; } getComponentProperty(propName) { return this.client.transportActions.executeScript(function(property) { // eslint-disable-next-line if (!window['@@component_element']) { throw new Error('Component was not rendered.'); } // eslint-disable-next-line if (window['@@component_element'].componentVM) { // eslint-disable-next-line return window['@@component_element'].componentVM[property]; } // eslint-disable-next-line return window['@@component_element'][property]; }, [propName]).then(result => { if (result.error instanceof Error) { throw result.error; } return result.value; }); } createCommand(commandName, commandToExecute, nightwatchName) { return function executeFn(...args) { const deferred = Utils.createPromise(); const stackTrace = Utils.getOriginalStackTrace(executeFn); this.init(); const {api, commandQueue, nightwatchInstance} = this; const commandFn = async () => { const isElement = await this.setElement(stackTrace); if (!isElement) { return null; } if (commandName === 'findElement' && args.length === 0) { return this.element; } args = this.computeArguments(args, commandName); let value; let error; try { if (['getProperty', 'property', 'prop'].includes(nightwatchName) && this.isComponent && this.componentType) { value = await this.getComponentProperty(args[0]); } else { value = await this.element[commandName].apply(this.element, args); } } catch (err) { error = err; } if (['find', 'get', 'element'].includes(nightwatchName) && (value instanceof WebElement)) { value = ElementGlobal.element({locator: value, client: nightwatchInstance}); } else if (nightwatchName === 'findAll') { value = value.map(element => { if (element instanceof WebElement) { return ElementGlobal.element({locator: element, client: nightwatchInstance}); } return element; }); } const lastArg = args[args.length - 1]; if (isFunction(lastArg)) { if (error) { return lastArg.call(this.api, { value, error, status: 0 }); } const callbackResult = lastArg.call(this.api, { value, status: 0 }); if (isDefined(callbackResult)) { return callbackResult; } } if (['find', 'get', 'element'].includes(nightwatchName) && error) { return null; } if (error) { throw error; } return value; }; const isES6Async = true; const node = commandQueue.add({ commandName: `element().${nightwatchName}`, commandFn: commandToExecute ? commandToExecute({stackTrace}) : commandFn, context: api, args, stackTrace, namespace: null, alwaysResolvePromise: true, rejectPromise: true, deferred, isES6Async }); Object.assign(node.deferred.promise, api); if (commandName === 'findElement') { node.deferred.promise['@nightwatch_element'] = true; node.deferred.promise['@nightwatch_args'] = args; if (this.isComponent) { node.deferred.promise['@nightwatch_component'] = true; } } else if (commandName === 'findElements') { node.deferred.promise['@nightwatch_multiple_elements'] = true; } return node.deferred.promise; }.bind(this); } async setElement(stackTrace) { try { await this.findElement(); return true; } catch (err) { if (this.suppressNotFoundErrors) { return null; } err.stack = stackTrace; Logger.error(err); if (this.abortOnFailure) { this.reporter.registerTestError(err); } return null; } } loadCommandsOntoObject(exportedElement) { ElementGlobal.availableElementCommands.forEach(commandName => { let names = commandName; if (!Array.isArray(names)) { names = [names]; } const seleniumName = names[0]; names.forEach(commandName => { Object.defineProperty(exportedElement, commandName, { value: this.createCommand.call(this, seleniumName, null, commandName), writable: false }); }); }); Object.defineProperty(exportedElement, 'getWebElement', { value: this.createCommand.call(this, 'getWebElement', ({stackTrace}) => { return async () => { const isElement = await this.setElement(stackTrace); if (!isElement) { return null; } return this.element; }; }), writable: false }); if (this.isComponent) { Object.defineProperty(exportedElement, 'isComponent', { value: this.isComponent, writable: false }); } return this; } } module.exports = ElementGlobal; ================================================ FILE: lib/api/_loaders/ensure.js ================================================ const util = require('util'); const {until, WebElement} = require('selenium-webdriver'); const EventEmitter = require('events'); const Utils = require('../../utils'); const {AssertionRunner} = require('../../assertion'); const Element = require('../../element'); const BaseLoader = require('./_base-loader.js'); const {Locator} = Element; const {Logger} = Utils; class SeleniumCommand extends EventEmitter { constructor({negate, args, settings, commandName}) { super(); this.negate = negate; this.commandName = commandName; this.settings = settings; this.defaultTimeout = this.settings.globals.waitForConditionTimeout; this.retryInterval = this.settings.globals.waitForConditionPollInterval; this.args = args; this.actual = null; this.message = ''; this.setExpected(); } setExpected() { let operator = this.getVerb(); this.expected = `${operator} '${this.args[0]}'`; } isElementCommand() { return EnsureAssertionLoader.elementCommands.includes(this.commandName); } requiresLocatingElement() { return EnsureAssertionLoader.requiresLocating.includes(this.commandName); } getVerb() { let message = EnsureAssertionLoader.assertOperators[this.commandName]; let actionVerb; let operator; if (Utils.isString(message)) { actionVerb = EnsureAssertionLoader.defaultVerb; } else if (Array.isArray(message) && message[1]) { actionVerb = message[1]; } operator = this.negate ? actionVerb[1] : actionVerb[0]; return operator; } getMessage(passed) { let message = EnsureAssertionLoader.assertOperators[this.commandName]; if (!message) { return ''; } let operator = this.getVerb(); if (Array.isArray(message)) { message = message[0]; } message = this.isElementCommand() ? util.format(message, this.args[0], operator) : util.format(message, operator, this.args[0]); return message; } static isSeleniumElement(element) { return (element instanceof WebElement); } async adaptElementArgument(transport) { if (!this.requiresLocatingElement()) { this.args[0] = Locator.create(this.args[0]); return; } if (SeleniumCommand.isSeleniumElement(this.args[0])) { return; } const locator = Locator.create(this.args[0]); this.args[0] = await transport.driver.wait(until.elementLocated(locator), this.defaultTimeout); } async execute(transport) { if (this.isElementCommand()) { await this.adaptElementArgument(transport); } return transport.driver.wait(until[this.commandName](...this.args), this.defaultTimeout, undefined, this.retryInterval); } } class EnsureAssertionLoader extends BaseLoader { static get defaultVerb() { return ['is', 'is not']; } static get assertOperators() { return { ableToSwitchToFrame: 'browser driver %s able to switch to the designated frame', alertIsPresent: 'alert %s present', titleIs: 'title %s %s', titleContains: ['title %s %s', ['contains', 'does not contain']], titleMatches: ['title %s %s', ['matches', 'does not match']], urlIs: 'url %s %s', urlContains: ['url %s %s', ['contains', 'does not contain']], urlMatches: ['url %s %s', ['matches', 'does not match']], elementLocated: ['element "%s" %s located', ['is', 'is not']], elementsLocated: ['elements "%s" %s located', ['are', 'are not']], stalenessOf: 'element "%s" %s stale', elementIsVisible: 'element "%s" %s visible', elementIsNotVisible: 'element "%s" %s not visible', elementIsEnabled: 'element "%s" %s enabled', elementIsDisabled: 'element "%s" %s disabled', elementIsSelected: 'element "%s" %s selected', elementIsNotSelected: 'element "%s" %s not selected', elementTextIs: 'element "%s" text %s', elementTextContains: ['element "%s" text %s', ['contains', 'does not contain']], elementTextMatches: ['element "%s" text %s', ['matches', 'does not match']] }; } static get elementCommands() { return [ 'elementLocated', 'elementsLocated', 'stalenessOf', 'elementIsVisible', 'elementIsNotVisible', 'elementIsEnabled', 'elementIsDisabled', 'elementIsSelected', 'elementIsNotSelected', 'elementTextIs', 'elementTextContains', 'elementTextMatches' ]; } static get requiresLocating() { return [ 'stalenessOf', 'elementIsVisible', 'elementIsNotVisible', 'elementIsEnabled', 'elementIsDisabled', 'elementIsSelected', 'elementIsNotSelected', 'elementTextIs', 'elementTextContains', 'elementTextMatches' ]; } async runAssertion({negate, args, commandName, abortOnFailure, assertFn}) { const {reporter, transport, nightwatchInstance} = this; const {settings} = nightwatchInstance; const startTime = new Date(); const command = new SeleniumCommand({negate, args, settings, commandName}); const {stackTrace, expected} = command; const message = command.getMessage(); let passed = !negate; let actual = ''; try { await command.execute(transport); } catch (err) { passed = !!negate; const lines = err.message.split('\n'); if (!negate) { Logger.error(err); } if (lines.length > 1) { actual = lines[1]; } else { actual = err.message; } command.error = err; } const elapsedTime = new Date() - startTime; const runner = new AssertionRunner({ passed, err: { expected, actual }, message, calleeFn: assertFn, abortOnFailure, stackTrace, reporter, elapsedTime }); return runner.run(); } /** * @param commandName * @param abortOnFailure * @returns {Function} */ createAssertion(commandName, abortOnFailure) { return function assertFn({negate, args}) { const namespace = 'ensure'; const isES6Async = Utils.isUndefined(this.isES6Async) ? (this.nightwatchInstance.isES6AsyncTestcase || this.nightwatchInstance.settings.always_async_commands) : this.isES6Async; const commandFn = () => { return this.runAssertion({ negate, args, commandName, abortOnFailure, assertFn }); }; const deferred = Utils.createPromise(); const node = this.commandQueue.add({ commandName: negate ? `not.${commandName}` : commandName, commandFn, context: this.api, args: [], stackTrace: assertFn.stackTrace, namespace, deferred, isES6Async }); if (isES6Async) { this.commandQueue.tree.once('asynctree:finished', (err) => { node.deferred.resolve(); }); Object.assign(node.deferred.promise, this.api); return node.deferred.promise; } return this.api; }.bind(this); } /** * * @param {object} [parent] * @return {ApiLoader} */ loadAssertions(parent = null) { Object.keys(until).forEach(propertyName => { let namespace; if (parent) { namespace = parent.ensure = parent.ensure || {}; } this.nightwatchInstance.setApiMethod(propertyName, namespace || 'ensure', (function(commandName) { return this.createAssertion(commandName, false); }.bind(this))(propertyName)); }); return this; } } module.exports = EnsureAssertionLoader; ================================================ FILE: lib/api/_loaders/expect-assertion.js ================================================ const chaiNightwatch = require('chai-nightwatch'); const BaseLoader = require('./_base-loader.js'); class ExpectAssertionLoader extends BaseLoader { static get ChaiAssertionType() { return { PROPERTY: 'property', METHOD: 'method' }; } static getChaiAssertionType(assertionType) { const {PROPERTY, METHOD} = ExpectAssertionLoader.ChaiAssertionType; switch (assertionType) { case PROPERTY: return 'addProperty'; case METHOD: return 'addMethod'; } } static createMatcher({ assertionType, commandName, aliases = [], nightwatchInstance, AssertModule, expectCommandName }) { if (!Array.isArray(commandName)) { commandName = [commandName]; } const method = ExpectAssertionLoader.getChaiAssertionType(assertionType); commandName.forEach(command => { const assertFn = function(...args) { const assertion = new AssertModule({nightwatchInstance, chaiExpect: this, expectCommandName}); assertion.init(...args); this.assertion = assertion; return this; }; if (!Array.isArray(aliases)) { throw new Error(`Error while creating expect assertion: .aliases property needs to be an Array; received: ${aliases}`); } aliases.push(command); aliases.forEach(alias => chaiNightwatch.Assertion[method](alias, assertFn)); }); } loadAssertion(expectCommandName) { if (!this.commandName) { return; } const {nightwatchInstance} = this; const assertionType = this.module.assertionType; const commandName = this.module.assertionName || this.commandName; const aliases = this.module.aliases; ExpectAssertionLoader.createMatcher({ nightwatchInstance, assertionType, commandName, expectCommandName, aliases, AssertModule: this.module }); } } module.exports = ExpectAssertionLoader; ================================================ FILE: lib/api/_loaders/expect.js ================================================ const chai = require('chai-nightwatch'); const path = require('path'); const fs = require('fs'); const Utils = require('../../utils'); const BaseCommandLoader = require('./_command-loader.js'); const ExpectAssertionLoader = require('./expect-assertion.js'); class ExpectLoader extends BaseCommandLoader { static get interfaceMethods() { return { command: 'function' }; } static createInstance(nightwatchInstance, CommandModule, opts) { const commandInstance = new CommandModule({ nightwatchInstance, commandName: opts.commandName, commandArgs: opts.args }); Object.keys(ExpectLoader.interfaceMethods).forEach(method => { const type = ExpectLoader.interfaceMethods[method]; if (!BaseCommandLoader.isTypeImplemented(commandInstance, method, type)) { throw new Error(`Command class must implement method .${method}()`); } }); return commandInstance; } createWrapper(abortOnFailure) { if (this.module) { this.abortOnFailure = abortOnFailure; const commandName = this.commandName; this.commandFn = function commandFn({args, stackTrace}) { this.stackTrace = stackTrace; const flagsOk = this.checkFlags(); if (!flagsOk) { this.emit('error', new Error(`Incomplete expect assertion for "expect.${commandName}()". Please consult the docs at https://nightwatchjs.org/api/expect/`)); return this; } if ((args[0] instanceof Promise) && args[0]['@nightwatch_element']) { let selector = ''; const elementArgs = args[0]['@nightwatch_args'] || []; if (elementArgs[0] && elementArgs[0].value) { selector = elementArgs[0].value; } args[0].then(result => { args[0] = result; if (!result) { const err = new Error(`Unable to find element <${selector}>.`); err.isExpect = true; this.emit('error', err); return this; } this.run(...args); }); } else { this.run(...args); } this.promise.catch(err => { if (!(this.instance instanceof chai.Assertion)) { this.emit('error', new Error(this.promiseRejectedMsg || `An error occurred while running .expect.${commandName}`)); } }); return this; }; } return this; } define(parent = null) { const {commandName, nightwatchInstance, commandFn, commandQueue} = this; const namespace = (parent || nightwatchInstance.api).expect; nightwatchInstance.setApiMethod(commandName, namespace, function queuedCommandFn(...args) { const stackTrace = Utils.getOriginalStackTrace(queuedCommandFn); const expectCommand = ExpectLoader.createInstance(nightwatchInstance, this.module, { args, commandName }); const isAsyncCommand = this.isES6Async; const isES6Async = Utils.isUndefined(this.isES6Async) ? (nightwatchInstance.isES6AsyncTestcase || nightwatchInstance.settings.always_async_commands) : isAsyncCommand; const {deferred} = expectCommand; this.loadAssertions(expectCommand); const node = commandQueue.add({ commandName, namespace: 'expect', commandFn, deferred, context: expectCommand, isAsyncCommand, isES6Async, args, stackTrace }); if (isES6Async && (expectCommand.instance instanceof Promise)) { // prevent unhandledRejection errors expectCommand.instance.catch(err => { deferred.reject(err); }); } return expectCommand.instance; }.bind(this)); } loadAssertions(expectCommand) { const {nightwatchInstance, commandName} = this; // expect commands like .element() and .elements() have individual assertions (e.g. visible, present... / count); // however expect commands like .url(), .title(), and .cookie only have an implicit assertion which is based on their value if (expectCommand.hasAssertions) { const assertionsPath = path.join(__dirname, '../expect/assertions', expectCommand.assertionsPath || commandName); let modules = fs.readdirSync(assertionsPath); modules = modules.filter(moduleName => { return !moduleName.startsWith('_'); }); modules.forEach(assertionFileName => { const loader = new ExpectAssertionLoader(nightwatchInstance); loader .loadModule(assertionsPath, assertionFileName) .loadAssertion(commandName); }); } } } module.exports = ExpectLoader; ================================================ FILE: lib/api/_loaders/firefox.js ================================================ const BaseLoader = require('./_base-loader.js'); class FirefoxCommandLoader extends BaseLoader { static get firefoxCommands() { return [ 'getContext', 'setContext', 'installAddon', 'uninstallAddon' ]; } loadCommands() { const commands = FirefoxCommandLoader.firefoxCommands; this.loadDriverCommands({ commands, namespace: 'firefox' }); return this; } } module.exports = FirefoxCommandLoader; ================================================ FILE: lib/api/_loaders/page-object.js ================================================ const lodashMerge = require('lodash/merge'); const BaseLoader = require('./_base-loader.js'); const Page = require('../../page-object'); let __page_object_cache = null; class PageObjectLoader extends BaseLoader { get loadSubDirectories() { return true; } get pageObjectCache() { return __page_object_cache; } static loadApiCommands(nightwatchInstance) { __page_object_cache = { __is_page_object_cache: true }; const ApiLoader = require('../index.js'); const StaticApis = require('./static.js'); const apiLoader = new ApiLoader(nightwatchInstance); const staticApis = new StaticApis(nightwatchInstance); staticApis.loadStaticAssertions(__page_object_cache); staticApis.loadStaticExpect(__page_object_cache); if (nightwatchInstance.startSessionEnabled) { return apiLoader.loadCustomCommands(__page_object_cache) .then(() => apiLoader.loadCustomAssertions(__page_object_cache)) .then(() => apiLoader.loadApiCommandsSync(__page_object_cache)) .then(() => apiLoader.loadPlugins(__page_object_cache)) .then(() => { // TODO: possibly load .ensure assertions as well __page_object_cache.expect.section = __page_object_cache.expect.element; }); } return Promise.resolve(); } loadApi(pageObject) { const result = lodashMerge(pageObject, this.pageObjectCache); return result; } createWrapper() { return this; } pageObjectDefinition() { return new Page(this.module, this.loadApi.bind(this), this.nightwatchInstance); } define() { if (this.module) { const parent = this.api.page; let namespace; if (Array.isArray(this.namespace) && this.namespace.length > 0) { namespace = BaseLoader.unflattenNamespace(parent, this.namespace.slice()); } try { this.module.name = this.commandName; // eslint-disable-next-line } catch (err) {} this.nightwatchInstance.setApiMethod(this.commandName, namespace || 'page', this.pageObjectDefinition.bind(this)); } } } module.exports = PageObjectLoader; ================================================ FILE: lib/api/_loaders/plugin.js ================================================ const fs = require('fs'); const path = require('path'); const Utils = require('../../utils'); const __plugins = {}; module.exports = class PluginLoader { static get plugins() { return __plugins; } static load(pluginName) { try { if (PluginLoader.plugins[pluginName]) { return PluginLoader.plugins[pluginName]; } const plugin = {}; const pluginPath = Utils.getPluginPath(pluginName); const dirName = path.dirname(pluginPath); plugin.instance = require(pluginPath); let isFolder; try { isFolder = fs.lstatSync(path.join(dirName, 'nightwatch')).isDirectory(); } catch (e) { plugin.hasFolder = false; } if (isFolder) { // load custom commands, if any const commandsFolder = path.join(dirName, 'nightwatch', 'commands'); try { const hasCommands = fs.lstatSync(commandsFolder).isDirectory(); if (hasCommands) { plugin.commands = commandsFolder; } } catch (err) { plugin.commands = false; } // load custom assertions, if any const assertionsFolder = path.join(dirName, 'nightwatch', 'assertions'); try { const hasAssertions = fs.lstatSync(assertionsFolder).isDirectory(); if (hasAssertions) { plugin.assertions = assertionsFolder; } } catch (err) { plugin.assertions = false; } // load plugin globals, if defined const globalsPath = path.join(dirName, 'nightwatch', 'globals.js'); plugin.globals = fs.existsSync(globalsPath) ? Utils.requireModule(globalsPath) : false; // load plugin transforms, if defined const transformsPath = path.join(dirName, 'nightwatch', 'transforms.js'); plugin.transforms = fs.existsSync(transformsPath) ? Utils.requireModule(transformsPath) : null; } PluginLoader.plugins[pluginName] = plugin; return PluginLoader.plugins[pluginName]; } catch (err) { const msg = err.message.split('\n')[0]; const error = new Error(`Unable to load plugin: ${pluginName}: [${err.code}] ${msg}`); error.showTrace = true; error.displayed = false; throw error; } } }; ================================================ FILE: lib/api/_loaders/static.js ================================================ const util = require('util'); const chai = require('@nightwatch/chai'); const assertModule = require('assert'); const EventEmitter = require('events'); const Utils = require('../../utils'); const Element = require('../../element'); const chaiExpect = chai.expect; const {flag} = chai.util; const {AssertionRunner} = require('../../assertion'); const namespacedApi = require('../../core/namespaced-api.js'); let __last_deferred__ = null; module.exports = class StaticAssert { static get assertOperators() { return { ok: ['ok', 'ko'], equal: ['==', '!='], notEqual: ['!=', '=='], deepEqual: ['deepEqual', 'not deepEqual'], notDeepEqual: ['not deepEqual', 'deepEqual'], strictEqual: ['===', '!=='], notStrictEqual: ['!==', '==='], deepStrictEqual: ['deep strict equal', 'not deep strict equal'], throws: ['throws', 'doesNotThrow'], doesNotThrow: ['doesNotThrow', 'throws'], match: ['matches', 'does not match'], fail: 'fail', ifError: 'ifError' }; } static get lastDeferred() { return __last_deferred__; } static set lastDeferred(value) { __last_deferred__ = value; } get api() { return this.nightwatchInstance.api; } get reporter() { return this.nightwatchInstance.reporter; } get commandQueue() { return this.nightwatchInstance.queue; } constructor(nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; } /** * Extends the node.js assert module * * @param commandName * @param abortOnFailure * @param apiToReturn * @returns {Function} */ createStaticAssertion(commandName, abortOnFailure, apiToReturn) { class Assertion extends EventEmitter { constructor({negate, args}) { super(); StaticAssert.lastDeferred = null; this.negate = negate; this.args = args; this.passed = null; this.expected = null; this.actual = null; const lastArgument = args[args.length - 1]; const isLastArgString = Utils.isString(lastArgument); this.message = isLastArgString && (args.length > 2 || Utils.isBoolean(args[0])) && lastArgument || Utils.isFunction(args[0]) && '[Function]'; } getMessage(propName) { if (!Array.isArray(StaticAssert.assertOperators[propName])) { return StaticAssert.assertOperators[propName] || ''; } const operator = (this.passed && !this.negate) ? StaticAssert.assertOperators[propName][0] : StaticAssert.assertOperators[propName][1]; let message = ''; if (this.negate) { message += ' not '; } if (this.args.length === 2) { this.args.splice(1, 0, operator); } else { this.args.push(operator); } this.args = this.args.map(function(argument) { if (Utils.isObject(argument)) { argument = util.inspect(argument); } return argument; }); return message + this.args.join(' '); } assert(propName) { try { assertModule[propName].apply(null, this.args); this.passed = !this.negate; this.message = `${this.negate ? 'Failed' : 'Passed'} [${propName}]: ${this.message || this.getMessage(propName)}`; } catch (ex) { this.passed = !!this.negate; if (!this.passed && (ex instanceof Error) && propName === 'fail') { this.message = ex.message; } else { this.message = `${this.negate ? 'Passed' : 'Failed'} [${propName}]: (${ex.message || this.message || this.getMessage(propName)})`; } if (Utils.isDefined(ex.showTrace)) { this.showTrace = ex.showTrace; } if (Utils.isDefined(ex.link)) { this.link = ex.link; } if (Utils.isDefined(ex.help)) { this.help = ex.help; } this.actual = ex.actual; this.expected = ex.expected; this.stackTrace = ex.stack; } } } return function assertFn({negate, args}) { const assertion = new Assertion({negate, args}); const {reporter, nightwatchInstance} = this; assertion.assert(commandName); const startTime = new Date(); const namespace = abortOnFailure ? 'assert' : 'verify'; const commandFn = () => { const {passed, expected, actual, message, stackTrace, showTrace, link, help} = assertion; const elapsedTime = new Date() - startTime; this.runner = new AssertionRunner({ passed, err: { expected, actual }, message, calleeFn: assertFn, abortOnFailure, stackTrace, showTrace, reporter, elapsedTime, link, help }); return this.runner.run(); }; const isES6Async = nightwatchInstance.isES6AsyncTestcase || nightwatchInstance.settings.always_async_commands; const deferred = Utils.createPromise(); const node = this.commandQueue.add({ commandName, commandFn, context: this.api, args: [], stackTrace: assertFn.stackTrace, namespace, deferred, isES6Async }); if (isES6Async || node.isES6Async) { StaticAssert.lastDeferred = deferred; Object.assign(node.deferred.promise, apiToReturn || this.api); //prevent unhandled rejection node.deferred.promise.catch(err => { return StaticAssert.lastDeferred.reject(err); }); return node.deferred.promise; } return apiToReturn || this.api; }.bind(this); } /** * * @param {object} [parent] * @return {ApiLoader} */ loadStaticAssertions(parent = null) { Object.keys(assertModule).forEach(prop => { let namespace; if (parent) { namespace = parent.assert = parent.assert || {}; } let apiToReturn; if (parent && parent.__is_page_object_cache) { apiToReturn = parent; } const namespacedApiToReturn = (namespace) => { return new Proxy(namespacedApi[namespace], { get(_, name) { return namespacedApi[namespace][name]; } }); }; this.nightwatchInstance.setApiMethod(prop, namespace || 'assert', (function(prop) { return this.createStaticAssertion(prop, true, apiToReturn); }.bind(this))(prop)); if (!parent) { this.nightwatchInstance.setNamespacedApiMethod(prop, 'assert', (function(prop) { return this.createStaticAssertion(prop, true, namespacedApiToReturn('assert')); }.bind(this))(prop)); } if (this.nightwatchInstance.startSessionEnabled) { let namespace; if (parent) { namespace = parent.verify = parent.verify || {}; } this.nightwatchInstance.setApiMethod(prop, namespace || 'verify', (function(prop) { return this.createStaticAssertion(prop, false); }.bind(this))(prop)); if (!parent) { this.nightwatchInstance.setNamespacedApiMethod(prop, 'verify', (function(prop) { return this.createStaticAssertion(prop, true, namespacedApiToReturn('verify')); }.bind(this))(prop)); } } }); return this; } /** * @param {object} [parent] */ loadStaticExpect(parent = null) { try { this.nightwatchInstance.setApiMethod('expect', parent, (...args) => { let obj = args[0]; const isElement = Element.isElementObject(obj) || Utils.isObject(obj) && obj['@nightwatch_element']; if (!(obj instanceof Promise) && !isElement) { args[0] = obj = new Promise(resolve => resolve(obj)); } const assertion = chaiExpect(...args); if (!obj) { return assertion; } if (isElement) { if (obj.isComponent || obj['@nightwatch_component']) { return this.api.expect.component(...args); } return this.api.expect.element(...args); } flag(assertion, 'actionFn', (opts) => { return this.addToQueue(opts); }); return assertion; }); } catch (err) { this.nightwatchInstance.setApiMethod('expect', parent, {}); } return this; } addToQueue() { return function assertFn(valueDisplay, assertionName, handlerFn) { const abortOnFailure = true; const startTime = new Date(); const namespace = function() { return 'expect()'; }; const {nightwatchInstance, reporter} = this; const commandFn = () => { let passed = true; let expected; let actual; let message = `Expected ${valueDisplay} ${assertionName.join(' ')}: `; const stackTrace = ''; try { handlerFn(); } catch (err) { passed = false; expected = err.expected; actual = err.actual; message = err.message; } const elapsedTime = new Date() - startTime; this.runner = new AssertionRunner({ passed, err: { expected, actual }, message, calleeFn: assertFn, abortOnFailure, stackTrace, reporter, elapsedTime }); return this.runner.run(); }; const isES6Async = nightwatchInstance.isES6AsyncTestcase || nightwatchInstance.settings.always_async_commands; const deferred = Utils.createPromise(); const node = this.commandQueue.add({ commandName: assertionName.join('.'), commandFn, context: this.api, args: [], stackTrace: assertFn.stackTrace, namespace, deferred, isES6Async }); if (isES6Async) { Object.assign(node.deferred.promise, this.api); return node.deferred.promise; } return this.api; }.bind(this); } }; ================================================ FILE: lib/api/_loaders/within-context.js ================================================ const BaseLoader = require('./_base-loader.js'); const CommandWrapper = require('../../page-object/command-wrapper.js'); const Element = require('../../element'); const __commands_cache = { __commands_cache: true }; class WithinLoader extends BaseLoader { get loadSubDirectories() { return false; } static loadCommandCache(nightwatchInstance) { if (!__commands_cache.loaded) { const ApiLoader = require('../index.js'); const apiLoader = new ApiLoader(nightwatchInstance); return new Promise(resolve => { __commands_cache.loaded = true; if (nightwatchInstance.startSessionEnabled) { return apiLoader .loadCustomCommands(__commands_cache) .then(() => apiLoader.initPluginTransforms()) .then(() => apiLoader.loadPlugins(__commands_cache)) .then(() => resolve()); } resolve(); }); } return Promise.resolve(); } loadApi(context) { const ApiLoader = require('../index.js'); Object.keys(__commands_cache).forEach((command) => { context[command] = ((commandName) => { return (...args) => { return this.nightwatchInstance.api[commandName](...args); }; })(command); }); const elementCommands = ApiLoader.getElementsCommandsStrict(); if (elementCommands.length > 0) { elementCommands.forEach(command => { context[command] = ((commandName) => { return (...args) => { return this.nightwatchInstance.api[commandName](...args); }; })(command); }); } return context; } createWrapper() { return this; } withinDefinition(...args) { if (args.length !== 1) { throw new Error('within() expects exactly one argument.'); } this.__instance = new WithinContext(this.loadApi.bind(this), this.nightwatchInstance, args[0]); return this.instance; } define() { this.nightwatchInstance.setApiMethod('within', this.withinDefinition.bind(this)); } } class WithinContext { get api() { return this.__api; } get client() { return this.__client; } get args() { return this.__args; } constructor(loadApi, nightwatchInstance, container) { this.commandLoader = loadApi; this.__client = nightwatchInstance; this.__api = Object.assign({}, nightwatchInstance.api); this.__element = Element.createFromSelector(container); this.__needsRecursion = true; this.__promise = CommandWrapper.addWrappedCommandsAsync(this, this.commandLoader); } } module.exports = WithinLoader; ================================================ FILE: lib/api/assertions/_assertionInstance.js ================================================ const util = require('util'); const EventEmitter = require('events'); const Utils = require('../../utils'); const Element = require('../../element'); const {Logger} = Utils; class AssertionInstance { static isElementNotFoundResult(result) { if (!result) { return false; } if (result.status === -1) { result.value = result.value || []; return result.value.length === 0; } return false; } static init({nightwatchInstance, args, fileName, options}) { if (Utils.isFunction(args[args.length - 1])) { this.__doneCallback = args.pop(); } else { this.__doneCallback = function(result) { return Promise.resolve(result); }; } this.__nightwatchInstance = nightwatchInstance; this.__args = args; this.fileName = fileName; this.__negate = options.negate || false; this.__commandResult = null; this.retryAssertionTimeout = Utils.isObject(args[0]) ? args[0].timeout : undefined; this.rescheduleInterval = Utils.isObject(args[0]) ? args[0].retryInterval : undefined; } constructor({nightwatchInstance, args, fileName, options} = {}, skipInit = false) { if (!skipInit) { AssertionInstance.init.call(this, {nightwatchInstance, args, fileName, options}); EventEmitter.prototype.constructor.call(this); } } initialize() { let msgReplaceArgs = this.args; if (Utils.isFunction(this.formatMessage)) { const format = this.formatMessage(); this.message = format.message; if (this.message.includes('%s')) { msgReplaceArgs = format.args; } } else { msgReplaceArgs = msgReplaceArgs.map(arg => { if (Utils.isObject(arg) && arg.selector) { return `'<${arg.selector}>'`; } return `'${arg}'`; }); } if (this.message && this.message.includes('%s')) { this.message = Logger.formatMessage(this.message, ...msgReplaceArgs); } } set options(val) { this.__options = val; } get options() { return this.__options || {}; } get result() { return this.__commandResult; } set args(value) { throw new Error(`Attempting to override ".args" which is a reserved property in "${this.fileName}".`); } set elementSelector(value) { throw new Error(`Attempting to override ".elementSelector" which is a reserved property in "${this.fileName}".`); } set api(value) { throw new Error(`Attempting to override ".api" which is a reserved property in "${this.fileName}".`); } get args() { return this.__args; } get negate() { return this.__negate; } /** * @deprecated */ get client() { return this.__nightwatchInstance.client; } get api() { return this.__nightwatchInstance.api; } get doneCallback() { return this.__doneCallback; } set doneCallback(value) { this.__doneCallback = value; } get elementSelector() { if (this.args[0] instanceof Element) { return `<${this.args[0].toString()}>`; } if (Utils.isObject(this.args[0]) && this.args[0].selector) { return this.args[0].selector ? `<${this.args[0].selector}>` : ''; } if (Utils.isObject(this.args[0]) && this.args[0].webElementLocator) { return `<${this.args[0].webElementLocator}>`; } if (this.args[0].id_ && this.args[0].driver_) { return ''; } const content = this.args[0]; return this.options.elementSelector ? `<${content}>` : `'${content}'`; } runFailure() { const isFailed = this.failure(this.result); if (isFailed && this.message.includes('%s')) { this.message = Logger.formatMessage(this.message, this.args); } return isFailed; } hasFailure() { if (Utils.isFunction(this.failure)) { return this.runFailure(); } if (!this.result || this.result.status === -1) { return true; } const {error} = this.result; return (error instanceof Error) && error.name !== 'NoSuchElementError'; } getValue() { if (Utils.isObject(this.result) && Utils.isUndefined(this.result.value) || this.result.status === -1) { return null; } if (Utils.isFunction(this.value)) { const result = this.value(this.result); if (result === undefined) { return null; } return result; } return this.result.value; } isOk(value = '') { let passed; // Backwards compatibility check: // if there is a "pass" function declared, treat it like "evaluate" because "pass" // needs to take the negate into account now if (Utils.isFunction(this.pass)) { passed = this.pass(value); } else { passed = this.evaluate(value); } return this.negate ? !passed : passed; } getActual() { if (this.hasFailure()) { const elementNotFound = AssertionInstance.isElementNotFoundResult(this.result); return this.options.elementSelector ? (elementNotFound ? 'element could not be located' : 'error while locating the element') : ''; } if (Utils.isFunction(this.actual)) { const passed = this.evaluate(this.result.value); return this.actual(passed); } return this.getValue(); } /** * @param {Object} result */ setResult(result) { this.__commandResult = result; } } module.exports.create = function(opts = {}) { const {assertionModule, nightwatchInstance = {}, fileName, args = [], options = {}} = opts; if (assertionModule.prototype && assertionModule.prototype.constructor.toString().startsWith('class')) { const err = new Error('ES6 class assertions are not supported yet.'); err.help = [ 'Please use the module.exports = function() { ... } syntax instead', 'If you have an ESM project, you need to use the .cjs extension' ]; err.link = 'https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-assertions.html'; throw err; } const AssertionModule = Utils.isObject(assertionModule) && Utils.isFunction(assertionModule.assertion) ? assertionModule.assertion : assertionModule; util.inherits(AssertionModule, EventEmitter); util.inherits(AssertionInstance, AssertionModule); const instance = new AssertionInstance({nightwatchInstance, args, fileName, options}); // call the assertion function AssertionModule.prototype.constructor.apply(instance, args); instance.initialize(); return instance; }; ================================================ FILE: lib/api/assertions/attributeContains.js ================================================ /** * Checks if the given attribute of an element contains the expected value. * * @example * this.demoTest = function (browser) { * browser.assert.attributeContains('#someElement', 'href', 'google.com'); * }; * * @method assert.attributeContains * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} attribute The attribute name * @param {string} expected The expected contained value of the attribute to check. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, attribute, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if attribute %s of element %s ${this.negate ? 'doesn\'t contain %s' : 'contains %s'}`; return { message, args: [`'${attribute}'`, this.elementSelector, `'${expected}'`] }; }; this.evaluate = function(value) { value = value || ''; return value.includes(expected); }; this.actual = function() { const value = this.getValue(); if (typeof value != 'string') { return `Element does not have a '${attribute}' attribute`; } return value; }; this.expected = function() { return this.negate ? `not contains '${expected}'` : `contains '${expected}'`; }; this.command = function(callback) { return this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), attribute, callback); }; }; ================================================ FILE: lib/api/assertions/attributeEquals.js ================================================ /** * Checks if the given attribute of an element has the expected value. * * @example * this.demoTest = function (browser) { * browser.assert.attributeEquals('body', 'data-attr', 'some value'); * }; * * @method assert.attributeEquals * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} attribute The attribute name * @param {string} expected The expected value of the attribute to check. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, attribute, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if attribute %s of element %s ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [`'${attribute}'`, this.elementSelector, `'${expected}'`] }; }; this.evaluate = function(value) { return value === expected; }; this.actual = function(passed) { const value = this.getValue(); if (typeof value != 'string') { return `Element does not have a '${attribute}' attribute`; } return value; }; this.expected = function() { return this.negate ? `not equals '${expected}'` : `equals '${expected}'`; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), attribute, callback); }; }; ================================================ FILE: lib/api/assertions/attributeMatches.js ================================================ /** * Check if an element's attribute value matches a regular expression. * * @example * this.demoTest = function (browser) { * browser.assert.attributeMatches('body', 'data-attr', '(value)'); * }; * * @method assert.attributeMatches * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} attribute The attribute name * @param {string|RegExp} regexExpression Regex expression to match attribute value. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, attribute, regexExpression, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `does not matches '${regexExpression}'` : `matches '${regexExpression}'`; }; this.formatMessage = function() { const message = msg || `Testing if attribute %s of element %s ${this.negate ? 'doesn\'t matches %s' : 'matches %s'}`; return { message, args: [`'${attribute}'`, this.elementSelector, `'${regexExpression}'`] }; }; this.evaluate = function(value) { const regex = value instanceof RegExp ? value : new RegExp(regexExpression); return regex.test(value); }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), attribute, callback); }; }; ================================================ FILE: lib/api/assertions/contains.js ================================================ const Utils = require('../../utils/index.js'); exports.assertion = function (actual, expected, message) { this.options = { elementSelector: false }; this.expected = function () { return this.negate ? `does not contain ${expected}` : `contains ${expected}`; }; this.formatMessage = function () { return { args: [], message: '' }; }; this.refineFormattedMessage = function (value) { const finalMessage = message || `Testing if a value %s ${ this.negate ? 'does not contain %s' : 'contains %s' }`; this.message = finalMessage.includes('%s') ? Utils.Logger.formatMessage(finalMessage, value, expected) : finalMessage; }; this.evaluate = function (value) { return value.includes(expected); }; this.command = async function (callback) { return callback({value: await actual}, null); }; }; ================================================ FILE: lib/api/assertions/containsText.js ================================================ /** * Checks if the given element contains the specified text. * * ``` * this.demoTest = function (browser) { * browser.assert.containsText('#main', 'The Night Watch'); * }; * ``` * * @method assert.containsText * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expectedText The text to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expectedText, msg) { this.options = { elementSelector: true }; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .containsText() has been deprecated and will be ' + 'removed from future versions. Use assert.textContains().'); /*! * Returns the message format which will be used to output the message in the console and also * the arguments which will be used for replace the place holders, used in the order of appearance */ this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'does not contain text %s' : 'contains text %s'}`; return { message, args: [this.elementSelector, `'${expectedText}'`] }; }; /*! * Returns the expected value of the assertion which is displayed in the case of a failure * * @return {string} */ this.expected = function() { return this.negate ? `does not contain text '${expectedText}'` : `contains text '${expectedText}'`; }; /*! * Given the value, the condition used to evaluate if the assertion is passed * @param {*} value * @return {Boolean} */ this.evaluate = function(value) { if (typeof value != 'string') { return false; } return value.includes(expectedText); }; /*! * When defined, this method is called by the assertion runner with the command result, to determine if the * value can be retrieved successfully from the result object * * @param result * @return {boolean|*} */ this.failure = function(result) { return result === false || result && result.status === -1; }; /*! * Called with the result object of the command to retrieve the value which is to be evaluated * * @param {Object} result * @return {*} */ this.value = function(result) { if (result.status === -1) { return null; } return result.value; }; /*! * When defined, this method is called by the assertion runner with the command result to determine the actual * state of the assertion in the event of a failure * * @param {Boolean} passed * @return {string} */ this.actual = function(passed) { return passed ? `contains '${expectedText}'` : `does not contain '${expectedText}'`; }; /*! * The command which is to be executed by the assertion runner * @param {function} callback */ this.command = function(callback) { this.api.getText(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/cssClassNotPresent.js ================================================ /** * Checks if the given element does not have the specified CSS class. * * ``` * this.demoTest = function (browser) { * browser.assert.cssClassNotPresent('#main', 'container'); * }; * ``` * * @method assert.cssClassNotPresent * @param {string|object} definition The selector (CSS / Xpath) used to locate the element. * @param {string} className The CSS class to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const classListRegexp = /\s/; const classNameRegexp = /\w/; const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, className, msg) { this.options = { elementSelector: true }; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .cssClassNotPresent() has been deprecated and will be ' + 'removed from future versions. Use assert.not.hasClass().'); this.formatMessage = function() { let message = msg || 'Testing if element %s doesn\'t have css class %s'; return { message, args: [this.elementSelector, `'${className}'`] }; }; this.expected = function() { return `has not ${className}`; }; this.evaluate = function() { return !this.classList.includes(className); }; this.value = function(result) { this.classList = result.value .split(classListRegexp) .filter(item => classNameRegexp.test(item)); return result.value; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), 'class', callback); }; }; ================================================ FILE: lib/api/assertions/cssClassPresent.js ================================================ /** * Checks if the given element has the specified CSS class. Multiple css classes can be specified either as an array or a space-delimited string. * * In case the expected value is a space delimited string, the order is not taken into account - each value will individually be checked against. * * ``` * this.demoTest = function (browser) { * browser.assert.cssClassPresent('#main', 'container'); * browser.assert.cssClassPresent('#main', ['visible', 'container']); * browser.assert.cssClassPresent('#main', 'visible container'); * }; * ``` * * @method assert.cssClassPresent * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} className The CSS class to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const classListRegexp = /\s/; const classNameRegexp = /\w/; const {containsMultiple, setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .cssClassPresent() has been deprecated and will be ' + 'removed from future versions. Use assert.hasClass().'); this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have css class %s' : 'has css class %s'}`; return { message, args: [this.elementSelector, `'${Array.isArray(expected) ? expected.join(' ') : expected}'`] }; }; this.expected = function() { return this.negate ? `has not ${expected}` : `has ${expected}`; }; this.evaluate = function() { if (!this.classList) { return false; } return containsMultiple(this.classList, expected, ' '); }; this.value = function(result) { if (!result || !result.value) { return ''; } this.classList = result.value .split(classListRegexp) .filter(item => classNameRegexp.test(item)); return result.value; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), 'class', callback); }; }; ================================================ FILE: lib/api/assertions/cssProperty.js ================================================ /** * Checks if the specified css property of a given element has the expected value. * * @example * this.demoTest = function (browser) { * browser.assert.cssProperty('#main', 'display', 'block'); * }; * * @method assert.cssProperty * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} cssProperty The CSS property. * @param {string} expected The expected value of the css property to check. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, cssProperty, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have css property %s: %s' : 'has css property %s: %s'}`; return { message, args: [this.elementSelector, `'${cssProperty}`, `${expected}'`] }; }; this.actual = function(passed) { const value = this.getValue(); if (typeof value != 'string') { return `Element does not have a '${cssProperty}' css property`; } return value; }; this.expected = function() { return this.negate ? `not ${expected}` : expected; }; this.evaluate = function(value) { return value === expected; }; this.command = function(callback) { this.api.getCssProperty(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), cssProperty, callback); }; }; ================================================ FILE: lib/api/assertions/domPropertyContains.js ================================================ /** * Checks if the specified DOM property of a given element has the expected value. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * Several properties can be specified (either as an array or command-separated list). Nightwatch will check each one for presence. * * @example * this.demoTest = function (browser) { * browser.assert.domPropertyContains('#main', 'classList', 'visible'); * * // in case the resulting property is an array, several elements could be specified * browser.assert.domPropertyEquals('#main', 'classList', ['class-one', 'class-two']); * browser.assert.domPropertyEquals('#main', 'classList', 'class-one,class-two'); * }; * * @method assert.domPropertyContains * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} domProperty The DOM property name. * @param {string} expected The expected value of the DOM property to check. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {formatMessage} = require('./domPropertyEquals.js'); const {containsMultiple, setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, domProperty, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { return formatMessage.call(this, {msg, expected, domProperty, verb(negate) { return negate ? 'doesn\'t contain %s' : 'contains %s'; }}); }; this.actual = function(passed) { const value = this.getValue(); if (value === null) { return `Element does not have a '${domProperty}' dom property`; } return value; }; this.expected = function() { return this.negate ? `not contains '${expected}'` : `contains '${expected}'`; }; this.evaluate = function(value) { if (Array.isArray(value) && expected) { return containsMultiple(value, expected); } value = value || ''; return value.includes(expected); }; this.command = function(callback) { this.api.getElementProperty(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), domProperty, callback); }; }; ================================================ FILE: lib/api/assertions/domPropertyEquals.js ================================================ /** * Checks if the specified DOM property of a given element has the expected value. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * If the result value is JSON object or array, a deep equality comparison will be performed. * * @example * this.demoTest = function (browser) { * browser.assert.domPropertyEquals('#main', 'className', 'visible'); * * // deep equal will be performed * browser.assert.domPropertyEquals('#main', 'classList', ['class-one', 'class-two']); * * // split on ',' and deep equal will be performed * browser.assert.domPropertyEquals('#main', 'classList', 'class-one,class-two']); * }; * * @method assert.domPropertyEquals * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} domProperty The DOM property name. * @param {string} expected The expected value of the DOM property to check. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const assert = require('assert'); const Utils = require('../../utils'); const {Logger, setElementSelectorProps} = Utils; function formatMsg({msg, domProperty, expected, verb = function(negate) {}}) { const message = msg || `Testing if dom property %s of element %s ${verb(this.negate)}`; let expectedArg = expected; if (Utils.isObject(expected)) { try { expectedArg = JSON.stringify(expected); } catch (e) { Logger.error(e); expectedArg = `[${e.name} ${e.message}]`; } } return { message, args: [`'${domProperty}'`, this.elementSelector, `'${expectedArg}'`] }; } exports.assertion = function(definition, domProperty, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { return formatMsg.call(this, {msg, domProperty, expected, verb(negate) { return negate ? 'doesn\'t equal %s' : 'equals %s'; }}); }; this.expected = function() { return this.negate ? `not ${expected}` : expected; }; this.actual = function(passed) { const value = this.getValue(); if (value === null) { return `Element does not have a '${domProperty}' dom property`; } return value; }; this.evaluate = function(value) { if (Utils.isObject(value)) { try { if (Array.isArray(value) && Utils.isString(expected)) { expected = expected.split(','); } // deepStrictEqual doesn't seem to catch the JSON stringify errors correctly try { JSON.stringify(expected); } catch (e) { return false; } assert.deepStrictEqual(value, expected); return true; } catch (err) { return false; } } return value === expected; }; this.command = function(callback) { this.api.getElementProperty(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), domProperty, callback); }; }; exports.formatMessage = formatMsg; ================================================ FILE: lib/api/assertions/domPropertyMatches.js ================================================ /** * Check if specified DOM property value of a given element matches a regex. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * * @example * this.demoTest = function (browser) { * browser.assert.domPropertyMatches('#main', 'tagName', /^frame/); * } * * @method assert.domPropertyMatches * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} domProperty The DOM property name. * @param {string|RegExp} regexExpression Regex to match against. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function (definition, domProperty, regexExpression, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `does not matches '${regexExpression}'` : `matches '${regexExpression}'`; }; this.formatMessage = function() { const message = msg || `Testing if dom property %s of element %s ${this.negate ? 'doesn\'t matches %s' : 'matches %s'}`; return { message, args: [`'${domProperty}'`, this.elementSelector, `'${regexExpression}'`] }; }; this.evaluate = function (value) { const regex = value instanceof RegExp ? value : new RegExp(regexExpression); if (!Array.isArray(value)) { return regex.test(value); } return false; }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.getElementProperty(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), domProperty, callback); }; }; ================================================ FILE: lib/api/assertions/elementNotPresent.js ================================================ /** * Checks if the given element does not exist in the DOM. * * ``` * this.demoTest = function (browser) { * browser.assert.elementNotPresent(".should_not_exist"); * }; * ``` * * @method assert.elementNotPresent * @param {string} selector The selector (CSS / Xpath) used to locate the element. * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const Element = require('../../element'); exports.assertion = function(selector, msg) { this.options = { elementSelector: true }; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .elementNotPresent() has been deprecated and will be ' + 'removed from future versions. Use assert.not.elementPresent().'); this.element = Element.createFromSelector(selector, this.client.locateStrategy); this.formatMessage = function() { const message = msg || 'Testing if element %s is not present'; return { message, args: [this.elementSelector] }; }; this.pass = function(value) { return value === 'not present'; }; this.expected = function() { return 'is not present'; }; this.value = function(result) { return result.value && result.value.length > 0 ? 'present' : 'not present'; }; this.command = function(callback) { this.api.elements(this.client.locateStrategy, this.element, callback); }; }; ================================================ FILE: lib/api/assertions/elementPresent.js ================================================ /** * Checks if the given element exists in the DOM. * * @example * this.demoTest = function (browser) { * browser.assert.elementPresent("#main"); * }; * * @method assert.elementPresent * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const Element = require('../../element'); exports.assertion = function(selector, msg) { this.options = { elementSelector: true }; this.element = Element.createFromSelector(selector, this.client.locateStrategy); this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'is not present' : 'is present'}`; return { message, args: [this.elementSelector] }; }; this.pass = function(value) { return value === 'present'; }; this.expected = function() { return this.negate ? 'is not present' : 'is present'; }; this.value = function(result) { return result.value && result.value.length > 0 ? 'present' : 'not present'; }; this.command = function(callback) { this.api.elements(this.client.locateStrategy, this.element, callback); }; }; ================================================ FILE: lib/api/assertions/elementsCount.js ================================================ /** * Checks if the number of elements specified by a selector is equal to a given value. * * @example * this.demoTest = function (browser) { * browser.assert.elementsCount('div', 10); * browser.assert.not.elementsCount('div', 10); * } * * @method assert.elementsCount * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} count expected number of elements to be present. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const Element = require('../../element'); exports.assertion = function (definition, count, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `does not count ${count}` : `counts ${count}`; }; this.formatMessage = function() { const message = msg || `Testing if the element count for %s ${this.negate ? 'is not %s' : 'is %s'}`; return { message, args: [this.elementSelector, count] }; }; this.evaluate = function(value) { return value === count; }; this.value = function(result = {}) { if (!result || !result.value) { return ''; } return result.value.length; }; this.command = async function(callback) { await this.api.findElements(definition, callback); }; }; ================================================ FILE: lib/api/assertions/enabled.js ================================================ /** * Checks if the given element is enabled (as indicated by the 'disabled' attribute). * * @example * this.demoTest = function (browser) { * browser.assert.enabled('.should_be_enabled'); * browser.assert.enabled({selector: '.should_be_enabled'}); * browser.assert.enabled({selector: '.should_be_enabled', suppressNotFoundErrors: true}); * }; * * @method assert.enabled * @param {string|object} definition The selector (CSS / Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/working-with-page-objects/#element-properties). * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(definition, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'is not enabled' : 'is enabled'}`; return { message, args: [this.elementSelector] }; }; this.expected = function() { return this.negate ? 'is not enabled' : 'is enabled'; }; this.evaluate = function(value) { return value === true; }; this.actual = function(passed) { return passed ? 'enabled' : 'not enabled'; }; this.command = function(callback) { this.api.isEnabled(definition, callback); }; }; ================================================ FILE: lib/api/assertions/hasAttribute.js ================================================ /** * Checks if the given element contains the specified DOM attribute. * * Equivalent of: https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute * * @example * this.demoTest = function (browser) { * browser.assert.hasAttribute('#main', 'data-track'); * }; * * @method assert.hasAttribute * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expectedAttribute The DOM attribute to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps, isString} = require('../../utils'); exports.assertion = function(definition, expectedAttribute, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `has not ${expectedAttribute}` : `has ${expectedAttribute}`; }; this.formatMessage = function() { if (!isString(expectedAttribute)) { throw new Error('Expected attribute must be a string'); } const message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have attribute %s' : 'has attribute %s'}`; return { message, args: [this.elementSelector, `'${expectedAttribute}'`] }; }; this.evaluate = function() { const {result} = this; if (!result || !result.value) { return false; } return true; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), expectedAttribute, callback); }; }; ================================================ FILE: lib/api/assertions/hasClass.js ================================================ /** * Checks if the given element has the specified CSS class. * * @example * this.demoTest = function (browser) { * browser.assert.hasClass('#main', 'container'); * browser.assert.hasClass('#main', ['visible', 'container']); * browser.assert.hasClass('#main', 'visible container'); * }; * * @method hasClass * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} className The CSS class to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const classListRegexp = /\s/; const classNameRegexp = /\w/; const {containsMultiple, setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `has not ${expected}` : `has ${expected}`; }; this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have css class %s' : 'has css class %s'}`; return { message, args: [this.elementSelector, `'${Array.isArray(expected) ? expected.join(' ') : expected}'`] }; }; this.evaluate = function() { if (!this.classList) { return false; } return containsMultiple(this.classList, expected, ' '); }; this.value = function(result) { if (!result || !result.value) { return ''; } this.classList = result.value .split(classListRegexp) .filter(item => classNameRegexp.test(item)); return result.value; }; this.command = function(callback) { this.api.getAttribute(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), 'class', callback); }; }; ================================================ FILE: lib/api/assertions/hasDescendants.js ================================================ /** * Checks if the given element has child elements. * * @example * this.demoTest = function (browser) { * browser.assert.hasDescendants('#main'); * browser.assert.hasDescendants('#main', 'element has child elements'); * }; * * @method assert.hasDescendants * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} className The CSS class to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function (definition, message) { this.options = { elementSelector: true }; this.expected = function () { return `has ${this.negate ? 'not ' : ''}descendants`; }; this.formatMessage = function () { const finalMessage = message || `Testing if an element %s has ${this.negate ? 'not ' : ''}descendants`; return { args: [this.elementSelector], message: finalMessage }; }; this.evaluate = function (value) { return Boolean(value); }; this.command = function (callback) { this.api.hasDescendants( setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback ); }; }; ================================================ FILE: lib/api/assertions/hidden.js ================================================ /** * Checks if the given element is not visible on the page. * * ``` * this.demoTest = function (browser) { * browser.assert.hidden('.should_not_be_visible'); * }; * ``` * * @method hidden * @param {string} definition The selector (CSS / Xpath) used to locate the element. * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const {setElementSelectorProps} = require('../../utils'); const MSG_ELEMENT_NOT_FOUND = 'Testing if element %s is hidden. ' + 'Element could not be located.'; exports.assertion = function(definition, msg) { this.expected = true; this.message = msg || 'Testing if element %s is hidden.'; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .hidden() has been deprecated and will be ' + 'removed from future versions. Use assert.not.visible() instead.'); this.pass = function(value) { return value === this.expected; }; this.failure = function(result) { let failed = result === false || result && result.status === -1; if (failed) { this.message = msg || MSG_ELEMENT_NOT_FOUND; } return failed; }; this.value = function(result) { return !result.value; }; this.command = function(callback) { this.api.isVisible(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/promisedValue.js ================================================ exports.assertion = function({callback, evaluate, expected, verb, message, failure = function() {}}) { this.options = { elementSelector: false }; this.expected = function() { return this.negate ? `not ${verb} '${expected}'` : `${verb} '${expected}'`; }; this.formatMessage = function() { const msg = message || `Testing if element ${this.negate ? 'doesn\'t %s' : '%s'}`; return { message: msg, args: [`'${expected}'`] }; }; this.getValue = function() { return this.value; }; this.failure = function() { const failed = failure(expected); if (failed) { return failed.message; } return null; }; this.evaluate = function(value) { return evaluate(value); }; this.command = function(done) { callback().then((value) => { this.value = value; done(value); }, function(error) { done(error); }); }; }; ================================================ FILE: lib/api/assertions/selected.js ================================================ /** * Checks if the given element is selected. * * @example * this.demoTest = function (browser) { * browser.assert.selected('.should_be_selected'); * browser.assert.selected({selector: '.should_be_selected'}); * browser.assert.selected({selector: '.should_be_selected', suppressNotFoundErrors: true}); * }; * * @method assert.selected * @param {string|object} definition The selector (CSS / Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/working-with-page-objects/#element-properties). * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(definition, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'is not selected' : 'is selected'}`; return { message, args: [this.elementSelector] }; }; this.expected = function() { return this.negate ? 'is not selected' : 'is selected'; }; this.evaluate = function(value) { return value === true; }; this.actual = function(passed) { return passed ? 'selected' : 'not selected'; }; this.command = function(callback) { this.api.isSelected(definition, callback); }; }; ================================================ FILE: lib/api/assertions/textContains.js ================================================ /** * Checks if the given element contains the specified text. * * @example * this.demoTest = function (browser) { * browser.assert.textContains('#main', 'The Night Watch'); * }; * * @method assert.textContains * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expectedText The text to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expectedText, msg) { this.options = { elementSelector: true }; /*! * Returns the message format which will be used to output the message in the console and also * the arguments which will be used for replace the place holders, used in the order of appearance */ this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'does not contain text %s' : 'contains text %s'}`; return { message, args: [this.elementSelector, `'${expectedText}'`] }; }; /*! * Returns the expected value of the assertion which is displayed in the case of a failure * * @return {string} */ this.expected = function() { return this.negate ? `does not contain text '${expectedText}'` : `contains text '${expectedText}'`; }; /*! * Given the value, the condition used to evaluate if the assertion is passed * @param {*} value * @return {Boolean} */ this.evaluate = function(value) { if (typeof value != 'string') { return false; } return value.includes(expectedText); }; /*! * When defined, this method is called by the assertion runner with the command result, to determine if the * value can be retrieved successfully from the result object * * @param result * @return {boolean|*} */ this.failure = function(result) { return result === false || result && result.status === -1; }; /*! * Called with the result object of the command to retrieve the value which is to be evaluated * * @param {Object} result * @return {*} */ this.value = function(result) { if (result.status === -1) { return null; } return result.value; }; /*! * When defined, this method is called by the assertion runner with the command result to determine the actual * state of the assertion in the event of a failure * * @param {Boolean} passed * @return {string} */ this.actual = function(passed) { return passed ? `contains '${expectedText}'` : `does not contain '${expectedText}'`; }; /*! * The command which is to be executed by the assertion runner * @param {function} callback */ this.command = function(callback) { this.api.getText(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/textEquals.js ================================================ /** * Check if an element's inner text equals the expected text. * * @example * this.demoTest = function (browser) { * browser.assert.textEquals('#main', 'The Night Watch'); * }; * * @method assert.textEquals * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expected text to match text. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if element's %s inner text ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [this.elementSelector, `'${expected}'`] }; }; this.expected = function() { return this.negate ? `doesn't equal '${expected}'` : `equals '${expected}'`; }; this.evaluate = function(value) { if (typeof value != 'string') { return false; } return value === expected; }; this.failure = function(result) { return result === false || result && result.status === -1; }; this.actual = function(passed) { const value = this.getValue(); if (typeof value != 'string') { return 'Element does not have an innerText attribute'; } return value; }; this.value = function(result) { if (result.status === -1) { return null; } return result.value; }; this.command = function(callback) { this.api.getText(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/textMatches.js ================================================ /** * Check if an elements inner text matches a regular expression. * * @example * this.demoTest = function (browser) { * browser.assert.textMatches('#main', '^Nightwatch'); * }; * * @method assert.textMatches * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|RegExp} regexExpression Regex expression to match text. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, regexExpression, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `does not matches '${regexExpression}'` : `matches '${regexExpression}'`; }; this.formatMessage = function() { const message = msg || `Testing if the text ${this.negate ? 'doesn\'t matches %s' : 'matches %s'}`; return { message, args: [this.elementSelector, `'${regexExpression}'`] }; }; this.evaluate = function(value) { const regex = value instanceof RegExp ? value : new RegExp(regexExpression); return regex.test(value); }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.getText(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/title.js ================================================ /** * Checks if the page title equals the given value. * * ``` * this.demoTest = function (browser) { * browser.assert.title('Nightwatch.js'); * }; * ``` * * @method title * @param {string} expected The expected page title. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ exports.assertion = function(expected, msg) { // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .title() has been deprecated and will be ' + 'removed from future versions. Use assert.titleEquals().'); this.formatMessage = function() { const message = msg || `Testing if the page title ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [`'${expected}'`] }; }; this.expected = function() { return this.negate ? `is not '${expected}'` : `is '${expected}'`; }; this.pass = function(value) { return value === expected; }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.title(callback); }; }; ================================================ FILE: lib/api/assertions/titleContains.js ================================================ /** * Checks if the page title contains the given value. * * @example * this.demoTest = function (browser) { * browser.assert.titleContains('Nightwatch.js'); * }; * * @method assert.titleContains * @param {string} value The value to look for. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(expected, msg) { this.expected = function() { return this.negate ? `not contains '${expected}'` : `contains '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if the page title ${this.negate ? 'doesn\'t contain %s' : 'contains %s'}`; return { message, args: [`'${expected}'`] }; }; this.pass = function(value) { value = value || ''; return value.includes(expected); }; this.value = function(result = {}) { if (!result) { return ''; } return result.value || ''; }; this.command = function(callback) { this.api.title(callback); }; }; ================================================ FILE: lib/api/assertions/titleEquals.js ================================================ /** * Checks if the page title equals the given value. * * @example * this.demoTest = function (client) { * browser.assert.titleEquals('https://www.google.com'); * }; * * @method assert.titleEquals * @param {string} expected The expect page title * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(expected, msg) { this.expected = function() { return this.negate ? `is not '${expected}'` : `is '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if the page title ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [`'${expected}'`] }; }; this.evaluate = function(value) { return value === expected; }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.title(callback); }; }; ================================================ FILE: lib/api/assertions/titleMatches.js ================================================ /** * Checks if the current title matches a regular expression. * * @example * this.demoTest = function (client) { * browser.assert.titleMatches('^Nightwatch'); * }; * * @method assert.titleMatches * @param {string|RegExp} regexExpression Regex expression to match current title of a page * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(regexExpression, msg) { this.expected = function() { return this.negate ? `does not matches '${regexExpression}'` : `matches '${regexExpression}'`; }; this.formatMessage = function() { const message = msg || `Testing if the page title ${this.negate ? 'doesn\'t matches %s' : 'matches %s'}`; return { message, args: [`'${regexExpression}'`] }; }; this.evaluate = function(value) { const regex = value instanceof RegExp ? value : new RegExp(regexExpression); return regex.test(value); }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.title(callback); }; }; ================================================ FILE: lib/api/assertions/urlContains.js ================================================ /** * Checks if the current URL contains the given value. * * @example * this.demoTest = function (browser) { * browser.assert.urlContains('nightwatchjs.org'); * }; * * @method assert.urlContains * @param {string} expected The value expected to exist within the current URL. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(expected, msg) { this.expected = function() { return this.negate ? `not contains '${expected}'` : `contains '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if the URL ${this.negate ? 'doesn\'t contain %s' : 'contains %s'}`; return { message, args: [`'${expected}'`] }; }; this.pass = function(value) { return value.includes(expected); }; this.value = function(result = {}) { if (!result) { return ''; } return result.value || ''; }; this.command = function(callback) { this.api.url(function(result) { return callback.call(this, result); }); }; }; ================================================ FILE: lib/api/assertions/urlEquals.js ================================================ /** * Checks if the current url equals the given value. * * @example * this.demoTest = function (client) { * browser.assert.urlEquals('https://www.google.com'); * }; * * @method assert.urlEquals * @param {string} expected The expected url. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(expected, msg) { this.expected = function() { return this.negate ? `is not '${expected}'` : `is '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if the URL ${this.negate ? 'is not %s' : 'is %s'}`; return { message, args: [`'${expected}'`] }; }; this.pass = function(value) { return value === expected; }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.url(callback); }; }; ================================================ FILE: lib/api/assertions/urlMatches.js ================================================ /** * Checks if the current url matches a regular expression. * * @example * this.demoTest = function (client) { * browser.assert.urlMatches('^https'); * }; * * @method assert.urlMatches * @param {string|RegExp} regexExpression Regex expression to match URL * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ exports.assertion = function(regexExpression, msg) { this.expected = function() { return this.negate ? `does not matches '${regexExpression}'` : `matches '${regexExpression}'`; }; this.formatMessage = function() { const message = msg || `Testing if the URL ${this.negate ? 'doesn\'t matches %s' : 'matches %s'}`; return { message, args: [`'${regexExpression}'`] }; }; this.evaluate = function (value) { const regex = value instanceof RegExp ? value : new RegExp(regexExpression); return regex.test(value); }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.url(callback); }; }; ================================================ FILE: lib/api/assertions/value.js ================================================ /** * Checks if the given form element's value equals the expected value. * * ``` * this.demoTest = function (browser) { * browser.assert.value("form.login input[type=text]", "username"); * }; * ``` * * @method value * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expected The expected text. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions * @deprecated */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; // eslint-disable-next-line no-console console.warn('DEPRECATED: the assertion .value() has been deprecated and will be ' + 'removed from future versions. Use assert.valueEquals() instead.'); this.expected = function() { return this.negate ? `not equals '${expected}'` : `equals '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if value of element %s ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [this.elementSelector, `'${expected}'`] }; }; this.actual = function(passed) { const value = this.getValue(); if (typeof value != 'string') { return 'Element does not have a value attribute'; } return this.getValue(); }; this.evaluate = function(value) { return value === expected; }; this.command = function(callback) { this.api.getValue(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/valueContains.js ================================================ /** * Checks if the given form element's value contains the expected value. * * @example * this.demoTest = function (browser) { * browser.assert.valueContains("form.login input[type=text]", "username"); * }; * * @method assert.valueContains * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expected The expected text. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `not contains '${expected}'` : `contains '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if value of element %s ${this.negate ? 'doesn\'t contain %s' : 'contains %s'}`; return { message, args: [this.elementSelector, `'${expected}'`] }; }; this.actual = function(passed) { const value = this.getValue(); if (typeof value != 'string') { return 'Element does not have a value attribute'; } return this.getValue(); }; this.evaluate = function(value) { value = value || ''; return value.includes(expected); }; this.command = function(callback) { this.api.getValue(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/valueEquals.js ================================================ /** * Checks if the given form element's value equals the expected value. * * The existing .assert.value() command. * * * @example * this.demoTest = function (browser) { * browser.assert.valueEquals("form.login input[type=text]", "username"); * }; * * @method assert.valueEquals * @param {string|object} definition The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} expected The expected text. * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, expected, msg) { this.options = { elementSelector: true }; this.expected = function() { return this.negate ? `not equals '${expected}'` : `equals '${expected}'`; }; this.formatMessage = function() { const message = msg || `Testing if value of element %s ${this.negate ? 'doesn\'t equal %s' : 'equals %s'}`; return { message, args: [this.elementSelector, `'${expected}'`] }; }; this.evaluate = function(value) { return value === expected; }; this.value = function(result = {}) { return result.value || ''; }; this.command = function(callback) { this.api.getValue(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/assertions/visible.js ================================================ /** * Checks if the given element is visible on the page. * * @example * this.demoTest = function (browser) { * browser.assert.visible('.should_be_visible'); * browser.assert.visible({selector: '.should_be_visible'}); * browser.assert.visible({selector: '.should_be_visible', suppressNotFoundErrors: true}); * }; * * @method assert.visible * @param {string|object} definition The selector (CSS / Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/working-with-page-objects/#element-properties). * @param {string} [msg] Optional log message to display in the output. If missing, one is displayed by default. * @api assertions */ const {setElementSelectorProps} = require('../../utils'); exports.assertion = function(definition, msg) { this.options = { elementSelector: true }; this.formatMessage = function() { const message = msg || `Testing if element %s ${this.negate ? 'is not visible' : 'is visible'}`; return { message, args: [this.elementSelector] }; }; this.expected = function() { return this.negate ? 'is not visible' : 'is visible'; }; this.evaluate = function(value) { return value === true; }; this.actual = function(passed) { return passed ? 'visible' : 'not visible'; }; this.command = function(callback) { this.api.isVisible(setElementSelectorProps(definition, { suppressNotFoundErrors: true }), callback); }; }; ================================================ FILE: lib/api/client-commands/_base-command.js ================================================ module.exports = class ClientCommand { static get isTraceable() { return false; } /** * * @param {function} performAction Function to run which contains the command, passing a callback containing the result object * @param {function} userSuppliedCallback * @param {boolean} fullResultObject Weather to call the user-supplied callback with the entire result object or just the value * @param {boolean} fullPromiseResolve Weather to resolve the promise with the full result object or just the "value" property * @return {Promise} */ static makePromise({performAction, userSuppliedCallback = function() {}, fullResultObject = true, fullPromiseResolve = true}) { return new Promise(function(resolve, reject) { performAction(function(result) { try { if (result instanceof Error) { const {name, message, code} = result; result = { status: -1, code, name, value: { message }, error: message }; } const resultValue = fullResultObject ? result : result.value; let promise = userSuppliedCallback.call(this, resultValue); if (!(promise instanceof Promise)) { promise = Promise.resolve(promise); } // the final result returned in the test would be same irrespective of // the value of fullPromiseResolve, thanks to `getResult` method in // lib/core/treenode.js but the result value everywhere else in Nightwatch // would appear to be the value of resolveValue below. const resolveValue = fullPromiseResolve ? result : result.value; promise.then(_ => resolve(resolveValue)).catch(err => reject(err)); } catch (e) { reject(e); } }); }); } get returnsFullResultObject() { return true; } get resolvesWithFullResultObject() { return true; } reportProtocolErrors(result) { return true; } command(userSuppliedCallback) { const {performAction} = this; return ClientCommand.makePromise({ performAction: performAction.bind(this), userSuppliedCallback, fullResultObject: this.returnsFullResultObject, fullPromiseResolve: this.resolvesWithFullResultObject }); } /*! * Helper function for execute and execute_async * * @param {string} method * @param {string|function} script * @param {Array} args * @param {function} callback * @private */ executeScriptHandler(method, script, args, callback) { let fn; if (script.originalTarget) { script = script.originalTarget; } if (typeof script === 'function') { fn = 'var passedArgs = Array.prototype.slice.call(arguments,0); return (' + script.toString() + ').apply(window, passedArgs);'; } else { fn = script; } return this.transportActions[method](fn, args, callback); } }; ================================================ FILE: lib/api/client-commands/_locateStrategy.js ================================================ const EventEmitter = require('events'); class Command extends EventEmitter { command(callback = function() {}) { this.client.setLocateStrategy(this.strategy); process.nextTick(() => { callback.call(this.api); this.emit('complete'); }); return this; } } module.exports = Command; ================================================ FILE: lib/api/client-commands/alerts/accept.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Accepts the currently displayed alert dialog. Usually, this is equivalent to clicking on the 'OK' button in the dialog. * * @example * module.exports = { * 'accept open alert': function (browser) { * browser * .alerts.accept(function () { * console.log('alert accepted successfully'); * }); * }, * * 'accept open alert with ES6 async/await': async function (browser) { * await browser.alerts.accept(); * } * }; * * @syntax .alerts.accept([callback]) * @method alerts.accept * @param {function} [callback] Optional callback function to be called when the command finishes. * @see alerts.dismiss * @see alerts.getText * @see alerts.setText * @link /#accept-alert * @api protocol.userprompts */ class AcceptAlert extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { this.transportActions.acceptAlert(callback); } } module.exports = AcceptAlert; ================================================ FILE: lib/api/client-commands/alerts/dismiss.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Dismisses the currently displayed alert dialog. * * For confirm() and prompt() dialogs, this is equivalent to clicking the 'Cancel' button. * For alert() dialogs, this is equivalent to clicking the 'OK' button. * * @example * module.exports = { * 'dismiss open alert': function (browser) { * browser * .alerts.dismiss(function () { * console.log('alert dismissed successfully'); * }); * }, * * 'dismiss open alert with ES6 async/await': async function (browser) { * await browser.alerts.dismiss(); * } * }; * * @syntax .alerts.dismiss([callback]) * @method alerts.dismiss * @param {function} [callback] Optional callback function to be called when the command finishes. * @see alerts.accept * @see alerts.getText * @see alerts.setText * @link /#dismiss-alert * @api protocol.userprompts */ class DismissAlert extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { this.transportActions.dismissAlert(callback); } } module.exports = DismissAlert; ================================================ FILE: lib/api/client-commands/alerts/getText.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Get the text of the currently displayed JavaScript alert(), confirm(), or prompt() dialog. * * @example * module.exports = { * 'get open alert text': function (browser) { * browser * .alerts.getText(function (result) { * console.log('text on open alert:', result.value); * }); * }, * * 'get open alert text with ES6 async/await': async function (browser) { * const alertText = await browser.alerts.getText(); * console.log('text on open alert:', alertText); * } * }; * * @syntax .alerts.getText([callback]) * @method alerts.getText * @param {function} [callback] Callback function which is called with the result value. * @returns {string} The text of the currently displayed alert. * @see alerts.accept * @see alerts.dismiss * @see alerts.setText * @link /#get-alert-text * @api protocol.userprompts */ class GetAlertText extends ClientCommand { performAction(callback) { this.transportActions.getAlertText(callback); } } module.exports = GetAlertText; ================================================ FILE: lib/api/client-commands/alerts/setText.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Send keystrokes to a JavaScript prompt() dialog. * * @example * module.exports = { * 'set text on JS prompt': function (browser) { * browser * .alerts.setText('some text', function () { * console.log('text sent to JS prompt successfully'); * }); * }, * * 'set text on JS prompt with ES6 async/await': async function (browser) { * await browser.alerts.setText('some text'); * } * }; * * @syntax .alerts.setText(value, [callback]) * @method alerts.setText * @param {string} value Keystrokes to send to the prompt() dialog * @param {function} [callback] Optional callback function to be called when the command finishes. * @see alerts.accept * @see alerts.dismiss * @see alerts.getText * @link /#send-alert-text * @api protocol.userprompts */ class SetAlertText extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { const {alertText} = this; this.transportActions.setAlertText(alertText, callback); } command(value, callback) { if (typeof value !== 'string') { throw new Error('First argument passed to .alerts.setText() must be a string.'); } this.alertText = value; return super.command(callback); } } module.exports = SetAlertText; ================================================ FILE: lib/api/client-commands/axeInject.js ================================================ /** * Injects the [axe-core](https://github.com/dequelabs/axe-core) js library into the current page (using the `.executeScript()` command). * * To be paired with `.axeRun()` to evaluate the axe-core accessibility rules. * * @example * describe('accessibility testing', function () { * it('accessibility rule subset', function (browser) { * browser * .url('https://www.w3.org/WAI/demos/bad/after/home.html') * .assert.titleEquals('Welcome to CityLights! [Accessible Home Page]') * .axeInject() * .axeRun('body', { * runOnly: ['color-contrast', 'image-alt'], * }); * }); * }); * * @method axeInject * @syntax browser.axeInject() * @api protocol.accessibility * @since 2.3.6 */ module.exports = class AxeInjectAbstract { static get allowOverride() { return true; } }; ================================================ FILE: lib/api/client-commands/axeRun.js ================================================ /** * Analyzes the current page against applied axe rules. * * @example * describe('accessibility testing', function () { * it('accessibility rule subset', function (browser) { * browser * .url('https://www.w3.org/WAI/demos/bad/after/home.html') * .assert.titleEquals('Welcome to CityLights! [Accessible Home Page]') * .axeInject() * .axeRun('body', { * runOnly: ['color-contrast', 'image-alt'], * }); * }); * }); * * @method axeRun * @syntax browser.axeRun('body') * @param {any} [context] Defines the scope of the analysis, will cascade to child elements. See [axe-core docs](https://github.com/dequelabs/axe-core/blob/master/doc/API.md#context-parameter) for more details. * @param {object} [options] Object containing rules configuration to use when performing the analysis. See [axe-core docs](https://github.com/dequelabs/axe-core/blob/master/doc/API.md#options-parameter) for more details. * @param {function} [callback] Optional callback function which is called with the results. * @api protocol.accessibility * @since 2.3.6 * @see https://www.deque.com/axe/core-documentation/api-documentation/#api-name-axerun */ module.exports = class AxeInject { static get allowOverride() { return true; } }; ================================================ FILE: lib/api/client-commands/cookies/delete.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Delete the cookie with the given name. This command is a no-op if there is no such cookie visible to the current page. * * @example * module.exports = { * 'delete a cookie': function (browser) { * browser * .cookies.delete('test_cookie', function () { * console.log('cookie deleted successfully'); * }); * }, * * 'delete a cookie with ES6 async/await': async function (browser) { * await browser.cookies.delete('test_cookie'); * } * }; * * @syntax .cookies.delete(cookieName, [callback]) * @method cookies.delete * @param {string} cookieName The name of the cookie to delete. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see cookies.deleteAll * @see cookies.get * @see cookies.getAll * @see cookies.set * @api protocol.cookies */ class DeleteCookie extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { const {cookieName} = this; this.transportActions.deleteCookie(cookieName, callback); } command(cookieName, callback) { if (typeof cookieName !== 'string') { throw new Error('First argument passed to .cookies.delete() must be a string.'); } this.cookieName = cookieName; return super.command(callback); } } module.exports = DeleteCookie; ================================================ FILE: lib/api/client-commands/cookies/deleteAll.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Delete all cookies visible to the current page. * * @example * module.exports = { * 'delete all cookies': function (browser) { * browser * .cookies.deleteAll(function() { * console.log('all cookies deleted successfully'); * }); * }, * * 'delete all cookies with ES6 async/await': async function (browser) { * await browser.cookies.deleteAll(); * } * }; * * @syntax .cookies.deleteAll([callback]) * @method cookies.deleteAll * @param {function} [callback] Optional callback function to be called when the command finishes. * @see cookies.delete * @see cookies.get * @see cookies.getAll * @see cookies.set * @api protocol.cookies */ class DeleteCookies extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { this.transportActions.deleteAllCookies(callback); } } module.exports = DeleteCookies; ================================================ FILE: lib/api/client-commands/cookies/get.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Retrieve a single cookie visible to the current page. * * The cookie is returned as a cookie JSON object, with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion). * * @example * module.exports = { * 'get a cookie': function (browser) { * browser * .cookies.get('test_cookie', function (result) { * const cookie = result.value; * this.assert.equal(cookie.name, 'test_cookie'); * this.assert.equal(cookie.value, '123456'); * }); * }, * * 'get a cookie with ES6 async/await': async function (browser) { * const cookie = await browser.cookies.get('test_cookie'); * browser.assert.equal(cookie.name, 'test_cookie'); * browser.assert.equal(cookie.value, '123456'); * } * }; * * @syntax .cookies.get(name, [callback]) * @method cookies.get * @param {string} name The cookie name. * @param {function} [callback] Callback function which is called with the result value. * @returns {object|null} The cookie object with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion), or `null` if the cookie wasn't found. * @see cookies.getAll * @see cookies.set * @see cookies.delete * @see cookies.deleteAll * @api protocol.cookies */ class GetCookie extends ClientCommand { performAction(callback) { const {cookieName} = this; this.transportActions.getCookie(cookieName, callback); } command(name, callback) { if (typeof name !== 'string') { throw new Error('First argument passed to .cookies.get() must be a string.'); } this.cookieName = name; return super.command(callback); } } module.exports = GetCookie; ================================================ FILE: lib/api/client-commands/cookies/getAll.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Retrieve all cookies visible to the current page. * * The cookies are returned as an array of cookie JSON object, with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion). * * @example * module.exports = { * 'get all cookies': function (browser) { * browser * .cookies.getAll(function (result) { * this.assert.equal(result.value.length, 1); * this.assert.equal(result.value[0].name, 'test_cookie'); * }); * }, * * 'get all cookies with ES6 async/await': async function (browser) { * const cookies = await browser.cookies.getAll(); * browser.assert.equal(cookies.length, 1); * browser.assert.equal(cookies[0].name, 'test_cookie'); * } * }; * * @syntax .cookies.getAll([callback]) * @method cookies.getAll * @param {function} [callback] Callback function which will receive the response as an argument. * @returns {Array.} A list of cookie JSON objects, with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion). * @see cookies.get * @see cookies.set * @see cookies.delete * @see cookies.deleteAll * @api protocol.cookies */ class GetCookies extends ClientCommand { performAction(callback) { this.transportActions.getCookies(callback); } } module.exports = GetCookies; ================================================ FILE: lib/api/client-commands/cookies/set.js ================================================ const ClientCommand = require('../_base-command.js'); const Utils = require('../../../utils/index.js'); /** * Set a cookie, specified as a cookie JSON object, with properties as defined [here](https://www.w3.org/TR/webdriver/#dfn-table-for-cookie-conversion). * * @example * module.exports = { * 'set a cookie': function (browser) { * browser * .cookies.set({ * name: "test_cookie", * value: "test_value", * path: "/", // (Optional) * domain: "example.org", // (Optional) * secure: false, // (Optional) * httpOnly: false, // (Optional) * expiry: 1395002765 // (Optional) time in seconds since midnight, January 1, 1970 UTC * }); * }, * * 'set a cookie with ES6 async/await': async function (browser) { * await browser.cookies.set({ * name: 'test_cookie', * value: 'test_value', * domain: 'example.org', // (Optional) * sameSite: 'Lax' // (Optional) * }); * } * }; * * @syntax .cookies.set(cookie, [callback]) * @method cookies.set * @param {object} cookie The cookie object. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see cookies.get * @see cookies.getAll * @see cookies.delete * @see cookies.deleteAll * @api protocol.cookies */ class SetCookie extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { const {cookie} = this; this.transportActions.addCookie(cookie, callback); } command(cookie, callback) { if (!Utils.isObject(cookie)) { throw new Error(`First argument passed to .cookies.set() must be an object; received: ${typeof cookie} (${cookie}).`); } this.cookie = cookie; return super.command(callback); } } module.exports = SetCookie; ================================================ FILE: lib/api/client-commands/debug.js ================================================ const EventEmitter = require('events'); const NightwatchRepl = require('../../testsuite/repl'); const Debuggability = require('../../utils/debuggability.js'); const {By} = require('selenium-webdriver'); /** * This command halts the test execution and provides users with a REPL interface where they can type * any of the available Nightwatch commands or assertions and it will be executed in the running browser * in real-time. * * This can be used to debug why a certain command in not working as expected or a certain assertion is * failing by trying out the commands and assertions in different ways (trying an assertion with different * locators until the correct one if found), or just play around with the available Nightwatch commands * and assertions. * * You can also expose local variables and helper functions from your test to the REPL by passing * them via the `context` option; they will then be available in the debug prompt alongside * the `browser` object. * * @example * // async function is required while using the debug * // command to get the correct result as output. * it('demos debug command', async function (browser) { * const someLocalVariable = 'something random'; * function someLocalFunction() { * return 'local function result'; * } * * // with default options * browser.debug(); * * // with no auto-complete * browser.debug({preview: false}); * * // with a timeout of 6000 ms (time for which the interface * // would wait for a result). * browser.debug({timeout: 6000}); * * // expose local variables/functions to the debug REPL * browser.debug({ * // both values below will be directly available in the debug REPL * context: {someLocalVariable, someLocalFunction} * }); * }); * * @method debug * @param {object} [config] Config options for the REPL interface. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.utilities */ class Debug extends EventEmitter { static get avoidPrematureParentNodeResolution() { return true; } command(config, callback) { // Create context for vm const context = { browser: this.api, app: this.api, by: By, By: By }; // set the user provided context if (config?.context) { Object.assign(context, config.context); delete config.context; } const repl = new NightwatchRepl(config); // eslint-disable-next-line console.log(NightwatchRepl.introMessage()); // TODO: what's the use of this `if` block? if (config?.selector) { this.api.executeScript('console.log("Element ' + config.selector + ':", document.querySelector("' + config.selector + '"))'); } // Before starting REPL server, Set debugMode to true Debuggability.debugMode = true; // Set isES6AsyncTestcase to true in debugmode const isES6AsyncTestcase = this.client.isES6AsyncTestcase; this.client.isES6AsyncTestcase = true; repl.startServer(context); repl.onExit(() => { // On exit, Set debugMode to false Debuggability.debugMode = false; this.client.isES6AsyncTestcase = isES6AsyncTestcase; // if we have a callback, call it right before the complete event if (callback) { callback.call(this.client.api); } this.emit('complete'); }); return this; } } module.exports = Debug; ================================================ FILE: lib/api/client-commands/deleteCookie.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Delete the cookie with the given name. This command is a no-op if there is no such cookie visible to the current page. * * @example * this.demoTest = function(browser) { * browser.deleteCookie("test_cookie", function() { * // do something more in here * }); * } * * * @method deleteCookie * @syntax .deleteCookie(cookieName, [callback]) * @param {string} cookieName The name of the cookie to delete. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.cookies * @see cookie * @deprecated In favour of `.cookies.delete()`. */ class DeleteCookie extends ClientCommand { get returnsFullResultObject() { return true; } performAction(callback) { this.api.cookie('DELETE', this.cookieName, callback); } static get isTraceable() { return true; } command(cookieName, callback) { this.cookieName = cookieName; return super.command(callback); } } module.exports = DeleteCookie; ================================================ FILE: lib/api/client-commands/deleteCookies.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Delete all cookies visible to the current page. * * @example * this.demoTest = function(browser) { * browser.deleteCookies(function() { * // do something more in here * }); * } * * * @method deleteCookies * @syntax .deleteCookies([callback]) * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.cookies * @see cookie * @deprecated In favour of `.cookies.deleteAll()`. */ class DeleteCookie extends ClientCommand { get returnsFullResultObject() { return true; } static get isTraceable() { return true; } performAction(callback) { this.api.cookie('DELETE', callback); } } module.exports = DeleteCookie; ================================================ FILE: lib/api/client-commands/document/executeAsyncScript.js ================================================ const ClientCommand = require('../_base-command.js'); const {isFunction} = require('../../../utils/index.js'); /** * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. The executed script is assumed to be asynchronous. * * The function to be injected receives the `done` callback as argument which needs to be called when the asynchronous operation finishes. The value passed to the `done` callback is returned to the client. * Additional arguments for the injected function may be passed as a non-empty array which will be passed before the `done` callback. * * Asynchronous script commands may not span page loads. If an unload event is fired while waiting for the script result, an error will be returned. * * @example * describe('execute async script', function() { * it('executes async script in browser', function(browser) { * browser.executeAsyncScript(function(done) { * setTimeout(function() { * done(true); * }, 500); * }, function(result) { * // whatever is passed to the `done` callback in the script above * // will be available as result.value * console.log(result.value); // true * }); * }); * * it('executes a script with ES6 async/await', async function(browser) { * const result = await browser * .document.executeAsync(function(arg1, arg2, done) { * setTimeout(function() { * done(arg1); * }, 500); * }, [arg1, arg2]); * * // whatever is passed to the `done` callback in the script above * // will be returned by the command when used with `await`. * console.log(result); // arg1 * }); * }); * * @method document.executeAsyncScript * @syntax .executeAsync(body, [args], [callback]) * @syntax .executeAsyncScript(body, [args], [callback]) * @syntax .document.executeAsync(body, [args], [callback]) * @syntax .document.executeAsyncScript(body, [args], [callback]) * @param {string|function} body The function body to be injected. * @param {Array} args An array of arguments which will be passed to the function. * @param {function} [callback] Optional callback function to be called when the command finishes. * @returns {*} The script result. * @see document.executeScript * @link /#execute-async-script * @api protocol.document */ class ExecuteAsyncScript extends ClientCommand { static get namespacedAliases() { return ['document.executeAsync', 'executeAsync', 'executeAsyncScript']; } static get isTraceable() { return true; } performAction(callback) { const {script, scriptArgs} = this; this.executeScriptHandler('executeAsyncScript', script, scriptArgs, callback); } command(script, args, callback) { if (!script) { throw new Error('First argument passed to .executeAsyncScript() must be defined.'); } if (arguments.length === 1) { args = []; } else if (arguments.length === 2 && isFunction(arguments[1])) { callback = arguments[2]; args = []; } this.script = script; this.scriptArgs = args; return super.command(callback); } }; module.exports = ExecuteAsyncScript; ================================================ FILE: lib/api/client-commands/document/executeScript.js ================================================ const ClientCommand = require('../_base-command.js'); const {isFunction} = require('../../../utils/index.js'); /** * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame. The executed script is assumed to be synchronous. * The script argument defines the script to execute in the form of a function body. The value returned by that function will be returned to the client. * * The function will be invoked with the provided args array and the values may be accessed via the arguments object in the order specified. * * Under the hood, if the `body` param is a function it is converted to a string with `function.toString()`. Any references to your current scope are ignored. * * To ensure cross-browser compatibility, the specified function should not be in ES6 format (i.e. `() => {}`). If the execution of the function fails, the first argument of the callback contains error information. * * @example * describe('execute script', function() { * it('executes a script in browser', function(browser) { * browser.executeScript(function(imageData) { * // resize operation * return true; * }, [imageData], function(result) { * // whatever is returned by the script passed above will be available * // as result.value * console.log(result.value); // true * }); * * // scroll to the bottom of the page. * browser.executeScript('window.scrollTo(0,document.body.scrollHeight);'); * }); * * it('executes a script with ES6 async/await', async function(browser) { * const result = await browser * .document.executeScript(function(imageData) { * // resize operation * return true; * }, [imageData]); * * console.log(result); // true * }); * }); * * @method document.executeScript * @syntax .execute(body, [args], [callback]) * @syntax .executeScript(body, [args], [callback]) * @syntax .document.execute(body, [args], [callback]) * @syntax .document.executeScript(body, [args], [callback]) * @param {string|function} body The function body to be injected. * @param {Array} args An array of arguments which will be passed to the function. * @param {function} [callback] Optional callback function to be called when the command finishes. * @returns {*} The script result. * @see document.executeAsyncScript * @link /#executing-script * @api protocol.document */ class ExecuteScript extends ClientCommand { static get namespacedAliases() { return ['document.execute', 'execute', 'executeScript']; } static get isTraceable() { return true; } performAction(callback) { const {script, scriptArgs} = this; this.executeScriptHandler('executeScript', script, scriptArgs, callback); } command(script, args, callback) { if (!script) { throw new Error('First argument passed to .executeScript() must be defined.'); } if (arguments.length === 1) { args = []; } else if (arguments.length === 2 && isFunction(arguments[1])) { callback = arguments[2]; args = []; } this.script = script; this.scriptArgs = args; return super.command(callback); } }; module.exports = ExecuteScript; ================================================ FILE: lib/api/client-commands/document/injectScript.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Utility command to load an external script into the page specified by url. * * @example * module.exports = { * 'inject external script': function (browser) { * browser.document.injectScript('', function () { * console.log('script injected successfully'); * }); * }, * * 'inject external script using ES6 async/await': async function (browser) { * await browser.document.injectScript('', 'injected-script'); * } * }; * * @syntax .document.injectScript(scriptUrl, [callback]) * @syntax .document.injectScript(scriptUrl, id, [callback]) * @method document.injectScript * @param {string} scriptUrl The script file url * @param {string} [id] DOM element id to be set on the script tag. * @param {function} [callback] Optional callback function to be called when the command finishes. * @returns {HTMLScriptElement} The newly created script tag. * @api protocol.document */ class InjectScript extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { const {scriptFn, scriptArgs} = this; this.transportActions.executeScript(scriptFn, scriptArgs, callback); } command(scriptUrl, id, callback) { const args = [scriptUrl]; if (arguments.length === 2 && typeof arguments[1] == 'function') { callback = arguments[1]; } else if (typeof id == 'string') { args.push(id); } // eslint-disable-next-line const script = function(u,i) {return (function(d){var e=d.createElement('script');var m=d.getElementsByTagName('head')[0];e.src=u;if(i){e.id=i;}m.appendChild(e);return e;})(document);}; this.scriptFn = 'var passedArgs = Array.prototype.slice.call(arguments,0); return (' + script.toString() + ').apply(window, passedArgs);'; this.scriptArgs = args; return super.command(callback); } }; module.exports = InjectScript; ================================================ FILE: lib/api/client-commands/document/source.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Get the string serialized source of the current page. * * @example * module.exports = { * 'get page source': function (browser) { * browser.document.source(function (result) { * console.log('current page source:', result.value); * }); * }, * * 'get page source using ES6 async/await': async function (browser) { * const pageSource = await browser.document.source(); * console.log('current page source:', pageSource); * } * }; * * @syntax .document.source([callback]) * @method document.source * @param {function} [callback] Callback function which is called with the result value. * @returns {string} String serialized source of the current page. * @link /#get-page-source * @api protocol.document */ class Source extends ClientCommand { static get namespacedAliases() { return 'document.pageSource'; } performAction(callback) { this.transportActions.getPageSource(callback); } } module.exports = Source; ================================================ FILE: lib/api/client-commands/enablePerformanceMetrics.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Enable/disable the collection of performance metrics in the browser. Metrics collection only begin after this command is called. * * @example * describe('collect performance metrics', function() { * it('enables the metrics collection, does some stuff and collects the metrics', function() { * browser * .enablePerformanceMetrics() * .navigateTo('https://www.google.com') * .getPerformanceMetrics((result) => { * if (result.status === 0) { * const metrics = result.value; * console.log(metrics); * } * }); * }); * }); * * @method enablePerformanceMetrics * @syntax .enablePerformanceMetrics([enable], [callback]) * @param {boolean} [enable] Whether to enable or disable the performance metrics collection. Default: `true`. * @param {function} [callback] callback function to be called when the command finishes. * @api protocol.cdp * @since 2.2.0 * @moreinfo web.dev/metrics/ * @moreinfo pptr.dev/api/puppeteer.page.metrics/ */ class EnablePerformanceMetrics extends ClientCommand { performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .enablePerformanceMetrics() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const {enable = true} = this; this.transportActions.enablePerformanceMetrics(enable, callback); } command(enable, callback) { this.enable = enable; return super.command(callback); } } module.exports = EnablePerformanceMetrics; ================================================ FILE: lib/api/client-commands/end.js ================================================ const EventEmitter = require('events'); /** * Ends the session. Uses session protocol command. * * @example * this.demoTest = function (browser) { * browser.end(); * }; * * @method end * @syntax .end([callback]) * @param {function} [callback] Optional callback function to be called when the command finishes. * @see session * @api protocol.sessions */ class End extends EventEmitter { command(forceEnd = !this.reuseBrowser, callback) { if (arguments.length === 1 && typeof arguments[0] === 'function') { callback = forceEnd; forceEnd = !this.reuseBrowser; } const client = this.client; if (this.api.sessionId && forceEnd) { this.api.session('delete', result => { client.sessionId = null; client.setApiProperty('sessionId', null); this.client.transport.closeDriver('FINISHED').then(() => { this.complete(callback, result); }); }); } else { setImmediate(() => { this.complete(callback, null); }); } return this.client.api; } complete(callback, response) { let result; if (typeof callback === 'function') { result = callback.call(this.api, response); } this.emit('complete', result); } } module.exports = End; ================================================ FILE: lib/api/client-commands/getCookie.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Retrieve a single cookie visible to the current page. The cookie is returned as a cookie JSON object, as defined [here](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#cookie-json-object). * * Uses `cookie` protocol command. * * @example * this.demoTest = function(browser) { * browser.getCookie(name, function callback(result) { * this.assert.equal(result.value, '123456'); * this.assert.equal(result.name, 'test_cookie'); * }); * } * * * @method getCookie * @param {string} name The cookie name. * @param {function} callback Callback function which is called with the result value. * @api protocol.cookies * @syntax .getCookie(name, callback) * @see cookie * @returns {object|null} The cookie object as a selenium cookie JSON object or null if the cookie wasn't found. * @deprecated In favour of `.cookies.get()`. */ class GetCookie extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } /** * Perform the .cookie() protocol action and pass the result to the supplied callback * with the original "this" context * * @param {function} actionCallback */ performAction(actionCallback) { const {cookieName} = this; this.api.cookie('GET', function(result) { let value = null; if (Array.isArray(result.value) && result.value.length > 0) { for (let i = 0; i < result.value.length; i++) { if (result.value[i].name === cookieName) { value = result.value[i]; break; } } } actionCallback.call(this, { value }); }); } command(name, callback) { this.cookieName = name; return super.command(callback); } } module.exports = GetCookie; ================================================ FILE: lib/api/client-commands/getCookies.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Retrieve all cookies visible to the current page. The cookies are returned as an array of cookie JSON object, as defined [here](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#cookie-json-object). * * Uses `cookie` protocol command. * * @example * this.demoTest = function(browser) { * browser.getCookies(function callback(result) { * this.assert.equal(result.value.length, 1); * this.assert.equal(result.value[0].name, 'test_cookie'); * }); * } * * * @method getCookies * @param {function} callback The callback function which will receive the response as an argument. * @syntax .getCookies(callback) * @api protocol.cookies * @see cookie * @returns {Array.} A list of cookies. * @deprecated In favour of `.cookies.getAll()`. */ class GetCookies extends ClientCommand { performAction(callback) { this.api.cookie('GET', callback); } } module.exports = GetCookies; ================================================ FILE: lib/api/client-commands/getLog.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Gets a log from Selenium. * * @example * this.demoTest = function(client) { * this.getLog('browser', function(logEntriesArray) { * console.log('Log length: ' + logEntriesArray.length); * logEntriesArray.forEach(function(log) { * console.log('[' + log.level + '] ' + log.timestamp + ' : ' + log.message); * }); * }); * }; * * * @method getLog * @syntax .getLog([typeString], callback) * @param {string|function} typeString Log type to request * @param {function} callback Callback function which is called with the result value. * @api protocol.sessions * @see getLogTypes * @deprecated In favour of `.logs.getSessionLog()`. */ class GetLog extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } performAction(actionCallback) { this.api.sessionLog(this.typeString, actionCallback); } command(typeString = 'browser', callback) { if (arguments.length === 1 && typeof arguments[0] == 'function') { callback = arguments[0]; typeString = 'browser'; } this.typeString = typeString; return super.command(callback); } } module.exports = GetLog; ================================================ FILE: lib/api/client-commands/getLogTypes.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Gets the available log types. More info about log types in WebDriver can be found here: https://github.com/SeleniumHQ/selenium/wiki/Logging * * @example * this.demoTest = function(client) { * this.getLogTypes(function(typesArray) { * console.log(typesArray); * }); * }; * * * @method getLogTypes * @syntax .getLogTypes(callback) * @param {function} callback Callback function which is called with the result value. * @returns {Array} Available log types * @api protocol.sessions * @see sessionLogTypes * @deprecated In favour of `.logs.getSessionLogTypes()`. */ class GetLogTypes extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } performAction(actionCallback) { this.api.sessionLogTypes(actionCallback); } } module.exports = GetLogTypes; ================================================ FILE: lib/api/client-commands/getPerformanceMetrics.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Get the performance metrics from the browser. Metrics collection only begin after `enablePerformanceMetrics()` command is called. * * @example * describe('collect performance metrics', function() { * it('enables the metrics collection, does some stuff and collects the metrics', function() { * browser * .enablePerformanceMetrics() * .navigateTo('https://www.google.com') * .getPerformanceMetrics((result) => { * if (result.status === 0) { * const metrics = result.value; * console.log(metrics); * } * }); * }); * }); * * @method getPerformanceMetrics * @syntax .getPerformanceMetrics(callback) * @param {function} callback Callback function called with an object containing the performance metrics as argument. * @returns {Promise} Metrics collected between the last call to `enablePerformanceMetrics()` command and this command. * @api protocol.cdp * @since 2.2.0 * @moreinfo web.dev/metrics/ * @moreinfo pptr.dev/api/puppeteer.page.metrics/ */ class GetPerformanceMetrics extends ClientCommand { performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .getPerformanceMetrics() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } this.transportActions.getPerformanceMetrics(callback); } command(callback) { return super.command(callback); } } module.exports = GetPerformanceMetrics; ================================================ FILE: lib/api/client-commands/getTitle.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Returns the title of the current page. Uses title protocol command. * * @example * this.demoTest = function (browser) { * browser.getTitle(function(title) { * this.assert.equal(typeof title, 'string'); * this.assert.equal(title, 'Nightwatch.js'); * }); * }; * * * @method getTitle * @syntax .getTitle(callback) * @param {function} callback Callback function which is called with the result value. * @see title * @returns {string} The page title. * @api protocol.navigation */ class GetTitle extends ClientCommand { get returnsFullResultObject() { return false; } performAction(callback) { this.api.title(callback); } } module.exports = GetTitle; ================================================ FILE: lib/api/client-commands/getWindowPosition.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Retrieves the current window position. * * For clients which are compatible with the [W3C Webdriver API](https://w3c.github.io/webdriver/), `getWindowPosition` is an alias of `getWindowRect`. * * The `getWindowRect` command returns both dimensions and position of the window, using the `windowRect` protocol command. * * @example * module.exports = { * 'demo test .getWindowPosition()': function(browser) { * // Retrieve the attributes * browser.getWindowPosition(function(value) { * console.log(value); * }); * }, * * 'getWindowPosition ES6 demo test': async function(browser) { * const value = await browser.getWindowPosition(); * console.log('value', value); * } * } * * @method getWindowPosition * @syntax .getWindowPosition([callback]) * @param {function} callback Callback function to be called when the command finishes. * @see windowRect * @api protocol.contexts * @deprecated In favour of `.window.getPosition()`. */ class GetWindowPosition extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } performAction(callback) { this.api.windowPosition('current', callback); } } module.exports = GetWindowPosition; ================================================ FILE: lib/api/client-commands/getWindowRect.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Change or get the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). This is defined as a dictionary of the `screenX`, `screenY`, `outerWidth` and `outerHeight` attributes of the window. * * Its JSON representation is the following: * - `x` - window's screenX attribute; * - `y` - window's screenY attribute; * - `width` - outerWidth attribute; * - `height` - outerHeight attribute. * * All attributes are in in CSS pixels. To change the window react, you can either specify `width` and `height`, `x` and `y` or all properties together. * * @example * module.exports = { * 'demo test .getWindowRect()': function(browser) { * // Retrieve the attributes * browser.getWindowRect(function(value) { * console.log(value); * }); * }, * * 'getWindowRect ES6 demo test': async function(browser) { * const resultValue = await browser.getWindowRect(); * console.log('result value', resultValue); * } * } * * @w3c * @method getWindowRect * @link /#dfn-get-window-rect * @param {function} callback Callback function to be called when the command finishes. * @see windowRect * @api protocol.contexts * @deprecated In favour of `.window.getRect()`. */ class GetWindowRect extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } performAction(callback) { this.api.windowRect(null, callback); } } module.exports = GetWindowRect; ================================================ FILE: lib/api/client-commands/getWindowSize.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Retrieves the current window size. * * For clients which are compatible with the [W3C Webdriver API](https://w3c.github.io/webdriver/), `getWindowSize` is an alias of `getWindowRect`. * * The `getWindowRect` command returns both dimensions and position of the window, using the `windowRect` protocol command. * * @example * module.exports = { * 'demo test .getWindowSize()': function(browser) { * // Retrieve the attributes * browser.getWindowSize(function(value) { * console.log(value); * }); * }, * * 'getWindowSize ES6 demo test': async function(browser) { * const value = await browser.getWindowSize(); * console.log('value', value); * } * } * * * @method getWindowSize * @syntax .getWindowSize([callback]) * @param {function} callback Callback function to be called when the command finishes. * @see windowRect * @api protocol.contexts * @deprecated In favour of `.window.getSize()`. */ class GetWindowSize extends ClientCommand { get returnsFullResultObject() { return false; } get resolvesWithFullResultObject() { return false; } performAction(callback) { this.api.windowSize('current', callback); } } module.exports = GetWindowSize; ================================================ FILE: lib/api/client-commands/init.js ================================================ const ClientCommand = require('./_base-command.js'); /** * This command is an alias to url and also a convenience method when called without any arguments in the sense that it performs a call to .url() with passing the value of `launch_url` field from the settings file. * Uses `url` protocol command. * * @example * this.demoTest = function (client) { * client.init(); * }; * * * @method init * @param {string} [url] Url to navigate to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see url * @api protocol.navigation */ class Init extends ClientCommand { performAction(callback) { this.api.url(this.url, callback); } command(url, callback) { if (arguments.length === 0 || typeof arguments[0] == 'function') { url = this.api.launchUrl; } if (arguments.length === 0) { callback = function() {}; } else if (typeof arguments[0] == 'function') { callback = arguments[0]; } if (!url) { // eslint-disable-next-line no-console console.warn('No url defined for .init() command.'); } this.url = url; return super.command(callback); } } module.exports = Init; ================================================ FILE: lib/api/client-commands/injectScript.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Utility command to load an external script into the page specified by url. * * @example * this.demoTest = function(client) { * this.injectScript("{script-url}", function() { * // we're all done here. * }); * }; * * * @method injectScript * @param {string} scriptUrl The script file url * @param {string} [id] Dom element id to be set on the script tag. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.document * @returns {HTMLScriptElement} The newly created script tag. * @deprecated In favour of `.document.injectScript()`. */ class InjectScript extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { // eslint-disable-next-line this.api.execute(function(u,i) {return (function(d){var e=d.createElement('script');var m=d.getElementsByTagName('head')[0];e.src=u;if(i){e.id=i;}m.appendChild(e);return e;})(document);}, this.args, callback); } command(scriptUrl, id, callback) { this.args = [scriptUrl]; if (arguments.length === 2 && typeof arguments[1] == 'function') { callback = arguments[1]; } else if (typeof id == 'string') { this.args.push(id); } return super.command(callback); } } module.exports = InjectScript; ================================================ FILE: lib/api/client-commands/isLogAvailable.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Utility command to test if the log type is available. * * @example * this.demoTest = function(browser) { * browser.isLogAvailable('browser', function(isAvailable) { * // do something more in here * }); * } * * * @method isLogAvailable * @syntax .isLogAvailable([typeString], callback) * @param {string|function} typeString Type of log to test * @param {function} callback Callback function which is called with the result value. * @api protocol.sessions * @see getLogTypes * @deprecated In favour of `.logs.isSessionLogAvailable()`. */ class IsLogAvailable extends ClientCommand { performAction(actionCallback) { const {typeString} = this; this.api.getLogTypes(function(types) { let isAvailable; try { isAvailable = Array.isArray(types) && types.indexOf(typeString) >= 0; } catch (err) { isAvailable = false; } actionCallback.call(this, isAvailable); }); } command(typeString = 'browser', callback) { if (arguments.length === 1 && typeof arguments[0] == 'function') { callback = arguments[0]; typeString = 'browser'; } this.typeString = typeString; return super.command(callback); } } module.exports = IsLogAvailable; ================================================ FILE: lib/api/client-commands/logs/captureBrowserConsoleLogs.js ================================================ const ClientCommand = require('../_base-command.js'); const {Logger} = require('../../../utils'); /** * Listen to the `console` events (ex. `console.log` event) and register callback to process the same. * * @example * describe('capture console events', function() { * it('captures and logs console.log event', function() { * browser * .captureBrowserConsoleLogs((event) => { * console.log(event.type, event.timestamp, event.args[0].value); * }) * .navigateTo('https://www.google.com') * .executeScript(function() { * console.log('here'); * }, []); * }); * }); * * @method captureBrowserConsoleLogs * @syntax .captureBrowserConsoleLogs(onEventCallback) * @param {function} onEventCallback Callback function called whenever a new console event is captured. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/running-tests/capture-console-messages.html */ class StartCapturingLogs extends ClientCommand { static get namespacedAliases() { return 'captureBrowserConsoleLogs'; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .captureBrowserConsoleLogs() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const userCallback = this.userCallback; if (userCallback === undefined) { const error = new Error('Callback is missing from .captureBrowserConsoleLogs() command.'); Logger.error(error); return callback(error); } this.transportActions.startLogsCapture(userCallback, callback); } command(userCallback, callback) { this.userCallback = userCallback; return super.command(callback); } } module.exports = StartCapturingLogs; ================================================ FILE: lib/api/client-commands/logs/captureBrowserExceptions.js ================================================ const ClientCommand = require('../_base-command.js'); const {Logger} = require('../../../utils'); /** * Catch the JavaScript exceptions thrown in the browser. * * @example * describe('catch browser exceptions', function() { * it('captures the js exceptions thrown in the browser', async function() { * await browser.captureBrowserExceptions((event) => { * console.log('>>> Exception:', event); * }); * * await browser.navigateTo('https://duckduckgo.com/'); * * const searchBoxElement = await browser.findElement('input[name=q]'); * await browser.executeScript(function(_searchBoxElement) { * _searchBoxElement.setAttribute('onclick', 'throw new Error("Hello world!")'); * }, [searchBoxElement]); * * await browser.elementIdClick(searchBoxElement.getId()); * }); * }); * * @method captureBrowserExceptions * @syntax .captureBrowserExceptions(onExceptionCallback) * @param {function} onExceptionCallback Callback function called whenever a new exception is thrown in the browser. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/running-tests/catch-js-exceptions.html */ class CatchJsExceptions extends ClientCommand { static get namespacedAliases() { return 'captureBrowserExceptions'; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .captureBrowserExceptions() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const userCallback = this.userCallback; if (userCallback === undefined) { const error = new Error('Callback is missing from .captureBrowserExceptions() command.'); Logger.error(error); return callback(error); } this.transportActions.catchJsExceptions(userCallback, callback); } command(userCallback, callback) { this.userCallback = userCallback; return super.command(callback); } } module.exports = CatchJsExceptions; ================================================ FILE: lib/api/client-commands/logs/getSessionLog.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Gets a log from Selenium. * * @example * describe('get log from Selenium', function() { * it('get browser log (default)', function(browser) { * browser.logs.getSessionLog(function(result) { * const logEntriesArray = result.value; * console.log('Log length: ' + logEntriesArray.length); * logEntriesArray.forEach(function(log) { * console.log('[' + log.level + '] ' + log.timestamp + ' : ' + log.message); * }); * }); * }); * * it('get driver log with ES6 async/await', async function(browser) { * const driverLogAvailable = await browser.logs.isSessionLogAvailable('driver'); * if (driverLogAvailable) { * const logEntriesArray = await browser.logs.getSessionLog('driver'); * logEntriesArray.forEach(function(log) { * console.log('[' + log.level + '] ' + log.timestamp + ' : ' + log.message); * }); * } * }); * }); * * @syntax .logs.getSessionLog([typeString], [callback]) * @method logs.getSessionLog * @param {string} typeString Log type to request. Default: 'browser'. Use `.logs.getLogTypes()` command to get all available log types. * @param {function} [callback] Callback function which is called with the result value. * @returns {Array} An array of log Entry objects with properties as defined [here](https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/logging_exports_Entry.html) (see Instance Properties). * @see logs.getSessionLogTypes * @api protocol.sessions */ class GetSessionLog extends ClientCommand { performAction(callback) { this.transportActions.getLogContents(this.typeString, callback); } command(typeString = 'browser', callback) { if (arguments.length === 1 && typeof arguments[0] == 'function') { callback = arguments[0]; typeString = 'browser'; } this.typeString = typeString; return super.command(callback); } } module.exports = GetSessionLog; ================================================ FILE: lib/api/client-commands/logs/getSessionLogTypes.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Gets the available log types. More info about log types in WebDriver can be found here: https://github.com/SeleniumHQ/selenium/wiki/Logging * * @example * describe('get available log types', function() { * it('get log types', function(browser) { * browser.logs.getSessionLogTypes(function(result) { * const logTypes = result.value; * console.log('Log types available:', logTypes); * }); * }); * * it('get log types with ES6 async/await', async function(browser) { * const logTypes = await browser.logs.getSessionLogTypes(); * console.log('Log types available:', logTypes); * }); * }); * * @syntax .logs.getSessionLogTypes([callback]) * @method logs.getSessionLogTypes * @param {function} [callback] Callback function which is called with the result value. * @returns {Array} Available log types. * @see logs.getSessionLog * @api protocol.sessions */ class GetSessionLogTypes extends ClientCommand { performAction(callback) { this.transportActions.getSessionLogTypes(callback); } } module.exports = GetSessionLogTypes; ================================================ FILE: lib/api/client-commands/logs/isSessionLogAvailable.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Utility command to test if the log type is available. * * @example * describe('test if the log type is available', function() { * it('test browser log type', function(browser) { * browser.logs.isSessionLogAvailable('browser', function(result) { * const isAvailable = result.value; * if (isAvailable) { * // do something more in here * } * }); * }); * * it('test driver log type with ES6 async/await', async function(browser) { * const isAvailable = await browser.logs.isSessionLogAvailable('driver'); * if (isAvailable) { * // do something more in here * } * }); * }); * * @syntax .logs.isSessionLogAvailable([typeString], [callback]) * @method logs.isSessionLogAvailable * @param {string} typeString Type of log to test. Default: 'browser'. * @param {function} [callback] Callback function which is called with the result value. * @returns {boolean} True if log type is available. * @see logs.getSessionLogTypes * @api protocol.sessions */ class IsSessionLogAvailable extends ClientCommand { performAction(callback) { const {typeString} = this; this.transportActions.getSessionLogTypes(function(result) { if (result.status === 0) { const types = result.value; let isAvailable; try { isAvailable = Array.isArray(types) && types.indexOf(typeString) >= 0; } catch (err) { isAvailable = false; } result.value = isAvailable; } callback.call(this, result); }); } command(typeString = 'browser', callback) { if (arguments.length === 1 && typeof arguments[0] == 'function') { callback = arguments[0]; typeString = 'browser'; } this.typeString = typeString; return super.command(callback); } } module.exports = IsSessionLogAvailable; ================================================ FILE: lib/api/client-commands/maximizeWindow.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Maximizes the current window. * * @example * this.demoTest = function (browser) { * browser.maximizeWindow(); * }; * * * @method maximizeWindow * @param {function} [callback] Optional callback function to be called when the command finishes. * @see windowMaximize * @api protocol.contexts * @deprecated In favour of `.window.maximize()`. */ class WindowMaximize extends ClientCommand { performAction(callback) { this.api.windowMaximize('current', callback); } } module.exports = WindowMaximize; ================================================ FILE: lib/api/client-commands/network/captureRequests.js ================================================ const ClientCommand = require('../_base-command.js'); const {Logger} = require('../../../utils'); /** * Capture outgoing network calls from the browser. * * @example * describe('capture network requests', function() { * it('captures and logs network requests as they occur', function(this: ExtendDescribeThis<{requestCount: number}>) { * this.requestCount = 1; * browser * .network.captureRequests((requestParams) => { * console.log('Request Number:', this.requestCount!++); * console.log('Request URL:', requestParams.request.url); * console.log('Request method:', requestParams.request.method); * console.log('Request headers:', requestParams.request.headers); * }) * .navigateTo('https://www.google.com'); * }); * }); * * @method network.captureRequests * @syntax .captureNetworkRequests(onRequestCallback) * @syntax .network.captureRequests(onRequestCallback) * @param {function} onRequestCallback Callback function called whenever a new outgoing network request is made. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/network-requests/capture-network-calls.html */ class CaptureNetworkCalls extends ClientCommand { static get namespacedAliases() { return 'captureNetworkRequests'; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .captureNetworkRequests() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const userCallback = this.userCallback; if (userCallback === undefined) { const error = new Error('Callback is missing from .captureNetworkRequests() command.'); Logger.error(error); return callback(error); } this.transportActions.interceptNetworkCalls(userCallback, callback); } command(userCallback, callback) { this.userCallback = userCallback; return super.command(callback); } } module.exports = CaptureNetworkCalls; ================================================ FILE: lib/api/client-commands/network/mockResponse.js ================================================ const ClientCommand = require('../_base-command.js'); const {Logger} = require('../../../utils'); /** * Intercept the request made on a particular URL and mock the response. * * @example * describe('mock network response', function() { * it('intercepts the request made to Google search and mocks its response', function() { * browser * .network.mockResponse('https://www.google.com/', { * status: 200, * headers: { * 'Content-Type': 'UTF-8' * }, * body: 'Hello there!' * }) * .navigateTo('https://www.google.com/') * .pause(2000); * }); * }); * * @method network.mockResponse * @syntax .mockNetworkResponse(urlToIntercept, {status, headers, body}, [callback]) * @syntax .network.mockResponse(urlToIntercept, {status, headers, body}, [callback]) * @param {string} urlToIntercept URL to intercept and mock the response from. * @param {object} response Response to return. Defaults: `{status: 200, headers: {}, body: ''}`. * @param {function} [callback] Callback function to be called when the command finishes. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/network-requests/mock-network-response.html */ class MockNetworkResponse extends ClientCommand { static get namespacedAliases() { return 'mockNetworkResponse'; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .mockNetworkResponse() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const {response = {}} = this; let {urlToIntercept = ''} = this; if (urlToIntercept.startsWith('/')) { const launchUrl = this.api.launchUrl || this.api.globals.launchUrl || ''; const needsSlash = launchUrl.endsWith('/'); const slashDel = needsSlash ? '/' : ''; urlToIntercept = `${launchUrl}${slashDel}${urlToIntercept}`; } this.transportActions.mockNetworkResponse(urlToIntercept, response, callback); } command(urlToIntercept, response, callback) { this.urlToIntercept = urlToIntercept; this.response = response; return super.command(callback); } } module.exports = MockNetworkResponse; ================================================ FILE: lib/api/client-commands/network/setConditions.js ================================================ const ClientCommand = require('../_base-command.js'); const {Logger} = require('../../../utils'); /** * * Command to set Chrome network emulation settings. * * @example * describe('set network conditions', function() { * it('sets the network conditions',function() { * browser * .network.setConditions({ * offline: false, * latency: 3000, * download_throughput: 500 * 1024, * upload_throughput: 500 * 1024 * }); * }); * }); * * @method network.setConditions * @syntax .setNetworkConditions(spec, [callback]) * @syntax .network.setConditions(spec, [callback]) * @param {object} spec * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ class SetNetworkConditions extends ClientCommand { static get namespacedAliases() { return 'setNetworkConditions'; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .setNetworkConditions() is only supported in Chromium based drivers'); Logger.error(error); return callback(error); } const {spec} = this; this.transportActions.setNetworkConditions(spec, callback); } command(spec, callback) { this.spec = spec; return super.command(callback); } } module.exports = SetNetworkConditions; ================================================ FILE: lib/api/client-commands/pageSource.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Returns the page source. * * @example * this.demoTest = function (browser) { * browser.pageSource(function(pageSource) { * console.log(pageSource); * }); * }; * * * @method pageSource * @syntax .pageSource(callback) * @param {function} callback Callback function which is called with the result value. * @see pageSource * @returns {string} The page source. * @api protocol.pageSource * @deprecated In favour of `.document.pageSource()`. */ class PageSource extends ClientCommand { get returnsFullResultObject() { return true; } performAction(callback) { this.api.source(callback); } } module.exports = PageSource; ================================================ FILE: lib/api/client-commands/pause.js ================================================ const EventEmitter = require('events'); const readline = require('readline'); const Debuggability = require('../../utils/debuggability.js'); /** * pause() command provides the following functionalities: * - Pause the test execution for the given time in milliseconds. * - Pause the test execution indefinitely, until resumed by pressing a key in terminal. * - Pause the test execution, and then step over to the next test command (execute the next test command) and pause again. * * This command will allow you to pause the test execution in between, hop on to the browser to check the state of your * application (or use DevTools to debug), and once satisfied, either resume the test execution from where it was left * off or step over to the next test command (execute the next test command) and pause again. * * Stepping over to the next test command would allow you to see what exactly changed in your application when the next * test command was executed. You can also use DevTools to monitor those changes, like the network calls that were made * during the execution of that command, etc. * * @example * this.demoTest = function (browser) { * // pause for 1000 ms * browser.pause(1000); * // pause indefinitely until resumed * browser.pause(); * }; * * @method pause * @param {number} [ms] The number of milliseconds to wait. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.utilities */ class Pause extends EventEmitter { command(ms, callback) { // If we don't pass the milliseconds, the client will // be suspended indefinitely, until the user presses some // key in the terminal to resume it. if (ms === undefined) { // eslint-disable-next-line console.log(`Paused... Press or F10 to step over to the next test command and pause again. Press d to enter the DEBUG mode. Press Ctrl+C to exit. Press any other key to RESUME.`); readline.emitKeypressEvents(process.stdin); process.stdin.resume(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.once('keypress', (str, key) => { if (key.ctrl && key.name === 'c') { this.api.end(function() { process.exit(0); }); } else if (key.name === 'space' || key.name === 'f10') { Debuggability.stepOverAndPause = true; } else if (key.name === 'd') { this.api.debug(); } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.stdin.pause(); // Remove the logged paused... information above readline.moveCursor(process.stdout, 0, -5); readline.clearScreenDown(process.stdout); if (callback) { callback.call(this.client.api); } this.emit('complete'); }); } else { setTimeout(() => { // if we have a callback, call it right before the complete event if (callback) { callback.call(this.client.api); } this.emit('complete'); }, ms); } return this; } } module.exports = Pause; ================================================ FILE: lib/api/client-commands/perform.js ================================================ /** * A simple perform command which allows access to the Nightwatch API in a callback. Can be useful if you want to read variables set by other commands. * * The callback signature can have up to two parameters. * - no parameters: callback runs and perform completes immediately at the end of the execution of the callback. * - one parameter: allows for asynchronous execution within the callback providing a done callback function for completion as the first argument. * - two parameters: allows for asynchronous execution with the Nightwatch `api` object passed in as the first argument, followed by the done callback. * * In the case of asynchronous execution, the timeout can be controlled by setting the `asyncHookTimeout` global. See [Using test globals](https://nightwatchjs.org/gettingstarted/concepts/#using-test-globals) for more info. * * @example * describe('perform example', function() { * var elementValue; * * it('basic perform', function(browser) { * browser * .getValue('.some-element', function(result) { * elementValue = result.value; * }) * // other stuff going on ... * * // self-completing callback * .perform(function() { * console.log('elementValue', elementValue); * // without any defined parameters, perform * // completes immediately (synchronously) * }) * * // returning a Promise * .perform(async function() { * console.log('elementValue', elementValue); * // potentially other async stuff going on * * return elementValue; * }) * * // DEPRECATED: asynchronous completion including api (client) * .perform(function(client, done) { * console.log('elementValue', elementValue); * done(); * }); * }); * * it('perform with async', function(browser) { * const result = await browser.perform(async function() { * return 100; * }); * console.log('result:', result); // 100 * }) * } * * * @method perform * @param {function} callback The function to run as part of the queue. * @api protocol.utilities */ const {Actions} = require('selenium-webdriver/lib/input'); const EventEmitter = require('events'); class Perform extends EventEmitter { static get alwaysAsync() { return true; } static get avoidPrematureParentNodeResolution() { return true; } command(callback = function() {}) { let doneCallback; const asyncHookTimeout = this.client.settings.globals.asyncHookTimeout; this.timeoutId = setTimeout(() => { this.emit('error', new Error(`Timeout while waiting (${asyncHookTimeout}ms) for the .perform() command callback to be called.`)); }, asyncHookTimeout); if (callback.length === 0) { let cbResult = this.runCallback(callback, [this.api]); // support for Selenium Actions API: // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html if (cbResult instanceof Actions) { cbResult = cbResult.perform(); } if (cbResult instanceof Promise) { clearTimeout(this.timeoutId); cbResult.then(async result => { if (result instanceof Actions) { result = await result.perform(); } this.emit('complete', result); }).catch(err => { this.emit('error', err); }); return this; } doneCallback = () => { clearTimeout(this.timeoutId); this.emit('complete', cbResult); }; } else { doneCallback = () => { const args = [(result) => { clearTimeout(this.timeoutId); this.emit('complete', result); }]; if (callback.length > 1) { args.unshift(this.api); } this.runCallback(callback, args); }; } process.nextTick(doneCallback); return this; } runCallback(cb, args) { try { return cb.apply(this.api, args); } catch (err) { if (this.timeoutId) { clearTimeout(this.timeoutId); } this.emit('error', err); } } } module.exports = Perform; ================================================ FILE: lib/api/client-commands/registerBasicAuth.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Automate the input of basic auth credentials whenever they arise. * This feature is currently implemented on top of Selenium 4’s CDP(Chrome DevTools Protocol) support, and so only works on those browser that support that protocol * * @example * module.exports = { * 'input basic auth credentials': function (browser) { * browser * .registerBasicAuth('admin', 'admin') * .navigateTo('https://the-internet.herokuapp.com/basic_auth'); * } * }; * * @syntax .registerBasicAuth(username, password, [callback]) * @method registerBasicAuth * @param {string} username * @param {string} password * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.userprompts */ class RegisterBasicAuth extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .registerBasicAuth() is only supported in Chromium based drivers'); Logger.error(error); return callback(error); } const {username, password} = this; this.transportActions .registerAuth(username, password, callback) .catch(err => { return err; }) .then(result => callback(result)); } command(username, password, callback) { this.username = username; this.password = password; return super.command(callback); } } module.exports = RegisterBasicAuth; ================================================ FILE: lib/api/client-commands/resizeWindow.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Resizes the current window. * * @example * this.demoTest = function (browser) { * browser.resizeWindow(1000, 800); * }; * * * @method resizeWindow * @param {number} width The new window width. * @param {number} height The new window height. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see windowSize * @api protocol.contexts * @deprecated In favour of `.window.resize()`. */ class ResizeWindow extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { const {width, height} = this; this.transportActions.setWindowSize( width, height ).catch(err => { return err; }).then(result => callback(result)); } command(width, height, callback) { this.width = width; this.height = height; return super.command(callback); } } module.exports = ResizeWindow; ================================================ FILE: lib/api/client-commands/saveScreenshot.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger, writeToFile} = require('../../utils'); const {COMMON_EVENTS: {ScreenshotCreated}, NightwatchEventHub} = require('../../runner/eventHub.js'); /** * Take a screenshot of the current page and saves it as the given filename. * * @example * this.demoTest = function (browser) { * browser.saveScreenshot('/path/to/fileName.png'); * }; * * * @method saveScreenshot * @param {string} fileName The complete path to the file name where the screenshot should be saved. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see screenshot * @api protocol.screens */ class SaveScreenshot extends ClientCommand { performAction(callback) { const {fileName, client} = this; this.api.screenshot(this.api.options.log_screenshot_data, function(result) { if (client.transport.isResultSuccess(result) && result.value) { writeToFile(fileName, result.value, 'base64').then(() => { callback.call(this, result); NightwatchEventHub.emit(ScreenshotCreated, { path: fileName }); }).catch(err => { Logger.error(`Couldn't save screenshot to "${fileName}":`); Logger.error(err); callback.call(this, result); }); } else { Logger.error(`Couldn't save screenshot to "${fileName}":`); callback.call(this, result); } }); } command(fileName, callback) { this.fileName = fileName; return super.command(callback); } } module.exports = SaveScreenshot; ================================================ FILE: lib/api/client-commands/saveSnapshot.js ================================================ const {URL} = require('url'); const {JSDOM} = require('jsdom'); const ClientCommand = require('./_base-command.js'); const {writeToFile, Logger} = require('../../utils'); /** * Take a DOM Snapshot of the current page and saves it as the given filename. * * @example * this.demoTest = function (browser) { * browser.saveSnapshot('/path/to/fileName.html'); * }; * * * @method saveSnapshot * @param {string} fileName The complete path to the file name where the screenshot should be saved. * @param {function} [callback] Optional callback function to be called when the command finishes. */ class SaveSnapshot extends ClientCommand { performAction(callback) { const {fileName, client} = this; Promise.all([ this.transportActions.getCurrentUrl(), this.transportActions.getPageSource() ]) .then(results => { let baseUrl; let pageSource; if (client.transport.isResultSuccess(results[0]) && results[0].value) { baseUrl = new URL(results[0].value).origin; } else { throw new Error('failed to execute getCurrentUrl'); } if (client.transport.isResultSuccess(results[1]) && results[1].value) { pageSource = results[1].value; } else { throw new Error('failed to fetch pageSource'); } return this.createSnapshot(baseUrl, pageSource).then(result => { return {data: result, baseUrl}; }); }).then(result => { const {data, baseUrl} = result; return writeToFile(fileName, data).then((fileName => { return { snapshotFilePath: fileName, snapshotUrl: baseUrl }; })); }).then(result => { callback.call(this, result); }).catch(err => { Logger.warn(`Couldn't save snapshot to "${fileName}: ${err.message}`); callback.call(this, err); }); } async createSnapshot(baseUrl, pageSource) { const dom = new JSDOM(pageSource); const document = dom.window.document; // Remove all script tags const scriptTags = document.querySelectorAll('script'); scriptTags.forEach(element => { element.remove(); }); const headTag = document.querySelector('head'); const baseTag = document.createElement('base'); baseTag.setAttribute('href', baseUrl); headTag.prepend(baseTag); return dom.serialize(); } command(fileName, callback) { this.fileName = fileName; return super.command(callback); } } module.exports = SaveSnapshot; ================================================ FILE: lib/api/client-commands/setCookie.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Set a cookie, specified as a cookie JSON object, as defined [here](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#cookie-json-object). * * Uses `cookie` protocol command. * * @example * this.demoTest = function(browser) { * browser.setCookie({ * name : "test_cookie", * value : "test_value", * path : "/", (Optional) * domain : "example.org", (Optional) * secure : false, (Optional) * httpOnly : false, // (Optional) * expiry : 1395002765 // (Optional) time in seconds since midnight, January 1, 1970 UTC * }); * } * * * @method setCookie * @param {object} cookie The cookie object. * @syntax .setCookie(cookie, [callback]) * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.cookies * @see cookie * @deprecated In favour of `.cookies.set()`. */ class SetCookie extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { this.api.cookie('POST', this.cookie, callback); } command(cookie, callback) { this.cookie = cookie; return super.command(callback); } } module.exports = SetCookie; ================================================ FILE: lib/api/client-commands/setDeviceDimensions.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Override device mode/dimensions. Call without any arguments to reset the device dimensions back to original. * * @example * describe('modify device dimensions', function() { * it('modifies the device dimensions and then resets it', function() { * browser * .setDeviceDimensions({ * width: 400, * height: 600, * deviceScaleFactor: 50, * mobile: true * }) * .navigateTo('https://www.google.com') * .pause(1000) * .setDeviceDimensions() // resets the device dimensions * .navigateTo('https://www.google.com') * .pause(1000); * }); * }); * * @method setDeviceDimensions * @syntax .setDeviceDimensions({width, height, deviceScaleFactor, mobile}, [callback]) * @param {object} [metrics] Device metrics to set. Metric defaults to original if not set. * @param {function} [callback] Callback function to be called when the command finishes. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/mobile-web-testing/override-device-dimensions.html */ class SetDeviceDimensions extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .setDeviceDimensions() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } // The default values below disables the override for that property (if a user // has not set a property, they'd want that property to not be overridden). const {width = 0, height = 0, deviceScaleFactor = 0, mobile = false} = this.metrics; const metrics = {width, height, deviceScaleFactor, mobile}; this.transportActions.setDeviceMetrics(metrics, callback); } command(metrics, callback) { this.metrics = metrics || {}; return super.command(callback); } } module.exports = SetDeviceDimensions; ================================================ FILE: lib/api/client-commands/setGeolocation.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Mock the geolocation of the browser. Call without any arguments to reset the geolocation back to original. * * @example * describe('mock geolocation', function() { * it('sets the geolocation to Tokyo, Japan and then resets it', () => { * browser * .setGeolocation({ * latitude: 35.689487, * longitude: 139.691706, * accuracy: 100 * }) // sets the geolocation to Tokyo, Japan * .navigateTo('https://www.gps-coordinates.net/my-location') * .pause(3000) * .setGeolocation() // resets the geolocation * .navigateTo('https://www.gps-coordinates.net/my-location') * .pause(3000); * }); * }); * * @method setGeolocation * @syntax .setGeolocation({latitude, longitude, accuracy}, [callback]) * @param {object} [coordinates] Latitude, longitude, and accuracy. * @param {function} [callback] Callback function to be called when the command finishes. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/network-requests/mock-geolocation.html */ class SetGeolocation extends ClientCommand { static get isTraceable() { return true; } performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .setGeolocation() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const {latitude, longitude, accuracy} = this.coordinates; if (latitude !== undefined && longitude !== undefined) { // Both latitude and longitude are provided const coordinates = {latitude, longitude, accuracy}; // Set accuracy as 100 if not provided. if (accuracy === undefined) {coordinates.accuracy = 100} this.transportActions.setGeolocation(coordinates, callback); return; } if (latitude === undefined && longitude === undefined) { // Clear geolocation override. this.transportActions.clearGeolocation(callback); return; } // Exactly one of them is undefined, throw error. const error = new Error('Please provide both latitude and longitude while using setGeolocation.'); Logger.error(error); return callback(error); } command(coordinates, callback) { this.coordinates = coordinates || {}; return super.command(callback); } } module.exports = SetGeolocation; ================================================ FILE: lib/api/client-commands/setWindowPosition.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Sets the current window position. * * @example * this.demoTest = function (browser) { * browser.setWindowPosition(0, 0); * }; * * * @method setWindowPosition * @param {number} offsetX The new window offset x-position. * @param {number} offsetY The new window offset y-position. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see windowPosition * @api protocol.contexts * @deprecated In favour of `.window.setPosition()`. */ class SetWindowPosition extends ClientCommand { performAction(callback) { this.api.windowPosition('current', this.offsetX, this.offsetY, callback); } command(offsetX, offsetY, callback) { this.offsetX = offsetX; this.offsetY = offsetY; return super.command(callback); } } module.exports = SetWindowPosition; ================================================ FILE: lib/api/client-commands/setWindowRect.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Change the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). This is defined as a dictionary of the `screenX`, `screenY`, `outerWidth` and `outerHeight` attributes of the window. * * Its JSON representation is the following: * - `x` - window's screenX attribute; * - `y` - window's screenY attribute; * - `width` - outerWidth attribute; * - `height` - outerHeight attribute. * * All attributes are in in CSS pixels. To change the window react, you can either specify `width` and `height`, `x` and `y` or all properties together. * * @example * module.exports = { * 'demo test .setWindowRect()': function(browser) { * * // Change the screenX and screenY attributes of the window rect. * browser.setWindowRect({x: 500, y: 500}); * * // Change the width and height attributes of the window rect. * browser.setWindowRect({width: 600, height: 300}); * * // Retrieve the attributes * browser.setWindowRect(function(result) { * console.log(result.value); * }); * }, * * 'setWindowRect ES6 demo test': async function(browser) { * await browser.setWindowRect({ * width: 600, * height: 300, * x: 100, * y: 100 * }); * } * } * * @w3c * @link /#dfn-get-window-rect * @method setWindowRect * @syntax .setWindowRect({width, height, x, y}, [callback]); * @param {object} options An object specifying either `width` and `height`, `x` and `y`, or all together to set properties for the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). * @param {function} [callback] Optional callback function to be called when the command finishes. * @see windowRect * @api protocol.contexts * @deprecated In favour of `.window.setRect()`. */ class SetWindowRect extends ClientCommand { performAction(callback) { this.api.windowRect(this.options, callback); } command(options, callback) { this.options = options; return super.command(callback); } } module.exports = SetWindowRect; ================================================ FILE: lib/api/client-commands/setWindowSize.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Sets the current window size in CSS pixels. * * @example * this.demoTest = function (browser) { * browser.setWindowSize(400, 600); * }; * * * @method setWindowSize * @param {number} width The new window width in CSS pixels * @param {number} height The new window height in CSS pixels * @param {function} [callback] Optional callback function to be called when the command finishes. * @see windowSize * @api protocol.contexts * @deprecated In favour of `.window.setSize()`. */ class SetWindowSize extends ClientCommand { performAction(callback) { this.api.windowSize('current', this.width, this.height, callback); } command(width, height, callback) { this.width = width; this.height = height; return super.command(callback); } } module.exports = SetWindowSize; ================================================ FILE: lib/api/client-commands/takeHeapSnapshot.js ================================================ const ClientCommand = require('./_base-command.js'); const {Logger} = require('../../utils'); /** * Take heap snapshot and save it as a `.heapsnapshot` file. * The saved snapshot file can then be loaded into Chrome DevTools' Memory tab for inspection. * * The contents of the heap snapshot are also available in the `value` property of the `result` * argument passed to the callback, in string-serialized JSON format. * * @example * describe('take heap snapshot', function() { * it('takes heap snapshot and saves it as snap.heapsnapshot file', function() { * browser * .navigateTo('https://www.google.com') * .takeHeapSnapshot('./snap.heapsnapshot'); * }); * }); * * @method takeHeapSnapshot * @syntax .takeHeapSnapshot([heapSnapshotLocation], [callback]) * @param {string} [heapSnapshotLocation] Location where the generated heap snapshot file should be saved. * @param {function} [callback] Callback function called with string-serialized heap snapshot as argument. * @returns {Promise} Heap snapshot in string-serialized JSON format. * @api protocol.cdp * @since 2.2.0 * @moreinfo nightwatchjs.org/guide/running-tests/take-heap-snapshot.html */ class TakeHeapSnapshot extends ClientCommand { performAction(callback) { if (!this.api.isChrome() && !this.api.isEdge()) { const error = new Error('The command .takeHeapSnapshot() is only supported in Chrome and Edge drivers'); Logger.error(error); return callback(error); } const {heapSnapshotLocation} = this; this.transportActions.takeHeapSnapshot(heapSnapshotLocation, callback); } command(heapSnapshotLocation, callback) { this.heapSnapshotLocation = heapSnapshotLocation; return super.command(callback); } } module.exports = TakeHeapSnapshot; ================================================ FILE: lib/api/client-commands/urlHash.js ================================================ const ClientCommand = require('./_base-command.js'); /** * Convenience command that adds the specified hash (i.e. url fragment) to the current value of the `launch_url` as set in `nightwatch.json`. * * @example * this.demoTest = function (client) { * client.urlHash('#hashvalue'); * // or * client.urlHash('hashvalue'); * }; * * * @method urlHash * @param {string} hash The hash to add/replace to the current url (i.e. the value set in the launch_url property in nightwatch.json). * @param {function} [callback] Optional callback function to be called when the command finishes. * @see url * @api protocol.navigation */ class UrlHash extends ClientCommand { get returnsFullResultObject() { return true; } performAction(callback) { this.api.url(this.api.launchUrl + '#' + this.hash, callback); } command(hash, callback) { if (hash.charAt(0) === '#') { hash = hash.substring(1); } this.hash = hash; return super.command(callback); } } module.exports = UrlHash; ================================================ FILE: lib/api/client-commands/useCss.js ================================================ const LocateStrategy = require('./_locateStrategy.js'); const Strategies = require('../../element').LocateStrategy; /** * Sets the locate strategy for selectors to `css selector`, therefore every following selector needs to be specified as css. * * @example * this.demoTest = function (browser) { * browser * .useCss() // we're back to CSS now * .setValue('input[type=text]', 'nightwatch'); * }; * * @method useCss * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.utilities */ class Command extends LocateStrategy { constructor() { super(); this.strategy = Strategies.CSS_SELECTOR; } } module.exports = Command; ================================================ FILE: lib/api/client-commands/useXpath.js ================================================ const LocateStrategy = require('./_locateStrategy.js'); const Strategies = require('../../element').LocateStrategy; /** * Sets the locate strategy for selectors to xpath, therefore every following selector needs to be specified as xpath. * * @example * this.demoTest = function (browser) { * browser * .useXpath() // every selector now must be xpath * .click("//tr[@data-recordid]/span[text()='Search Text']"); * }; * * @method useXpath * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.utilities */ class Command extends LocateStrategy { constructor() { super(); this.strategy = Strategies.XPATH; } } module.exports = Command; ================================================ FILE: lib/api/client-commands/window/close.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Close the current window or tab. This can be useful when you're working with multiple windows/tabs open (e.g. an OAuth login). * * After closing a window or tab, you must switch back to a valid window handle (using `.window.switchTo()` command) in order to continue execution. * * @example * module.exports = { * 'close current window/tab': function (browser) { * browser.window.close(function (result) { * console.log('current window/tab closed successfully'); * }); * }, * * 'close current window/tab with ES6 async/await': async function (browser) { * await browser.window.close(); * } * } * * @syntax .window.close([callback]) * @method window.close * @param {function} [callback] Optional callback function to be called when the command finishes. * @exampleLink /api/closeWindow.js * @link /#close-window * @see window.open * @api protocol.window */ class CloseWindow extends ClientCommand { performAction(callback) { this.transportActions.closeWindow(callback); } } module.exports = CloseWindow; ================================================ FILE: lib/api/client-commands/window/fullscreen.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Set the current window state to fullscreen, similar to pressing F11 in most browsers. * * @example * module.exports = { * 'make current window fullscreen': function (browser) { * browser.window.fullscreen(function () { * console.log('window in fullscreen mode'); * }); * }, * * 'make current window fullscreen with ES6 async/await': async function (browser) { * await browser.window.fullscreen(); * } * } * * @syntax .window.fullscreen([callback]) * @method window.fullscreen * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#fullscreen-window * @see window.maximize * @see window.minimize * @api protocol.window */ class FullscreenWindow extends ClientCommand { performAction(callback) { this.transportActions.fullscreenWindow(callback); } } module.exports = FullscreenWindow; ================================================ FILE: lib/api/client-commands/window/getAllHandles.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Retrieve the list of all window handles available to the session. * * @example * module.exports = { * 'get all window handles': function (browser) { * browser.window.getAllHandles(function (result) { * console.log('available window handles are:', result.value); * }); * }, * * 'get all window handles with ES6 async/await': async function (browser) { * const windowHandles = await browser.window.getAllHandles(); * console.log('available window handles are:', windowHandles); * } * } * * @syntax .window.getAllHandles([callback]) * @method window.getAllHandles * @param {function} callback Callback function which is called with the result value. * @returns {string[]} An array of all available window handles. * @see window.getHandle * @see window.switchTo * @link /#get-window-handles * @api protocol.window */ class GetAllWindowHandles extends ClientCommand { performAction(callback) { this.transportActions.getAllWindowHandles(callback); } } module.exports = GetAllWindowHandles; ================================================ FILE: lib/api/client-commands/window/getHandle.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Retrieve the current window handle. * * WebDriver does not make the distinction between windows and tabs. So, if your site opens a new tab or window, you can work with it using a window handle. * * @example * module.exports = { * 'get current window handle': function (browser) { * browser.window.getHandle(function (result) { * console.log('current window handle is:', result.value); * }); * }, * * 'get current window handle with ES6 async/await': async function (browser) { * const windowHandle = await browser.window.getHandle(); * console.log('current window handle is:', windowHandle); * } * } * * @syntax .window.getHandle([callback]) * @method window.getHandle * @param {function} callback Callback function which is called with the result value. * @returns {string} A unique identifier representing the window handle for the current window. * @link /#get-window-handle * @see window.getHandles * @see window.switchTo * @api protocol.window */ class GetWindowHandle extends ClientCommand { performAction(callback) { this.transportActions.getWindowHandle(callback); } } module.exports = GetWindowHandle; ================================================ FILE: lib/api/client-commands/window/getPosition.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Get the coordinates of the top left corner of the current window. * * @example * module.exports = { * 'get current window position': function (browser) { * browser.window.getPosition(function (result) { * console.log('Position of current window:', result.value.x, result.value.y); * }); * }, * * 'get current window position using ES6 async/await': async function (browser) { * const {x, y} = await browser.window.getPosition(); * console.log('Position of current window:', x, y); * } * } * * @syntax .window.getPosition([callback]) * @method window.getPosition * @param {function} [callback] Callback function which is called with the result value. * @returns {{x: number, y: number}} Coordinates representing the position of the current window. * @see window.getSize * @see window.getRect * @api protocol.window */ class GetWindowPosition extends ClientCommand { performAction(callback) { this.transportActions.getWindowPosition(callback); } } module.exports = GetWindowPosition; ================================================ FILE: lib/api/client-commands/window/getRect.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Fetches the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect) - size and position of the current window. * * Its JSON representation is the following: * - `x` - window's screenX attribute; * - `y` - window's screenY attribute; * - `width` - outerWidth attribute; * - `height` - outerHeight attribute. * * All attributes are in CSS pixels. * * @example * module.exports = { * 'get current window rect': function (browser) { * browser.window.getRect(function (result) { * console.log('Size of current window:', result.value.width, result.value.height); * console.log('Position of current window:', result.value.x, result.value.y); * }); * }, * * 'get current window rect using ES6 async/await': async function (browser) { * const {width, height, x, y} = await browser.window.getRect(); * console.log('Size of current window:', width, height); * console.log('Position of current window:', x, y); * } * } * * @syntax .window.getRect([callback]) * @method window.getRect * @param {function} [callback] Callback function which is called with the result value. * @returns {{width: number, height: number, x: number, y: number}} Size and position of the current window. * @link /#get-window-rect * @see window.getPosition * @see window.getSize * @api protocol.window */ class GetWindowRect extends ClientCommand { performAction(callback) { this.transportActions.getWindowRect(callback); } } module.exports = GetWindowRect; ================================================ FILE: lib/api/client-commands/window/getSize.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Get the size of the current window in pixels. * * @example * module.exports = { * 'get current window size': function (browser) { * browser.window.getSize(function (result) { * console.log('Size of current window:', result.value.width, result.value.height); * }); * }, * * 'get current window size using ES6 async/await': async function (browser) { * const {width, height} = await browser.window.getSize(); * console.log('Size of current window:', width, height); * } * } * * @syntax .window.getSize([callback]) * @method window.getSize * @param {function} [callback] Callback function which is called with the result value. * @returns {{width: number, height: number}} Size of the current window. * @see window.getPosition * @see window.getRect * @api protocol.window */ class GetWindowSize extends ClientCommand { performAction(callback) { this.transportActions.getWindowSize(callback); } } module.exports = GetWindowSize; ================================================ FILE: lib/api/client-commands/window/maximize.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Increases the window to the maximum available size without going full-screen. * * @example * module.exports = { * 'maximize current window': function (browser) { * browser.window.maximize(function () { * console.log('window maximized successfully'); * }); * }, * * 'maximize current window using ES6 async/await': async function (browser) { * await browser.window.maximize(); * } * } * * @syntax .window.maximize([callback]) * @method window.maximize * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#maximize-window * @see window.minimize * @see window.fullscreen * @api protocol.window */ class MaximizeWindow extends ClientCommand { performAction(callback) { this.transportActions.maximizeWindow(callback); } } module.exports = MaximizeWindow; ================================================ FILE: lib/api/client-commands/window/minimize.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Hides the window in the system tray. If the window happens to be in fullscreen mode, it is restored the normal state then it will be "iconified" - minimize or hide the window from the visible screen. * * @example * module.exports = { * 'minimize current window': function (browser) { * browser.window.minimize(function () { * console.log('window minimized successfully'); * }); * }, * * 'minimize current window using ES6 async/await': async function (browser) { * await browser.window.minimize(); * } * } * * @syntax .window.minimize([callback]) * @method window.minimize * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#minimize-window * @see window.maximize * @see window.fullscreen * @api protocol.window */ class MinimizeWindow extends ClientCommand { performAction(callback) { this.transportActions.minimizeWindow(callback); } } module.exports = MinimizeWindow; ================================================ FILE: lib/api/client-commands/window/open.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Opens a new tab (default) or a separate new window, and changes focus to the newly opened tab/window. * * This command is only available for W3C Webdriver compatible browsers. * * @example * module.exports = { * 'open a new tab/window': function (browser) { * // open a new tab (default) * browser.window.open(function () { * console.log('new tab opened successfully'); * }); * * // open a new window * browser.window.open('window', function () { * console.log('new window opened successfully'); * }); * }, * * 'open a new tab/window ES6 async demo Test': async function (browser) { * // open a new tab (default) * await browser.window.open(); * * // open a new window * await browser.window.open('window'); * } * } * * @syntax .window.open([callback]) * @syntax .window.open(type, [callback]) * @method window.open * @param {string} [type] Can be either "tab" or "window", with "tab" set as default if none is specified. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#new-window * @see window.close * @see window.switchTo * @api protocol.window */ class OpenNewWindow extends ClientCommand { static get isTraceable() { return true; } static get namespacedAliases() { return 'window.openNew'; } performAction(callback) { const {windowType} = this; this.transportActions.openNewWindow(windowType, callback); } command(type = 'tab', callback) { if (typeof type == 'function' && callback === undefined) { callback = arguments[0]; type = 'tab'; } this.windowType = type; return super.command(callback); } } module.exports = OpenNewWindow; ================================================ FILE: lib/api/client-commands/window/setPosition.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Set the position of the current window - move the window to the chosen position. * * @example * module.exports = { * 'set current window position': function (browser) { * // Move the window to the top left of the primary monitor * browser.window.setPosition(0, 0, function (result) { * console.log('window moved successfully'); * }); * }, * * 'set current window position using ES6 async/await': async function (browser) { * // Move the window to the top left of the primary monitor * await browser.window.setPosition(0, 0); * } * } * * @syntax .window.setPosition(x, y, [callback]) * @method window.setPosition * @param {number} x New x-coordinate of the top-left corner of the window. * @param {number} y New y-coordinate of the top-left corner of the window. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see window.setSize * @see window.setRect * @api protocol.window */ class SetWIndowPosition extends ClientCommand { performAction(callback) { const {xOffset, yOffset} = this; this.transportActions.setWindowPosition(xOffset, yOffset, callback); } command(x, y, callback) { if (typeof x !== 'number' || typeof y !== 'number') { throw new Error('Coordinates passed to .window.getPosition() must be of type number.'); } this.xOffset = x; this.yOffset = y; return super.command(callback); } } module.exports = SetWIndowPosition; ================================================ FILE: lib/api/client-commands/window/setRect.js ================================================ const ClientCommand = require('../_base-command.js'); const Utils = require('../../../utils/index.js'); /** * Change the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect) - size and position of the current window. * * Its JSON representation is the following: * - `x` - window's screenX attribute; * - `y` - window's screenY attribute; * - `width` - outerWidth attribute; * - `height` - outerHeight attribute. * * All attributes are in CSS pixels. * * To change the window rect, you can either specify `width` and `height` together, `x` and `y` together, or all properties together. * * @example * module.exports = { * 'set current window rect': function (browser) { * // Change the screenX and screenY attributes of the window rect. * browser.window.setRect({x: 500, y: 500}); * * // Change the outerWidth and outerHeight attributes of the window rect. * browser.window.setRect({width: 600, height: 300}); * }, * * 'set current window rect using ES6 async/await': async function (browser) { * // Change all attributes of the window rect at once. * await browser.window.setRect({ * width: 600, * height: 300, * x: 100, * y: 100 * }); * } * } * * @syntax .window.setRect({width, height, x, y}, [callback]) * @method window.setRect * @param {Object} options An object specifying either `width` and `height`, `x` and `y`, or all together to set properties for the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#set-window-rect * @see window.setPosition * @see window.setSize * @api protocol.window */ class SetWindowRect extends ClientCommand { performAction(callback) { const {windowOptions} = this; this.transportActions.setWindowRect(windowOptions, callback); } command(options, callback) { const {width, height, x, y} = options; if (!Utils.isUndefined(width) || !Utils.isUndefined(height)) { // either of width and height is defined, so both must be defined if (Utils.isUndefined(width) || Utils.isUndefined(height)) { throw new Error('Attributes "width" and "height" must be specified together when using .window.setRect() command.'); } if (!Utils.isNumber(width)) { throw new Error(`Width argument passed to .window.setRect() must be a number; received: ${typeof width} (${width}).`); } if (!Utils.isNumber(height)) { throw new Error(`Height argument passed to .window.setRect() must be a number; received: ${typeof height} (${height}).`); } } if (!Utils.isUndefined(x) || !Utils.isUndefined(y)) { // either of x and y is defined, so both must be defined if (Utils.isUndefined(x) || Utils.isUndefined(y)) { throw new Error('Attributes "x" and "y" must be specified together when using .window.setRect() command.'); } if (!Utils.isNumber(x)) { throw new Error(`X position argument passed to .window.setRect() must be a number; received: ${typeof x} (${x}).`); } if (!Utils.isNumber(y)) { throw new Error(`Y position argument passed to .window.setRect() must be a number; received: ${typeof y} (${y}).`); } } this.windowOptions = options; return super.command(callback); } } module.exports = SetWindowRect; ================================================ FILE: lib/api/client-commands/window/setSize.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Set the size of the current window in CSS pixels. * * @example * module.exports = { * 'set current window size': function (browser) { * browser.window.setSize(1024, 768, function (result) { * console.log('window resized successfully'); * }); * }, * * 'set current window size using ES6 async/await': async function (browser) { * await browser.window.setSize(1024, 768); * } * } * * @syntax .window.setSize(width, height, [callback]) * @method window.setSize * @param {number} width The new window width in CSS pixels. * @param {number} height The new window height in CSS pixels. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see window.setPosition * @see window.setRect * @api protocol.window */ class SetWindowSize extends ClientCommand { static get namespacedAliases() { return 'window.resize'; } performAction(callback) { const {width, height} = this; this.transportActions.setWindowSize(width, height, callback); } command(width, height, callback) { if (typeof width !== 'number' || typeof height !== 'number') { throw new Error('First two arguments passed to .window.getPosition() must be of type number.'); } this.width = width; this.height = height; return super.command(callback); } } module.exports = SetWindowSize; ================================================ FILE: lib/api/client-commands/window/switchTo.js ================================================ const ClientCommand = require('../_base-command.js'); /** * Change focus to another window. * * The window to change focus to must be specified by its server assigned window handle. To find out the window handle, use `window.getAllHandles()` command. * * @example * module.exports = { * 'switch to another window': function (browser) { * browser * .navigateTo('https://nightwatchjs.org/__e2e/window/') * .click('#openWindowBttn') * .waitUntil(function () { * // wait until window handle for the new window is available * return new Promise((resolve) => { * browser.window.getAllHandles(function (result) { * resolve(result.value.length === 2); * }); * }); * }) * .perform(async function () { * const originalWindow = await browser.window.getHandle(); * const allWindows = await browser.window.getAllHandles(); * * // loop through to find the new window handle * for (const windowHandle of allWindows) { * if (windowHandle !== originalWindow) { * await browser.window.switchTo(windowHandle); * break; * } * } * * const currentWindow = await browser.window.getHandle(); * browser.assert.notEqual(currentWindow, originalWindow); * }); * }, * * 'switch to another window with ES6 async/await': async function (browser) { * await browser.navigateTo('https://nightwatchjs.org/__e2e/window/'); * await browser.click('#openWindowBttn'); * * // wait until window handle for the new window is available * await browser.waitUntil(async function () { * const windowHandles = await browser.window.getAllHandles(); * * return windowHandles.length === 2; * }); * * const originalWindow = await browser.window.getHandle(); * const allWindows = await browser.window.getAllHandles(); * * // loop through available windows to find the new window handle * for (const windowHandle of allWindows) { * if (windowHandle !== originalWindow) { * await browser.window.switchTo(windowHandle); * break; * } * } * * const currentWindow = await browser.window.getHandle(); * await browser.assert.notEqual(currentWindow, originalWindow); * } * } * * @syntax .window.switchTo(windowHandle, [callback]) * @method window.switchTo * @param {string} windowHandle The server assigned window handle, should be one of the strings that was returned in a call to `.window.getAllHandles()`. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see window.getHandle * @see window.getAllHandles * @link /#switch-to-window * @api protocol.window */ class SwitchToWindow extends ClientCommand { static get isTraceable() { return true; } static get namespacedAliases() { return 'window.switch'; } performAction(callback) { const {windowHandle} = this; this.transportActions.switchToWindow(windowHandle, callback); } command(windowHandle, callback) { if (typeof windowHandle !== 'string') { throw new Error(`windowHandle argument passed to .window.switchTo() must be a string; received: ${typeof windowHandle} (${windowHandle}).`); } this.windowHandle = windowHandle; return super.command(callback); } } module.exports = SwitchToWindow; ================================================ FILE: lib/api/client-commands/within.js ================================================ /** * Returns a context for the given element which can be used for querying child elements. * * @example * describe('example using within()', function() { * * it('click a button within the element', async function(browser) { * const context = browser.within('#weblogin'); * await context.click('button'); * }); * }) * * @method within * @syntax browser.within('#element').click('button'); * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @api protocol.elements * @since 2.5.5 */ module.exports = class WithinAbstract { static get allowOverride() { return true; } }; ================================================ FILE: lib/api/element-commands/_baseElementCommand.js ================================================ const {WebElement} = require('selenium-webdriver'); const ElementCommand = require('../../element').Command; const Utils = require('../../utils'); const {Logger, filterStackTrace} = Utils; class BaseElementCommand extends ElementCommand { get w3c_deprecated() { return false; } static getErrorMessage(response = {}) { const {value, error = ''} = response; if (value && value.error) { return value.error; } return error; } get extraArgsCount() { return 0; } get retryOnFailure() { return true; } get elementProtocolAction() { return null; } static get isTraceable() { return false; } setOptionsFromSelector() { this.abortOnFailure = this.api.globals.abortOnElementLocateError; super.setOptionsFromSelector(); } async findElementAction({cacheElementId = true} = {}) { if (WebElement.isId(this.selector)) { return { value: this.selector, status: 0, result: {} }; } if ((this.selector instanceof Promise) && this.selector['@nightwatch_element']) { this.__element = await this.selector; } return this.findElement({cacheElementId}); } setupActions() { const isResultStale = (response) => { const result = this.transport.isRetryableElementError(response); return result; }; const validate = (result) => this.isResultSuccess(result); const successHandler = (result) => this.complete(null, result); this.elementCommandRetries = 0; this.maxElementCommandRetries = this.settings.element_command_retries; this.executor .queueAction({ action: (opts) => this.findElementAction(opts), retryOnSuccess: this.retryOnSuccess, shouldRetryOnError: (response) => { return !this.transport.invalidWindowReference(response.result || response); }, validate, errorHandler: err => { const result = err.response || {}; if (this.suppressNotFoundErrors) { return this.complete(null, result); } let error; if (result.error && result.error.name === 'NoSuchElementError') { error = result.error; } else { error = this.noSuchElementError(err); error.response = result; } return this.elementLocateError(error); } }) .queueAction({ action: (response) => this.elementFound(response), retryOnSuccess: this.retryOnValidActionResult, retryOnFailure: this.retryOnFailure, validate: (result) => this.transport.isResultSuccess(result), isResultStale, successHandler, errorHandler: (err) => this.handleElementError(err) }); } elementNotFound(err) { return this.handleElementError(err); } shouldRetryElementCommand(result) { if (this.transport.isRetryableElementError(result) && this.elementCommandRetries < this.maxElementCommandRetries) { return true; } return false; } async protocolAction() { if (!this.elementProtocolAction) { throw new Error('Define elementProtocolAction.'); } const result = await this.executeProtocolAction(this.elementProtocolAction, this.args); this.elementCommandRetries++; // if (this.shouldRetryElementCommand(result)) { // this.__retryOnFailure = true; // this.elementCommandRetries++; // } else { // //this.__retryOnFailure = false; // } if (result && result.error instanceof Error) { this.transport.registerLastError(result.error, this.elementCommandRetries); } return result; } handleElementError(err) { const showRegisterError = this.transport.shouldRegisterError(err); let originalErrorMessage; if (err instanceof Error) { originalErrorMessage = err.message; } else { originalErrorMessage = BaseElementCommand.getErrorMessage(err.response); } err.message = `An error occurred while running .${this.commandName}() command on <${this.element.toString()}>: ${originalErrorMessage}`; if (err.response) { err.detailedErr = JSON.stringify(err.response); } err.stack = filterStackTrace(this.stackTrace); if (showRegisterError) { Logger.error(err); this.reporter.registerTestError(err); err.registered = true; } const {message, stack} = err; const callbackResult = { status: -1, value: { error: message, message, stack } }; if (this.abortOnFailure) { return this.complete(err, err.response || {}); } return this.complete(null, callbackResult); } } module.exports = BaseElementCommand; ================================================ FILE: lib/api/element-commands/_waitFor.js ================================================ const Utils = require('../../utils'); const {AssertionRunner} = require('../../assertion'); const {LocateStrategy, Command: ElementCommand} = require('../../element'); /*! * Base class for waitForElement commands. It provides a command * method and element* methods to be overridden by subclasses * * @constructor */ class WaitForElement extends ElementCommand { get retryOnSuccess() { return false; } static get rejectNodeOnAbortFailure() { return true; } static get isTraceable() { return true; } static isElementVisible(result) { return result.value === true; } constructor(opts) { super(opts); this.expectedValue = 'found'; } validateArgsCount() { if (LocateStrategy.isValid(this.args[0]) && (Utils.isString(this.args[1]) || ElementCommand.isSelectorObject(this.args[1]))) { this.setStrategyFromArgs(); } } setupActions() { const validate = (result) => this.isResultSuccess(result); const successHandler = (result) => this.elementFound(result); this.executor .queueAction({ action: () => this.findElement({cacheElementId: false}), retryOnSuccess: this.retryOnSuccess, retryOnFailure: !this.retryOnSuccess, validate, successHandler, errorHandler: (err) => { if (err.name !== 'TimeoutError') { throw err; } const {response} = err; if (response && response.error instanceof Error) { if (response.error.name === 'NoSuchElementError') { return this.elementNotFound(response); } this.reporter.registerTestError(response.error); return this.fail(response, 'error while locating the element', this.expectedValue, 'Timed out while waiting for element <%s> for %d milliseconds'); } if (response && response.value) { return this.elementFound(response); } return this.elementNotFound(response); } }); } /*! * The public command function which will be called by the test runner. Arguments can be passed in a variety of ways. * * The custom message always is last and the callback is always before the message or last if a message is not passed. * * The second argument is always the time in milliseconds. The third argument can be either of: * - abortOnFailure: this can overwrite the default behaviour of aborting the test if the condition is not met within the specified time * - rescheduleInterval: this can overwrite the default polling interval (currently 500ms) * The above can be supplied also together, in which case the rescheduleInterval is specified before the abortOnFailure. * * Some of the multiple usage possibilities: * --------------------------------------------------------------------------- * - with no arguments; in this case a global default timeout is used * waitForElement('body'); * * - with a global default timeout and a callback * waitForElement('body', function() {}); * * - with a global default timeout, a callback, and a custom message * waitForElement('body', function() {}, 'test message'); * * - with a global default timeout a custom message * waitForElement('body', 'test message'); * * - with only the timeout * waitForElement('body', 500); * * - with a timeout and a custom message * waitForElement('body', 500, 'test message); * * - with a timeout and a callback * waitForElement('body', 500, function() { .. }); * * - with a timeout and a custom abortOnFailure * waitForElement('body', 500, true); * * - with a timeout, a custom abortOnFailure, and a custom message * waitForElement('body', 500, true, 'test message'); * * - with a timeout, a custom abortOnFailure, and a callback * waitForElement('body', 500, true, function() { .. }); * * - with a timeout, a custom abortOnFailure, a callback and a custom message * waitForElement('body', 500, true, function() { .. }, 'test message'); * * - with a timeout, a custom reschedule interval, and a callback * waitForElement('body', 500, 100, function() { .. }); * * - with a timeout, a custom rescheduleInterval, and a custom abortOnFailure * waitForElement('body', 500, 100, false); * * * @param {string} selector * @param {number|function|string} milliseconds * @param {function|boolean|string|number} callbackOrAbort * @returns {WaitForElement} */ setArguments() { super.setArguments(); let rescheduleInterval; //////////////////////////////////////////////////////////////////////////// // custom timeout value // // waitForElement('body', 100); //////////////////////////////////////////////////////////////////////////// if (Utils.isNumber(this.args[0])) { this.setMilliseconds(this.args[0]); } //////////////////////////////////////////////// // DEPRECATED // backwards compatibility //////////////////////////////////////////////// if (Utils.isBoolean(this.args[1])) { //////////////////////////////////////////////// // The command was called with a custom abortOnFailure: // // waitForElement('body', 500, false); //////////////////////////////////////////////// this.abortOnFailure = this.args[1]; // eslint-disable-next-line brace-style } //////////////////////////////////////////////// // The command was called with a custom timeout and rescheduleInterval: // // waitForElement('body', 500, 100); //////////////////////////////////////////////// else if (Utils.isNumber(this.args[1])) { rescheduleInterval = this.args[1]; //////////////////////////////////////////////// // The command was called with a custom timeout, rescheduleInterval, and custom abortOnFailure: // // waitForElement('body', 500, 100, false); // waitForElement('body', 500, 100, false, function() {}); //////////////////////////////////////////////// if (Utils.isBoolean(this.args[2])) { this.abortOnFailure = this.args[2]; } } if (rescheduleInterval) { this.setRescheduleInterval(rescheduleInterval); } } pass(result, defaultMsg, timeMs) { this.message = this.formatMessage(defaultMsg, timeMs); return this.assert({ result, passed: true, err: { expected: this.expectedValue } }); } fail(result, actual, expected, defaultMsg) { this.message = this.formatMessage(defaultMsg); return this.assert({ result, passed: false, err: { actual, expected } }); } assert({result, passed, err}) { this.elapsedTime = this.executor.elapsedTime; const {reporter} = this.client; const {elapsedTime, message, abortOnFailure, stackTrace} = this; const runner = new AssertionRunner({ passed, err, message, abortOnFailure, stackTrace, reporter, elapsedTime }); return runner.run(result) .catch(err => (err)) .then(err => { if (Utils.isObject(result.value) && !Array.isArray(result.value)) { result.value = [result.value]; } if (err instanceof Error) { err.abortOnFailure = this.abortOnFailure; err.waitFor = true; return this.complete(err, result); } return this.complete(null, result); }); } /** * @param {string} defaultMsg * @param {number} [timeMs] * @returns {string} */ formatMessage(defaultMsg, timeMs) { return Utils.format(this.message || defaultMsg, this.element.selector, timeMs || this.ms); } elementNotFound(result) { const defaultMsg = 'Timed out while waiting for element <%s> to be present for %d milliseconds.'; return this.fail(result, 'not found', this.expectedValue, defaultMsg); } } module.exports = WaitForElement; ================================================ FILE: lib/api/element-commands/_waitForDisplayed.js ================================================ const WaitForElement = require('./_waitFor.js'); class WaitForDisplayed extends WaitForElement { protocolAction() { return this.executeProtocolAction('isElementDisplayed'); } shouldRetryAction(elementVisible) { throw new Error('Override'); } setupActions() { const isResultStale = (result) => { const errorResponse = this.transport.getErrorResponse(result); return this.transport.staleElementReference(errorResponse); }; const validate = (result) => this.isResultSuccess(result); const successHandler = (result) => this.protocolActionHandler(result); this.executor .queueAction({ action: () => this.findElement({cacheElementId: false}), retryOnSuccess: this.retryOnSuccess, validate, errorHandler: (err) => { if (err.name !== 'TimeoutError') { return err; } return this.elementNotFound(err.response); } }) .queueAction({ action: (response) => this.elementFound(response), retryOnSuccess: (result) => { const elementVisible = WaitForElement.isElementVisible(result); return this.shouldRetryAction(elementVisible); }, validate, isResultStale, successHandler, errorHandler: (err) => { if (err.name !== 'TimeoutError') { return err; } return this.complete(err, {}); } }); } protocolActionHandler(result) { if (WaitForElement.isElementVisible(result)) { return this.elementVisible(result); } return this.elementNotVisible(result); } elementVisible(response) {} elementNotVisible(response) {} } module.exports = WaitForDisplayed; ================================================ FILE: lib/api/element-commands/check.js ================================================ 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; ================================================ FILE: lib/api/element-commands/clearValue.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Clear a textarea or a text input element's value. * * @example * module.exports = { * demoTest(browser) { * browser.clearValue('#login input[type=text]'); * * browser.clearValue('#login input[type=text]', function(result) { * console.log('clearValue result', result); * }); * * // with explicit locate strategy * browser.clearValue('css selector', '#login input[type=text]'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.clearValue({ * selector: '#login input[type=text]', * index: 1, * suppressNotFoundErrors: true * }); * * browser.clearValue({ * selector: '#login input[type=text]', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * } * } * * @method clearValue * @syntax browser.clearValue('<SELECTOR>', function (result) { }]) * @syntax * // using global element() * browser.clearValue(element('<SELECTOR>')) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.elementinteraction * @link /#dfn-element-clear */ class ClearValue extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'clearElementValue'; } static get isTraceable() { return true; } } module.exports = ClearValue; ================================================ FILE: lib/api/element-commands/click.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Simulates a click event on the given DOM element. The element is scrolled into view if it is not already pointer-interactable. See the WebDriver specification for element interactability. * * @example * module.exports = { * demoTest(browser) { * browser.click('#main ul li a.first'); * * browser.click('#main ul li a.first', function(result) { * console.log('Click result', result); * }); * * // with explicit locate strategy * browser.click('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.click({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.click({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.click('#main ul li a.first'); * console.log('Click result', result); * } * } * * @method click * @syntax .click(selector, [callback]) * @syntax .click(using, selector, [callback]) * @syntax browser.element(selector).click() * @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 * @link /#dfn-element-click */ class ClickElement extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'clickElement'; } static get isTraceable() { return true; } async protocolAction() { if (this.api.isSafari() && this.api.isIOS()) { return this.executeProtocolAction('clickElementWithJS'); } return this.executeProtocolAction(this.elementProtocolAction); } } module.exports = ClickElement; ================================================ FILE: lib/api/element-commands/clickAndHold.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Move to the element and click (without releasing) in the middle of the given element. * * @example * module.exports = { * demoTest() { * browser.clickAndHold('#main ul li a.first'); * * browser.clickAndHold('#main ul li a.first', function(result) { * console.log('Click result', result); * }); * * // with explicit locate strategy * browser.clickAndHold('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.clickAndHold({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.clickAndHold({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function() { * const result = await browser.clickAndHold('#main ul li a.first'); * console.log('Right click result', result); * } * } * * @method clickAndHold * @syntax .clickAndHold(selector, [callback]) * @syntax .clickAndHold(using, selector, [callback]) * @syntax browser.element(selector).clickAndHold() * @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 * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#click-and-hold * @since 2.0.0 */ class ClickAndHold extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'pressAndHold'; } } module.exports = ClickAndHold; ================================================ FILE: lib/api/element-commands/doubleClick.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Move to the element and peforms a double-click in the middle of the element. * * @example * module.exports = { * demoTest() { * browser.doubleClick('#main ul li a.first'); * * browser.doubleClick('#main ul li a.first', function(result) { * console.log('double click result', result); * }); * * // with explicit locate strategy * browser.doubleClick('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.doubleClick({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.doubleClick({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function() { * const result = await browser.doubleClick('#main ul li a.first'); * console.log('double click result', result); * } * } * * @method doubleClick * @syntax .doubleClick(selector, [callback]) * @syntax .doubleClick(using, selector, [callback]) * @syntax browser.element(selector).doubleClick() * @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 * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#double-click */ class doubleClick extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'doubleClick'; } } module.exports = doubleClick; ================================================ FILE: lib/api/element-commands/dragAndDrop.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Drag an element to the given position or destination element. * * @example * module.exports = { * demoTest(browser) { * browser.dragAndDrop('#main', {x: 100, y:100}) * } * * // using an Element ID as a destination * demoTestAsync: async function(browser) { * const destination = await browser.findElement('#upload'); * browser.dragAndDrop('#main', destination.getId()); * } * } * * @method dragAndDrop * @syntax .dragAndDrop(selector, destination, callback) * @syntax .dragAndDrop(using, selector, destination, callback) * @syntax browser.element(selector).dragAndDrop(coordinates) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://w3c.github.io/webdriver/#capabilities) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](/guide/using-nightwatch/finding-and-interacting-with-elements.html#element-properties). * @param {string} destination Either another element to drag to (will drag to the center of the element), or an {x, y} object specifying the offset to drag by, in pixels. * @param {function} callback Callback function which is called with the result value; not required if using `await` operator. * @returns {*} null */ class DragElement extends BaseElementCommand { get extraArgsCount() { return 1; } get elementProtocolAction() { return 'dragElement'; } } module.exports = DragElement; ================================================ FILE: lib/api/element-commands/findElement.js ================================================ /** * Search for an elements on the page, starting from the document root. The located element will be returned as web element JSON object (with an added .getId() convenience method). * First argument is the element selector, either specified as a string or as an object (with 'selector' and 'locateStrategy' properties). * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElement = await browser.findElement('.features-container li:first-child'); * * console.log('Element Id:', resultElement.getId()); * }, * * * @link /#find-element * @syntax browser.findElement(selector, callback) * @syntax await browser.findElement(selector); * @param {string} selector The search target. * @param {function} [callback] Callback function to be invoked with the result when the command finishes. * @since 1.7.0 * @api protocol.elements */ const FindElements = require('./findElements.js'); module.exports = class FindElement extends FindElements { async elementFound(response) { if (response && response.value) { const elementId = this.transport.getElementId(response.value); response.value = Object.assign(response.value, { get getId() { return function() { return elementId; }; } }); } return response; } findElementAction() { return this.findElement(); } }; ================================================ FILE: lib/api/element-commands/findElements.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Search for multiple elements on the page, starting from the document root. The located elements will be returned as web element JSON objects (with an added .getId() convenience method). * First argument is the element selector, either specified as a string or as an object (with 'selector' and 'locateStrategy' properties). * * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElements = await browser.findElements('.features-container li'); * * resultElements.forEach(item => console.log('Element Id:', item.getId())); * }, * * * @link /#find-elements * @syntax browser.findElements(selector, callback) * @syntax await browser.findElements(selector); * @param {string} selector The search target. * @param {function} [callback] Callback function to be invoked with the result when the command finishes. * @since 1.7.0 * @api protocol.elements */ module.exports = class Elements extends BaseElementCommand { async elementFound(response) { if (response && Array.isArray(response.value)) { response.value = response.value.map(entry => { const elementId = this.transport.getElementId(entry); return Object.assign(entry, { get getId() { return function() { return elementId; }; } }); }); } return response; } findElementAction() { return this.findElement({returnSingleElement: false}); } async complete(err, response) { const result = await super.complete(err, response); if (result instanceof Error) { return result; } return result; } }; ================================================ FILE: lib/api/element-commands/getAccessibleName.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the computed WAI-ARIA label of an element. * * @example * module.exports = { * demoTest(browser) { * browser.getAccessibleName('*[name="search"]', function(result) { * this.assert.equal(typeof result, 'object); * this.assert.equal(result.value, 'search input'); * }); * * // with explicit locate strategy * browser.getAccessibleName('css selector', '*[name="search"]', function(result) { * console.log('getAccessibleName result', result.value); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getAccessibleName({ * selector: '*[name="search"]', * index: 1 * }, function(result) { * console.log('getAccessibleName result', result.value); * }); * * browser.getAccessibleName({ * selector: '*[name="search"]', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }, function(result) { * console.log('getAccessibleName result', result.value); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getAccessibleName('*[name="search"]'); * console.log('getAccessibleName result', result); * } * } * * @method getAccessibleName * @syntax .getAccessibleName(selector, callback) * @syntax .getAccessibleName(using, selector, callback) * @syntax browser.element(selector).getAccessibleName() * @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 Callback function which is called with the result value. * @returns {string} The computed WAI-ARIA label of element. * @link /#dfn-get-computed-label * @api protocol.elementstate */ class GetAccessibleName extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementAccessibleName'; } } module.exports = GetAccessibleName; ================================================ FILE: lib/api/element-commands/getAriaRole.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the computed WAI-ARIA role of an element. * * @example * module.exports = { * demoTest(browser) { * browser.getAriaRole('*[name="search"]', function(result) { * this.assert.equal(typeof result, 'object'); * this.assert.equal(result.value, 'combobox'); * }); * * // with explicit locate strategy * browser.getAriaRole('css selector', '*[name="search"]', function(result) { * console.log('getAriaRole result', result.value); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getAriaRole({ * selector: '*[name="search"]', * index: 1 * }, function(result) { * console.log('getAriaRole result', result.value); * }); * * browser.getAriaRole({ * selector: '*[name="search"]', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }, function(result) { * console.log('getAriaRole result', result.value); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getAriaRole('*[name="search"]'); * console.log('getAriaRole result', result); * } * } * * @method getAriaRole * @syntax .getAriaRole(selector, callback) * @syntax .getAriaRole(using, selector, callback) * @syntax browser.element(selector).getAriaRole() * @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 Callback function which is called with the result value. * @returns {string} The computed WAI-ARIA role of element. * @link /#dfn-get-computed-role * @api protocol.elementstate */ class GetAriaRole extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementAriaRole'; } } module.exports = GetAriaRole; ================================================ FILE: lib/api/element-commands/getAttribute.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Retrieve the value of an attribute for a given DOM element. * * @example * module.exports = { * demoTest(browser) { * browser.getAttribute('#main ul li a.first', 'href', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getAttribute('css selector', '#main ul li a.first', 'href', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getAttribute({ * selector: '#main ul li a.first', * index: 1, * suppressNotFoundErrors: true * }, 'href', function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getAttribute('#main ul li a.first', 'href'); * console.log('attribute', result); * } * } * * @method getAttribute * @syntax .getAttribute(selector, attribute, callback) * @syntax .getAttribute(using, selector, attribute, callback) * @syntax browser.element(selector).getAttribute(name) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} attribute The attribute name to inspect. * @param {function} callback Callback function which is called with the result value; not required if using `await` operator. * @returns {*} The value of the attribute * @api protocol.elementstate * @link /#dfn-get-element-attribute */ class GetAttribute extends BaseElementCommand { static get namespacedAliases() { return 'getElementAttribute'; } get extraArgsCount() { return 1; } get elementProtocolAction() { return 'getElementAttribute'; } } module.exports = GetAttribute; ================================================ FILE: lib/api/element-commands/getCssProperty.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Retrieve the value of a css property for a given DOM element. * * @example * module.exports = { * demoTest(browser) { * browser.getCssProperty('#main ul li a.first', 'display', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getCssProperty('css selector', '#main ul li a.first', 'display', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getCssProperty({ * selector: '#main ul li a.first', * index: 1, * suppressNotFoundErrors: true * }, 'display', function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getCssProperty('#main ul li a.first', 'display'); * console.log('display', result); * } * } * * @method getCssProperty * @syntax .getCssProperty(selector, cssProperty, callback) * @syntax .getCssProperty(using, selector, cssProperty, callback) * @syntax browser.element(selector).getCssProperty(name) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} cssProperty The CSS property to inspect. * @param {function} callback Callback function which is called with the result value; not required if using `await` operator. * @returns {*} The value of the css property * @api protocol.elementstate * @link /#get-element-css-value */ class GetCssProperty extends BaseElementCommand { get extraArgsCount() { return 1; } get elementProtocolAction() { return 'getElementCSSValue'; } } module.exports = GetCssProperty; ================================================ FILE: lib/api/element-commands/getElementProperty.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Retrieve the value of a specified DOM property for the given element. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * @example * module.exports = { * demoTest(browser) { * browser.getElementProperty('#login input[type=text]', 'classList', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getElementProperty('css selector', '#login input[type=text]', 'classList', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getElementProperty({ * selector: '#login input[type=text]', * index: 1, * suppressNotFoundErrors: true * }, 'classList', function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getElementProperty('#login input[type=text]', 'classList'); * console.log('classList', result); * } * } * * @method getElementProperty * @syntax .getElementProperty(selector, property, callback) * @syntax .getElementProperty(using, selector, property, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} property The property to inspect. * @param {function} callback Callback function which is called with the result value; not required if using `await` operator. * @returns {*} The value of the property * @api protocol.elementstate * @link /#get-element-property */ class GetElementProperty extends BaseElementCommand { get extraArgsCount() { return 1; } get elementProtocolAction() { return 'getElementProperty'; } } module.exports = GetElementProperty; ================================================ FILE: lib/api/element-commands/getElementRect.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determine an element's size in pixels. For W3C Webdriver compatible clients (such as GeckoDriver), this command is equivalent to `getLocation` and both return * the dimensions and coordinates of the given element: * - x: X axis position of the top-left corner of the element, in CSS pixels * - y: Y axis position of the top-left corner of the element, in CSS pixels * - height: Height of the element’s bounding rectangle in CSS pixels; * - width: Width of the web element’s bounding rectangle in CSS pixels. * * @example * module.exports = { * demoTest(browser) { * browser.getElementRect('#login', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getElementRect('css selector', '#login', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getElementRect({ * selector: '#login', * index: 1, * suppressNotFoundErrors: true * }, function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getElementRect('#login'); * console.log('classList', result); * } * } * * @method getElementRect * @syntax .getElementRect(selector, callback) * @syntax .getElementRect(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {{width: number, height: number}} The width and height of the element in pixels * @link /#dfn-get-element-rect * @api protocol.elementstate */ class GetElementRect extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementRect'; } } module.exports = GetElementRect; ================================================ FILE: lib/api/element-commands/getElementSize.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determine an element's size in pixels. For W3C Webdriver compatible clients (such as GeckoDriver), this command is equivalent to `getLocation` and both return * the dimensions and coordinates of the given element: * - x: X axis position of the top-left corner of the element, in CSS pixels * - y: Y axis position of the top-left corner of the element, in CSS pixels * - height: Height of the element’s bounding rectangle in CSS pixels; * - width: Width of the web element’s bounding rectangle in CSS pixels. * * @example * module.exports = { * demoTest(browser) { * browser.getElementSize('#login', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getElementSize('css selector', '#login', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getElementSize({ * selector: '#login', * index: 1, * suppressNotFoundErrors: true * }, function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getElementSize('#login'); * console.log('classList', result); * } * } * * @method getElementSize * @syntax .getElementSize(selector, callback) * @syntax .getElementSize(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {{width: number, height: number}} The width and height of the element in pixels * @link /#dfn-get-element-rect * @api protocol.elementstate */ class GetElementSize extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementRect'; } } module.exports = GetElementSize; ================================================ FILE: lib/api/element-commands/getFirstElementChild.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns an element's first child. The child element will be returned as web element JSON object (with an added .getId() convenience method). * * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElement = await browser.getFirstElementChild('.features-container'); * * console.log('last child element Id:', resultElement.getId()); * }, * * @syntax browser.getFirstElementChild(selector, callback) * @syntax browser.getFirstElementChild(selector) * @syntax browser.element(selector).getFirstElementChild() * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method getFirstElementChild * @since 2.0.0 * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild * @api protocol.elements */ class GetFirstElementChild extends BaseElementCommand { get extraArgsCount() { return 0; } async protocolAction() { const result = await this.executeProtocolAction('getFirstElementChild'); return result; } } module.exports = GetFirstElementChild; ================================================ FILE: lib/api/element-commands/getLastElementChild.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns an element's last child. The child element will be returned as web element JSON object (with an added .getId() convenience method). * * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElement = await browser.getLastElementChild('.features-container'); * * console.log('last child element Id:', resultElement.getId()); * }, * * @syntax browser.getLastElementChild(selector, callback) * @syntax browser.getLastElementChild(selector) * @syntax browser.element(selector).getLastElementChild() * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method getLastElementChild * @since 2.0.0 * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/lastElementChild * @api protocol.elements */ class GetLastElementChild extends BaseElementCommand { get extraArgsCount() { return 0; } async protocolAction() { return this.executeProtocolAction('getLastElementChild'); } } module.exports = GetLastElementChild; ================================================ FILE: lib/api/element-commands/getLocation.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determine an element's location on the page. The point (0, 0) refers to the upper-left corner of the page. The element's coordinates are returned as a JSON object with x and y properties. * * For W3C Webdriver compatible clients (such as GeckoDriver), this command is equivalent to `getElementSize` and both return * the dimensions and coordinates of the given element: * - x: X axis position of the top-left corner of the element, in CSS pixels * - y: Y axis position of the top-left corner of the element, in CSS pixels * - height: Height of the element’s bounding rectangle in CSS pixels; * - width: Width of the web element’s bounding rectangle in CSS pixels. * * @example * module.exports = { * demoTest(browser) { * browser.getLocation('#login', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getLocation('css selector', '#login', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getLocation({ * selector: '#login', * index: 1, * suppressNotFoundErrors: true * }, function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getLocation('#login'); * console.log('location', result); * } * } * * @method getLocation * @syntax .getLocation(selector, callback) * @syntax .getLocation(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {{x:number, y:number}} The X and Y coordinates for the element on the page. * @link /#dfn-get-element-rect * @api protocol.elementlocation */ class GetLocation extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementRect'; } } module.exports = GetLocation; ================================================ FILE: lib/api/element-commands/getLocationInView.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determine an element's location on the screen once it has been scrolled into view. * * @example * this.demoTest = function (browser) { * browser.getLocationInView("#main ul li a.first", function(result) { * this.assert.equal(typeof result, "object"); * this.assert.equal(result.status, 0); * this.assert.equal(result.value.x, 200); * this.assert.equal(result.value.y, 200); * }); * }; * * * @method getLocationInView * @syntax .getLocationInView(selector, callback) * @syntax .getLocationInView(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {{x: number, y: number}} The X and Y coordinates for the element on the page. * @jsonwire * @api protocol.elementlocation * @deprecated This is a JSON Wire Protocol command and is no longer supported. */ class GetLocationInView extends BaseElementCommand { get w3c_deprecated() { return true; } get extraArgsCount() { return 0; } get elementProtocolAction() { return 'isElementLocationInView'; } } module.exports = GetLocationInView; ================================================ FILE: lib/api/element-commands/getNextSibling.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the element immediately following the specified one in their parent's childNodes. The element will be returned as web element JSON object (with an added .getId() convenience method). * * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElement = await browser.getNextSibling('.features-container li:first-child'); * * console.log('next sibling element Id:', resultElement.getId()); * }, * * @syntax browser.getNextSibling(selector, callback) * @syntax browser.getNextSibling(selector) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method getNextSibling * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling * @since 2.0.0 * @api protocol.elements * @exampleLink /api/getNextSibling.js */ class GetNextSibling extends BaseElementCommand { get extraArgsCount() { return 0; } async protocolAction() { return await this.executeProtocolAction('getNextSibling'); } } module.exports = GetNextSibling; ================================================ FILE: lib/api/element-commands/getPreviousSibling.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the element immediately preceding the specified one in its parent's child elements list. The element will be returned as web element JSON object (with an added `.getId()` convenience method). * * * @example * module.exports = { * 'demo Test': function(browser) { * const resultElement = await browser.getPreviousSibling('.features-container li:second-child'); * * console.log('previous sibling element Id:', resultElement.getId()); * }, * * @syntax browser.getPreviousSibling('#web-button', function(result) { * * console.log(result.value) * }}) * await browser.getPreviousSibling('#web-button') * await browser.getPreviousSibling({selector: '#web-button', locateStrategy: 'css selector'}) * * @syntax * // with global element(): * const formEl = element('form'); * const result = await browser.getPreviousSibling(formEl) * * @syntax * // with Selenium By() locators * // https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_By.html * const locator = by.tagName('form'); * const result = await browser.getPreviousSibling(locator) * * @syntax * // with browser.findElement() * const formEl = await browser.findElement('form'); * const result = await browser.getPreviousSibling(formEl) * * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method getPreviousSibling * @returns {object} The resolved element object, which contains a convenience `.getId()` method that can be used to retrieve the element ID * @since 2.0.0 * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/previousElementSibling * @api protocol.elements */ class GetPreviousSibling extends BaseElementCommand { get extraArgsCount() { return 0; } async protocolAction() { return this.executeProtocolAction('getPreviousSibling'); } } module.exports = GetPreviousSibling; ================================================ FILE: lib/api/element-commands/getShadowRoot.js ================================================ const {ShadowRoot} = require('selenium-webdriver/lib/webdriver'); const {WEB_ELEMENT_ID} = require('../../transport/selenium-webdriver/session.js'); const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the `shadowRoot` read-only property which represents the shadow root hosted by the element. This can further be used to retrieve elements part of the shadow root element. * * @example * describe('Shadow Root example test', function() { * it('retrieve the shadowRoot', async function(browser) { * await browser * .navigateTo('https://mdn.github.io/web-components-examples/popup-info-box-web-component/') * .waitForElementVisible('form'); * * const shadowRootEl = await browser.getShadowRoot('popup-info'); * const infoElement = await shadowRootEl.find('.info'); * * await expect(infoElement.property('innerHTML')).to.include('card validation code'); * const iconElement = await shadowRootEl.find('.icon'); * const firstElement = await browser.getFirstElementChild(iconElement); * * await expect.element(firstElement).to.be.an('img'); * }); * }); * * @syntax browser.getShadowRoot(selector, callback) * @syntax browser.getShadowRoot(selector) * @syntax browser.element(selector).getShadowRoot() * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object|WebElement|By} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method getShadowRoot * @since 2.0.0 * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot * @api protocol.elements */ class ElementCommand extends BaseElementCommand { async protocolAction() { const result = await this.executeProtocolAction('getShadowRoot'); if (result instanceof ShadowRoot) { // treat the ShadowRoot as WebElement, so that the WebElement // methods continue to be available on shadow root. return this.api.createElement({[WEB_ELEMENT_ID]: result.getId()}); } return result; } } module.exports = ElementCommand; ================================================ FILE: lib/api/element-commands/getTagName.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Query for an element's tag name. * * @example * module.exports = { * demoTest(browser) { * browser.getTagName('#login', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getTagName('css selector', '#login', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getTagName({ * selector: '#login', * index: 1, * suppressNotFoundErrors: true * }, function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getTagName('#login'); * console.log('tagName', result); * } * } * * @method getTagName * @syntax .getTagName(selector, callback) * @syntax .getTagName(using, selector, callback) * @syntax browser.element(selector).getTagName() * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {number} The element's tag name, as a lowercase string. * @link /#dfn-get-element-tag-name * @api protocol.elementstate */ class GetTagName extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementTagName'; } } module.exports = GetTagName; ================================================ FILE: lib/api/element-commands/getText.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns the visible text for the element. * * @example * module.exports = { * demoTest(browser) { * browser.getText('#main ul li a.first', function(result) { * this.assert.equal(typeof result, 'object); * this.assert.strictEqual(result.status, 0); // only when using Selenium / JSONWire * this.assert.equal(result.value, 'nightwatchjs.org'); * }); * * // with explicit locate strategy * browser.getText('css selector', '#main ul li a.first', function(result) { * console.log('getText result', result.value); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getText({ * selector: '#main ul li a', * index: 1 * }, function(result) { * console.log('getText result', result.value); * }); * * browser.getText({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }, function(result) { * console.log('getText result', result.value); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getText('#main ul li a.first'); * console.log('getText result', result); * } * } * * @method getText * @syntax .getText(selector, callback) * @syntax .getText(using, selector, callback) * @syntax browser.element(selector).getText() * @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 Callback function which is called with the result value. * @returns {string} The element's visible text. * @link /#dfn-get-element-text * @api protocol.elementstate */ class GetText extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'getElementText'; } } module.exports = GetText; ================================================ FILE: lib/api/element-commands/getValue.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns a form element current value. * * @example * module.exports = { * demoTest(browser) { * browser.getValue('#login input[type=text]', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.getValue('css selector', '#login input[type=text]', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.getValue({ * selector: '#login input[type=text]', * index: 1, * suppressNotFoundErrors: true * }, function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.getValue('#login input[type=text]'); * console.log('Value', result); * } * } * * @method getValue * @syntax .getValue(selector, callback) * @syntax browser.element(selector).getValue() * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @returns {string} The element's value. * @link /#get-element-property * @api protocol.elementstate */ class GetValue extends BaseElementCommand { get extraArgsCount() { return 0; } protocolAction() { return this.executeProtocolAction('getElementProperty', ['value']); } } module.exports = GetValue; ================================================ FILE: lib/api/element-commands/hasDescendants.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Returns true or false based on whether the DOM has any child nodes * * @example * module.exports = { * 'demo Test': function(browser) { * const result = await browser.hasDescendants('.features-container'); * * console.log('true or false:', result); * }, * * @syntax browser.hasDescendants(selector, callback) * @syntax browser.hasDescendants(selector) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @method hasDescendants * @api protocol.elementstate * @since 2.0.0 * @moreinfo developer.mozilla.org/en-US/docs/Web/API/Element/childElementCount */ class HasDescendants extends BaseElementCommand { async protocolAction() { return await this.executeProtocolAction('elementHasDescendants'); } } module.exports = HasDescendants; ================================================ FILE: lib/api/element-commands/isEnabled.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determines if an element is enabled, as indicated by the 'disabled' attribute. * * @example * module.exports = { * demoTest(browser) { * browser.isEnabled('#main select option.first', function(result) { * this.assert.equal(typeof result, "object"); * this.assert.equal(result.status, 0); * this.assert.equal(result.value, true); * }); * * // with explicit locate strategy * browser.isEnabled('css selector', '#main select option.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.isEnabled({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.isEnabled({ * selector: '#main select option.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.isEnabled('#main select option.first'); * console.log('isVisible result', result); * } * } * * @method isEnabled * @syntax .isEnabled(selector, callback) * @syntax .isEnabled(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @link /#is-element-enabled * @api protocol.elementstate */ class IsEnabled extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'isElementEnabled'; } } module.exports = IsEnabled; ================================================ FILE: lib/api/element-commands/isPresent.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); const {Logger, filterStackTrace} = require('../../utils'); /** * Determines if an element is present in the DOM. * * @example * module.exports = { * demoTest(browser) { * browser.isPresent('#main ul li a.first', function(result) { * this.assert.equal(typeof result, "object"); * this.assert.equal(result.status, 0); * this.assert.equal(result.value, true); * }); * * // with explicit locate strategy * browser.isPresent('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.isPresent({ * selector: '#main ul li a', * index: 1, * }); * * browser.isPresent({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.isPresent('#main ul li a.first'); * console.log('isPresent result', result); * } * } * * @method isPresent * @syntax .isPresent(selector, callback) * @syntax .isPresent(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @api protocol.elementstate */ class isPresent extends BaseElementCommand { elementLocateError(error) { if (error.response) { error.detailedErr = JSON.stringify(error.response); } error.stack = filterStackTrace(this.stackTrace); Logger.error(error); return this.complete(null, { status: 0, value: false }); } async protocolAction() { const result = { status: 0, value: false }; if (this.webElement || this.elementId) { result.value = true; } return result; } } module.exports = isPresent; ================================================ FILE: lib/api/element-commands/isSelected.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Determines if an element is selected. * * @example * module.exports = { * demoTest(browser) { * browser.isSelected('#main select option.first', function(result) { * this.assert.equal(typeof result, "object"); * this.assert.equal(result.status, 0); * this.assert.equal(result.value, true); * }); * * // with explicit locate strategy * browser.isSelected('css selector', '#main select option.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.isSelected({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.isSelected({ * selector: '#main select option.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.isSelected('#main select option.first'); * console.log('isVisible result', result); * } * } * * @method isSelected * @syntax .isSelected(selector, callback) * @syntax .isSelected(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @link /#is-element-selected * @api protocol.elementstate */ class IsSelected extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'isElementSelected'; } } module.exports = IsSelected; ================================================ FILE: lib/api/element-commands/isVisible.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); const {isUndefined} = require('../../utils'); /** * Determine if an element is currently displayed. * * @example * module.exports = { * demoTest(browser) { * browser.isVisible('#main ul li a.first', function(result) { * this.assert.equal(typeof result, "object"); * this.assert.equal(result.status, 0); * this.assert.equal(result.value, true); * }); * * // with explicit locate strategy * browser.isVisible('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.isVisible({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.isVisible({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function(browser) { * const result = await browser.isVisible('#main ul li a.first'); * console.log('isVisible result', result); * } * } * * @method isVisible * @syntax .isVisible(selector, callback) * @syntax .isVisible(using, selector, callback) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} callback Callback function which is called with the result value. * @link /#element-displayedness * @api protocol.elementstate */ class IsVisible extends BaseElementCommand { get extraArgsCount() { return 0; } protocolAction() { return this.executeProtocolAction('isElementDisplayed'); } } module.exports = IsVisible; ================================================ FILE: lib/api/element-commands/moveToElement.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Move the mouse by an offset of the specified element. If an element is provided but no offset, the mouse will be moved to the center of the element. If the element is not visible, it will be scrolled into view. * * @example * this.demoTest = function (browser) { * browser.moveToElement('#main', 10, 10); * }; * * * @method moveToElement * @syntax .moveToElement(selector, xoffset, yoffset, [callback]) * @syntax .moveToElement(using, selector, xoffset, yoffset, [callback]) * @syntax browser.element(selector).moveTo([x], [y]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {number} xoffset X offset to move to, relative to the center of the element. * @param {number} yoffset Y offset to move to, relative to the center of the element. * @param {function} [callback] Optional callback function to be called when the command finishes. * @jsonwire * @api protocol.elementinteraction */ class MoveToElement extends BaseElementCommand { get extraArgsCount() { return 2; } get elementProtocolAction() { return 'moveTo'; } } module.exports = MoveToElement; ================================================ FILE: lib/api/element-commands/rightClick.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Simulates a context-click(right click) event on the given DOM element. The element is scrolled into view if it is not already pointer-interactable. See the WebDriver specification for element [interactability](https://www.w3.org/TR/webdriver/#element-interactability). * * @example * module.exports = { * demoTest() { * browser.rightClick('#main ul li a.first'); * * browser.rightClick('#main ul li a.first', function(result) { * console.log('Click result', result); * }); * * // with explicit locate strategy * browser.rightClick('css selector', '#main ul li a.first'); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.rightClick({ * selector: '#main ul li a', * index: 1, * suppressNotFoundErrors: true * }); * * browser.rightClick({ * selector: '#main ul li a.first', * timeout: 2000 // overwrite the default timeout (in ms) to check if the element is present * }); * }, * * demoTestAsync: async function() { * const result = await browser.rightClick('#main ul li a.first'); * console.log('Right click result', result); * } * } * * @method rightClick * @syntax .rightClick(selector, [callback]) * @syntax .rightClick(using, selector, [callback]) * @syntax browser.element(selector).rightClick() * @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 * @since 2.0.0 * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#context-click */ class RightClick extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'contextClick'; } } module.exports = RightClick; ================================================ FILE: lib/api/element-commands/sendKeys.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Types a key sequence on the DOM element. Can be used to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * * * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * @example * // send some simple text to an input * this.demoTest = function (browser) { * browser.sendKeys('input[type=text]', 'nightwatch'); * }; * // * // send some text to an input and hit enter. * this.demoTest = function (browser) { * browser.sendKeys('input[type=text]', ['nightwatch', browser.Keys.ENTER]); * }; * * * @link /session/:sessionId/element/:id/value * @method sendKeys * @syntax .sendKeys(selector, inputValue, [callback]) * @syntax browser.element(selector).sendKeys(...keys) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|array} inputValue The text to send to the element or key strokes. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html#sendKeys * @api protocol.elementinteraction */ class SendKeys extends BaseElementCommand { static get isTraceable() { return true; } get extraArgsCount() { return 1; } get elementProtocolAction() { return 'sendKeysToElement'; } } module.exports = SendKeys; ================================================ FILE: lib/api/element-commands/setAttribute.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Set the value of a specified DOM attribute for the given element. For all the available DOM attributes, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * @example * module.exports = { * demoTest(browser) { * browser.setAttribute('#login input[type=text]', 'disabled', 'true', function(result) { * console.log('result', result); * }); * * // with explicit locate strategy * browser.setAttribute('css selector', '#login input[type=text]', 'disabled', 'true', function(result) { * console.log('result', result); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.setAttribute({ * selector: '#login input[type=text]', * index: 1, * suppressNotFoundErrors: true * }, 'disabled', 'true', function(result) { * console.log('result', result); * }); * }, * * demoTestAsync: async function(browser) { * await browser.setAttribute('#login input[type=text]', 'disabled', 'true'); * } * } * * @method setAttribute * @syntax .setAttribute(selector, attribute, value, [callback]) * @syntax .setAttribute(using, selector, attribute, value, [callback]) * @syntax browser.element(selector).setAttribute(name, value) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string} attribute The attribute name to set. * @param {string} value The attribute value name to set. * @param {function} callback Callback function which is called with the result value; not required if using `await` operator. * @api protocol.elementinteraction */ class SetAttribute extends BaseElementCommand { get extraArgsCount() { return 2; } get elementProtocolAction() { return 'setElementAttribute'; } } module.exports = SetAttribute; ================================================ FILE: lib/api/element-commands/setPassword.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * An alias of "setValue" command, but hides the content from the nightwatch logs. * *
setValue/setPassword also clears the existing value of the element by calling the clear() command beforehand.
* * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * @example * // send some simple text to an input * this.demoTest = function (browser) { * browser.setPassword('input[type=text]', 'nightwatch'); * }; * * // send some text to an input and hit enter. * this.demoTest = function (browser) { * browser.setPassword('input[type=text]', ['nightwatch', browser.Keys.ENTER]); * }; * * @method setPassword * @syntax .setPassword(selector, inputValue, [callback]) * @syntax .setPassword(using, selector, inputValue, [callback]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|array} inputValue The text to send to the element or key strokes. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#element-send-keys * @api protocol.elementinteraction */ class SetPassword extends BaseElementCommand { static get namespacedAliases() { return 'sendKeysRedacted'; } static get RedactParams(){ return true; } get extraArgsCount() { return 1; } get elementProtocolAction() { return 'setElementValueRedacted'; } } module.exports = SetPassword; ================================================ FILE: lib/api/element-commands/setValue.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Sends some text to an element. Can be used to set the value of a form element or to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * *
From Nightwatch v2, setValue also clears the existing value of the element by calling the clearValue() beforehand.
* * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * @example * // send some simple text to an input * this.demoTest = function (browser) { * browser.setValue('input[type=text]', 'nightwatch'); * }; * * // send some text to an input and hit enter. * this.demoTest = function (browser) { * browser.setValue('input[type=text]', ['nightwatch', browser.Keys.ENTER]); * }; * * * @link /session/:sessionId/element/:id/value * @method setValue * @syntax .setValue(selector, inputValue, [callback]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|array} inputValue The text to send to the element or key strokes. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#element-send-keys * @api protocol.elementinteraction */ class SetValue extends BaseElementCommand { static get isTraceable() { return true; } get extraArgsCount() { return 1; } get elementProtocolAction() { return 'setElementValue'; } } module.exports = SetValue; ================================================ FILE: lib/api/element-commands/submitForm.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Submits the form containing this element (or this element if it is itself a FORM element). his command is a no-op if the element is not contained in a form. * * @example * this.demoTest = function (browser) { * browser.submitForm('form.login'); * }; * * * @method submitForm * @syntax .submitForm(selector, [callback]) * @syntax .submitForm(using, selector, [callback]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {function} [callback] Optional callback function to be called when the command finishes. * @jsonwire * @api protocol.elementinteraction */ class SubmitForm extends BaseElementCommand { get extraArgsCount() { return 0; } get elementProtocolAction() { return 'elementSubmit'; } } module.exports = SubmitForm; ================================================ FILE: lib/api/element-commands/takeElementScreenshot.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Take a screenshot of the visible region encompassed by this element's bounding rectangle. * * @example * module.exports = { * demoTest(browser) { * browser.takeElementScreenshot('#main', function (imageData, err) { * require('fs').writeFile('out.png', imageData.value, 'base64', function (err) { * console.log(err); * }); * }); * * // with explicit locate strategy * browser.takeElementScreenshot('css selector', '#main', function(imageData, err) { * require('fs').writeFile('out.png', imageData.value, 'base64', function (err) { * console.log(err); * }); * }); * * // with selector object - see https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties * browser.takeElementScreenshot({ * selector: '#main ul li a', * index: 1 * }, function(imageData, err) { * require('fs').writeFile('out.png', imageData.value, 'base64', function (err) { * console.log(err); * }); * }); * }, * * demoTestAsync: async function(browser) { * const data = await browser.takeElementScreenshot('#main'); * require('fs').writeFile('out.png', data, 'base64'); * } * } * * @method takeElementScreenshot * @syntax .takeElementScreenshot(selector, callback) * @syntax .takeElementScreenshot(using, selector, callback) * @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 Callback function which is called with the result value. * @returns {string} Take a screenshot of the visible region encompassed by this element's bounding rectangle. * @link /#dfn-take-element-screenshot * @api protocol.screens * @since 2.0.0 */ class TakeElementScreenshot extends BaseElementCommand { get elementProtocolAction() { return 'takeElementScreenshot'; } } module.exports = TakeElementScreenshot; ================================================ FILE: lib/api/element-commands/uncheck.js ================================================ 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; ================================================ FILE: lib/api/element-commands/updateValue.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Sends some text to an element. Can be used to set the value of a form element or to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * *
updateValue is equivalent with setValue in that it also clears the value beforehand.
* * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * @example * // send some simple text to an input * this.demoTest = function (browser) { * browser.updateValue('input[type=text]', 'nightwatch'); * }; * * // send some text to an input and hit enter. * this.demoTest = function (browser) { * browser.updateValue('input[type=text]', ['nightwatch', browser.Keys.ENTER]); * }; * * * @link /session/:sessionId/element/:id/value * @method updateValue * @syntax .updateValue(selector, inputValue, [callback]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|array} inputValue The text to send to the element or key strokes. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link /#element-send-keys * @api protocol.elementinteraction */ class SetValue extends BaseElementCommand { get extraArgsCount() { return 1; } get elementProtocolAction() { return 'setElementValue'; } protocolAction() { return this.executeProtocolAction('clearElementValue', this.args).then(_ => { return this.executeProtocolAction('setElementValue', this.args); }); } } module.exports = SetValue; ================================================ FILE: lib/api/element-commands/uploadFile.js ================================================ const BaseElementCommand = require('./_baseElementCommand.js'); /** * Uploads file to an element using absolute file path. * * @example * // send a file to for upload to a field. * this.demoTest = function (browser) { * browser.uploadFile('#myFile', '/path/file.pdf'); * }; * // * * @method uploadFile * @syntax .uploadFile(selector, absoluteFilePath, [callback]) * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {string|array} inputValue The file path to upload. * @param {function} [callback] Optional callback function to be called when the command finishes. * @moreinfo www.selenium.dev/documentation/en/remote_webdriver/remote_webdriver_client/ * @api protocol.elementinteraction * @since 2.0.0 */ class UploadFile extends BaseElementCommand { get extraArgsCount() { return 1; } get elementProtocolAction() { return 'uploadFile'; } } module.exports = UploadFile; ================================================ FILE: lib/api/element-commands/waitForElementNotPresent.js ================================================ const WaitForElement = require('./_waitFor.js'); /** * Opposite of `waitForElementPresent`. Waits a given time in milliseconds (default 5000ms) for an element to be not present (i.e. removed) in the page before performing any other commands or assertions. * If the element is still present after the specified amount of time, the test fails. * * You can change the polling interval by defining a `waitForConditionPollInterval` property (in milliseconds) in as a global property in your `nightwatch.json` or in your external globals file. * Similarly, a default timeout can be specified as a global `waitForConditionTimeout` property (in milliseconds). * * @example * module.exports = { * 'demo Test': function(browser) { * // with default implicit timeout of 5000ms (can be overwritten in settings under 'globals.waitForConditionTimeout') * browser.waitForElementNotPresent('#dialog'); * * // specify the locate strategy (css selector/xpath) as the first argument * browser.waitForElementNotPresent('css selector', '#dialog'); * * // with explicit timeout (in milliseconds) * browser.waitForElementNotPresent('#dialog', 1000); * * // continue if failed * browser.waitForElementNotPresent('#dialog', 1000, false); * * // with callback * browser.waitForElementNotPresent('#dialog', 1000, function() { * // do something while we're here * }); * * // with custom output message - the locate strategy is required * browser.waitForElementNotPresent('css selector', '#dialog', 'The dialog container is removed.'); * * // with custom Spanish message * browser.waitForElementNotPresent('#dialog', 1000, 'elemento %s no era presente en %d ms'); * * // many combinations possible - the message is always the last argument * browser.waitForElementNotPresent('#dialog', 1000, false, function() {}, 'elemento %s no era presente en %d ms'); * }, * * 'demo Test with selector objects': function(browser) { * browser.waitForElementNotPresent({ * selector: '#dialog', * timeout: 1000 * }); * * browser.waitForElementNotPresent({ * selector: '#dialog', * locateStrategy: 'css selector' * }, 'Custom output message'); * * browser.waitForElementNotPresent({ * selector: '.container', * index: 2, * retryInterval: 100, * abortOnFailure: true * }); * } * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch * .navigate() * .assert.titleContains('Nightwatch.js'); * * nightwatch..waitForElementNotPresent('@dialogContainer', function(result) { * console.log(result); * }); * } * } * * @syntax .waitForElementNotPresent([using], selector, [timeout], [pollInterval], [abortOnAssertionFailure], [callback], [message]); * @method waitForElementNotPresent * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {number} [time=waitForConditionTimeout] The total number of milliseconds to wait before failing. * @param {number} [poll=waitForConditionPollInterval] The number of milliseconds to wait between checks. You can use this only if you also specify the time parameter. * @param {boolean} [abortOnFailure=abortOnAssertionFailure] By the default if the element is not found the test will fail. Set this to false if you wish for the test to continue even if the assertion fails. To set this globally you can define a property `abortOnAssertionFailure` in your globals. * @param {function} [callback] Optional callback function to be called when the command finishes. * @param {string} [message] Optional message to be shown in the output; the message supports two placeholders: %s for current selector and %d for the time (e.g. Element %s was not in the page for %d ms). * @see waitForElementPresent * @sortIndex 10 * @api protocol.waitforelements */ class WaitForElementNotPresent extends WaitForElement { get retryOnSuccess() { return true; } constructor(opts) { super(opts); this.expectedValue = 'not found'; } /** * Overriding elementFound to fail the test * * @param result * @returns {Promise} */ elementFound(result) { const defaultMsg = 'Timed out while waiting for element <%s> to be removed for %d milliseconds.'; result.passed = false; return this.fail(result, 'found', this.expectedValue, defaultMsg); } elementNotFound(result) { const defaultMsg = 'Element <%s> was not present after %d milliseconds.'; result.passed = true; return this.pass(result, defaultMsg, this.executor.elapsedTime); } } module.exports = WaitForElementNotPresent; ================================================ FILE: lib/api/element-commands/waitForElementNotVisible.js ================================================ const WaitForDisplayed = require('./_waitForDisplayed.js'); /** * Opposite of `waitForElementVisible`. Waits a given time in milliseconds (default 5000ms) for an element to be not visible (i.e. hidden but existing) in the page before performing any other commands or assertions. * If the element fails to be hidden in the specified amount of time, the test fails. * * You can change the polling interval by defining a `waitForConditionPollInterval` property (in milliseconds) in as a global property in your `nightwatch.json` or in your external globals file. * Similarly, a default timeout can be specified as a global `waitForConditionTimeout` property (in milliseconds). * @example * module.exports = { * 'demo Test': function(browser) { * // with default implicit timeout of 5000ms (can be overwritten in settings under 'globals.waitForConditionTimeout') * browser.waitForElementNotVisible('#dialog'); * * // specify the locate strategy (css selector/xpath) as the first argument * browser.waitForElementNotVisible('css selector', '#dialog'); * * // with explicit timeout (in milliseconds) * browser.waitForElementNotVisible('#dialog', 1000); * * // continue if failed * browser.waitForElementNotVisible('#dialog', 1000, false); * * // with callback * browser.waitForElementNotVisible('#dialog', 1000, function() { * // do something while we're here * }); * * // with custom output message - the locate strategy is required * browser.waitForElementNotVisible('css selector', '#dialog', 'The dialog container is not visible.'); * * // with custom Spanish message * browser.waitForElementNotVisible('#dialog', 1000, 'elemento %s no era visible en %d ms'); * * // many combinations possible - the message is always the last argument * browser.waitForElementNotVisible('#dialog', 1000, false, function() {}, 'elemento %s no era visible en %d ms'); * }, * * 'demo Test with selector objects': function(browser) { * browser.waitForElementNotVisible({ * selector: '#dialog', * timeout: 1000 * }); * * browser.waitForElementNotVisible({ * selector: '#dialog', * locateStrategy: 'css selector' * }, 'Custom output message'); * * browser.waitForElementNotVisible({ * selector: '.container', * index: 2, * retryInterval: 100, * abortOnFailure: true * }); * } * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch * .navigate() * .assert.titleContains('Nightwatch.js'); * * nightwatch.waitForElementNotVisible('@mainDialog', function(result) { * console.log(result); * }); * } * } * * @syntax .waitForElementNotVisible([using], selector, [timeout], [pollInterval], [abortOnAssertionFailure], [callback], [message]); * @method waitForElementNotVisible * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {number} [time=waitForConditionTimeout] The total number of milliseconds to wait before failing. * @param {number} [poll=waitForConditionPollInterval] The number of milliseconds to wait between checks. You can use this only if you also specify the time parameter. * @param {boolean} [abortOnFailure=abortOnAssertionFailure] By the default if the element is not found the test will fail. Set this to false if you wish for the test to continue even if the assertion fails. To set this globally you can define a property `abortOnAssertionFailure` in your globals. * @param {function} [callback] Optional callback function to be called when the command finishes. * @param {string} [message] Optional message to be shown in the output; the message supports two placeholders: %s for current selector and %d for the time (e.g. Element %s was not in the page for %d ms). * @see waitForElementVisible * @sortIndex 30 * @api protocol.waitforelements */ class WaitForElementNotVisible extends WaitForDisplayed { constructor(opts) { super(opts); this.expectedValue = 'not visible'; } shouldRetryAction(elementVisible) { return elementVisible; } elementVisible(response) { const defaultMsg = 'Timed out while waiting for element <%s> to not be visible for %d milliseconds.'; return this.fail(response, 'visible', this.expectedValue, defaultMsg); } elementNotVisible(response) { const defaultMsg = 'Element <%s> was not visible after %d milliseconds.'; return this.pass(response, defaultMsg, this.executor.elapsedTime); } } module.exports = WaitForElementNotVisible; ================================================ FILE: lib/api/element-commands/waitForElementPresent.js ================================================ const WaitForElement = require('./_waitFor.js'); /** * Waits a given time in milliseconds (default 5000ms) for an element to be present in the page before performing any other commands or assertions. * If the element fails to be present in the specified amount of time, the test fails. You can change this by setting `abortOnFailure` to `false`. * * You can change the polling interval by defining a `waitForConditionPollInterval` property (in milliseconds) in as a global property in your `nightwatch.json` or in your external globals file. * Similarly, the default timeout can be specified as a global `waitForConditionTimeout` property (in milliseconds). * * @example * module.exports = { * 'demo Test': function(browser) { * // with default implicit timeout of 5000ms (can be overwritten in settings under 'globals.waitForConditionTimeout') * browser.waitForElementPresent('#index-container'); * * // specify the locate strategy (css selector/xpath) as the first argument * browser.waitForElementPresent('css selector', '#index-container'); * * // with explicit timeout (in milliseconds) * browser.waitForElementPresent('#index-container', 1000); * * // continue if failed * browser.waitForElementPresent('#index-container', 1000, false); * * // with callback * browser.waitForElementPresent('#index-container', 1000, function() { * // do something while we're here * }); * * // with custom output message - the locate strategy is required * browser.waitForElementPresent('css selector', '#index-container', 'The index container is found.'); * * // with custom Spanish message * browser.waitForElementPresent('#index-container', 1000, 'elemento %s no era presente en %d ms'); * * // many combinations possible - the message is always the last argument * browser.waitForElementPresent('#index-container', 1000, false, function() {}, 'elemento %s no era presente en %d ms'); * }, * * 'demo Test with selector objects': function(browser) { * browser.waitForElementPresent({ * selector: '#index-container', * timeout: 1000 * }); * * browser.waitForElementPresent({ * selector: '#index-container', * locateStrategy: 'css selector' * }, 'Custom output message'); * * browser.waitForElementPresent({ * selector: '.container', * index: 2, * retryInterval: 100, * abortOnFailure: true * }); * } * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch * .navigate() * .assert.titleContains('Nightwatch.js'); * * nightwatch.waitForElementPresent('@featuresList', function(result) { * console.log(result); * }); * } * } * * @method waitForElementPresent * @syntax .waitForElementPresent([using], selector, [timeout], [pollInterval], [abortOnAssertionFailure], [callback], [message]); * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {number} [time=waitForConditionTimeout] The total number of milliseconds to wait before failing. * @param {number} [poll=waitForConditionPollInterval] The number of milliseconds to wait between checks. You can use this only if you also specify the time parameter. * @param {boolean} [abortOnFailure=abortOnAssertionFailure] By the default if the element is not found the test will fail. Set this to false if you wish for the test to continue even if the assertion fails. To set this globally you can define a property `abortOnAssertionFailure` in your globals. * @param {function} [callback] Optional callback function to be called when the command finishes. * @param {string} [message] Optional message to be shown in the output; the message supports two placeholders: %s for current selector and %d for the time (e.g. Element %s was not in the page for %d ms). * @sortIndex 0 * @api protocol.waitforelements */ class WaitForElementPresent extends WaitForElement { get retryOnSuccess() { return false; } elementFound(result) { const defaultMsg = 'Element <%s> was present after %d milliseconds.'; return this.pass(result, defaultMsg, this.executor.elapsedTime); } } module.exports = WaitForElementPresent; ================================================ FILE: lib/api/element-commands/waitForElementVisible.js ================================================ const WaitForDisplayed = require('./_waitForDisplayed.js'); /** * Waits a given time in milliseconds (default 5000ms) for an element to be visible in the page before performing any other commands or assertions. * * If the element fails to be present and visible in the specified amount of time, the test will be marked as failed, and ordinarily, the subsequent steps/commands within the test-case/section will not be performed. However, you have the option to prevent the remaining steps/commands from being skipped by setting `abortOnFailure` to `false`. * * You can change the polling interval by defining a `waitForConditionPollInterval` property (in milliseconds) in as a global property in your `nightwatch.json` or in your external globals file. * * Similarly, a default timeout can be specified as a global `waitForConditionTimeout` property (in milliseconds). * * @example * module.exports = { * 'demo Test': function(browser) { * // with default implicit timeout of 5000ms (can be overwritten in settings under 'globals.waitForConditionTimeout') * browser.waitForElementVisible('#index-container'); * * // specify the locate strategy (css selector/xpath) as the first argument * browser.waitForElementVisible('css selector', '#index-container'); * * // with explicit timeout (in milliseconds) * browser.waitForElementVisible('#index-container', 1000); * * // continue if failed * browser.waitForElementVisible('#index-container', 1000, false); * * // with callback * browser.waitForElementVisible('#index-container', 1000, function() { * // do something while we're here * }); * * // with custom output message - the locate strategy is required * browser.waitForElementVisible('css selector', '#index-container', 'The index container is found.'); * * // with custom Spanish message * browser.waitForElementVisible('#index-container', 1000, 'elemento %s no era presente en %d ms'); * * // many combinations possible - the message is always the last argument * browser.waitForElementVisible('#index-container', 1000, false, function() {}, 'elemento %s no era visible en %d ms'); * }, * * 'demo Test with selector objects': function(browser) { * browser.waitForElementVisible({ * selector: '#index-container', * timeout: 1000 * }); * * browser.waitForElementVisible({ * selector: '#index-container', * locateStrategy: 'css selector' * }, 'Custom output message'); * * browser.waitForElementVisible({ * selector: '.container', * index: 2, * retryInterval: 100, * abortOnFailure: true * }); * } * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch * .navigate() * .assert.titleContains('Nightwatch.js'); * * nightwatch.waitForElementVisible('@featuresList', function(result) { * console.log(result); * }); * } * } * * @syntax .waitForElementVisible([using], selector, [timeout], [pollInterval], [abortOnAssertionFailure], [callback], [message]); * @method waitForElementVisible * @param {string} [using] The locator strategy to use. See [W3C Webdriver - locator strategies](https://www.w3.org/TR/webdriver/#locator-strategies) * @param {string|object} selector The selector (CSS/Xpath) used to locate the element. Can either be a string or an object which specifies [element properties](https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html#postdoc-element-properties). * @param {number} [time=waitForConditionTimeout] The total number of milliseconds to wait before failing. * @param {number} [poll=waitForConditionPollInterval] The number of milliseconds to wait between checks. You can use this only if you also specify the time parameter. * @param {boolean} [abortOnFailure=abortOnAssertionFailure] By the default if the element is not found the test will fail. Set this to false if you wish for the test to continue even if the assertion fails. To set this globally you can define a property `abortOnAssertionFailure` in your globals. * @param {function} [callback] Optional callback function to be called when the command finishes. * @param {string} [message] Optional message to be shown in the output; the message supports two placeholders: %s for current selector and %d for the time (e.g. Element %s was not in the page for %d ms). * @sortIndex 20 * @api protocol.waitforelements */ class WaitForElementVisible extends WaitForDisplayed { constructor(opts) { super(opts); this.expectedValue = 'visible'; } shouldRetryAction(elementVisible) { return !elementVisible; } elementVisible(response) { const defaultMsg = 'Element <%s> was visible after %d milliseconds.'; return this.pass(response, defaultMsg, this.executor.elapsedTime); } elementNotVisible(response) { const defaultMsg = 'Timed out while waiting for element <%s> to be visible for %d milliseconds.'; return this.fail(response, 'not visible', this.expectedValue, defaultMsg); } } module.exports = WaitForElementVisible; ================================================ FILE: lib/api/expect/_baseExpect.js ================================================ const EventEmitter = require('events'); const chaiNightwatch = require('chai-nightwatch'); const {Logger, format, createPromise} = require('../../utils'); const BaseAssertion = require('./assertions/_baseAssertion.js'); class ValueExpectAssertion extends BaseAssertion { getMessage(negate) { if (this.emitter.getMessage) { return this.emitter.getMessage(negate); } const initialMessage = this.message || `Expected ${this.expectCommandName}`; return `${initialMessage} to ${negate ? ' not' : ''}%s`; } init() { super.init(); this.flag('valueFlag', true); this.message = this.getMessage(this.negate); this.start(); } executeCommand() { return Promise.resolve({ value: this.emitter.resultValue }); } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; } formatMessage() { if (this.element) { let {selector, name} = this.element; let nameStr = ''; if (name) { nameStr = `@${name} `; } selector = selector ? `${nameStr}<${selector}>` : `<${this.element.toString()}>`; this.message = format(this.message, selector, this.elapsedTime); } super.formatMessage(); } } class ExpectInstance extends EventEmitter { flag(...args) { return chaiNightwatch.flag(this.instance, ...args); } get client() { return this.__nightwatchInstance; } get commandFileName() { return this.__commandName; } get commandArgs() { return this.__commandArgs; } get transport() { return this.client.transport; } get transportActions() { return this.client.transportActions; } get elementLocator() { return this.client.elementLocator; } get instance() { return this.__instance; } get stackTrace() { return this.__stackTrace; } set stackTrace(val) { this.__stackTrace = val; } toString() { return `${this.constructor.name} [name=expect.${this.commandFileName}]`; } constructor({commandName, nightwatchInstance, commandArgs}) { super(); this.__commandName = commandName; this.__commandArgs = commandArgs; this.__nightwatchInstance = nightwatchInstance; this.startTime = null; this.resolve = null; this.reject = null; this.createInstance(); if (!this.hasAssertions) { this.initAssertion(); } } /** * Main entry point * * @param args */ run(...args) { this.startTime = new Date().getTime(); const promise = this.command(...args); this.handleCommandPromise(promise); } createLocalPromise() { return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } createInstance() { this.promise = this.createLocalPromise(); this.deferred = { commandName: this.commandFileName }; this.__instance = chaiNightwatch.expect((resolve, reject) => { this.deferred.resolve = resolve; this.deferred.reject = reject; }); this.flag('deferred', this.deferred); if (!this.hasAssertions) { this.flag('valueFlag', true); } this.flag('emitter', this); this.flag('api', this.client.api); this.flag('promise', this.promise); } createRetryPromise() { this.promise = this.createLocalPromise(); this.flag('promise', this.promise); return this.promise; } retryCommand() { const promise = this.command(...this.commandArgs); this.handleCommandPromise(promise); } /** * @override * @param [args] * @return {Promise} */ command(...args) { throw new Error(`The expect command "${this.commandFileName}" should have a ".command()" method defined.`); } /** * @param {Promise} promise */ handleCommandPromise(promise) { promise .then(result => { this.resultValue = result && result.value || result; return this.resolve(this.resultValue); }) .catch(result => { if ((result instanceof Error) && result.name !== 'NoSuchElementError') { Logger.error(result); } this.reject(result); }); } checkFlags() { if (!this.needsFlags) { return true; } if (this.flag('component')) { return true; } return this.needsFlags.some((flag) => this.flag(`${flag}Flag`) !== undefined || this.flag(flag) !== undefined); } initAssertion({AssertModule = ValueExpectAssertion, message = null, args = []} = {}) { const nightwatchInstance = this.client; const assertion = new AssertModule({ nightwatchInstance, chaiExpect: this.instance, message, expectCommandName: this.commandFileName }); assertion.init(...args); this.instance.assertion = assertion; } } module.exports = ExpectInstance; ================================================ FILE: lib/api/expect/assertions/_baseAssertion.js ================================================ /** * Abstract assertion class that will subclass all defined Chai assertions * * All assertions must implement the following api: * * - @type {function} * executeCommand * - @type {string} * elementFound * - @type {function} * elementNotFound * - @type {function} * retryCommand * - @type {string} * expected * - @type {string} * actual * - @type {string} * message * - @type {boolean} * passed * * @constructor */ const assert = require('assert'); const {flag} = require('chai-nightwatch').util; const Utils = require('../../../utils'); const {AssertionRunner, getExpectedMessage} = require('../../../assertion'); const {Logger, isUndefined} = Utils; class BaseAssertion { static get AssertionType () { return { PROPERTY: 'property', METHOD: 'method' }; } static get ASSERT_FLAGS() { return [ 'be', 'that', 'and', 'have', 'which', 'equal', 'contains', 'startsWith', 'endsWith', 'matches', 'before', 'after', 'waitFor' ]; } get transport() { return this.client.transport; } get reporter() { return this.client.reporter; } get transportActions() { return this.emitter.transportActions; } get assertion() { return this.runner.assertion; } get selector() { return this.flag('element') && this.flag('element').selector; } /** * @override * @return {Promise} */ executeCommand() {} /** * @override */ onResultSuccess() {} /** * @override */ onResultFailed() {} flag(key, value) { if (typeof value == 'undefined') { return flag(this.chaiExpect, key); } flag(this.chaiExpect, key, value); return this; } getFlags() { return flag(this.chaiExpect); } setFlags() { this.emitter = this.flag('emitter'); this.setDeepEqualFlag(); this.element = this.flag('element'); if (this.element) { const {timeout, retryInterval, abortOnFailure, message} = this.element; if (Utils.isNumber(timeout)) { this.waitForMs = timeout; this.flag('waitFor', this.waitForMs); } if (Utils.isNumber(retryInterval)) { this.retryInterval = retryInterval; } if (Utils.isBoolean(abortOnFailure)) { this.abortOnFailure = abortOnFailure; } if (message) { this.message = message; this.hasCustomMessage = true; } } } /** * @param {Nightwatch} nightwatchInstance * @param {chai.Assertion} chaiExpect * @param {string} expectCommandName * @param {string} message */ constructor({nightwatchInstance, chaiExpect, expectCommandName, message = ''}) { this.client = nightwatchInstance; this.chaiExpect = chaiExpect; this.expectCommandName = expectCommandName; this.message = message; } init() { const {waitForConditionTimeout, waitForConditionPollInterval, abortOnAssertionFailure} = this.client.api.globals; const assertions = this.flag('assertions') || 0; this.flag('assertions', assertions + 1); this.promise = this.flag('promise'); this.setFlags(); this.setNegate(); this.setDeepEqualFlag(); this.setWaitForFlag(waitForConditionTimeout); this.retryInterval = waitForConditionPollInterval || 500; this.abortOnFailure = abortOnAssertionFailure; this.retries = 0; this.passed = undefined; this.actual = undefined; this.expected = undefined; this.resultValue = undefined; this.messageParts = []; this.flags = []; } start() { this.promise.then(this.onPromiseResolved.bind(this), this.onPromiseRejected.bind(this)); } getResultValue(result) { if (result && (result.error instanceof Error)) { return null; } return result.value; } getResultStatus(result) { if (result && (result.error instanceof Error)) { return -1; } return result.status; } getResultError(result) { if (result && (result.error instanceof Error)) { return result.error; } return null; } onPromiseResolved() { this.setFlags(); this.executeCommand().then(result => { if (isUndefined(result)) { return this.onPromiseRejected(new Error('Unknown error')); } this.onExecuteCommandResult(result); }); } onExecuteCommandResult(result) { this.resultValue = this.getResultValue(result); this.resultStatus = this.getResultStatus(result); this.resultErrorStatus = this.getResultError(result); this.processFlags(); this.onResultSuccess(); if ((this.passed === false) && this.shouldRetry()) { this.scheduleRetry(); } else { this.done(); } } elementNotFoundError(response) { return response instanceof Error && response.name === 'NotFoundError'; } onPromiseRejected(response) { if (response instanceof Error) { this.resultErrorStatus = response; } this.processFlags(); this.setFlags(); if (this.shouldRetry() && !this.negate) { this.scheduleRetry(); return; } this.addExpectedInMessagePart(); let notFoundStr = ' - element was not found'; if (this.elementNotFoundError(response)) { notFoundStr = ` - ${response.message}`; } if (this.hasCustomMessage) { this.message += notFoundStr; } else { this.messageParts.push(notFoundStr); } this.actual = this.getActual('not present'); if (!this.expected) { this.expected = 'present'; } this.onResultFailed(); this.done(); } getActual(actual) { if (this.resultErrorStatus && this.resultErrorStatus.name !== 'NoSuchElementError') { return `[${this.resultErrorStatus.name}]`; } return actual; } processFlags() { this.getFlags().forEach((value, key) => { if (BaseAssertion.ASSERT_FLAGS.includes(key) && Utils.isFunction(this[`@${key}Flag`])) { this.flags.push(key); this[`@${key}Flag`](value); } }); } hasFlag(type) { return this.flags.includes(type); } done() { this.formatMessage(); this.runAssertion(); } runAssertion() { const stackTrace = this.emitter.stackTrace; const reporter = this.client.reporter; const {passed, actual, expected, message, abortOnFailure} = this; if (passed === undefined) { const err = new Error(`Incomplete expect assertion for "expect.${this.emitter.commandFileName}()". Please consult the docs at https://nightwatchjs.org/api/expect/`); err.isExpect = true; this.emitter.emit('error', err); return this; } this.runner = new AssertionRunner({ passed, addExpected: false, err: { expected, actual }, message, abortOnFailure, stackTrace, reporter }); this.runner.run() .catch(err => (err)) .then(result => { const isError = result instanceof Error; if (isError) { result.isExpect = true; if (this.resultErrorStatus && this.reporter) { this.reporter.registerTestError(this.resultErrorStatus); } this.emitter.emit('error', result, this.abortOnFailure); } else { const assertions = this.flag('assertions'); if (this.flag('and') && assertions > 1) { this.flag('and', null); this.flag('assertions', assertions - 1); return; } this.emitter.emit('complete', result); } }); } getResult(value, fn) { const result = fn.call(this); this.setNegate(); return this.negate ? !result : result; } shouldRetry() { if (!this.waitForMs || this.emitter.retryUnavailable) { return false; } this.elapsedTime = this.getElapsedTime(); return (this.elapsedTime < this.waitForMs); } getElapsedTime() { const timeNow = new Date().getTime(); return timeNow - this.emitter.startTime; } scheduleRetry() { this.retries++; setTimeout(this.retryCommand.bind(this), this.retryInterval); } formatMessage() { if (this.passed === false) { this.addExpectedMessage(); } this.addTimeMessagePart(); if (!this.hasCustomMessage) { this.message += this.messageParts.join(''); } } '@containsFlag'(value) { const verb = (this.hasFlag('that') || this.hasFlag('which')) ? 'contains' : 'contain'; this.conditionFlag(value, function() { if (!Utils.isString(this.resultValue) && !Array.isArray(this.resultValue)) { Logger.warn(`Unexpected non-string or array result value returned for contains: ${this.resultValue}`); return false; } return this.resultValue.indexOf(value) > -1; }, [`not ${verb}`, verb]); return this; } '@startsWithFlag'(value) { const verb = (this.hasFlag('that') || this.hasFlag('which')) ? 'starts with' : 'start with'; this.conditionFlag(value, function() { if (!Utils.isString(this.resultValue)) { Logger.warn(`Unexpected non-string result value returned for startsWith: ${this.resultValue}`); return false; } return this.resultValue.indexOf(value) === 0; }, [ `not ${verb}`, verb ]); return this; } '@endsWithFlag'(value) { const verb = (this.hasFlag('that') || this.hasFlag('which')) ? 'ends with' : 'end with'; this.conditionFlag(value, function() { if (!Utils.isString(this.resultValue)) { Logger.warn(`Unexpected non-string result value returned for endsWith: ${this.resultValue}`); return false; } return (this.resultValue.lastIndexOf(value) + value.length) === this.resultValue.length; }, [ `not ${verb}`, verb ]); return this; } '@equalFlag'(value) { let verb; if (this.hasFlag('have')) { verb = (this.hasFlag('that') || this.hasFlag('which')) ? 'equals' : 'equal to'; } else { verb = 'equal'; } if (this.deepEqual) { verb = `deep ${verb}`; } this.conditionFlag(value, function() { if (this.deepEqual || Utils.isObject(value)) { try { assert.deepStrictEqual(value, this.resultValue); return true; } catch (err) { return false; } } return this.resultValue === value; }, [ `not ${verb}`, verb ]); return this; } '@matchesFlag'(re) { const adverb = this.hasFlag('that') || this.hasFlag('which'); const verb = adverb ? 'matches' : 'match'; this.conditionFlag(re, function() { return re.test(this.resultValue); }, [ (adverb ? 'does ' : '') + 'not match', verb ]); return this; } conditionFlag(value, conditionFn, arrverb) { this.passed = this.getResult(value, conditionFn); let verb = this.negate ? arrverb[0] : arrverb[1]; this.expected = `${verb} '${value}'`; this.actual = this.resultValue; if (this.retries > 0) { return; } const needsSpace = this.messageParts.length === 0 ? true : this.messageParts[this.messageParts.length - 1].slice(-1) !== ' '; if (needsSpace) { verb = ' ' + verb; } if (!this.hasCustomMessage) { this.messageParts.push(`${verb}: ${Logger.colors.brown('"' + value + '"')}`); } } setNegate() { this.negate = this.flag('negate') || false; return this; } setDeepEqualFlag() { this.deepEqual = this.flag('deep') || false; return this; } setWaitForFlag(waitForConditionTimeout) { this.waitForMs = waitForConditionTimeout || null; if (this.waitForMs) { this.flag('waitFor', this.waitForMs); } return this; } '@beforeFlag'(value) {} '@afterFlag'(value) {} '@haveFlag'(value) {} '@waitForFlag'(value) { if (this.waitForMs !== value) { this.waitForMs = value; } } '@andFlag'() { if (this.retries > 0) { return; } if (!this.hasCustomMessage) { this.messageParts.push(' and '); } return this; } '@thatFlag'() { if (this.retries > 0) { return; } if (!this.hasCustomMessage) { this.messageParts.push(' that '); } return this; } '@whichFlag'() { if (this.retries > 0) { return; } if (!this.hasCustomMessage) { this.messageParts.push(' which '); } return this; } '@beFlag'() {} addTimeMessagePart() { this.elapsedTime = this.getElapsedTime(); const timeStr = Logger.colors.stack_trace(' (' + this.elapsedTime + 'ms)'); if (this.hasCustomMessage) { this.message += timeStr; } else { this.messageParts.push(timeStr); } } addExpectedMessage() { const {actual, expected} = this; const message = getExpectedMessage({ actual, expected }); if (this.hasCustomMessage) { this.message += message; } else { this.messageParts.push(message); } } addExpectedInMessagePart() { const expectedIn = ` in ${this.waitForMs}ms`; if (!this.hasCustomMessage && (this.flag('before') || this.flag('after')) && !this.messageParts.includes(expectedIn)) { this.messageParts.push(expectedIn); } } hasCondition() { return ( this.hasFlag('contains') || this.hasFlag('equal') || this.hasFlag('matches') || this.hasFlag('startsWith') || this.hasFlag('endsWith') ); } retryCommand() { this.promise = this.emitter.createRetryPromise(); this.promise.then(this.onPromiseResolved.bind(this), this.onPromiseRejected.bind(this)); this.emitter.retryCommand(); } } module.exports = BaseAssertion; ================================================ FILE: lib/api/expect/assertions/element/_element-assertion.js ================================================ const Utils = require('../../../../utils'); const BaseAssertion = require('../_baseAssertion.js'); class ExpectElement extends BaseAssertion { /** * @override */ onResultSuccess() {} /** * @override */ onResultFailed() {} onPromiseResolved(value) { if (value) { if (Utils.isObject(value) && value.elementId) { this.elementId = value.elementId; } else { this.elementId = this.transport.getElementId(value); } } super.onPromiseResolved(); } formatMessage() { let {selector, name} = this.element; let nameStr = ''; if (name) { nameStr = `@${name} `; } selector = selector ? `${nameStr}<${selector}>` : `<${this.element.toString()}>`; this.message = Utils.format(this.message, selector, this.elapsedTime); super.formatMessage(); } /** * * @param {String} protocolAction * @param {Array} [args] * @return {Promise} */ executeProtocolAction(protocolAction, args = []) { if (!Array.isArray(args)) { args = [args]; } const {sessionId, element} = this.emitter; if (element && element.webElement) { this.elementId = element.resolvedElement || element.webElement; } args.unshift(this.elementId); if (sessionId) { return this.transportActions[protocolAction](args, sessionId); } return this.transportActions[protocolAction](...args); } retryCommand() { if (this.shouldRetryLocateElement()) { this.resultErrorStatus = null; this.elementId = null; this.resultValue = null; this.promise = this.emitter.createRetryPromise(); this.promise.then(this.onPromiseResolved.bind(this), this.onPromiseRejected.bind(this)); this.emitter.retryCommand(); } else { this.onPromiseResolved(); } } shouldRetryLocateElement() { return !this.elementId || this.isRetryableElementError(); } isRetryableElementError() { return this.transport.isRetryableElementError(this.resultErrorStatus || this.resultStatus); } isComponent() { return this.flag('component') === true; } } module.exports = ExpectElement; ================================================ FILE: lib/api/expect/assertions/element/active.js ================================================ /** * Property that checks if an element is active in the DOM. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').to.be.active; * browser.expect.element('#main').to.not.be.active; * browser.expect.element('#main').to.be.active.before(100); * }; * * * @method active * @display .active * @since v1.1 * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class ActiveAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.flag('active', true); this.message = 'Expected element %s to ' + (this.negate ? 'not be active' : 'be active'); this.start(); } executeCommand() { return this.executeProtocolAction('getActiveElement'); } compareElementIds() { return this.resultValue === this.elementId; } onResultSuccess() { const result = this.compareElementIds(); this.passed = this.negate ? result === false : result; this.expected = this.negate ? 'not active' : 'active'; this.actual = result ? 'active' : 'not active'; this.addExpectedInMessagePart(); } onResultFailed() { this.passed = this.negate; } } module.exports = ActiveAssertion; ================================================ FILE: lib/api/expect/assertions/element/attribute.js ================================================ /** * Checks if a given attribute of an element exists and optionally if it has the expected value. * * @example * this.demoTest = function (browser) { * browser.expect.element('body').to.have.attribute('data-attr'); * browser.expect.element('body').to.not.have.attribute('data-attr'); * browser.expect.element('body').to.not.have.attribute('data-attr', 'Testing if body does not have data-attr'); * browser.expect.element('body').to.have.attribute('data-attr').before(100); * browser.expect.element('body').to.have.attribute('data-attr') * .equals('some attribute'); * browser.expect.element('body').to.have.attribute('data-attr') * .not.equals('other attribute'); * browser.expect.element('body').to.have.attribute('data-attr') * .which.contains('something'); * browser.expect.element('body').to.have.attribute('data-attr') * .which.matches(/^something\ else/); * }; * * * @method attribute * @param {string} attribute The attribute name * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @display expect.element.attribute(name) * @syntax browser.expect.element(selector).to.have.attribute(attribute) * @since v0.7 * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class AttributeAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.METHOD; } init(attribute, msg) { super.init(); this.flag('attributeFlag', true); this.attribute = attribute; this.hasCustomMessage = typeof msg != 'undefined'; this.message = msg || 'Expected element %s to ' + (this.negate ? 'not have' : 'have') + ' attribute "' + attribute + '"'; this.start(); } executeCommand() { return this.executeProtocolAction('getElementAttribute', [this.attribute]); } onExecuteCommandResult(result) { this.resultValue = this.getResultValue(result); this.resultStatus = this.getResultStatus(result); this.resultErrorStatus = this.getResultError(result); if (this.resultValue === null && !this.resultErrorStatus) { this.attributeNotFound(); return; } this.processFlags(); this.onResultSuccess(); if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.done(); } } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } if (!this.hasCondition()) { this.passed = !this.negate; this.expected = this.negate ? 'not found' : 'found'; this.actual = 'found'; } this.addExpectedInMessagePart(); } attributeNotFound() { this.processFlags(); this.passed = this.hasCondition() ? false : this.negate; if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.addExpectedInMessagePart(); if (!this.hasCondition()) { this.expected = this.negate ? 'not found' : 'found'; this.actual = this.getActual('not found'); } if (!this.negate) { const attrNotFound = ' - attribute was not found'; if (this.hasCustomMessage) { this.message += attrNotFound; } else { this.messageParts.push(attrNotFound); } } this.done(); } } onResultFailed() { this.passed = false; } } module.exports = AttributeAssertion; ================================================ FILE: lib/api/expect/assertions/element/css.js ================================================ /** * Checks a given css property of an element exists and optionally if it has the expected value. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').to.have.css('display'); * browser.expect.element('#main').to.have.css('display', 'Testing for display'); * browser.expect.element('#main').to.not.have.css('display'); * browser.expect.element('#main').to.have.css('display').before(100); * browser.expect.element('#main').to.have.css('display').which.equals('block'); * browser.expect.element('#main').to.have.css('display').which.contains('some value'); * browser.expect.element('#main').to.have.css('display').which.matches(/some\ value/); * }; * * * @method css * @param {string} property The css property name * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @display .css(property) * @since v0.7 * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class CssAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.METHOD; } init(property, msg) { super.init(); this.cssProperty = property; this.flag('cssFlag', true); this.hasCustomMessage = typeof msg != 'undefined'; this.message = msg || 'Expected element %s to ' + (this.negate ? 'not have' : 'have') + ' css property "' + property + '"'; this.start(); } executeCommand() { return this.executeProtocolAction('getElementCSSValue', [this.cssProperty]); } '@haveFlag'() { this.passed = this.negate ? (this.resultValue === '') : (this.resultValue !== ''); this.expected = this.negate ? 'not present' : 'present'; this.actual = this.resultValue === '' ? 'not present' : 'present'; } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; } } module.exports = CssAssertion; ================================================ FILE: lib/api/expect/assertions/element/enabled.js ================================================ /** * Property that checks if an element is currently enabled. * * @example * this.demoTest = function (browser) { * browser.expect.element('#weblogin').to.be.enabled; * browser.expect.element('#main').to.not.be.enabled; * browser.expect.element('#main').to.be.enabled.before(100); * }; * * * @method enabled * @display .enabled * @since v0.7 * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class EnabledAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.message = 'Expected element %s to ' + (this.negate ? 'not be enabled' : 'be enabled'); this.flag('enabledFlag', true); this.start(); } executeCommand() { return this.executeProtocolAction('isElementEnabled'); } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.passed = this.negate ? !this.resultValue : this.resultValue; this.expected = this.negate ? 'not enabled' : 'enabled'; this.actual = this.resultValue ? 'enabled' : 'not enabled'; this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; this.expected = this.negate ? 'not enabled' : 'enabled'; this.actual = 'not found'; } } module.exports = EnabledAssertion; ================================================ FILE: lib/api/expect/assertions/element/present.js ================================================ /** * Property that checks if an element is present in the DOM. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').to.be.present; * browser.expect.element('#main').to.not.be.present; * browser.expect.element('#main').to.be.present.before(100); * }; * * * @method present * @display .present * @since v0.7 * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class PresentAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } isElementNotFoundError() { return this.resultErrorStatus.name === 'NoSuchElementError'; } init() { super.init(); this.flag('present', true); this.message = 'Expected element %s to ' + (this.negate ? 'not be present' : 'be present'); this.start(); } executeCommand() { return Promise.resolve(this.elementId); } onResultSuccess() { this.passed = !this.negate; if (!this.passed && this.shouldRetry()) { return; } this.addExpectedInMessagePart(); if (this.negate) { this.actual = 'present'; this.expected = 'not present'; } } onResultFailed() { if ((this.resultErrorStatus instanceof Error) && !this.isElementNotFoundError()) { this.passed = false; this.actual = 'error while locating the element'; this.expected = this.negate ? 'not present' : 'present'; return; } this.expected = this.negate ? 'not present' : 'present'; this.passed = this.negate; } retryCommand() { this.elementId = null; super.retryCommand(); } } module.exports = PresentAssertion; ================================================ FILE: lib/api/expect/assertions/element/property.js ================================================ /** * Checks if a given DOM property of an element has the expected value. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * @example * this.demoTest = function (browser) { * browser.expect.element('body').to.have.property('className').equals('test-class'); * browser.expect.element('body').to.have.property('className').matches(/^something\ else/); * browser.expect.element('body').to.not.have.property('classList').equals('test-class'); * browser.expect.element('body').to.have.property('classList').deep.equal(['class-one', 'class-two']); * browser.expect.element('body').to.have.property('classList').contain('class-two'); * browser.expect.element('body').to.have.domProperty('classList').contain('class-two'); * }; * * @method property * @param {string} property The property name * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @display .property(name) * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class PropertyAssertion extends BaseAssertion { static get aliases() { return ['domProperty']; } static get assertionType() { return BaseAssertion.AssertionType.METHOD; } init(property, msg) { super.init(); this.flag('attributeFlag', true); this.property = property; this.hasCustomMessage = typeof msg != 'undefined'; this.message = msg || `Expected element %s to ${this.negate ? 'not have' : 'have'} dom property "${this.property}"`; this.start(); } executeCommand() { if (this.isComponent()) { if (!this.hasCustomMessage) { this.message = `Expected component %s to ${this.negate ? 'not have' : 'have'} property "${this.property}"`; } //return this.emitter.getComponentProperty(this.property); } return this.executeProtocolAction('getElementProperty', [this.property]); } hasProperty() { if (this.flag('component') === true) { return this.resultValue !== undefined; } return this.resultValue !== null; } onExecuteCommandResult(result) { this.resultValue = this.getResultValue(result); this.resultStatus = this.getResultStatus(result); this.resultErrorStatus = this.getResultError(result); if (!this.hasProperty() && !this.resultErrorStatus) { this.propertyNotFound(); return; } this.processFlags(); this.onResultSuccess(); if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.done(); } } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } if (!this.hasCondition()) { this.passed = !this.negate; this.expected = this.negate ? 'not found' : 'found'; this.actual = 'found'; } this.addExpectedInMessagePart(); } propertyNotFound() { this.processFlags(); this.passed = this.hasCondition() ? false : this.negate; if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.addExpectedInMessagePart(); if (!this.hasCondition()) { this.actual = this.getActual('not found'); } if (!this.negate) { this.messageParts.push(' - property was not found'); } this.done(); } } onResultFailed() { this.passed = false; } } module.exports = PropertyAssertion; ================================================ FILE: lib/api/expect/assertions/element/selected.js ================================================ /** * Property that checks if an OPTION element, or an INPUT element of type checkbox or radio button is currently selected. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').to.be.selected; * browser.expect.element('#main').to.not.be.selected; * browser.expect.element('#main').to.be.selected.before(100); * }; * * * @method selected * @display .selected * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class SelectedAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.message = 'Expected element %s to ' + (this.negate ? 'not be selected' : 'be selected'); this.flag('selectedFlag', true); this.start(); } executeCommand() { return this.executeProtocolAction('isElementSelected'); } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.passed = this.negate ? !this.resultValue : this.resultValue; this.expected = this.negate ? 'not selected' : 'selected'; this.actual = this.resultValue ? 'selected' : 'not selected'; this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; this.expected = this.negate ? 'not selected' : 'selected'; this.actual = 'not found'; } } module.exports = SelectedAssertion; ================================================ FILE: lib/api/expect/assertions/element/text.js ================================================ /** * Property that retrieves the text contained by an element. Can be chained to check if contains/equals/matches the specified text or regex. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').text.to.equal('The Night Watch'); * browser.expect.element('#main').text.to.not.equal('The Night Watch'); * browser.expect.element('#main').text.to.equal('The Night Watch').before(100); * browser.expect.element('#main').text.to.contain('The Night Watch'); * browser.expect.element('#main').text.to.match(/The\ Night\ Watch/); * }; * * * @method text * @since v0.7 * @display .text * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class TextAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.flag('textFlag', true); this.message = 'Expected element %s text to' + (this.negate ? ' not' : ''); this.start(); } executeCommand() { return this.executeProtocolAction('getElementText'); } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; } } module.exports = TextAssertion; ================================================ FILE: lib/api/expect/assertions/element/type.js ================================================ /** * Checks if the type (i.e. tag name) of a specified element is of an expected value. * * @example * this.demoTest = function (browser) { * browser.expect.element('#q').to.be.an('input'); * browser.expect.element('#q').to.be.an('input', 'Testing if #q is an input'); * browser.expect.element('#w').to.be.a('span'); * } * * @method a * @display .a(type) * @alias an * @since v0.7 * @param {string} type The expected type * @param {string} [message] Optional log message to display in the output. If missing, one is displayed by default. * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class TypeAssertion extends BaseAssertion { static get assertionName() { return ['a', 'an']; } static get assertionType() { return BaseAssertion.AssertionType.METHOD; } init(type, msg) { super.init(); this.type = type; this.article = ['a', 'e', 'i', 'o'].indexOf(type.toString().substring(0, 1)) > -1 ? 'an' : 'a'; this.hasCustomMessage = typeof msg != 'undefined'; this.flag('typeFlag', true); this.message = msg || 'Expected element %s to ' + (this.negate ? 'not be' : 'be') + ' ' + this.article + ' ' + type; this.start(); } executeCommand() { return this.executeProtocolAction('getElementTagName'); } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } if (this.type instanceof RegExp) { const result = this.type.test(this.resultValue); this.passed = this.negate ? !result : result; } else { this.type = this.type.toLowerCase(); this.resultValue = this.resultValue.toLowerCase(); this.passed = this.negate ? (this.resultValue !== this.type) : (this.resultValue === this.type); } this.expected = this.negate ? 'not be ' + this.article + ' ' + this.type : 'be ' + this.article + ' ' + this.type; this.actual = this.resultValue; this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; } } module.exports = TypeAssertion; ================================================ FILE: lib/api/expect/assertions/element/value.js ================================================ /** * Property that retrieves the value (i.e. the value attributed) of an element. Can be chained to check if contains/equals/matches the specified text or regex. * * @example * this.demoTest = function (browser) { * browser.expect.element('#q').to.have.value.that.equals('search'); * browser.expect.element('#q').to.have.value.not.equals('search'); * browser.expect.element('#q').to.have.value.which.contains('search'); * browser.expect.element('#q').to.have.value.which.matches(/search/); * }; * * * @display .value * @method value * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class ValueAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.flag('valueFlag', true); this.message = 'Expected element %s to have value' + (this.negate ? ' not' : ''); this.start(); } executeCommand() { return this.executeProtocolAction('getElementProperty', ['value']); } onExecuteCommandResult(result) { this.resultValue = this.getResultValue(result); this.resultStatus = this.getResultStatus(result); this.resultErrorStatus = this.getResultError(result); if (this.resultValue === null && !this.resultErrorStatus) { this.valueNotFound(); return; } this.processFlags(); this.onResultSuccess(); if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.done(); } } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.addExpectedInMessagePart(); } valueNotFound() { this.processFlags(); this.passed = this.hasCondition() ? false : this.negate; if (!this.passed && this.shouldRetry()) { this.scheduleRetry(); } else { this.addExpectedInMessagePart(); if (!this.hasCondition()) { this.expected = this.negate ? 'not found' : 'found'; this.actual = this.getActual('not found'); } if (!this.negate) { this.messageParts.push(' - value attribute was not found'); } this.done(); } } onResultFailed() { this.passed = false; } } module.exports = ValueAssertion; ================================================ FILE: lib/api/expect/assertions/element/visible.js ================================================ /** * Property that asserts the visibility of a specified element. * * @example * this.demoTest = function (browser) { * browser.expect.element('#main').to.be.visible; * browser.expect.element('#main').to.not.be.visible; * browser.expect.element('#main').to.be.visible.before(100); * }; * * * @display .visible * @method visible * @api expect.element */ const BaseAssertion = require('./_element-assertion.js'); class VisibleAssertion extends BaseAssertion { static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.flag('visibleFlag', true); this.message = 'Expected element %s to ' + (this.negate ? 'not be visible' : 'be visible'); this.start(); } executeCommand() { return this.executeProtocolAction('isElementDisplayed'); } onResultSuccess() { this.passed = this.negate ? this.resultValue === false : !!this.resultValue; this.expected = this.negate ? 'not visible' : 'visible'; this.actual = this.getActual(); this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; this.expected = this.negate ? 'not visible' : 'visible'; this.actual = 'not found'; } getActual() { const value = this.resultValue === true ? 'visible' : 'not visible'; return super.getActual(value); } } module.exports = VisibleAssertion; ================================================ FILE: lib/api/expect/assertions/elements/count.js ================================================ /** * Checks if the number of elements specified by a selector is equal or not to a given value. * * @example * this.demoTest = function (browser) { * browser.expect.elements('div').count.to.equal(10); * browser.expect.elements('p').count.to.not.equal(1); * } * * @method count * @display .elements().count * @since v1.1 * @api expect.elements */ const BaseAssertion = require('../_baseAssertion.js'); class CountAssertion extends BaseAssertion { static get assertionName() { return ['count']; } static get assertionType() { return BaseAssertion.AssertionType.PROPERTY; } init() { super.init(); this.flag('valueFlag', true); this.start(); const selector = this.element && this.element.selector ? this.element.selector : '...'; this.message = `Expected elements <${selector}> count to`; } executeCommand() { this.message = `Expected elements <${this.element.selector}> count to`; const {length} = this.emitter.resultValue; this.actual = length; return Promise.resolve({ value: length }); } getActual(actual) { if (this.actual === undefined) { return super.getActual(actual); } return this.actual; } onResultSuccess() { if (this.retries > 0 && this.negate) { return; } this.addExpectedInMessagePart(); } onResultFailed() { this.passed = false; } } module.exports = CountAssertion; ================================================ FILE: lib/api/expect/component.js ================================================ const ExpectElement = require('./element.js'); class ExpectComponent extends ExpectElement { constructor(...args) { super(...args); this.flag('component', true); } } module.exports = ExpectComponent; ================================================ FILE: lib/api/expect/cookie.js ================================================ /** * Checks if the content of the title element is of an expected value. * * @example * this.demoTest = function (browser) { * browser.expect.cookie('cookie-name').to.contain('cookie-value'); * browser.expect.cookie('cookie-name').to.match(/regex/); * browser.expect.cookie('loginCookie', 'example.org').to.contain('cookie-value'); * } * * @method cookie * @display .expect.cookie() * @param {string} name The name of the cookie to be inspected * @param {string} domain The domain name for which the cookie is set. * @since v1.1 * @api expect */ const BaseExpect = require('./_baseExpect.js'); class ExpectCookie extends BaseExpect { get needsFlags() { return ['contains', 'startsWith', 'endsWith', 'matches', 'equal']; } get hasAssertions() { return false; } getMessage(negate) { this.cookieName = this.commandArgs[0] || ''; this.cookieDomain = this.commandArgs[1] || ''; let {cookieName = '', cookieDomain} = this; cookieName = `"${cookieName}"`; if (cookieDomain) { cookieName += ` for domain "${cookieDomain}"`; } return `Expected cookie ${cookieName} to${negate ? ' not' : ''}`; } command(...args) { return this.transportActions.getCookie(...args); } handleCommandPromise(promise) { promise .then((result) => { const domainStr = this.cookieDomain ? ` for domain "${this.cookieDomain}"` : ''; if (!result.value) { return this.reject(new NotFoundError(`no cookie "${this.cookieName}"${domainStr} was found`)); } if (result && result.value) { if (Array.isArray(result.value)) { this.resultValue = result.value.length > 0 ? result.value[0].value : null; } else { // result.value represents the cookie object, so result.value.value would be the cookie value this.resultValue = result.value.value; } } else { this.resultValue = {}; } return this.resolve(this.resultValue); }) .catch((result) => { this.reject(result); }); } } class NotFoundError extends Error { constructor(props) { super(props); this.name = 'NotFoundError'; } } module.exports = ExpectCookie; ================================================ FILE: lib/api/expect/element.js ================================================ const {By} = require('selenium-webdriver'); const Element = require('../../element'); const BaseExpect = require('./_baseExpect.js'); class ExpectElement extends BaseExpect { get needsFlags() { return [ 'value', 'active', 'attribute', 'css', 'enabled', 'present', 'selected', 'text', 'type', 'visible' ]; } get hasAssertions() { return true; } get promiseRejectedMsg() { return 'Element was not found.'; } /** * If this is missing, it will be the main expect command name * @return {string} */ get assertionsPath() { return './element'; } /** * @param [args] * @return {Promise} */ command(...args) { this.createElement(...args); return this.locateElement(); } getComponentProperty(property) { return this.client.transportActions.executeScript(function(property) { // eslint-disable-next-line if (!window['@@component_element']) { throw new Error('Component was not rendered.'); } // eslint-disable-next-line return window['@@component_element'].componentVM[property]; }, [property]); } retryCommand() { const promise = this.locateElement(); this.handleCommandPromise(promise); } createElement(selector, using = this.client.locateStrategy) { let value = selector; if (selector.webElementLocator instanceof By) { const {webElementLocator} = selector; value = webElementLocator.value; using = webElementLocator.using; } this.element = Element.createFromSelector(value, using); this.flag('element', this.element); return this; } locateElement() { const {element} = this; return this.elementLocator .findElement({element, cacheElementId: false}) .then(elementResult => { if (elementResult && elementResult.value) { this.elementId = this.transport.getElementId(elementResult.value); return elementResult; } const {error} = elementResult; if (error instanceof Error) { throw error; } throw elementResult; }); } } module.exports = ExpectElement; ================================================ FILE: lib/api/expect/elements.js ================================================ const Element = require('../../element'); const BaseExpect = require('./_baseExpect.js'); class ExpectElements extends BaseExpect { get needsFlags() { return ['value']; } get hasAssertions() { return true; } get promiseRejectedMsg() { return 'Element was not found.'; } /** * If this is missing, it will be the main expect command name * @return {string} */ get assertionsPath() { return './elements'; } constructor(opts) { super(opts); if (this.commandArgs[0] instanceof Promise) { return; } this.createElement(...this.commandArgs); } async command(...args) { if (!this.element && (this.commandArgs[0] instanceof Promise)) { const value = await this.commandArgs[0]; this.element = { value, selector: 'elements' }; this.flag('element', this.element); this.resultValue = value; this.retryUnavailable = true; //retry is not possible in this case return this.resolve(value); } return this.locateElements(); } locateElements() { const {element} = this; return this.elementLocator .findElement({element, returnSingleElement: false, cacheElementId: false}) .then(result => { const {value, error} = result; let elements; if (value) { elements = this.transport.mapWebElementIds(value); } else if (error instanceof Error && error.name === 'NoSuchElementError') { elements = this.transport.mapWebElementIds([]); } else { throw result; } this.resultValue = elements; return this.resolve(elements); }); } retryCommand() { const promise = this.locateElements(); this.handleCommandPromise(promise); } createElement(selector, using = this.client.locateStrategy) { this.element = Element.createFromSelector(selector, using); this.flag('element', this.element); return this; } } module.exports = ExpectElements; ================================================ FILE: lib/api/expect/title.js ================================================ /** * Checks if the content of the page title is of an expected value. * * @example * this.demoTest = function (browser) { * browser.expect.title().to.contain('value'); * browser.expect.title().to.match(/value/); * } * * @method title * @display .title() * @since v1.1 * @api expect */ const BaseExpect = require('./_baseExpect.js'); class ExpectTitle extends BaseExpect { get needsFlags() { return [ 'contains', 'startsWith', 'endsWith', 'matches', 'equal' ]; } get assertionType() { return 'property'; } get hasAssertions() { return false; } getMessage(negate) { return `Expected page title to${negate ? ' not' : ''}`; } command() { return this.transportActions.getPageTitle(); } } module.exports = ExpectTitle; ================================================ FILE: lib/api/expect/url.js ================================================ /** * Checks if the page url is of an expected value. * * @example * this.demoTest = function (browser) { * browser.expect.url().to.contain('https://'); * browser.expect.url().to.endWith('.org'); * } * * @method url * @display .url() * @since v1.1 * @api expect */ const BaseExpect = require('./_baseExpect.js'); class ExpectUrl extends BaseExpect { get needsFlags() { return [ 'contains', 'startsWith', 'endsWith', 'matches', 'equal' ]; } get assertionType() { return 'method'; } get hasAssertions() { return false; } getMessage(negate) { return `Expected current url to${negate ? ' not' : ''}`; } command() { return this.transportActions.getCurrentUrl(); } } module.exports = ExpectUrl; ================================================ FILE: lib/api/index.js ================================================ const path = require('path'); const fs = require('fs'); const Utils = require('../utils'); const StaticApis = require('./_loaders/static.js'); const CommandLoader = require('./_loaders/command.js'); const ElementCommandLoader = require('./_loaders/element-command.js'); const AssertionLoader = require('./_loaders/assertion.js'); const ExpectLoader = require('./_loaders/expect.js'); const PageObjectLoader = require('./_loaders/page-object.js'); const WithinContextLoader = require('./_loaders/within-context.js'); const PluginLoader = require('./_loaders/plugin.js'); const __elementCommandsStrict = []; const __elementCommands = []; const DEFAULT_PLUGINS = ['nightwatch-axe-verbose']; class ApiLoader { static isElementCommand(commandName) { return __elementCommands.includes(commandName); } static get CommandFiles() { return { protocolActions: { dirPath: 'protocol', protocol: true }, clientCommands: 'client-commands', elementCommands: { dirPath: 'element-commands', element: true, loader: ElementCommandLoader }, assertions: { loader: AssertionLoader, dirPath: 'assertions', element: true, namespaces: { assert: { abortOnFailure: true }, verify: { abortOnFailure: false } } }, expect: { loader: ExpectLoader, dirPath: 'expect', element: true, namespaces: { expect: { abortOnFailure: true }, should: { abortOnFailure: false } } } }; } /** * @param {*} customPath * @return {Array} */ static adaptCustomPath(customPath) { return customPath.map(location => { return path.resolve(location); }); } static getElementsCommandsStrict() { return __elementCommandsStrict; } static init(nightwatchInstance) { const api = new ApiLoader(nightwatchInstance); const staticApis = new StaticApis(nightwatchInstance); staticApis.loadStaticAssertions(); staticApis.loadStaticExpect(); api.loadApiCommandsSync(); return WithinContextLoader.loadCommandCache(nightwatchInstance) .then(_ => api.loadCustomCommands()) .then(_ => api.loadCustomAssertions()) .then(_ => api.initPluginTransforms()) .then(_ => api.loadPlugins()) .then(_ => api.defineWithinContext()) .then(_ => api.loadPageObjects()) .then(_ => { if (!nightwatchInstance.unitTestingMode) { const EnsureApi = require('./_loaders/ensure.js'); const ensureApi = new EnsureApi(nightwatchInstance); ensureApi.loadAssertions(); const ChromeApi = require('./_loaders/chrome.js'); const chromeApis = new ChromeApi(nightwatchInstance); chromeApis.loadCommands(); const FirefoxApi = require('./_loaders/firefox.js'); const firefoxApis = new FirefoxApi(nightwatchInstance); firefoxApis.loadCommands(); } }); } constructor(nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; } get api() { return this.nightwatchInstance.api; } get reporter() { return this.nightwatchInstance.reporter; } get settings() { return this.nightwatchInstance.settings; } get commandQueue() { return this.nightwatchInstance.queue; } static makeAssertProxy(api) { return new Proxy(api, { get(target, name) { if (name === '__isProxy') { return true; } if (name === 'not') { return new Proxy(api, { get(target, name) { return function (...args) { if (!Utils.isFunction(target[name])) { throw new Error(`Unknown api method .not."${name}".`); } return target[name]({negate: true, args}); }; } }); } return function (...args) { if (typeof name != 'string') { return null; } if (!Utils.isFunction(target[name])) { throw new Error(`Unknown api method "${name}".`); } return target[name]({negate: false, args}); }; } }); } initPluginTransforms() { this.nightwatchInstance.transforms = Promise.resolve([]); return Promise.resolve(); } loadPlugins(parent = null) { let plugins = DEFAULT_PLUGINS.concat(this.nightwatchInstance.options.plugins || []); plugins = [... new Set(plugins)]; const promises = []; plugins.forEach(pluginName => { const plugin = PluginLoader.load(pluginName); if (plugin.commands) { promises.push(this.__loadCustomObjects({ parent, sourcePath: plugin.commands, isUserDefined: true })); } if (plugin.assertions) { promises.push(this.__loadCustomObjects({ parent, sourcePath: plugin.assertions, namespaces: ApiLoader.CommandFiles.assertions.namespaces, Loader: AssertionLoader })); } if (plugin.transforms) { promises.push(Promise.resolve(this.__loadPluginTransforms(plugin.transforms))); } }); return Promise.all(promises); } async defineWithinContext(parent) { const loader = new WithinContextLoader(this.nightwatchInstance); loader.define(parent); return Promise.resolve(); } /** * Loads the page objects, if defined * @param parent */ async loadPageObjects(parent = null) { await PageObjectLoader.loadApiCommands(this.nightwatchInstance); return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.page_objects_path, isUserDefined: true, Loader: PageObjectLoader }); } /** * Loads custom assertions, if defined * * @param parent */ loadCustomAssertions(parent = null) { return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.custom_assertions_path, namespaces: ApiLoader.CommandFiles.assertions.namespaces, Loader: AssertionLoader }); } /** * Loads custom commands, if defined * @param parent */ loadCustomCommands(parent = null) { return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.custom_commands_path, isUserDefined: true }); } /** * Loads built-in api commands, assertions, and expect definitions * @param parent * @param {object} options */ loadApiCommandsSync(parent = null, {loadAssertions = true, loadClientCommands = true} = {}) { Object.keys(ApiLoader.CommandFiles) .forEach((key) => { const commands = ApiLoader.CommandFiles[key]; const {namespaces} = commands; if (!loadAssertions && key === 'assertions') { return; } if (!loadClientCommands && key === 'clientCommands') { return; } let elementCommandStrict = false; if (key === 'elementCommands') { elementCommandStrict = true; } let dirPath; let Loader = CommandLoader; let isProtocolCommand = false; let isElementCommand = false; if (Utils.isString(commands)) { dirPath = path.join(__dirname, commands); } else { dirPath = path.join(__dirname, commands.dirPath); isProtocolCommand = commands.protocol; isElementCommand = commands.element; if (commands.loader) { Loader = commands.loader; } } const opts = { dirPath, parent, Loader, isProtocolCommand, isElementCommand, elementCommandStrict, namespacesObj: namespaces }; // protocol commands are not loaded onto the main page object if (parent && isProtocolCommand) { return; } this.__loadCommandsSync(opts); }); } addCommandDefinitionSync({ dirPath, isUserDefined = false, fileName, parent, namespace = [], abortOnFailure = false, isProtocolCommand = false, isElementCommand = false, elementCommandStrict = false, Loader = CommandLoader }) { const loader = new Loader(this.nightwatchInstance); loader.isUserDefined = isUserDefined; if (namespace && namespace.length) { loader.setNamespace(namespace); } loader .loadModule(dirPath, fileName) .createWrapper(abortOnFailure) .define(parent); if (isElementCommand && loader.commandName && !__elementCommands.includes(loader.commandName)) { __elementCommands.push(loader.commandName); } if (elementCommandStrict && loader.commandName && !__elementCommandsStrict.includes(loader.commandName)) { __elementCommandsStrict.push(loader.commandName); } } async addCommandDefinitionAsync({ dirPath, isUserDefined = false, fileName, parent, namespace = [], abortOnFailure = false, isElementCommand = false, Loader = CommandLoader }) { const loader = new Loader(this.nightwatchInstance); loader.isUserDefined = isUserDefined; if (namespace && namespace.length) { loader.setNamespace(namespace); } await loader.loadModuleAsync(dirPath, fileName); loader.createWrapper(abortOnFailure).define(parent); if (isElementCommand && loader.commandName && !__elementCommands.includes(loader.commandName)) { __elementCommands.push(loader.commandName); } } __loadCustomObjects({parent = null, Loader = CommandLoader, namespaces, sourcePath}) { if (!sourcePath) { sourcePath = []; } if (!Array.isArray(sourcePath)) { sourcePath = [sourcePath]; } const dirPathArr = ApiLoader.adaptCustomPath(sourcePath); const commandFiles = []; dirPathArr.forEach((dirPath, index) => this.__loadCommandsSync({ parent, dirPath, originalSourcePath: sourcePath[index], isUserDefined: true, namespacesObj: namespaces, Loader, loadAction(opts) { commandFiles.push(opts); } })); const hasCorrespondingJsFile = (parsedResource) => { const jsFile = path.join(parsedResource.dir, `${parsedResource.name}${Utils.jsFileExt}`); const entry = commandFiles.find(({dirPath, fileName}) => { return path.join(dirPath, fileName) === jsFile; }); return !!entry; }; const commandFilesProcessed = commandFiles.reduce((prev, opts) => { const {dirPath, fileName} = opts; const fullPath = path.join(dirPath, fileName); const parsedResource = path.parse(fullPath); const isTypeScriptFile = parsedResource.ext === Utils.tsFileExt; // ignore `.ts` files if a `.js` has already been loaded if (isTypeScriptFile && hasCorrespondingJsFile(parsedResource)) { return prev; } prev.push(opts); return prev; }, []); return Promise.all(commandFilesProcessed.map(opts => this.addCommandDefinitionAsync(opts))); //commandFilesProcessed.forEach(opts => this.addCommandDefinitionSync(opts)); } __loadCommandsSync({ parent = null, namespacesObj = null, ns = null, dirPath, originalSourcePath, abortOnFailure = false, isProtocolCommand = false, isElementCommand = false, isUserDefined = false, elementCommandStrict = false, Loader = CommandLoader, loadAction = this.addCommandDefinitionSync.bind(this) }) { if (namespacesObj) { Object.keys(namespacesObj).forEach((ns) => this.__loadCommandsSync({ ns, parent, dirPath, isUserDefined, isElementCommand, elementCommandStrict, Loader, loadAction: isUserDefined ? loadAction : this.addCommandDefinitionSync.bind(this), abortOnFailure: namespacesObj[ns].abortOnFailure })); } else { Utils.readFolderRecursively(dirPath, [], (dirPath, fileName, namespace) => { loadAction.call(this, { dirPath, fileName, parent, isUserDefined, namespace: ns || namespace, isProtocolCommand, isElementCommand, elementCommandStrict, Loader, abortOnFailure }); }, function readDirFn(sourcePath) { try { return { resources: fs.readdirSync(sourcePath), sourcePath }; } catch (err) { if (err.code === 'ENOENT' && originalSourcePath && originalSourcePath.startsWith('examples/')) { return readDirFn(path.join('node_modules/nightwatch/', originalSourcePath)); } throw err; } }); } } __loadCommands({ parent = null, namespacesObj = null, ns = null, dirPath, abortOnFailure = false, isProtocolCommand = false, isElementCommand = false, isUserDefined = false, Loader = CommandLoader, loadAction = this.addCommandDefinitionAsync.bind(this) }) { if (namespacesObj) { return Promise.all(Object.keys(namespacesObj).map((ns) => { return this.__loadCommands({ ns, parent, dirPath, isUserDefined, isElementCommand, Loader, abortOnFailure: namespacesObj[ns].abortOnFailure }); })); } const promises = []; Utils.readFolderRecursively(dirPath, [], (dirPath, fileName, namespace) => { const result = Promise.resolve(loadAction.call(this, { dirPath, fileName, parent, isUserDefined, namespace: ns || namespace, isProtocolCommand, isElementCommand, Loader, abortOnFailure })); promises.push(result); }, function readDirFn(sourcePath) { return { resources: fs.readdirSync(sourcePath), sourcePath }; }); return Promise.all(promises); } /** * @param {function|Array} transforms * @returns {Promise} */ __normalizeTransforms(transforms) { if (typeof transforms === 'function') { return transforms(); } return Promise.resolve(transforms || []); } /** * @param {function|Array} nextTransforms */ __loadPluginTransforms(nextTransforms) { this.nightwatchInstance.transforms = this.nightwatchInstance.transforms.then(existingTransforms => this.__normalizeTransforms(nextTransforms).then(items => existingTransforms.concat(items))); } } module.exports = ApiLoader; ================================================ FILE: lib/api/protocol/_base-action.js ================================================ const Element = require('../../element'); const {isFunction, isString, makePromise} = require('../../utils'); const {By} = require('selenium-webdriver'); const {LocateStrategy} = Element; module.exports = class ProtocolAction { static get ScreenOrientation() { return ['LANDSCAPE', 'PORTRAIT']; } static get MouseButton() { return { LEFT: 'left', MIDDLE: 'middle', RIGHT: 'right' }; } static validateElementId(id, apiMethod) { if (!isString(id)) { const err = new Error(`First argument passed to .${apiMethod}() should be a web element ID string. Received ${typeof id}.`); err.detailedErr = `See https://nightwatchjs.org/api/${apiMethod}.html\n`; throw err; } } static get elementCommandToActionMap() { return { element: { transportAction: 'locateSingleElement', returnSingleElement: true }, elements: { transportAction: 'locateMultipleElements', returnSingleElement: false }, elementIdElement: { transportAction: 'locateSingleElementByElementId', returnSingleElement: true }, elementIdElements: { transportAction: 'locateMultipleElementsByElementId', returnSingleElement: false }, findElement: { transportAction: 'locateMultipleElements', returnSingleElement: true }, findElements: { transportAction: 'locateMultipleElements', returnSingleElement: false } }; } reportProtocolErrors(result) { return !(result && (result.error instanceof Error) && result.error.registered); } get elementLocator() { return this.client.elementLocator; } get transport() { return this.client.transport; } get requiresSeleniumWebdriver() { return false; } get settings() { return this.client.settings; } /** * @type {Nightwatch} * @return {*} */ get client() { return this.__nightwatchInstance; } get api() { return this.client.api; } constructor(nightwatchInstance) { this.__nightwatchInstance = nightwatchInstance; } /*! * @override * @return {Promise} */ async command() {} /*! * * @param {Element} [element] * @param {Element} [parentElement] * @param {String} [id] * @param {String} using * @param {String} value * @param {function} [callback] * * @return {Promise} */ findElements({element, parentElement, id, using, value, commandName, callback = function(r) {return r}}) { if (!(element instanceof Element) && !(element instanceof By)) { try { element = Element.createFromSelector(value, using); } catch (err) { return Promise.reject(err); } } if ((element instanceof Element) && !element.usingRecursion) { LocateStrategy.validate(element.locateStrategy, commandName); } return this.locate({id, element, parentElement, commandName}) .then(result => { if (commandName !== 'element' && result && (result.error instanceof Error) && result.error.name === 'NoSuchElementError') { result = { value: [], status: 0 }; } return makePromise(callback, this.api, [result]); }); } /*! * * @param {Element} element * @param {string} protocolCommand either "elements"(multiple) or "element"(single) */ async locate({id, element, parentElement, commandName}) { const {transportAction, returnSingleElement} = ProtocolAction.elementCommandToActionMap[commandName]; if ((element instanceof Element) && element.usingRecursion) { return this.elementLocator.findElement({element, transportAction, returnSingleElement}); } if (parentElement) { const elementResponse = await this.elementLocator.findElement({element: parentElement}); if (!elementResponse.value) { return { value: null, status: -1, err: new Error(`No element found for ${parentElement}.`) }; } id = this.transport.getElementId(elementResponse.value); } return this.elementLocator.executeProtocolAction({id, element, transportAction, commandName}); } /*! * Helper function for mouseButton actions * * @param {string} handler * @param {string|number} button * @param {function} callback * @private */ mouseButtonHandler(handler, button, callback) { let buttonIndex; if (button === undefined && callback === undefined) { button = 0; } else { if (isFunction(button)) { callback = button; button = 0; } if (isString(button)) { buttonIndex = [ ProtocolAction.MouseButton.LEFT, ProtocolAction.MouseButton.MIDDLE, ProtocolAction.MouseButton.RIGHT ].indexOf(button.toLowerCase()); if (buttonIndex !== -1) { button = buttonIndex; } } } return this.transportActions[handler](button, callback); } }; ================================================ FILE: lib/api/protocol/acceptAlert.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Accepts the currently displayed alert dialog. Usually, this is equivalent to clicking on the 'OK' button in the dialog. * * @method acceptAlert * @link /#accept-alert * @param {function} [callback] Optional callback function to be called when the command finishes. * @exampleLink /api/acceptAlert.js * @api protocol.userprompts * @deprecated In favour of `.alerts.accept()`. */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.acceptAlert(callback); } }; ================================================ FILE: lib/api/protocol/appium/getContext.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get the current context in which Appium is running. Used when testing hybrid mobile apps using Appium. * * More info here: https://appium.io/docs/en/commands/context/get-context/ * * @example * module.exports = { * 'get current context': function (app) { * app * .appium.getContext(function (result) { * console.log('the current context is:', result.value); * }); * }, * * 'get current context with ES6 async/await': async function (app) { * const context = await app.appium.getContext(); * console.log('the current context is:', context); * } * }; * * @syntax .appium.getContext([callback]) * @method getContext * @param {function} [callback] Callback function which is called with the result value. * @returns {string|null} A string representing the current context, or `null` representing "no context". * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html * @see appium.setContext * @see appium.getContexts * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getCurrentContext(callback); } }; ================================================ FILE: lib/api/protocol/appium/getContexts.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get a list of the available contexts. Used when testing hybrid mobile apps using Appium. * * More info here: https://appium.io/docs/en/commands/context/get-contexts/ * * @example * module.exports = { * 'get available contexts': function (app) { * app * .appium.getContexts(function (result) { * console.log('the available contexts are:', result.value); * }); * }, * * 'get available contexts with ES6 async/await': async function (app) { * const contexts = await app.appium.getContexts(); * console.log('the available contexts are:', contexts); * } * }; * * @syntax .appium.getContexts([callback]) * @method getContexts * @param {function} [callback] Callback function which is called with the result value. * @returns {Array} An array of strings representing available contexts, e.g 'WEBVIEW_', or 'NATIVE_APP' * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html * @see appium.getContext * @see appium.setContext * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getAvailableContexts(callback); } }; ================================================ FILE: lib/api/protocol/appium/getCurrentActivity.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get the name of the current Android activity. * * @example * module.exports = { * 'get current activity name': function (app) { * app * .appium.getCurrentActivity(function (result) { * console.log('current android activity is:', result.value); * }); * }, * * 'get current activity name with ES6 async/await': async function (app) { * const activity = await app.appium.getCurrentActivity(); * console.log('current android activity is:', activity); * } * }; * * @syntax .appium.getCurrentActivity([callback]) * @method getCurrentActivity * @param {function} [callback] Callback function which is called with the result value. * @returns {string} Name of the current activity. * @see appium.getCurrentPackage * @see appium.startActivity * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getCurrentActivity(callback); } }; ================================================ FILE: lib/api/protocol/appium/getCurrentPackage.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get the name of the current Android package. * * @example * module.exports = { * 'get current package name': function (app) { * app * .appium.getCurrentPackage(function (result) { * console.log('current android package is:', result.value); * }); * }, * * 'get current package name with ES6 async/await': async function (app) { * const packageName = await app.appium.getCurrentPackage(); * console.log('current android package is:', packageName); * } * }; * * @syntax .appium.getCurrentPackage([callback]) * @method getCurrentPackage * @param {function} [callback] Callback function which is called with the result value. * @returns {string} Name of the current package. * @see appium.getCurrentActivity * @see appium.startActivity * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getCurrentPackage(callback); } }; ================================================ FILE: lib/api/protocol/appium/getGeolocation.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get the current geolocation of the mobile device. * * @example * module.exports = { * 'get device geolocation': function (app) { * app * .appium.getGeolocation(function (result) { * console.log('current device geolocation is:', result.value); * }); * }, * * 'get device geolocation with ES6 async/await': async function (app) { * const location = await app.appium.getGeolocation(); * console.log('current device geolocation is:', location); * } * }; * * @syntax .appium.getGeolocation([callback]) * @method getGeolocation * @param {function} [callback] Callback function which is called with the result value. * @returns {object} The current geolocation: `{latitude: number, longitude: number, altitude: number}`. * @see appium.setGeolocation * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getDeviceGeolocation(callback); } }; ================================================ FILE: lib/api/protocol/appium/getOrientation.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Get the current device orientation. * * @example * module.exports = { * 'get current device orientation': function (app) { * app * .appium.getOrientation(function (result) { * console.log('current device orientation is:', result.value); * }); * }, * * 'get current device orientation with ES6 async/await': async function (app) { * const orientation = await app.appium.getOrientation(); * console.log('current device orientation is:', orientation); * } * }; * * @syntax .appium.getOrientation([callback]) * @method getOrientation * @param {function} callback Callback function which is called with the result value. * @returns {string} The current device orientation: `LANDSCAPE` or `PORTRAIT`. * @see appium.setOrientation * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getScreenOrientation(callback); } }; ================================================ FILE: lib/api/protocol/appium/hideKeyboard.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Hide soft keyboard. * * @example * module.exports = { * 'hide device soft keyboard': function (app) { * app * .appium.hideKeyboard(); * }, * * 'hide device soft keyboard with ES6 async/await': async function (app) { * await app.appium.hideKeyboard(); * } * }; * * @syntax .appium.hideKeyboard([callback]) * @method hideKeyboard * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.isKeyboardShown * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.hideDeviceKeyboard({}, callback); } }; ================================================ FILE: lib/api/protocol/appium/isKeyboardShown.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Whether or not the soft keyboard is shown. * * @example * module.exports = { * 'whether keyboard is shown': function (app) { * app * .appium.isKeyboardShown(function (result) { * console.log('result value of whether keyboard is shown:', result.value); * }); * }, * * 'whether keyboard is shown with ES6 async/await': async function (app) { * const result = await app.appium.isKeyboardShown(); * console.log('result value of whether keyboard is shown:', result); * } * }; * * @syntax .appium.isKeyboardShown([callback]) * @method isKeyboardShown * @param {function} [callback] Callback function which is called with the result value. * @returns {boolean} True if the keyboard is shown. * @see appium.hideKeyboard * @api protocol.appium */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.isDeviceKeyboardShown(callback); } }; ================================================ FILE: lib/api/protocol/appium/longPressKeyCode.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Press and hold a particular key on an Android Device. * * See [official Android Developers docs](https://developer.android.com/reference/android/view/KeyEvent.html) for reference of available Android key code values. * * @example * module.exports = { * 'long press e with caps lock on (keycode 33 and metastate 1048576)': function (app) { * app * .appium.longPressKeyCode(33, 1048576); * }, * * 'long press g (keycode 35) with ES6 async/await': async function (app) { * await app.appium.longPressKeyCode(35); * } * }; * * @syntax .appium.longPressKeyCode(keycode, [callback]) * @syntax .appium.longPressKeyCode(keycode, metastate, flags, [callback]) * @method longPressKeyCode * @param {number} keycode Key code to press on the device. * @param {number} [metastate] Meta state to press the keycode with. * @param {number} [flags] Flags for the keypress. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.pressKeyCode * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(keycode, ...args) { let metastate; let flags; let callback; if (typeof keycode !== 'number') { throw new Error('The first argument to longPressKeyCode is mandatory and must be a number.'); } if (typeof args[0] === 'function') { callback = args[0]; } else { [metastate, flags, callback] = args; } const opts = { keycode, ...(metastate && {metastate}), ...(flags && {flags}) }; return this.transportActions.longPressDeviceKeyCode(opts, callback); } }; ================================================ FILE: lib/api/protocol/appium/pressKeyCode.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Press a particular key on an Android Device. * * See [official Android Developers docs](https://developer.android.com/reference/android/view/KeyEvent.html) for reference of available Android key code values. * * @example * module.exports = { * 'press e with caps lock on (keycode 33 and metastate 1048576)': function (app) { * app * .appium.pressKeyCode(33, 1048576); * }, * * 'press g (keycode 35) with ES6 async/await': async function (app) { * await app.appium.pressKeyCode(35); * } * }; * * @syntax .appium.pressKeyCode(keycode, [callback]) * @syntax .appium.pressKeyCode(keycode, metastate, flags, [callback]) * @method pressKeyCode * @param {number} keycode Key code to press on the device. * @param {number} [metastate] Meta state to press the keycode with. * @param {number} [flags] Flags for the keypress. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.longPressKeyCode * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(keycode, ...args) { let metastate; let flags; let callback; if (typeof keycode !== 'number') { throw new Error('The first argument to pressKeyCode is mandatory and must be a number.'); } if (typeof args[0] === 'function') { callback = args[0]; } else { [metastate, flags, callback] = args; } const opts = { keycode, ...(metastate && {metastate}), ...(flags && {flags}) }; return this.transportActions.pressDeviceKeyCode(opts, callback); } }; ================================================ FILE: lib/api/protocol/appium/resetApp.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Reset the app during the test. * * More info here: https://appium.io/docs/en/2.3/commands/base-driver/#reset * * @example * module.exports = { * 'reset the app': function (app) { * app * .appium.resetApp(); * }, * }; * * @syntax .appium.resetApp([callback]) * @method resetApp * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.resetApp(callback); } }; ================================================ FILE: lib/api/protocol/appium/setContext.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Set the context to be automated. Used when testing hybrid mobile apps using Appium. * * More info here: https://appium.io/docs/en/commands/context/set-context/ * * @example * module.exports = { * 'switch to webview context': async function (app) { * app * .waitUntil(async function() { * // wait for webview context to be available * // initially, this.getContexts() only returns ['NATIVE_APP'] * const contexts = await this.appium.getContexts(); * * return contexts.length > 1; * }) * .perform(async function() { * // switch to webview context * const contexts = await this.appium.getContexts(); // contexts: ['NATIVE_APP', 'WEBVIEW_'] * await this.appium.setContext(contexts[1]); * }); * }, * * 'switch to native context': function (app) { * app.appium.setContext('NATIVE_APP'); * } * }; * * @syntax .appium.setContext(context, [callback]) * @method setContext * @param {string} context context name to switch to - a string representing an available context. * @param {function} [callback] Optional callback function to be called when the command finishes. * @moreinfo appium.io/docs/en/writing-running-appium/web/hybrid/index.html * @see appium.getContext * @see appium.getContexts * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(context, callback) { return this.transportActions.setCurrentContext(context, callback); } }; ================================================ FILE: lib/api/protocol/appium/setGeolocation.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Set the current geolocation of the mobile device. * * @example * module.exports = { * 'set geolocation to Tokyo, Japan': function (app) { * app * .appium.setGeolocation({latitude: 35.689487, longitude: 139.691706, altitude: 5}); * }, * * 'set geolocation to Tokyo, Japan with ES6 async/await': async function (app) { * await app.appium.setGeolocation({latitude: 35.689487, longitude: 139.691706}); * } * }; * * @syntax .appium.setGeolocation({latitude, longitude, altitude}, [callback]) * @method setGeolocation * @param {object} [coordinates] `latitude` and `longitude` are required; `altitude` is optional. All should be of type `number`. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.getGeolocation * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(coordinates = {}, callback) { if (!('latitude' in coordinates && 'longitude' in coordinates)) { throw new Error('Please provide both latitude and longitude while using setGeolocation.'); } return this.transportActions.setDeviceGeolocation(coordinates, callback); } }; ================================================ FILE: lib/api/protocol/appium/setOrientation.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Set the current device orientation. * * @example * module.exports = { * 'set orientation to LANDSCAPE': function (app) { * app * .appium.setOrientation('LANDSCAPE'); * }, * * 'set orientation to PORTRAIT with ES6 async/await': async function (app) { * await app.appium.setOrientation('PORTRAIT'); * } * }; * * @syntax .appium.setOrientation(orientation, [callback]) * @method setOrientation * @param {string} orientation The new device orientation: `LANDSCAPE` or `PORTRAIT`. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.getOrientation * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(orientation, callback) { orientation = orientation.toUpperCase(); if (!ProtocolAction.ScreenOrientation.includes(orientation)) { throw new Error('Invalid screen orientation value specified. Accepted values are: ' + ProtocolAction.ScreenOrientation.join(', ')); } return this.transportActions.setScreenOrientation(orientation, callback); } }; ================================================ FILE: lib/api/protocol/appium/startActivity.js ================================================ const ProtocolAction = require('../_base-action.js'); /** * Start an Android activity by providing package name, activity name and other optional parameters. * * More info here: https://appium.io/docs/en/commands/device/activity/start-activity/ * * @example * module.exports = { * 'start an android activity': function (app) { * app * .appium.startActivity({ * appPackage: 'com.android.chrome', * appActivity: 'com.google.android.apps.chrome.Main' * }); * }, * * 'start the main Android activity and wait for onboarding activity to start': function (app) { * app * .appium.startActivity({ * appPackage: 'org.wikipedia', * appActivity: 'org.wikipedia.main.MainActivity', * appWaitActivity: 'org.wikipedia.onboarding.InitialOnboardingActivity' * }); * } * }; * * @syntax .appium.startActivity(opts, [callback]) * @method startActivity * @param {string} opts Options to start the activity with. `appPackage` and `appActivity` are required, [others](https://appium.io/docs/en/commands/device/activity/start-activity/#json-parameters) are optional. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see appium.getCurrentActivity * @see appium.getCurrentPackage * @api protocol.appium */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(opts = {}, callback) { if (!('appPackage' in opts && 'appActivity' in opts)) { throw new Error('Please provide both appPackage and appActivity options while using startActivity.'); } return this.transportActions.startActivity(opts, callback); } }; ================================================ FILE: lib/api/protocol/back.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Navigate backwards in the browser history, if possible (the equivalent of hitting the browser back button). * * @method back * @link /#back * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.navigateBack(callback); } }; ================================================ FILE: lib/api/protocol/closeWindow.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Close the current window. This can be useful when you're working with multiple windows open (e.g. an OAuth login). * * @link /#close-window * @syntax .closeWindow([callback]) * @exampleLink /api/closeWindow.js * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.window * @deprecated In favour of `.window.close()`. */ module.exports = class Action extends ProtocolAction { command(callback) { return this.transportActions.closeWindow(callback); } }; ================================================ FILE: lib/api/protocol/contexts.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get a list of the available contexts. * * Used by Appium when testing hybrid mobile web apps. More info here: https://appium.readthedocs.io/en/latest/en/commands/context/get-contexts/ * * @method contexts * @param {function} callback Callback function to be called when the command finishes. * @returns {Array} an array of strings representing available contexts, e.g 'WEBVIEW', or 'NATIVE' * @api protocol.mobile * @deprecated In favour of `.appium.getContexts()` */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getAvailableContexts(callback); } }; ================================================ FILE: lib/api/protocol/cookie.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve or delete all cookies visible to the current page or set a cookie. Normally this shouldn't be used directly, instead the cookie convenience methods should be used: getCookie, getCookies, setCookie, deleteCookie, deleteCookies. * * @link /#cookies * @param {string} method Http method * @param {function|object} [callbackOrCookie] Optional callback function to be called when the command finishes. * @see getCookies * @see getCookie * @see setCookie * @see deleteCookie * @see deleteCookies * @api protocol.cookies * @deprecated */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(method, callbackOrCookie) { switch (method) { case 'GET': return this.transportActions.getCookies(callbackOrCookie); case 'POST': if (arguments.length < 2) { throw new Error('POST requests to /cookie must include a cookie object parameter also.'); } return this.transportActions.addCookie(callbackOrCookie, arguments[2]); case 'DELETE': if (typeof callbackOrCookie === 'undefined' || typeof callbackOrCookie === 'function') { return this.transportActions.deleteAllCookies(callbackOrCookie); } return this.transportActions.deleteCookie(callbackOrCookie, arguments[2]); default: throw new Error('This method expects first argument to be either GET, POST or DELETE.'); } } }; ================================================ FILE: lib/api/protocol/currentContext.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get current context. * * @param {function} callback Callback function to be called when the command finishes. * @returns {string|null} a string representing the current context or `null`, representing "no context" * @api protocol.mobile * @deprecated In favour of `.appium.getContext()` */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getCurrentContext(callback); } }; ================================================ FILE: lib/api/protocol/dismissAlert.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Dismisses the currently displayed alert dialog. For confirm() and prompt() dialogs, this is equivalent to clicking the 'Cancel' button. * * For alert() dialogs, this is equivalent to clicking the 'OK' button. * * @link /#dismiss-alert * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.userprompts * @deprecated In favour of `.alerts.dismiss()`. */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.dismissAlert(callback); } }; ================================================ FILE: lib/api/protocol/element.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Search for an element on the page, starting from the document root. The located element will be returned as a web element JSON object. * First argument to be passed is the locator strategy, which is detailed on the [WebDriver docs](https://www.w3.org/TR/webdriver/#locator-strategies). * * The locator strategy can be one of: * - `css selector` * - `link text` * - `partial link text` * - `tag name` * - `xpath` * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.element('css selector', 'body', function(result) { * console.log(result.value) * }); * }, * * 'es6 async demo Test': async function(browser) { * const result = await browser.element('css selector', 'body'); * console.log('result value is:', result.value); * } * } * * // Example with using page object elements * module.exports = { * 'demo Test with page object' : function(browser) { * const loginPage = browser.page.login(); * loginPage.api.element('@resultContainer', function(result) { * console.log(result.value) * }); * } * } * * * @link /#find-element * @syntax .element(using, value, callback) * @editline L680 * @deprecated * @param {string} using The locator strategy to use. * @param {string} value The search target. * @param {function} callback Callback function which is called with the result value. * @api protocol.elements */ const {By} = require('selenium-webdriver'); const Element = require('../../element'); module.exports = class Session extends ProtocolAction { reportProtocolErrors(result) { return result.code && result.message; } command(using, value, callback) { const commandName = 'element'; if (using instanceof Element || using instanceof By) { return this.findElements({ element: using, callback: typeof value == 'function' ? value : callback, commandName }); } return this.findElements({ using, value, commandName, callback }); } }; ================================================ FILE: lib/api/protocol/elementActive.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get the element on the page that currently has focus. The element will be returned as a [Web Element](https://www.w3.org/TR/webdriver1/#dfn-web-elements) JSON object. * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.elementActive(function(result) { * console.log(result.value) * }); * } * } * * @name elementActive * @editline L866 * @link /#get-active-element * @param {function} callback Callback function which is called with the result value. * @api protocol.elementstate * @internal */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getActiveElement(callback); } }; ================================================ FILE: lib/api/protocol/elementIdAttribute.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get the value of an element's attribute. * * @link /#get-element-attribute * @param {string} webElementId ID of the element to route the command to. * @param {string} attributeName The attribute name * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, attributeName, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdAttribute'); return this.transportActions.getElementAttribute(webElementId, attributeName, callback); } }; ================================================ FILE: lib/api/protocol/elementIdClear.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Scrolls into view a submittable element excluding buttons or editable element, and then attempts to clear its value, reset the checked state, or text content. * * @link /#dfn-element-clear * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.elementinternal * @internal */ module.exports = class ElementClear extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdClear'); return this.transportActions.clearElementValue(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdClick.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Scrolls into view the element and clicks the in-view center point. If the element is not pointer-interactable, an element not interactable error is returned. * * @link /#element-click * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.elementinternal * @internal */ module.exports = class ElementClick extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdClick'); return this.transportActions.clickElement(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdCssProperty.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the computed value of the given CSS property of the given element. * * The CSS property to query should be specified using the CSS property name, not the JavaScript property name (e.g. background-color instead of backgroundColor). * * @link /#get-element-css-value * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string} cssPropertyName * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, cssPropertyName, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdCssProperty'); return this.transportActions.getElementCSSValue(webElementId, cssPropertyName, callback); } }; ================================================ FILE: lib/api/protocol/elementIdDisplayed.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine if an element is currently displayed. * * @link /#element-displayedness * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { async command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdDisplayed'); return this.transportActions.isElementDisplayed(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdDoubleClick.js ================================================ const ProtocolAction = require('./_base-action.js'); const {isFunction} = require('../../utils'); /** * Move to the element and performs a double-click in the middle of the given element if element is given else double-clicks at the current mouse coordinates (set by `.moveTo()`). * * @method elementIdDoubleClick * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class elementIdDoubleClick extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, callback) { if (isFunction(webElementId)) { callback = webElementId; webElementId = null; } return this.transportActions.doubleClick(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdElement.js ================================================ const {LocateStrategy} = require('../../element'); const ProtocolAction = require('./_base-action.js'); /** * Search for an element on the page, starting from the identified element. The located element will be returned as a Web Element JSON object. * * This command operates on a protocol level and requires a [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements). Read more on [Element retrieval](https://www.w3.org/TR/webdriver1/#element-retrieval) on the W3C WebDriver spec page. * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.elementIdElement('', 'css selector', '.new-element', function(result) { * console.log(result.value) * }); * }, * * 'es6 async demo Test': async function(browser) { * const result = await browser.elementIdElement('', 'css selector', '.new-element'); * console.log(result.value); * }, * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch.navigate(); * * const navbarHeader = nightwatch.section.navbarHeader; * * navbarHeader.api.elementIdElement('@versionDropdown', 'css selector', 'option', function(result) { * browser.assert.ok(client.WEBDRIVER_ELEMENT_ID in result.value, 'The Webdriver Element Id is found in the result'); * }); * } * } * * @link /#find-element-from-element * @syntax .elementIdElement(webElementId, using, value, callback) * @editline L794 * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string} using The locator strategy to use. * @param {string} value The search target. * @param {function} callback Callback function which is called with the result value. * @internal * @api protocol.elements */ const Element = require('../../element'); module.exports = class Session extends ProtocolAction { command(webElementId, using, value, callback = function (r) {return r}) { const commandName = 'elementIdElement'; if (webElementId instanceof Element) { return this.findElements({ parentElement: webElementId, callback, using, value, commandName }); } ProtocolAction.validateElementId(webElementId, commandName); LocateStrategy.validate(using, commandName); return this.findElements({ id: webElementId, using, value, commandName, callback }); } }; ================================================ FILE: lib/api/protocol/elementIdElements.js ================================================ const {LocateStrategy} = require('../../element'); const ProtocolAction = require('./_base-action.js'); /** * Search for multiple elements on the page, starting from the identified element. The located element will be returned as a web element JSON objects. * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.elementIdElements('', 'css selector', 'ul li', function(result) { * console.log(result.value) * }); * }, * * 'es6 async demo Test': async function(browser) { * const result = await browser.elementIdElements('', 'css selector', 'ul li'); * console.log(result.value); * }, * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch.navigate(); * * const navbarHeader = nightwatch.section.navbarHeader; * * navbarHeader.api.elementIdElements('@versionDropdown', 'css selector', 'option', function(result) { * browser.assert.equal(result.value.length, 2, 'There are two option elements in the drop down'); * }); * } * } * * * @link /#find-elements-from-element * @syntax .elementIdElements(webElementId, using, value, callback) * @editline L840 * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string} using The locator strategy to use. * @param {string} value The search target. * @param {function} callback Callback function which is called with the result value. * @api protocol.elements * @internal */ const Element = require('../../element'); module.exports = class Session extends ProtocolAction { command(webElementId, using, value, callback = function (r) {return r}) { const commandName = 'elementIdElements'; if (webElementId instanceof Element) { return this.findElements({ parentElement: webElementId, callback, using, value, commandName }); } ProtocolAction.validateElementId(webElementId, 'elementIdElements'); LocateStrategy.validate(using, 'elementIdElements'); return this.findElements({ id: webElementId, using, value, commandName, callback }); } }; ================================================ FILE: lib/api/protocol/elementIdEnabled.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine if an element is currently enabled. * * @link /#is-element-enabled * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdEnabled'); return this.transportActions.isElementEnabled(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdEquals.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Test if two web element IDs refer to the same DOM element. * * This command is __deprecated__ and is only available on the [JSON Wire protocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidelementidequalsother) * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.elementIdEquals('', '', function(result) { * console.log(result.value) * }); * } * } * * @link /#finding-elements-to-interact * @editline L772 * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string} otherId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the other element to compare against. * @param {function} callback Callback function which is called with the result value. * @internal * @api protocol.elements */ module.exports = class Session extends ProtocolAction { get w3c_deprecated() { return true; } get deprecationNotice() { return 'Please use WebElement.equals(a, b) instead from Selenium Webdriver.'; } command(webElementId, otherId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdEquals'); return this.transportActions.elementIdEquals(webElementId, otherId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdLocation.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine an element's location on the page. The point (0, 0) refers to the upper-left corner of the page. * * The element's coordinates are returned as a JSON object with x and y properties. * * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @returns {object} The X and Y coordinates for the element on the page. */ module.exports = class Session extends ProtocolAction { get w3c_deprecated() { return true; } get deprecationNotice() { return 'Please use .getElementRect().'; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdLocation'); return this.transportActions.getElementRect(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdLocationInView.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine an element's location on the screen once it has been scrolled into view. * * @link * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.elementinternal * @deprecated This is a JSON Wire Protocol command and is no longer supported. */ module.exports = class Session extends ProtocolAction { get w3c_deprecated() { return true; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdLocationInView'); return this.transportActions.isElementLocationInView(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdName.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the qualified tag name of the given element. * * @link /#get-element-tag-name * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdName'); return this.transportActions.getElementTagName(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdProperty.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the value of a specified DOM property for the given element. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * @link /#get-element-property * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string} DOMPropertyName * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class ElementIdProperty extends ProtocolAction { command(webElementId, DOMPropertyName, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdProperty'); return this.transportActions.getElementProperty(webElementId, DOMPropertyName, callback); } }; ================================================ FILE: lib/api/protocol/elementIdSelected.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine if an OPTION element, or an INPUT element of type checkbox or radio button is currently selected. * * @link /#is-element-selected * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdSelected'); return this.transportActions.isElementSelected(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdSize.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Determine an element's size in pixels. The size will be returned as a JSON object with width and height properties. * * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { get w3c_deprecated() { return true; } get deprecationNotice() { return 'Please use .getElementRect().'; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdSize'); return this.transportActions.getElementRect(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdText.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Returns the visible text for the element. * * @link /#get-element-text * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdText'); return this.transportActions.getElementText(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/elementIdValue.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Scrolls into view the form control element and then sends the provided keys to the element, or returns the current value of the element. In case the element is not keyboard interactable, an element not interactable error is returned. * * @link /#element-send-keys * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {string|array|none} [value] Value to send to element in case of a POST * @param {function} callback Callback function which is called with the result value. * @api protocol.elementinternal * @internal */ module.exports = class Session extends ProtocolAction { get w3c_deprecated() { return true; } command(webElementId, value, callback) { ProtocolAction.validateElementId(webElementId, 'elementIdValue'); if (arguments.length === 1 || typeof arguments[1] == 'function') { callback = arguments[1] || function (r) {return r}; return this.transportActions.getElementAttribute(webElementId, 'value', callback); } return this.transportActions.setElementValue(webElementId, value, callback); } }; ================================================ FILE: lib/api/protocol/elements.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Search for multiple elements on the page, starting from the document root. The located elements will be returned as web element JSON objects. * First argument to be passed is the locator strategy, which is detailed on the [WebDriver docs](https://www.w3.org/TR/webdriver/#locator-strategies). * * The locator strategy can be one of: * - `css selector` * - `link text` * - `partial link text` * - `tag name` * - `xpath` * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.elements('css selector', 'ul li', function(result) { * console.log(result.value) * }); * }, * * 'es6 async demo Test': async function(browser) { * const result = await browser.elements('css selector', 'ul li'); * console.log('result value is:', result.value); * }, * * 'page object demo Test': function (browser) { * var nightwatch = browser.page.nightwatch(); * nightwatch * .navigate() * .assert.titleContains('Nightwatch.js'); * * nightwatch.api.elements('@featuresList', function(result) { * console.log(result); * }); * * browser.end(); * } * } * * @link /#find-elements * @deprecated * @syntax .elements(using, value, callback) * @editline L734 * @param {string|null} using The locator strategy to use. * @param {string} value The search target. * @param {function} callback Callback function to be invoked with the result when the command finishes. * @api protocol.elements */ const Element = require('../../element'); module.exports = class Elements extends ProtocolAction { command(using, value, callback) { const commandName = 'elements'; let element; if (using instanceof Element) { element = using; } else if (value instanceof Element) { element = value; } if (element) { return this.findElements({ element, callback: typeof value == 'function' ? value : callback, commandName }); } return this.findElements({ using, value, commandName, callback }); } }; ================================================ FILE: lib/api/protocol/forward.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Navigate forward in the browser history, if possible (the equivalent of hitting the browser forward button). * * @method forward * @link /#back * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.navigateForward(callback); } }; ================================================ FILE: lib/api/protocol/frame.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change focus to another frame on the page. * * Changes the focus of all future commands to another frame on the page. The * target frame may be specified as one of the following: * * - A number that specifies a (zero-based) index into [window.frames]( * https://developer.mozilla.org/en-US/docs/Web/API/Window.frames) * - An element (css selector) which correspond to a `frame` or `iframe` * DOM element * - The `null` value, to select the topmost frame on the page. * * If the specified frame can not be found, a `NoSuchFrameError` will be thrown * * @example * this.demoTest = function (browser) { * browser.frame('', function(result) { * console.log(result); * }); * } * * @method frame * @link /#switch-to-frame * @param {string|number|null} [frameId] Identifier for the frame to change focus to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.frames */ const findElement = function(selector) { return new Promise((resolve, reject) => { this.api.findElement({selector, suppressNotFoundErrors: true}, function(res) { if (res.status === -1 && res.error) { return reject(res.error); } resolve(res.value); }); }); }; module.exports = class Session extends ProtocolAction { static get avoidPrematureParentNodeResolution() { return true; } async command(frameId, callback) { if (arguments.length === 1 && typeof frameId === 'function') { callback = frameId; return this.transportActions.switchToFrame(callback); } if (typeof frameId == 'string') { frameId = await findElement.call(this, frameId) .catch(err => { return findElement.call(this, `*[name="${frameId}"]`); }) .catch(err => { return findElement.call(this, `*[id="${frameId}"]`); }) .catch(err => { throw new Error(`Unable to locate frame element ${frameId}.`); }); delete frameId['getId']; frameId.ELEMENT = frameId[Object.keys(frameId)[0]]; } return this.transportActions.switchToFrame(frameId, callback); } }; ================================================ FILE: lib/api/protocol/frameParent.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change focus to the parent context. If the current context is the top level browsing context, the context remains unchanged. * * @example * this.demoTest = function (browser) { * browser.frameParent(function(result) { * console.log(result); * }); * } * * @method frameParent * @link /#switch-to-parent-frame * @param {function} [callback] Optional callback function to be called when the command finishes. * @since v0.4.8 * @api protocol.frames */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.switchToParentFrame(callback); } }; ================================================ FILE: lib/api/protocol/fullscreenWindow.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Sets the current window state to fullscreen. * * @example * module.exports = { * 'demo Test': function(browser) { * browser.fullscreenWindow(function(result) { * console.log(result); * }); * }, * * 'ES6 async demo Test': async function(browser) { * const result = await browser.fullscreenWindow(); * console.log('result value is:', result.value); * } * } * * @link /#dfn-fullscreen-window * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.contexts * @deprecated In favour of `.window.fullscreen()`. */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.fullscreenWindow(callback); } }; ================================================ FILE: lib/api/protocol/getAlertText.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Gets the text of the currently displayed JavaScript alert(), confirm(), or prompt() dialog. * * @link /#get-alert-text * @param {function} callback Callback function which is called with the result value. * @returns {string} The text of the currently displayed alert. * @api protocol.userprompts * @deprecated In favour of `.alerts.getText()`. */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getAlertText(callback); } }; ================================================ FILE: lib/api/protocol/getCurrentUrl.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the URL of the current page. * * @example * describe('Navigation commands demo', function() { * test('demoTest', function(browser) { * // navigate to new url: * browser.navigateTo('https://nightwatchjs.org'); * * // Retrieve to url with callback: * browser.getCurrentUrl(function(result) { * console.log(result.value); * }); * }); * * test('demoTestAsync', async function(browser) { * const currentUrl = await browser.navigateTo('https://nightwatchjs.org').getCurrentUrl(); * console.log('currentUrl:', currentUrl); // will print 'https://nightwatchjs.org' * }); * * }); * * @method getCurrentUrl * @link /#get-current-url * @syntax .getCurrentUrl([callback]) * @param {Function} [callback] * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { command(callback = function(r) {return r}) { return this.transportActions.getCurrentUrl().then(result => { return callback.call(this.api, result); }); } }; ================================================ FILE: lib/api/protocol/getOrientation.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get the current browser orientation. * * @param {function} callback Callback function which is called with the result value. * @returns {string} The current browser orientation: {LANDSCAPE|PORTRAIT} * @api protocol.mobile * @deprecated In favour of `.appium.getOrientation()` */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getScreenOrientation(callback); } }; ================================================ FILE: lib/api/protocol/keys.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Send a sequence of key strokes to the active element. The sequence is defined in the same format as the `sendKeys` command. * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * Rather than the `setValue`, the modifiers are not released at the end of the call. The state of the modifier keys is kept between calls, so mouse interactions can be performed while modifier keys are depressed. Pass `client.keys.NULL` to the keys function to release modifiers. * * **Since v2.0, this command is deprecated.** It is only available on older JSONWire-based drivers. Please use the new [User Actions API](/api/useractions/). * * @example * module.exports = { * 'demo Test': function(browser) { * browser * .keys(browser.Keys.CONTROL) // hold down CONTROL key * .click('#element') * .keys(browser.Keys.NULL) // release all keys * } * } * * @param {Array|string} keysToSend The keys sequence to be sent. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions * @deprecated */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(keysToSend, callback) { if (!Array.isArray(keysToSend)) { keysToSend = [keysToSend]; } return this.transportActions.sendKeys(keysToSend, callback); } get w3c_deprecated() { return true; } get deprecationNotice() { return 'Nightwatch now supports the extended Selenium user actions API which is available via the the .perform() command.'; } }; ================================================ FILE: lib/api/protocol/minimizeWindow.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Hides the window in the system tray. If the window happens to be in fullscreen mode, it is restored the normal state then it will be "iconified" - minimize or hide the window from the visible screen. * * @example * module.exports = { * 'demo Test': function(browser) { * browser.minimizeWindow(function(result) { * console.log(result); * }); * }, * * 'ES6 async demo Test': async function(browser) { * const result = await browser.minimizeWindow(); * console.log('result value is:', result.value); * } * } * * @link /#dfn-minimize-window * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.contexts * @deprecated In favour of `.window.minimize()`. */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.minimizeWindow(callback); } }; ================================================ FILE: lib/api/protocol/mouseButtonClick.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Click at the current mouse coordinates (set by `.moveTo()`). * * The button can be (0, 1, 2) or ('left', 'middle', 'right'). It defaults to left mouse button. * * **Since v2.0, this command is deprecated.** It is only available on older JSONWire-based drivers. Please use the new [User Actions API](/api/useractions/). * * @param {string|number} button The mouse button * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class Command extends ProtocolAction { get w3c_deprecated() { return true; } get deprecationNotice() { return 'Nightwatch now supports the extended Selenium user actions API which is available via the the .perform() command.'; } static get isTraceable() { return true; } command(button, callback) { return this.mouseButtonHandler('mouseButtonClick', button, callback); } }; ================================================ FILE: lib/api/protocol/mouseButtonDown.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Click and hold the left mouse button (at the coordinates set by the last `moveTo` command). Note that the next mouse-related command that should follow is `mouseButtonUp` . Any other mouse command (such as click or another call to buttondown) will yield undefined behaviour. * * Can be used for implementing drag-and-drop. The button can be (0, 1, 2) or ('left', 'middle', 'right'). It defaults to left mouse button, and if you don't pass in a button but do pass in a callback, it will handle it correctly. * * **Since v2.0, this command is deprecated.** It is only available on older JSONWire-based drivers. Please use the new [User Actions API](/api/useractions/). * * @param {string|number} button The mouse button * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(button, callback) { return this.mouseButtonHandler('mouseButtonDown', button, callback); } }; ================================================ FILE: lib/api/protocol/mouseButtonUp.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Releases the mouse button previously held (where the mouse is currently at). Must be called once for every `mouseButtonDown` command issued. * * Can be used for implementing drag-and-drop. The button can be (0, 1, 2) or ('left', 'middle', 'right'). It defaults to left mouse button, and if you don't pass in a button but do pass in a callback, it will handle it correctly. * * * **Since v2.0, this command is deprecated.** It is only available on older JSONWire-based drivers. Please use the new [User Actions API](/api/useractions/). * * @param {string|number} button The mouse button * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(button, callback) { return this.mouseButtonHandler('mouseButtonUp', button, callback); } }; ================================================ FILE: lib/api/protocol/moveTo.js ================================================ const {Origin} = require('selenium-webdriver'); const Utils = require('../../utils/'); const ProtocolAction = require('./_base-action.js'); const {isNumber, isString} = Utils; /** * Move the mouse by an offset of the specified [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) or relative to the current mouse cursor, if no element is specified. If an element is provided but no offset, the mouse will be moved to the center of the element. * * If an element is provided but no offset, the mouse will be moved to the center of the element. If the element is not visible, it will be scrolled into view. * * @example * this.demoTest = function (browser) { * browser.moveTo(null, 110, 100); * }; * * @syntax .moveTo([webElementId], xoffset, yoffset, [callback]) * @syntax .moveTo(null, xoffset, yoffset, [callback]) * @editline L1335 * @param {string} [webElementId] The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) assigned to the element to move to. If not specified or is null, the offset is relative to current position of the mouse. * @param {number} xoffset X offset to move to, relative to the center of the element. If not specified, the mouse will move to the middle of the element. * @param {number} yoffset Y offset to move to, relative to the center of the element. If not specified, the mouse will move to the middle of the element. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class Command extends ProtocolAction { static get isTraceable() { return true; } command(...args) { let webElementId; let xoffset = 0; let yoffset = 0; let callback = function() {}; const lastItem = args[args.length - 1]; if (Utils.isFunction(lastItem)) { args.pop(); callback = lastItem; } if (isString(args[0]) || arguments[0] === null) { webElementId = args.shift(); if (webElementId === null) { webElementId = Origin.POINTER; } else { ProtocolAction.validateElementId(webElementId); } } if (args.length === 1 && isNumber(args[0]) && !isNaN(args[0])) { xoffset = args[0]; } else if (args.length === 2 && isNumber(args[0]) && !isNaN(args[0]) && isNumber(args[1]) && !isNaN(args[1])) { xoffset = args[0]; yoffset = args[1]; } else if (args.length > 2 || (args.length > 0 && (args[0] !== null || args[1] !== null))) { throw new Error('Invalid Parameters for moveTo Command'); } if (!webElementId) { webElementId = Origin.POINTER; } return this.transportActions.moveTo(webElementId, xoffset, yoffset, callback); } }; ================================================ FILE: lib/api/protocol/navigateTo.js ================================================ const {Logger, relativeUrl, uriJoin} = require('../../utils'); const ProtocolAction = require('./_base-action.js'); /** * Navigate to a new URL. This method will also call the `onBrowserNavigate()` test global, right after the page is loaded. * * @example * describe('Navigation commands demo', function() { * test('demoTest', function(browser) { * // navigate to new url: * browser.navigateTo('https://nightwatchjs.org'); * * // Retrieve to url with callback: * browser.getCurrentUrl(function(result) { * console.log(result.value); * }); * }); * * test('demoTestAsync', async function(browser) { * const currentUrl = await browser.navigateTo('https://nightwatchjs.org').getCurrentUrl(); * console.log('currentUrl:', currentUrl); // will print 'https://nightwatchjs.org' * }); * * }); * * @method navigateTo * @link /#navigate-to * @syntax .navigateTo(url, [callback]) * @param {string} url The url to navigate to * @param {Function} [callback] * @api protocol.navigation * @since 2.0.0 */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(url, callback = function(r) {return r}) { if (typeof url != 'string') { throw new Error('Missing url parameter.'); } if (relativeUrl(url)) { if (!this.api.baseUrl) { throw new Error(`Invalid URL ${url}. When using relative uris, you must ` + 'define a "baseUrl" or "launchUrl" in your nightwatch config.'); } url = uriJoin(this.api.baseUrl, url); } return this.transportActions.navigateTo(url) .then((result) => { if (result && result.error) { const {error} = result; const {message} = error; error.message = `Unable to navigate to url ${url}: ${message}`; throw error; } return callback.call(this.api, result); }) .then(async (result) => { try { await this.settings.globals.onBrowserNavigate(this.api, result); } catch (err) { const error = new Error(`Error during onBrowserNavigate() global hook: ${err.message}`); Logger.error(error); } return result; }); } }; ================================================ FILE: lib/api/protocol/openNewWindow.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Opens a new top-level browser window, which can be either a tab (default) or a separate new window. * * This command is only available for W3C Webdriver compatible browsers. * * @example * module.exports = { * 'demo Test': function(browser) { * // open a new window tab (default) * browser.openNewWindow(function(result) { * console.log(result); * }); * * // open a new window * browser.openNewWindow('window', function(result) { * console.log(result); * }); * }, * * 'ES6 async demo Test': async function(browser) { * const result = await browser.openNewWindow(); * console.log('result value is:', result.value); * } * } * * @link /#dfn-new-window * @param {string} [type] Can be either "tab" or "window", with "tab" set to default if none is specified. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.contexts * @deprecated In favour of `.window.open()`. */ module.exports = class Session extends ProtocolAction { command(type = 'tab', callback) { if (typeof type == 'function' && callback === undefined) { callback = arguments[0]; type = 'tab'; } return this.transportActions.openNewWindow(type, callback); } }; ================================================ FILE: lib/api/protocol/quit.js ================================================ const {Logger} = require('../../utils'); const ProtocolAction = require('./_base-action.js'); /** * Ends the session and closes down the test WebDriver server, if one is running. This is similar to calling the .end() command, but the former doesn't quit the WebDriver session. * * This command will also execute the `onBrowserQuit()` global, if one is defined. * * @example * this.demoTest = function (browser) { * browser.quit(function(result) { * console.log(result.value); * }); * } * * @method quit * @syntax .quit([callback]) * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions * @since 2.0.0 */ module.exports = class Quit extends ProtocolAction { async command(callback = function(r) {return r}) { try { await this.settings.globals.onBrowserQuit(this.api); } catch (err) { const error = new Error(`Error during onBrowserQuit() global hook: ${err.message}`); Logger.error(error); } return this.transportActions.sessionAction('DELETE').then(async (result) => { try { await callback.call(this.api, result); } catch (err) { const error = new Error(`Error while quiting: ${err.message}`); Logger.error(error); } this.client.sessionId = null; await this.transport.sessionFinished('FINISHED'); }); } }; ================================================ FILE: lib/api/protocol/refresh.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Refresh the current page. * * @method refresh * @link /#refresh * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.pageRefresh(callback); } }; ================================================ FILE: lib/api/protocol/releaseMouseButton.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Release the depressed left mouse button at the current mouse coordinates (set by `.moveTo()`). * * * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.useractions */ module.exports = class Command extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.release(callback); } }; ================================================ FILE: lib/api/protocol/screenshot.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Take a screenshot of the current page. * * @method screenshot * @link /#take-screenshot * @param {boolean} log_screenshot_data Whether or not the screenshot data should appear in the logs when running with --verbose * @param {function} callback Callback function which is called with the result value. * @api protocol.screens */ module.exports = class Session extends ProtocolAction { command(log_screenshot_data = false, callback = function (r) {return r}) { if (arguments.length === 1 && typeof arguments[0] === 'function') { return this.transportActions.getScreenshot(false, arguments[0]); } return this.transportActions.getScreenshot(log_screenshot_data, callback); } }; ================================================ FILE: lib/api/protocol/session.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get info about, delete or create a new session. Defaults to the current session. * * @example * this.demoTest = function (browser) { * browser.session(function(result) { * console.log(result.value); * }); * // * browser.session('delete', function(result) { * console.log(result.value); * }); * // * browser.session('delete', '12345-abc', function(result) { * console.log(result.value); * }); * } * * * @method session * @link /#new-session * @editline L141 * @syntax .session([action], [sessionId], [callback]) * @param {string} [action] The http verb to use, can be "get", "post" or "delete". If only the callback is passed, get is assumed by default. * @param {string} [sessionId] The id of the session to get info about or delete. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ module.exports = class Session extends ProtocolAction { static get SessionActions() { return { GET: 'GET', POST: 'POST', DELETE: 'DELETE' }; } command(action = Session.SessionActions.GET, sessionId, callback) { if (arguments.length === 1 && typeof arguments[0] == 'function') { callback = arguments[0]; action = Session.SessionActions.GET; } else if (arguments[0] && !(arguments[0].toUpperCase() in Session.SessionActions)) { sessionId = arguments[0]; action = Session.SessionActions.GET; } if (typeof arguments[1] === 'function') { callback = arguments[1]; sessionId = this.api.sessionId; } action = action.toUpperCase(); if (action !== Session.SessionActions.POST && !sessionId) { sessionId = this.api.sessionId; } return this.transportActions.sessionAction(action, sessionId, callback); } }; ================================================ FILE: lib/api/protocol/sessionLog.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Gets the text of the log type specified. To find out the available log types, use `.getLogTypes()`. * * Returns a [log entry JSON object](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#log-entry-json-object). * * @example * this.demoTest = function (browser) { * browser.sessionLog('client', function(result) { * console.log(result.value); * }); * } * * @syntax .sessionLog(typeString, callback) * @param {string} typeString Type of log to request. Can be one of: client, driver, browser, server * @param {function} callback Callback function which is called with the result value. * @returns {Array} Array of the text entries of the log. * @api protocol.sessions * @deprecated In favour of `.logs.getSessionLog()`. */ module.exports = class Action extends ProtocolAction { command(typeString, callback) { return this.transportActions.getLogContents(typeString, callback); } }; ================================================ FILE: lib/api/protocol/sessionLogTypes.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Gets an array of strings for which log types are available. This methods returns the entire WebDriver response, if you are only interested in the logs array, use `.getLogTypes()` instead. * * @example * this.demoTest = function (browser) { * browser.sessionLogTypes(function(result) { * console.log(result.value); * }); * } * * @param {function} callback Callback function which is called with the result value. * @api protocol.sessions * @deprecated In favour of `.logs.getSessionLogTypes()`. */ module.exports = class Action extends ProtocolAction { command(callback) { return this.transportActions.getSessionLogTypes(callback); } }; ================================================ FILE: lib/api/protocol/sessions.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Returns a list of the currently active sessions. * * @example * this.demoTest = function (browser) { * browser.sessions(function(result) { * console.log(result.value); * }); * } * * @method sessions * @editline L166 * @section sessions * @syntax .sessions(callback) * @param {function} callback Callback function which is called with the result value. * @api protocol.sessions */ module.exports = class Sessions extends ProtocolAction { command(callback) { return this.transportActions.getSessions(callback); } }; ================================================ FILE: lib/api/protocol/setAlertText.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Sends keystrokes to a JavaScript prompt() dialog. * * @link /#send-alert-text * @param {string} value Keystrokes to send to the prompt() dialog * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.userprompts * @deprecated In favour of `.alerts.setText()`. */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(value, callback) { return this.transportActions.setAlertText(value, callback); } }; ================================================ FILE: lib/api/protocol/setContext.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Sets the context. * * @param {string} context context name to switch to - a string representing an available context. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.mobile * @deprecated In favour of `.appium.setContext()` */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(context, callback) { return this.transportActions.setCurrentContext(context, callback); } }; ================================================ FILE: lib/api/protocol/setOrientation.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Sets the browser orientation. * * @param {string} orientation The new browser orientation: {LANDSCAPE|PORTRAIT} * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.mobile * @deprecated In favour of `.appium.setOrientation()` */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(orientation, callback) { orientation = orientation.toUpperCase(); if (!ProtocolAction.ScreenOrientation.includes(orientation)) { throw new Error('Invalid screen orientation value specified. Accepted values are: ' + ProtocolAction.ScreenOrientation.join(', ')); } return this.transportActions.setScreenOrientation(orientation, callback); } }; ================================================ FILE: lib/api/protocol/source.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Returns a string serialisation of the DOM of the current page. * * @link /#getting-page-source * @param {function} callback Callback function which is called with the result value. * @api protocol.document * @deprecated In favour of `.document.source()`. */ module.exports = class Session extends ProtocolAction { command(callback) { return this.transportActions.getPageSource(callback); } }; ================================================ FILE: lib/api/protocol/status.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Query the server's current status. * * @method status * @link /#status * @syntax .status([callback]) * @param {function} callback Callback function which is called with the result value. * @api protocol.sessions */ module.exports = class Action extends ProtocolAction { command(callback) { return this.transportActions.getStatus(callback); } }; ================================================ FILE: lib/api/protocol/submit.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Submit a FORM element. The submit command may also be applied to any element that is a descendant of a FORM element. * * @method submit * @param {string} webElementId The [Web Element ID](https://www.w3.org/TR/webdriver1/#dfn-web-elements) of the element to route the command to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.elementinternal */ module.exports = class Session extends ProtocolAction { static get isTraceable() { return true; } command(webElementId, callback) { ProtocolAction.validateElementId(webElementId, 'submit'); return this.transportActions.elementSubmit(webElementId, callback); } }; ================================================ FILE: lib/api/protocol/switchToWindow.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change focus to another window. The window to change focus to may be specified by its server assigned window handle, or by the value of its name attribute. * * To find out the window handle use `windowHandles` command * * @example * this.demoTest = function (browser) { * browser.windowHandles(function(result) { * var handle = result.value[0]; * browser.switchToWindow(handle); * }); * }; * * this.demoTestAsync = async function (browser) { * const result = await browser.windowHandles(); * var handle = result.value[0]; * browser.switchToWindow(handle); * }; * * * @method switchToWindow * @param {string} handleOrName The server assigned window handle or the name attribute. * @param {function} [callback] Optional callback function to be called when the command finishes. * @see window * @api protocol.contexts * @deprecated In favour of `.window.switchTo()`. */ module.exports = class Action extends ProtocolAction { static get namespacedAliases() { return 'switchWindow'; } static get isTraceable() { return true; } command(handleOrName, callback) { // TODO: error handling return this.transportActions.switchToWindow(handleOrName, callback); } }; ================================================ FILE: lib/api/protocol/timeouts.js ================================================ const Utils = require('../../utils'); const ProtocolAction = require('./_base-action.js'); /** * Configure or retrieve the amount of time that a particular type of operation can execute for before they are aborted and a |Timeout| error is returned to the client. * * If called with only a callback as argument, the command will return the existing configured timeout values. * * @example * this.demoTest = function (browser) { * browser.timeouts('script', 10000, function(result) { * console.log(result); * }); * * browser.timeouts(function(result) { * console.log('timeouts', result); * }); * } * * @method timeouts * @link /#set-timeout * @editline L188Í * @syntax .timeouts([callback]) * @syntax .timeouts(type, ms, [callback]) * @section sessions * @param {string} type The type of operation to set the timeout for. Valid values are "script" for script timeouts, "implicit" for modifying the implicit wait timeout and "pageLoad" (or "page load" for legacy JsonWire) for setting a page load timeout. * @param {number} ms The amount of time, in milliseconds, that time-limited commands are permitted to run. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ module.exports = class Timeouts extends ProtocolAction { static get TimeoutTypes() { return [ 'script', 'implicit', 'pageLoad' ]; } command(type, ms, callback) { if (Utils.isFunction(type) && arguments.length === 1 || arguments.length === 0) { return this.transportActions.getTimeouts(type); } if (!Timeouts.TimeoutTypes.includes(type)) { throw new Error(`Invalid timeouts type value: ${type}. Accepted values are: ${Timeouts.TimeoutTypes.join(', ')}`); } if (typeof ms != 'number') { throw new Error(`Second argument to .timeouts() command must be a number. ${ms} given.`); } return this.transportActions.setTimeoutType(type, ms, callback); } }; ================================================ FILE: lib/api/protocol/timeoutsAsyncScript.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Set the amount of time, in milliseconds, that asynchronous scripts executed by `.executeAsync` are permitted to run before they are aborted and a |Timeout| error is returned to the client. * * @example * this.demoTest = function (browser) { * browser.timeoutsAsyncScript(10000, function(result) { * console.log(result); * }); * } * * @method timeoutsAsyncScript * @syntax .timeoutsAsyncScript(ms, [callback]) * @jsonwire * @param {number} ms The amount of time, in milliseconds, that time-limited commands are permitted to run. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ module.exports = class Action extends ProtocolAction { command(ms, callback) { if (typeof ms != 'number') { throw new Error(`First argument to .timeoutsAsyncScript() command must be a number. ${typeof ms} given: ${ms}`); } return this.transportActions.setTimeoutsAsyncScript(ms, callback); } }; ================================================ FILE: lib/api/protocol/timeoutsImplicitWait.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Set the amount of time the driver should wait when searching for elements. If this command is never sent, the driver will default to an implicit wait of 0ms. * * @example * this.demoTest = function (browser) { * browser.timeoutsImplicitWait(10000, function(result) { * console.log(result); * }); * } * * @method timeoutsImplicitWait * @jsonwire * @syntax .timeoutsImplicitWait(ms, [callback]) * @param {number} ms The amount of time, in milliseconds, that time-limited commands are permitted to run. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.sessions */ module.exports = class Action extends ProtocolAction { command(ms, callback) { if (typeof ms != 'number') { throw new Error(`First argument to .timeoutsImplicitWait() command must be a number. ${typeof ms} given: ${ms}`); } return this.transportActions.setTimeoutsImplicitWait(ms, callback); } }; ================================================ FILE: lib/api/protocol/title.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Get the current page title. * * @example * this.demoTest = function (browser) { * browser.title(function(result) { * console.log(result.value); * }); * } * * @link /#get-title * @param {function} callback Callback function which is called with the result value. * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { command(callback) { return this.transportActions.getPageTitle(callback); } }; ================================================ FILE: lib/api/protocol/url.js ================================================ const ProtocolAction = require('./_base-action.js'); const ora = require('ora'); /** * Retrieve the URL of the current page or navigate to a new URL. * * @example * module.exports = { * 'demo Test' : function(browser) { * browser.url(function(result) { * // return the current url * console.log(result); * }); * * // navigate to new url: * browser.url('{URL}'); * * // navigate to new url: * browser.url('{URL}', function(result) { * console.log(result); * }); * } * } * * @method url * @link /#navigate-to * @syntax .url([url], [callback]) * @syntax .url(callback) * @param {string|function} [url] If missing, it will return the URL of the current page as an argument to the supplied callback. * @param {Function} [callback] * @api protocol.navigation */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(url, callback = function(r) {return r}) { if (typeof url == 'string') { const startTime = new Date(); let spinner; if (this.settings.output) { spinner = ora({ text: `Loading url: ${url}\n`, prefixText: ' ', discardStdin: false }).start(); } return this.transportActions.navigateTo(url).then(result => { if (spinner) { const ms = new Date() - startTime; spinner.info(`Loaded url ${url} in ${ms}ms`); } return callback.call(this.api, result); }); } if (typeof url == 'function') { callback = url; } return this.transportActions.getCurrentUrl().then(result => { return callback.call(this.api, result); }); } }; ================================================ FILE: lib/api/protocol/waitUntil.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Waits for a condition to evaluate to a "truthy" value. The condition may be specified by any function which * returns the value to be evaluated or a Promise to wait for. * * An optional wait time can be specified, otherwise the global waitForConditionTimeout value will be used. * * @example * describe('waitUntil Example', function() { * it('demo Test', function(browser) { * browser * .url('https://nightwatchjs.org) * .waitUntil(async function() { * const title = await this.execute(function() { * return document.title; * }); * * return title === 'Nightwatch.js'; * }, 1000); * }); * } * * @method waitUntil * @link https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html#wait * @selenium_webdriver * @syntax .waitUntil(conditionFn, [callback]) * @syntax .waitUntil(conditionFn, [waitTimeMs], [callback]) * @syntax .waitUntil(conditionFn, [waitTimeMs], [retryInterval], [callback]) * @syntax .waitUntil(conditionFn, [waitTimeMs], [retryInterval], [message], [callback]) * @param {function} conditionFn The condition to wait on, defined as function which returns a Promise * @param {number} [waitTimeMs] How long to wait for the condition to be true (in milliseconds). * @param {number} [retryInterval] The interval to use between checks (in milliseconds). * @param {Function} [callback] An optional callback which will be called with the result * @api protocol.utilities * @since 2.0.0 */ const {makePromise, isFunction, isUndefined, isNumber, isString} = require('../../utils'); module.exports = class WaitUntil extends ProtocolAction { get requiresSeleniumWebdriver() { return true; } static isParamTypeValid(param, expectedType) { if (typeof param !== expectedType) { // eslint-disable-next-line no-console console.warn(`Expected "${expectedType}" argument type for ${param}; "${typeof param}" given – using default value`); return false; } return true; } static hasMessage(msg) { return isString(msg) && msg || (msg instanceof Error); } static get isTraceable() { return true; } static get avoidPrematureParentNodeResolution() { return true; } static get rejectNodeOnAbortFailure() { return true; } //timeMs, retryInterval, message, callback = function(r) {return r} async command(conditionFn, ...args) { let callback = function(r) {return r}; let message; let timeMs = this.settings.globals.waitForConditionTimeout; let retryInterval = this.settings.globals.waitForConditionPollInterval; if (args.length >= 3 && WaitUntil.hasMessage(args[2])) { timeMs = WaitUntil.isParamTypeValid(args[0], 'number') ? args[0] : timeMs; retryInterval = WaitUntil.isParamTypeValid(args[1], 'number') ? args[1] : retryInterval; message = args[2]; callback = isFunction(args[3]) ? args[3] : callback; } else { switch (args.length) { case 2: case 3: timeMs = WaitUntil.isParamTypeValid(args[0], 'number') ? args[0] : timeMs; if (isFunction(args[1]) && isUndefined(args[2])) { callback = args[1]; } else { retryInterval = WaitUntil.isParamTypeValid(args[1], 'number') ? args[1] : retryInterval; callback = !isUndefined(args[2]) && WaitUntil.isParamTypeValid(args[2], 'function') ? args[2] : callback; } break; case 1: if (isFunction(args[0])) { callback = args[0]; } else if (isNumber(args[0])) { timeMs = args[0]; } break; } } const result = await this.transportActions.wait(() => { return conditionFn.bind(this.api); }, timeMs, isString(message) ? message : '', retryInterval); if (result && result.error instanceof Error) { await callback(result); if (message instanceof Error) { throw message; } throw result.error; } return makePromise(callback, this.api, [result]); } }; ================================================ FILE: lib/api/protocol/windowHandle.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the current window handle. * * @example * this.demoTest = function (browser) { * browser.windowHandle(function(result) { * console.log(result.value); * }); * } * * @link /#get-window-handle * @param {function} callback Callback function which is called with the result value. * @api protocol.window * @deprecated In favour of `.window.getHandle()`. */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.getWindowHandle(callback); } }; ================================================ FILE: lib/api/protocol/windowHandles.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Retrieve the list of all window handles available to the session. * * @example * this.demoTest = function (browser) { * browser.windowHandles(function(result) { * // An array of window handles. * console.log(result.value); * }); * } * * @link /#get-window-handles * @param {function} callback Callback function which is called with the result value. * @api protocol.window * @deprecated In favour of `.window.getAllHandles()`. */ module.exports = class Action extends ProtocolAction { static get isTraceable() { return true; } command(callback) { return this.transportActions.getAllWindowHandles(callback); } }; ================================================ FILE: lib/api/protocol/windowMaximize.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Increases the window to the maximum available size without going full-screen. * * @example * module.exports = { * 'demo Test with W3C Webdriver clients': function(browser) { * // W3C Webdriver API doesn't require the window handle parameter anymore * browser.windowMaximize(function(result) { * console.log(result); * }); * }, * * 'ES6 async demo Test': async function(browser) { * const result = await browser.windowMaximize(); * console.log('result value is:', result.value); * }, * * 'when using JSONWire (deprecated) clients': function(browser) { * browser.windowMaximize('current', function(result) { * console.log(result); * }); * } * } * * @link /#dfn-maximize-window * @param {string} [handleOrName] Only required when using non-W3C Webdriver protocols (such as JSONWire). windowHandle URL parameter; if it is "current", the currently active window will be maximized. * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.window * @deprecated In favour of `.window.maximize()`. */ module.exports = class Session extends ProtocolAction { command(handleOrName = 'current', callback) { return this.transportActions.maximizeWindow(handleOrName, callback); } }; ================================================ FILE: lib/api/protocol/windowPosition.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change or get the position of the specified window. If the second argument is a function it will be used as a callback and the call will perform a get request to retrieve the existing window position. * * @example * this.demoTest = function (browser) { * * // Change the position of the specified window. * // If the :windowHandle URL parameter is "current", the currently active window will be moved. * browser.windowPosition('current', 0, 0, function(result) { * console.log(result); * }); * * // Get the position of the specified window. * // If the :windowHandle URL parameter is "current", the position of the currently active window will be returned. * browser.windowPosition('current', function(result) { * console.log(result.value); * }); * } * * @see windowRect * @jsonwire * @param {string} windowHandle * @param {number} offsetX * @param {number} offsetY * @param {function} callback Callback function which is called with the result value. * @api protocol.window * @deprecated In favour of `.window.getPosition()` and `.window.setPosition()`. */ module.exports = class Session extends ProtocolAction { command(windowHandle, offsetX, offsetY, callback) { if (typeof windowHandle !== 'string') { throw new Error('First argument must be a window handle string.'); } if (arguments.length <= 2) { if (typeof arguments[1] != 'function') { throw new Error(`Second argument passed to .windowPosition() should be a callback when not passing offsetX and offsetY - ${typeof arguments[1]} given.`); } return this.transportActions.getWindowPosition(windowHandle, arguments[1]); } offsetX = Number(offsetX); offsetY = Number(offsetY); if (typeof offsetX !== 'number' || isNaN(offsetX)) { throw new Error('offsetX argument passed to .windowPosition() must be a number.'); } if (typeof offsetY !== 'number' || isNaN(offsetY)) { throw new Error('offsetY argument passed to .windowPosition() must be a number.'); } return this.transportActions.setWindowPosition(offsetX, offsetY, callback); } }; ================================================ FILE: lib/api/protocol/windowRect.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change or get the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). This is defined as a dictionary of the `screenX`, `screenY`, `outerWidth` and `outerHeight` attributes of the window. * * Its JSON representation is the following: * - `x` - window's screenX attribute; * - `y` - window's screenY attribute; * - `width` - outerWidth attribute; * - `height` - outerHeight attribute. * * All attributes are in in CSS pixels. To change the window react, you can either specify `width` and `height`, `x` and `y` or all properties together. * * @example * module.exports = { * 'demo test .windowRect()': function(browser) { * * // Change the screenX and screenY attributes of the window rect. * browser.windowRect({x: 500, y: 500}); * * // Change the width and height attributes of the window rect. * browser.windowRect({width: 600, height: 300}); * * // Retrieve the attributes * browser.windowRect(null, function(result) { * console.log(result.value); * }); * }, * * 'windowRect ES6 demo test': async function(browser) { * const resultValue = await browser.windowRect(null); * console.log('result value', resultValue); * } * } * * @w3c * @link /#dfn-get-window-rect * @syntax .windowRect({width, height, x, y}, [callback]); * @param {object} options An object specifying either `width` and `height`, `x` and `y`, or all together to set properties for the [window rect](https://w3c.github.io/webdriver/#dfn-window-rect). * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.window * @deprecated In favour of `.window.getRect()` and `.window.setRect()`. */ const Utils = require('../../utils'); module.exports = class Session extends ProtocolAction { command(options, callback = function(r) {return r}) { if (arguments[0] === null) { return this.transportActions.getWindowRect(callback); } const {width, height, x, y} = options; if (!Utils.isUndefined(width) && !Utils.isNumber(width)) { throw new Error(`Width argument passed to .windowRect() must be a number; received: ${typeof width} (${width}).`); } if (!Utils.isUndefined(height) && !Utils.isNumber(height)) { throw new Error(`Height argument passed to .windowRect() must be a number; received: ${typeof height} (${height}).`); } if (Utils.isNumber(width) && !Utils.isNumber(height) || !Utils.isNumber(width) && Utils.isNumber(height) ) { throw new Error('Attributes "width" and "height" must be specified together.'); } if (!Utils.isUndefined(x) && !Utils.isNumber(x)) { throw new Error(`X position argument passed to .windowRect() must be a number; received: ${typeof x} (${x}).`); } if (!Utils.isUndefined(y) && !Utils.isNumber(y)) { throw new Error(`Y position argument passed to .windowRect() must be a number; received: ${typeof y} (${y}).`); } if (Utils.isNumber(x) && !Utils.isNumber(y) || !Utils.isNumber(x) && Utils.isNumber(y) ) { throw new Error('Attributes "x" and "y" must be specified together.'); } return this.transportActions.setWindowRect(arguments[0], callback); } }; ================================================ FILE: lib/api/protocol/windowSize.js ================================================ const ProtocolAction = require('./_base-action.js'); /** * Change or get the size of the specified window. If the second argument is a function it will be used as a callback and the call will perform a get request to retrieve the existing window size. * * @example * this.demoTest = function (browser) { * * // Return the size of the specified window. If the :windowHandle URL parameter is "current", the size of the currently active window will be returned. * browser.windowSize('current', function(result) { * console.log(result.value); * }); * * // Change the size of the specified window. * // If the :windowHandle URL parameter is "current", the currently active window will be resized. * browser.windowSize('current', 300, 300, function(result) { * console.log(result.value); * }); * } * * @see windowRect * @jsonwire * @param {string} windowHandle * @param {number} width * @param {number} height * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol.window * @deprecated In favour of `.window.getSize()` and `.window.setSize()`. */ module.exports = class Session extends ProtocolAction { command(windowHandle, width, height, callback) { if (typeof windowHandle !== 'string') { throw new Error('First argument must be a window handle string.'); } if (arguments.length <= 2) { if (typeof arguments[1] != 'function') { throw new Error(`Second argument passed to .windowSize() should be a callback when not passing width and height - ${typeof arguments[1]} given.`); } return this.transportActions.getWindowSize(windowHandle, arguments[1]); } width = Number(width); height = Number(height); if (typeof width !== 'number' || isNaN(width)) { throw new Error('Width argument passed to .windowSize() must be a number.'); } if (typeof height !== 'number' || isNaN(height)) { throw new Error('Height argument passed to .windowSize() must be a number.'); } return this.transportActions.setWindowSize(width, height, callback); } }; ================================================ FILE: lib/api/web-element/assert/element-assertions.js ================================================ const {WebElement} = require('selenium-webdriver'); class ScopedElementAssertions { constructor(scopedElement, {negated, nightwatchInstance}) { this.negated = negated; this.scopedElement = scopedElement; this.nightwatchInstance = nightwatchInstance; } 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; 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); } }); // 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) { return this.nightwatchInstance.transportActions.executeScript(scriptFn, [this.scopedElement.webElement, ...args]).then(({value}) => value); } } module.exports.create = function createAssertions(scopedElement, {negated = false, nightwatchInstance}) { const instance = new ScopedElementAssertions(scopedElement, { negated, nightwatchInstance }); const exported = {}; Object.defineProperty(exported, 'not', { get() { return createAssertions(scopedElement, { negated: true, nightwatchInstance }); } }); Object.defineProperties(exported, { enabled: { value(message) { return instance.assert((assertApi, element) => assertApi.enabled(element, message)); }, enumerable: true, configurable: false }, selected: { value(message) { return instance.assert((assertApi, element) => assertApi.selected(element, message)); }, enumerable: true, configurable: false }, visible: { value(message) { return instance.assert((assertApi, element) => assertApi.visible(element, message)); }, enumerable: true, configurable: false }, hasDescendants: { value(message) { return instance.assert((assertApi, element) => assertApi.hasDescendants(element, message)); }, enumerable: true, configurable: false }, present: { value(message) { return instance.assert(async (assertApi, element) => { const el = await element; const id = await el?.getId(); return assertApi.ok(el instanceof WebElement, message || `Testing if the element is present.`); }); }, enumerable: true, configurable: false }, hasClass: { async value(name, message = `Testing if the element has "${name}" class.`) { const result = await instance.executeScript(function(element, className) { return element.classList.contains(className); }, [name]); return instance.assert((assertApi) => assertApi.ok(result, message)); }, enumerable: true, configurable: false }, hasAttribute: { async value(name, message = `Testing if the element has "${name}" attribute.`) { const result = await instance.executeScript(function(element, attributeName) { return element.hasAttribute(attributeName); }, [name]); return instance.assert((assertApi) => assertApi.ok(result, message)); }, enumerable: true, configurable: false }, customScript: { async value(scriptFn = function(el) {return el}, args = [], message = '') { const result = await instance.executeScript(scriptFn, args); return instance.assert((assertApi) => assertApi.ok(result, message)); }, enumerable: true, configurable: false } }); return exported; }; ================================================ FILE: lib/api/web-element/assert/elements-assertions.js ================================================ class ElementsAssertions { constructor(scopedElements, {negated, nightwatchInstance}) { this.negated = negated; this.scopedElements = scopedElements; this.nightwatchInstance = nightwatchInstance; } get not() { return new ElementsAssertions(this.scopedElements, { negated: true, nightwatchInstance: this.nightwatchInstance }); } } module.exports.ScopedElementsAssertions = ElementsAssertions; ================================================ FILE: lib/api/web-element/assert/value-assertions.js ================================================ const AssertionError = require('assertion-error'); module.exports.create = function createAssertions(scopedValue, {negated, nightwatchInstance}) { class ValueAssertions { constructor(scopedValue, {negated}) { this.negated = negated; this.scopedValue = scopedValue; } _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 assertPromise; } get not() { return createAssertions(this.scopedValue, { negated: true, nightwatchInstance }); } contains(expected, message) { return this._assert((assertApi, value) => { return assertApi.promisedValue({ callback() { return value; }, verb: 'contains', message, expected, evaluate(value) { return value.includes(expected); }, timeout: 0 }); }); } equals(expected, message) { return this._assert((assertApi, value) => { return assertApi.promisedValue({ callback() { return value; }, verb: 'equals', message, expected, evaluate(value) { return value === expected; }, timeout: 0 }); }); } matches(expected, message) { return this._assert((assertApi, value) => { return assertApi.promisedValue({ callback() { return value; }, verb: 'contains', message, expected, evaluate(value) { return expected.test(value); }, failure(expected) { if (expected instanceof RegExp) { return null; } return new AssertionError(`Expected value must be a regular expression; ${typeof expected} given.`); }, timeout: 0 }); }); } } return new ValueAssertions(scopedValue, { negated }); }; ================================================ FILE: lib/api/web-element/commands/check.js ================================================ /** * 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.7.0 * @method check * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).check() * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('checkElement'); }; ================================================ FILE: lib/api/web-element/commands/clear.js ================================================ /** * Clear a textarea or a text input element's value. * This command has no effect if the underlying DOM element is neither a text INPUT element nor a TEXTAREA element. * * 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=text]').clear(); * * browser.element('textarea').clear(); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('input[type=text]').clear(); * * await browser.element('textarea').clear(); * } * } * * @since 3.0.0 * @method clear * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).clear() * @link /#dfn-element-clear * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('clearElementValue'); }; ================================================ FILE: lib/api/web-element/commands/click.js ================================================ /** * Simulates a click event on the given DOM element. * 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('button.submit-form').click(); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('button.submit-form').click(); * } * } * * @since 3.0.0 * @method click * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).click() * @link /#dfn-element-click * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('clickElement'); }; ================================================ FILE: lib/api/web-element/commands/clickAndHold.js ================================================ /** * Move to the element and click (without releasing) in the middle of the given element. * * 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('button.submit-form').clickAndHold(); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('button.submit-form').clickAndHold(); * } * } * * @since 3.0.0 * @method clickAndHold * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).clickAndHold() * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#click-and-hold * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('pressAndHold'); }; ================================================ FILE: lib/api/web-element/commands/doubleClick.js ================================================ /** * Move to the element and peforms a double-click in the middle of the element. * *

The command doubleClick() will automatically wait for the element to be present (until the specified timeout). If the element is not found, an error is thrown which will cause the test to fail. You can suppress element not found errors by specifying the selector argument as an object and passing the suppressNotFoundErrors = true option.

* * 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('button.submit-form').doubleClick(); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('button.submit-form').doubleClick(); * } * } * * @since 3.0.0 * @method doubleClick * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).doubleClick() * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#double-click * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('doubleClick'); }; ================================================ FILE: lib/api/web-element/commands/dragAndDrop.js ================================================ /** * Drag an element to the given position or destination element. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * browser.element.find('.section').dragAndDrop({ x: 100, y: 100 }); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * // using destination coordinates * browser.element.find('.section').dragAndDrop({ x: 0, y: 500 }); * * // using WebElement of destination * const destWebElement = await browser.element.find('.dest-section'); * browser.element.find('.section').dragAndDrop(destWebElement); * } * } * * @since 3.0.0 * @method dragAndDrop * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).dragAndDrop({x, y}) * @syntax browser.element(selector).dragAndDrop() * @returns {ScopedWebElement} */ module.exports.command = function(destination) { return this.runQueuedCommand('dragElement', { args: [destination] }); }; ================================================ FILE: lib/api/web-element/commands/find.js ================================================ /** * Search for an element on the page. The located element will be returned as a special web element object (with added convenience methods). * The argument is the element selector, either specified as a string or as an object (with 'selector' and 'locateStrategy' properties). * Elements can be searched by using another element as the starting point. * If many elements match the locating criteria, only first one is returned. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Using element function (alias for find). * const button1 = browser.element('button.submit-form'); * // Using the find method of the element namespace. * const button2 = browser.element.find('button.submit-form'); * // Searching for the icon element inside the .submit-form button. * const icon = button2.find('i'); * * // Use an object to customise locating behaviour. * const main = browser.element({ selector: 'main', locateStrategy: 'css selector' }); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * // button is the WebElement object. * const button = await browser.element('button.submit-form'); * } * } * * @since 3.0.0 * @method find * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector) * @param {String|Object} selector * @returns {ScopedWebElement} * @alias get * @alias findElement */ module.exports.command = function(selector) { return this.createScopedElement(selector, this); }; ================================================ FILE: lib/api/web-element/commands/findAll.js ================================================ /** * Search for elements on the page. The located elements will be returned as a special web element object (with added convenience methods). * The argument is the element selector, either specified as a string or as an object (with 'selector' and 'locateStrategy' properties). * Elements can be searched by using another element as the starting point. * * @example * export default { * async demoTest(browser: NightwatchAPI): Promise { * const buttonsElement = browser.element.findAll('button.submit-form'); * * // Get an array of found elements. * const buttons = await buttonsElement; * * // Use an object to customise locating behaviour. * const sections = browser.element * .findAll({ selector: 'section', locateStrategy: 'css selector' }); * } * } * * @since 3.0.0 * @method findAll * @memberof ScopedWebElement * @instance * @syntax browser.element.findAll(syntax) * @param {Selector} selector * @returns {Array.} * @alias getAll * @alias findElements */ module.exports.command = function(selector) { return this.createScopedElements(selector, {parentElement: this, commandName: 'findAll'}); }; ================================================ FILE: lib/api/web-element/commands/findAllByAltText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for elements on the page that have the `alt` attribute with a specified text. * Elements can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const images = browser.element.findAllByAltText('group of people'); * * // Search for the exact occurrence. * const images = browser.element.findAllByAltText( * 'A group of people sitting in front of a computer.', * { exact: true } * ); * } * } * * @since 3.0.0 * @method findAllByAltText * @memberof ScopedWebElement * @instance * @syntax browser.element.findAllByAltText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {Array.} */ module.exports.command = function (text, {exact = true, ...options} = {}) { const comparingModifier = exact ? '' : '*'; return this.findAll({ ...options, selector: By.css(`[alt${comparingModifier}="${text}"]`) }); }; ================================================ FILE: lib/api/web-element/commands/findAllByPlaceholderText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for elements on the page that have the `placeholder` attribute with a specified text. * Elements can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const inputs = browser.element.findAllByPlaceholderText('group of people'); * * // Search for the exact occurrence. * const images = browser.element.findAllByPlaceholderText( * 'Search here', * { exact: true } * ); * * const images = browser.element.findAllByPlaceholderText( * 'Enter the number', * { exact: true, suppressNotFoundError: true } * ); * } * } * * @since 3.0.0 * @method findAllByPlaceholderText * @memberof ScopedWebElement * @instance * @syntax browser.element.findAllByPlaceholderText(text, [options]) * @param {string} text * @param { {exact: boolean, index: number, timeout: number, retryInterval: number, suppressNotFoundErrors: boolean} } [options] * @returns {Array.} */ module.exports.command = function (text, {exact = true, ...options} = {}) { const comparingModifier = exact ? '' : '*'; return this.findAll({ ...options, selector: By.css(`[placeholder${comparingModifier}="${text}"]`) }); }; ================================================ FILE: lib/api/web-element/commands/findAllByRole.js ================================================ const {By} = require('selenium-webdriver'); const {roles, roleElements} = require('aria-query'); /** * Search for elements on the page that meet the provided ARIA role. * Elements can be searched by using another element as the starting point. * * You can pass some options to narrow the search: * - `selected` - picks elements that has the `area-selected` attribute with `true` or `false` value. * - `checked` - picks elements that has the `area-checked` attribute with `true` or `false` value. * - `pressed` - picks elements that has the `area-pressed` attribute with `true` or `false` value. * - `current` - picks elements that has the `area-current` attribute with `true` or `false` value. * - `level` - picks elements that has the `area-level` attribute with `true` or `false` value. This option is suitable only for the `heading` role. * - `expanded` - picks elements that has the `area-expanded` attribute with `true` or `false` value. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * const comboboxes = browser.element.findAllByRole('combobox'); * * const headings = browser.element.findAllByRole( * 'heading', * { level: 2 } * ); * } * } * * @since 3.0.0 * @method findAllByRole * @memberof ScopedWebElement * @instance * @syntax browser.element.findAllByRole(role, [options]) * @param {string} role * @param {{selected: boolean, checked: boolean, pressed: boolean, current: boolean, level: number, expanded: boolean}} [options] * @returns {Array.} */ module.exports.command = function(role, {selected, checked, pressed, current, level, expanded, ...options} = {}) { const roleInformation = roles.get(role); if (!roleInformation) { throw new Error(`The specified role "${role}" is unknown.`); } if (selected !== undefined) { // guard against unknown roles if (roleInformation.props['aria-selected'] === undefined) { throw new Error(`"aria-selected" is not supported on role "${role}".`); } } if (checked !== undefined) { // guard against unknown roles if (roleInformation.props['aria-checked'] === undefined) { throw new Error(`"aria-checked" is not supported on role "${role}".`); } } if (pressed !== undefined) { // guard against unknown roles if (roleInformation.props['aria-pressed'] === undefined) { throw new Error(`"aria-pressed" is not supported on role "${role}".`); } } if (current !== undefined) { // guard against unknown roles // All currently released ARIA versions support `aria-current` on all roles. // Leaving this for symmetry and forward compatibility if (roleInformation.props['aria-current'] === undefined) { throw new Error(`"aria-current" is not supported on role "${role}".`); } } if (level !== undefined) { // guard against using `level` option with any role other than `heading` if (role !== 'heading') { throw new Error(`Role "${role}" cannot have "level" property.`); } } if (expanded !== undefined) { // guard against unknown roles if (roleInformation.props['aria-expanded'] === undefined) { throw new Error(`"aria-expanded" is not supported on role "${role}".`); } } const explicitRoleSelector = `*[role~="${role}"]`; const roleRelations = roleElements.get(role) || new Set(); const implicitRoleSelectors = new Set(Array.from(roleRelations).map(({name}) => name)); const selector = By.css([explicitRoleSelector].concat(Array.from(implicitRoleSelectors)).join(',')); return this.createScopedElements({selector, ...options}, {parentElement: this, commandName: 'findAllByRole'}); }; ================================================ FILE: lib/api/web-element/commands/findAllByText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for elements on the page that contain a specified text. * Elements can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const inputs = browser.element.findAllByText('group of people'); * * // Search for the exact occurrence. * const images = browser.element.findAllByText( * 'The nostalgic office', * { exact: true } * ); * } * } * * @since 3.0.0 * @method findAllByText * @memberof ScopedWebElement * @instance * @syntax browser.element.findAllByText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {Array.} */ module.exports.command = function (text, {exact = true} = {}) { const expr = exact ? `text()="${text}"` : `contains(text(),"${text}")`; const selector = By.xpath(`.//*[${expr}]`); return this.createScopedElements({selector}, {parentElement: this, commandName: 'findAllByText'}); }; ================================================ FILE: lib/api/web-element/commands/findByAltText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for an element on the page that has the `alt` attribute with a specified text. * Element can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring mathing. * const image = browser.element.findByAltText('group of people'); * * // Search for the exact occurrence. * const image = browser.element.findByAltText( * 'A group of people sitting in front of a computer.', * { exact: true } * ); * } * } * * @since 3.0.0 * @method findByAltText * @memberof ScopedWebElement * @instance * @syntax browser.element.findByAltText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {Array.} */ module.exports.command = function (text, {exact = true, ...options} = {}) { const comparingModifier = exact ? '' : '*'; return this.find({ ...options, selector: By.css(`[alt${comparingModifier}="${text}"]`) }); }; ================================================ FILE: lib/api/web-element/commands/findByLabelText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for the label that matches the given text, then find the element associated with that label. * Element can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const image = browser.element.findByLabelText('dreamers'); * * // Search for the exact occurrence. * const image = browser.element.findByLabelText( * 'A group of people sitting in front of a computer.', * { exact: true } * ); * } * } * * @since 3.0.0 * @method findByLabelText * @memberof ScopedWebElement * @instance * @syntax browser.element.findByLabelText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {ScopedWebElement} */ module.exports.command = function (text, {exact = true, timeout, retryInterval, suppressNotFoundErrors} = {}) { const findByForId = async (instance, labelElement) => { if (!labelElement) { return null; } const forAttribute = await labelElement.getAttribute('for'); if (!forAttribute) { return null; } const element = await instance.waitUntilElementsLocated({ selector: By.css(`input[id="${forAttribute}"]`), timeout, retryInterval }); return element; }; const findByAriaLabelled = async (instance, labelWebElement) => { if (!labelWebElement) { return null; } const idAttribute = await labelWebElement.getAttribute('id'); if (!idAttribute) { return null; } return instance.waitUntilElementsLocated({ selector: By.css(`input[aria-labelledby="${idAttribute}"]`), timeout, retryInterval }); }; const findByDirectNesting = async (labelWebElement) => { if (!labelWebElement) { return null; } try { return await labelWebElement.findElement(By.css('input')); } catch (err) { return null; } }; const findByDeepNesting = async (text, {exact}) => { const expr = exact ? `*[text()="${text}"]` : `*[contains(text(), "${text}")]`; const selector = By.xpath(`.//label[${expr}]`); const labelElement = await this.find({ selector, timeout, retryInterval, suppressNotFoundErrors: true }); if (!labelElement) { return null; } try { return await labelElement.findElement(By.css('input')); } catch (err) { return null; } }; const findByAriaLabel = async (text, {exact}) => { const labelElement = await this.find({ selector: By.css(`input[aria-label${exact ? '' : '*'}="${text}"]`), timeout, retryInterval, suppressNotFoundErrors: true }); return labelElement; }; const findFromLabel = async (instance, labelElement) => { let element = null; if (labelElement) { try { element = await findByForId(instance, labelElement); } catch (err) { // ignore } if (!element) { try { element = await findByAriaLabelled(instance, labelElement); } catch (err) { // ignore } } if (!element) { try { element = await findByDirectNesting(labelElement); } catch (err) { // ignore } } } return element; }; const createAction = function (labelElement) { const instance = this; return async function() { const element = await findFromLabel(instance, labelElement); if (element) { return element; } const error = new Error(`The element associated with label whose text ${exact ? 'equals' : 'contains'} "${text}" has not been found.`); if (!suppressNotFoundErrors) { throw error; } return null; }; }; const expr = exact ? `text()="${text}"` : `contains(text(),"${text}")`; const selector = By.xpath(`.//label[${expr}]`); // eslint-disable-next-line no-async-promise-executor return this.createScopedElement(new Promise(async (resolve, reject) => { const labelElement = await this.find({ selector, timeout, retryInterval, suppressNotFoundErrors: true }); if (labelElement) { const node = this.queueAction({name: 'findByLabelText', createAction: function () { return createAction.call(this, labelElement); }}); node.deferred.promise.then(resolve, reject); return; } const byDeepNesting = await findByDeepNesting(text, {exact}); if (byDeepNesting) { return resolve(byDeepNesting); } const byAriaLabel = await findByAriaLabel(text, {exact}); if (byAriaLabel) { return resolve(byAriaLabel); } resolve(null); })); }; ================================================ FILE: lib/api/web-element/commands/findByPlaceholderText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for an element on the page that have the `placeholder` attribute with a specified text. * Element can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const inputs = browser.element.findByPlaceholderText('group of people'); * * // Search for the exact occurrence. * const images = browser.element.findByPlaceholderText( * 'Search here', * { exact: true } * ); * * const images = browser.element.findByPlaceholderText( * 'Enter the number', * { exact: true, suppressNotFoundError: true } * ); * } * } * * @since 3.0.0 * @method findByPlaceholderText * @memberof ScopedWebElement * @instance * @syntax browser.element.findByPlaceholderText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {ScopedWebElement} */ module.exports.command = function (text, {exact = true, ...options} = {}) { const comparingModifier = exact ? '' : '*'; return this.find({ ...options, selector: By.css(`[placeholder${comparingModifier}="${text}"]`) }); }; ================================================ FILE: lib/api/web-element/commands/findByRole.js ================================================ /** * Search for an element on the page that meets the provided ARIA role. * Element can be searched by using another element as the starting point. * * You can pass some options to narrow the search: * - `selected` - picks an element that has the `area-selected` attribute with `true` or `false` value. * - `checked` - picks an element that has the `area-checked` attribute with `true` or `false` value. * - `pressed` - picks an element that has the `area-pressed` attribute with `true` or `false` value. * - `current` - picks an element that has the `area-current` attribute with `true` or `false` value. * - `level` - picks an element that has the `area-level` attribute with `true` or `false` value. This option is suitable only for the `heading` role. * - `expanded` - picks an element that has the `area-expanded` attribute with `true` or `false` value. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * const comboboxes = browser.element.findByRole('combobox'); * * const headings = browser.element.findByRole('heading', { level: 2 }); * } * } * * @since 3.0.0 * @method findByRole * @memberof ScopedWebElement * @instance * @syntax browser.element.findByRole(role, [options]) * @param {string} role * @param {{selected: boolean, checked: boolean, pressed: boolean, current: boolean, level: number, expanded: boolean}} [options] * @returns {ScopedWebElement} */ module.exports.command = function(role, options) { // eslint-disable-next-line no-async-promise-executor return this.createScopedElement(new Promise(async (resolve, reject) => { try { const elements = await this.findAllByRole(role, options); const element = elements[0]; if (!element) { throw new Error(`The element with "${role}" role is not found.`); } resolve(element); } catch (error) { reject(error); } }), this); }; ================================================ FILE: lib/api/web-element/commands/findByText.js ================================================ const {By} = require('selenium-webdriver'); /** * Search for an element on the page that contains a specified text. * Element can be searched by using another element as the starting point. * By default, provided text is treated as a substring, so for the `'foo'` will match `'foobar'` also. * If you need an exact comparison, provide the `{ exact: true }` as the second parameter. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * // Search by the substring matching. * const inputs = browser.element.findByText('group of people'); * * // Search for the exact occurrence. * const images = browser.element.findByText( * 'The nostalgic office', * { exact: true } * ); * } * } * * @since 3.0.0 * @method findByText * @memberof ScopedWebElement * @instance * @syntax browser.element.findByText(text, [options]) * @param {string} text * @param {{exact: boolean}} [options] * @returns {ScopedWebElement} */ module.exports.command = function(text, {exact = true, ...options} = {}) { const selector = exact ? By.xpath(`.//*[text()="${text}"]`) : By.xpath(`.//*[contains(text(),"${text}")]`); return this.find({ ...options, selector }); }; ================================================ FILE: lib/api/web-element/commands/getAccessibleName.js ================================================ /** * Returns the computed WAI-ARIA label of an element. * * 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('*[name="search"]') * .getAccessibleName() * .assert.valueEquals('Country calling code'); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * const result: string = await browser.element('*[name="search"]').getAccessibleName(); * console.log('getAccessibleName is ', result); * } * } * * @since 3.0.0 * @method getAccessibleName * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).getAccessibleName() * @syntax browser.element.find(selector).getComputedLabel() * @link /#get-computed-label * @returns {ScopedValue} A container with accessible name of an element. * @alias getComputedLabel */ module.exports.command = function() { return this.runQueuedCommandScoped('getElementAccessibleName'); }; ================================================ FILE: lib/api/web-element/commands/getAriaRole.js ================================================ /** * Returns the computed WAI-ARIA role of an element. * * 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('*[name="search"]').getAriaRole() * .assert.valueEquals('combobox'); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('*[name="search"]').getAriaRole(); * console.log('getAriaRole result', result); * } * } * * @since 3.0.0 * @method getAriaRole * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).getAriaRole() * @syntax browser.element.find(selector).getComputedRole() * @link /#get-computed-role * @returns {ScopedValue} The container with computed WAI-ARIA role of an element. * @alias getComputedRole */ module.exports.command = function() { return this.runQueuedCommandScoped('getElementAriaRole'); }; ================================================ FILE: lib/api/web-element/commands/getAttribute.js ================================================ /** * Retrieve the value of an attribute for a given DOM element. * * 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('#main ul li a.first') * .getAttribute('target') * .assert.valueEquals('_blank'); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#main ul li a.first').getAttribute('href'); * console.log('attribute', result); * } * } * * @since 3.0.0 * @method getAttribute * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getAttribute(name) * @link /#get-element-attribute * @param {string} name The attribute name to inspect. * @returns {ScopedValue} The value of the attribute */ module.exports.command = function(name) { return this.runQueuedCommandScoped('getElementValue', name); }; ================================================ FILE: lib/api/web-element/commands/getCssProperty.js ================================================ /** * Retrieve the value of a CSS property for a given DOM element. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * * @tsexample * export default { * demoTest(browser: NightwatchAPI): void { * browser.element('#main ul li a.first').getCssProperty('display') * .assert.valueEquals('block'); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#main ul li a.first').getCssProperty('display'); * console.log('display', result); * } * } * * @example * export default { * demoTest({ element }) { * element('#main ul li a.first') * .getCssProperty('display') * .assert.valueEquals('block'); * }, * * async demoTestAsync({ element }) { * const result = await element('#main ul li a.first').getCssProperty('display'); * console.log('display', result); * } * } * * @since 3.0.0 * @method getCssProperty * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getCssProperty(name) * @link /#get-element-css-value * @param {string} cssProperty The CSS property to inspect. * @returns {ScopedValue} The container with a value of the css property */ module.exports.command = function(cssProperty) { return this.runQueuedCommandScoped('getElementCSSValue', cssProperty); }; ================================================ FILE: lib/api/web-element/commands/getFirstElementChild.js ================================================ /** * Returns an element's first child. The child element will be returned as web element JSON object (with an added .getId() convenience method). * * 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('.features-container').getFirstElementChild().then(resultElement => { * console.log('first child element Id:', resultElement.getId()); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const resultElement = await browser.element('.features-container').getFirstElementChild(); * console.log('first child element Id:', resultElement.getId()); * } * } * * @since 3.0.0 * @method getFirstElementChild * @memberof ScopedWebElement * @instance * @tutorial developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function getFirstElementChild() { return actions.executeScript(function(element) { return element.firstElementChild; }, [webElement]); }; const node = this.queueAction({name: 'getFirstElementChild', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getId.js ================================================ /** * Returns the element ID * * @example * export default { * demoTest(browser: NightwatchAPI): void { * const elementId = browser.element('#main').getId(); * console.log('element id:', elementId) * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const elementId = await browser.element('#main').getId(); * console.log('element id:', elementId); * }, * } * * @since 3.0.0 * @method getId * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getId() * @returns {ScopedValue} */ module.exports.command = function() { return this.runQueuedCommandScoped(function (webElement) { return webElement.getId(); }); }; ================================================ FILE: lib/api/web-element/commands/getLastElementChild.js ================================================ /** * Returns an element's last child. The child element will be returned as web element JSON object (with an added .getId() convenience method). * * 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('.features-container').getLastElementChild().then(resultElement => { * console.log('last child element Id:', resultElement.getId()); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const resultElement = await browser.element('.features-container').getLastElementChild(); * console.log('last child element Id:', resultElement.getId()); * } * } * * @since 3.0.0 * @method getLastElementChild * @memberof ScopedWebElement * @instance * @tutorial developer.mozilla.org/en-US/docs/Web/API/Element/lastElementChild * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function getLastElementChild() { return actions.executeScript(function(element) { return element.lastElementChild; }, [webElement]); }; const node = this.queueAction({name: 'getLastElementChild', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getNextElementSibling.js ================================================ /** * Returns the element immediately following the specified one in their parent's childNodes. The element will be returned as web element JSON object (with an added .getId() convenience method). * * 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('.features-container li:first-child').getNextElementSibling().then(resultElement => { * console.log('next sibling element Id:', resultElement.getId()); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const resultElement = await browser.element('.features-container li:first-child').getNextElementSibling(); * console.log('next sibling element Id:', resultElement.getId()); * } * } * * @since 3.0.0 * @method getNextElementSibling * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getNextElementSibling() * @tutorial developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function() { return actions.executeScript(function(element) { return element.nextElementSibling; }, [webElement]); }; const node = this.queueAction({name: 'getNextElementSibling', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getParentElement.js ================================================ /** * Returns an element's last child. The child element will be returned as web element JSON object (with an added .getId() convenience method). * * 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('.features-container').getParentElement().then(resultElement => { * console.log('parent element Id:', resultElement.getId()); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const resultElement = await browser.element('.features-container').getParentElement(); * console.log('parent element Id:', resultElement.getId()); * } * } * * @since 3.3.1 * @method getParentElement * @memberof ScopedWebElement * @instance * @tutorial https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function getLastElementChild() { return actions.executeScript(function(element) { return element.parentElement; }, [webElement]); }; const node = this.queueAction({name: 'getParentElement', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getPreviousElementSibling.js ================================================ /** * Returns the element immediately preceding the specified one in its parent's child elements list. The element will be returned as web element JSON object (with an added `.getId()` convenience method). * * 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('.features-container li:second-child').getPreviousElementSibling().then(resultElement => { * console.log('previous sibling element Id:', resultElement.getId()); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const resultElement = await browser.element('.features-container li:second-child').getPreviousElementSibling(); * console.log('previous sibling element Id:', resultElement.getId()); * } * } * * @since 3.0.0 * @method getPreviousElementSibling * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getPreviousElementSibling() * @tutorial developer.mozilla.org/en-US/docs/Web/API/Element/previousElementSibling * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function() { return actions.executeScript(function(element) { return element.previousElementSibling; }, [webElement]); }; const node = this.queueAction({name: 'getPreviousElementSibling', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getProperty.js ================================================ /** * Retrieve the value of a specified DOM property for the given element. For all the available DOM element properties, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * * @tsexample * import { NightwatchBrowser } from 'nightwatch'; * * export default { * demoTest(browser: NightwatchAPI): void { * const result = browser.element('#login input[type=text]').getProperty('classList'); * console.log('classList', result); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#login input[type=text]').getProperty('classList'); * console.log('classList', result); * } * } * * @example * export default { * demoTest(browser) { * const result = browser.element('#login input[type=text]').getProperty('classList'); * console.log('classList', result); * }, * * async demoTestAsync(browser) { * const result = await browser.element('#login input[type=text]').getProperty('classList'); * console.log('classList', result); * } * } * * @since 3.0.0 * @method getProperty * @memberof ScopedWebElement * @param {string} name element property * @instance * @syntax browser.element(selector).getProperty(name) * @link /#get-element-property * @returns {ScopedValue} */ module.exports.command = function (name) { return this.runQueuedCommandScoped('getElementProperty', name); }; ================================================ FILE: lib/api/web-element/commands/getRect.js ================================================ /** * Determine an element's size in pixels. * * For W3C Webdriver compatible clients (such as GeckoDriver), this command is equivalent to `getLocation` and both return the dimensions and coordinates of the given element: * - x: X axis position of the top-left corner of the element, in CSS pixels * - y: Y axis position of the top-left corner of the element, in CSS pixels * - height: Height of the element’s bounding rectangle in CSS pixels; * - width: Width of the web element’s bounding rectangle in CSS pixels. * * 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 { * const result = browser.element('#login').getRect(); * console.log('result', result); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#login').getRect(); * console.log('result', result); * } * } * * @since 3.0.0 * @method getRect * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getRect() * @link /#dfn-get-element-rect * @returns {ScopedValue<{ width: number, height: number }>} * @alias getSize * @alias getLocation */ module.exports.command = function () { return this.runQueuedCommandScoped('getElementRect'); }; ================================================ FILE: lib/api/web-element/commands/getShadowRoot.js ================================================ /** * Returns the `shadowRoot` read-only property which represents the shadow root hosted by the element. This can further be used to retrieve elements part of the shadow root element. * * 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('#login').getShadowRoot().then(result => { * console.log('shadowRootEl', result); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#login').getShadowRoot(); * console.log('shadowRootEl', result); * } * } * * @since 3.0.0 * @method getShadowRoot * @memberof ScopedWebElement * @instance * @tutorial developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot * @returns {ScopedWebElement} */ module.exports.command = function() { const createAction = (actions, webElement) => function() { return actions.getShadowRoot(webElement); }; const node = this.queueAction({name: 'getShadowRoot', createAction}); return this.createScopedElement(node.deferred.promise); }; ================================================ FILE: lib/api/web-element/commands/getTagName.js ================================================ /** * Query for an element's tag name. * * 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 { * const result = browser.element('#login').getTagName(); * console.log('element tag:', result); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#login').getTagName(); * console.log('element tag:', result); * } * } * * @since 3.0.0 * @method getTagName * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getTagName() * @link /#get-element-tag-name * @returns {ScopedValue} */ module.exports.command = function() { return this.runQueuedCommandScoped('getElementTagName'); }; ================================================ FILE: lib/api/web-element/commands/getText.js ================================================ /** * Returns the visible text for the element. * * 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('#main ul li a.first') * .getText() * .assert.contains('custom text'); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#main ul li a.first').getText(); * console.log('element text:', result); * } * } * * @since 3.0.0 * @method getText * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getText() * @link /#get-element-text * @returns {ScopedValue} */ module.exports.command = function() { return this.runQueuedCommandScoped('getElementText'); }; ================================================ FILE: lib/api/web-element/commands/getValue.js ================================================ /** * Returns a form element current value. * * 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 { * const result = browser.element('#login input[type=text]').getValue(); * console.log('Value', result); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const result = await browser.element('#login input[type=text]').getValue(); * console.log('Value', result); * } * } * * @since 3.0.0 * @method getValue * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).getValue() * @link /#get-element-property * @returns {ScopedValue} */ module.exports.command = function () { return this.getProperty('value'); }; ================================================ FILE: lib/api/web-element/commands/inspectInDevTools.js ================================================ /** * Inspect element in DevTools. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * const lastElement = browser.element('#nestedt').getLastElementChild().inspectInDevTools('lastChild'); * console.log('last element:', lastElement); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const lastElement = await browser.element('#nestedt').getLastElementChild().inspectInDevTools('lastChild'); * console.log('last element:', lastElement); * } * } * * @since 3.0.0 * @method inspectInDevTools * @memberof ScopedWebElement * @param {string} txt * @instance * @returns {ScopedWebElement} */ module.exports.command = function(txt) { return this.runQueuedCommand('inspectInDevTools', {args: [txt]}); }; ================================================ FILE: lib/api/web-element/commands/isActive.js ================================================ /** * Determines if an element is currently active/focused in the DOM. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * For more info on the new `browser.element.find()` syntax, refer to the new Element API Overview 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} */ module.exports.command = function () { return this.runQueuedCommandScoped('isElementActive'); }; ================================================ FILE: lib/api/web-element/commands/isEnabled.js ================================================ /** * Determines if an element is enabled. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * For more info on the new `browser.element.find()` syntax, refer to the new Element API Overview page. * * @example * describe('isEnabled Demo', function() { * it('test isEnabled', function(browser) { * browser.element.find('#search') * .isEnabled() * .assert.equals(true); * }); * * it('test async isEnabled', async function(browser) { * const result = await browser.element.find('#search').isEnabled(); * browser.assert.equal(result, true); * }); * }); * * @since 3.5.0 * @method isEnabled * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).isEnabled() * @link /#is-element-enabled * @returns {ScopedValue} */ module.exports.command = function () { return this.runQueuedCommandScoped('isElementEnabled'); }; ================================================ FILE: lib/api/web-element/commands/isPresent.js ================================================ /** * Checks if an element is present in the DOM. * * This command is useful for verifying the presence of elements that may/may not be visible or interactable. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * For more info on the new `browser.element.find()` syntax, refer to the new Element API Overview page. * * @example * describe('isPresent Demo', function() { * it('test isPresent', function(browser) { * browser.element.find('#search') * .isPresent() * .assert.equals(true); * }); * * it('test async isPresent', async function(browser) { * const result = await browser.element.find('#search').isPresent(); * browser.assert.equal(result, true); * }); * }); * * @since 3.7.1 * @method isPresent * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).isPresent() * @returns {ScopedValue} A boolean value indicating if the element is present in the DOM. */ module.exports.command = function () { return this.runQueuedCommandScoped('isElementPresent', {suppressNotFoundErrors: true}); }; ================================================ FILE: lib/api/web-element/commands/isSelected.js ================================================ /** * Determines if an element is selected. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * For more info on the new `browser.element.find()` syntax, refer to the new Element API Overview page. * * @example * describe('isSelected Demo', function() { * it('test isSelected', function(browser) { * browser.element.find('#search') * .isSelected() * .assert.equals(true); * }); * * it('test async isSelected', async function(browser) { * const result = await browser.element.find('#search').isSelected(); * browser.assert.equal(result, true); * }); * }); * * @since 3.5.0 * @method isSelected * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).isSelected() * @link /#is-element-selected * @returns {ScopedValue} */ module.exports.command = function () { return this.runQueuedCommandScoped('isElementSelected'); }; ================================================ FILE: lib/api/web-element/commands/isVisible.js ================================================ /** * Determines if an element is visible. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * For more info on the new `browser.element.find()` syntax, refer to the new Element API Overview page. * * @example * describe('isVisible demo', function() { * it('test isVisible', function(browser) { * browser.element.find('#search') * .isVisible() * .assert.equals(true); * }); * * it('test async isVisible', async function(browser) { * const result = await browser.element.find('#search').isVisible(); * browser.assert.equal(result, true); * }); * }); * * @since 3.5.0 * @method isVisible * @memberof ScopedWebElement * @instance * @syntax browser.element.find(selector).isVisible() * @link /#element-displayedness * @returns {ScopedValue} * @alias isDisplayed */ module.exports.command = function() { return this.runQueuedCommandScoped('isElementDisplayed'); }; ================================================ FILE: lib/api/web-element/commands/moveTo.js ================================================ /** * Move the mouse by an offset of the specified element. If an element is provided but no offset, the mouse will be moved to the center of the element. If the element is not visible, it will be scrolled into view. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * *

This command has been deprecated and is not available when using W3C Webdriver clients (such as GeckoDriver). It's only available when using the Selenium JSONWire Protocol.

* * @example * export default { * demoTest(browser: NightwatchAPI): void { * browser.element('#main').moveTo(10, 10); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('#main').moveTo(10, 10); * } * } * * @since 3.0.0 * @method moveTo * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).moveTo([x], [y]) * @param {number} [x=0] X offset to move to, relative to the center of the element. * @param {number} [y=0] Y offset to move to, relative to the center of the element * @returns {ScopedWebElement} */ module.exports.command = function(x = 0, y = 0) { return this.runQueuedCommand('moveTo', { args: [x, y] }); }; ================================================ FILE: lib/api/web-element/commands/rightClick.js ================================================ /** * Simulates a context-click(right click) event on the given DOM element. The element is scrolled into view if it is not already pointer-interactable. See the WebDriver specification for element [interactability](https://www.w3.org/TR/webdriver/#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('#main ul li a.first').rightClick(); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('#main ul li a.first').rightClick(); * } * } * * @since 3.0.0 * @method rightClick * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).rightClick() * @see https://www.selenium.dev/documentation/webdriver/actions_api/mouse/#context-click * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('contextClick'); }; ================================================ FILE: lib/api/web-element/commands/sendKeys.js ================================================ /** * Types a key sequence on the DOM element. Can be used to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * 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=text]').sendKeys('nightwatch'); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * browser.element('input[type=text]').sendKeys('nightwatch'); * } * } * * @since 3.0.0 * @method sendKeys * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).sendKeys(...keys) * @param {...string} keys * @link /#element-send-keys * @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebElement.html#sendKeys * @returns {ScopedWebElement} */ module.exports.command = function (...args) { const keys = args.reduce((prev, key) => { const keyList = Array.isArray(key) ? key : [key]; prev.push(...keyList); return prev; }, []); return this.runQueuedCommand('sendKeysToElement', { args: [keys] }); }; ================================================ FILE: lib/api/web-element/commands/setAttribute.js ================================================ /** * Set the value of a specified DOM attribute for the given element. For all the available DOM attributes, consult the [Element doc at MDN](https://developer.mozilla.org/en-US/docs/Web/API/element). * * 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('#login input[type=text]').setAttribute('disabled', true); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('#login input[type=text]').setAttribute('disabled', true); * } * } * * @since 3.0.0 * @method setAttribute * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).setAttribute(name, value) * @param {string} name The attribute name to set. * @param value The attribute value name to set. * @returns {ScopedWebElement} */ module.exports.command = function(name, value) { return this.runQueuedCommand('setElementAttribute', { args: [name, value] }); }; ================================================ FILE: lib/api/web-element/commands/setProperty.js ================================================ /** * Set the value of a specified DOM property for the given element. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * browser.element('#login').setProperty('title', 'Hello'); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('#login').setProperty('title', 'Hello'); * } * } * * @since 3.0.0 * @method setProperty * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).setProperty(name, value) * @param {string} name The property name to set. * @param value The property value name to set. * @returns {ScopedWebElement} */ module.exports.command = function(name, value) { return this.runQueuedCommand('setElementProperty', { args: [name, value] }); }; ================================================ FILE: lib/api/web-element/commands/setValue.js ================================================ /** * Sends some text to an element. Can be used to set the value of a form element or to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * *
setValue also clears the existing value of the element by calling the clear() beforehand.
* * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * @example * // send some simple text to an input * this.demoTest = function (browser) { * const result = await browser.element('input[type=text]').setValue('nightwatch'); * }; * * // send some text to an input and hit enter. * this.demoTest = function (browser) { * const result = await browser.element('input[type=text]').setValue(['nightwatch', browser.Keys.ENTER]); * }; * * * @link /session/:sessionId/element/:id/value * @method setValue * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).setValue(inputValue) * @param {string|array} inputValue The text to send to the element or key strokes. * @param {function} [callback] Optional callback function to be called when the command finishes. * @link https://www.w3.org/TR/webdriver#element-send-keys */ module.exports.command = function(...args) { const keys = args.reduce((prev, key) => { const keyList = Array.isArray(key) ? key : [key]; prev.push(...keyList); return prev; }, []); return this.runQueuedCommand('setElementValue', { args: [keys] }); }; ================================================ FILE: lib/api/web-element/commands/submit.js ================================================ /** * Submits the form containing this element (or this element if it is itself a FORM element). his command is a no-op if the element is not contained in a form. * * For more info on working with DOM elements in Nightwatch, refer to the Finding & interacting with DOM Elements guide page. * *

This command has been deprecated and is not available when using W3C Webdriver clients (such as GeckoDriver). It's only available when using the Selenium JSONWire Protocol.

* * @example * export default { * demoTest(browser: NightwatchAPI): void { * browser.element('form.login').submit(); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('form.login').submit(); * } * } * * @since 3.0.0 * @method submit * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).submit() * @returns {ScopedWebElement} */ module.exports.command = function() { return this.runQueuedCommand('elementSubmit'); }; ================================================ FILE: lib/api/web-element/commands/takeScreenshot.js ================================================ /** * Take a screenshot of the visible region encompassed by this element's bounding rectangle. * * @example * export default { * demoTest(browser: NightwatchAPI): void { * const screenshot = browser.element('#main').takeScreenshot(); * screenshot.then((screenshotData) => { * require('fs/promises').writeFile('out.png', screenshotData, 'base64'); * }); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * const screenshotData = await browser.element('#main').takeScreenshot(); * require('fs/promises').writeFile('out.png', screenshotData, 'base64'); * } * } * * @since 3.0.0 * @method takeScreenshot * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).takeScreenshot() * @link /#dfn-take-element-screenshot * @returns {ScopedValue} */ module.exports.command = function () { return this.runQueuedCommandScoped('takeElementScreenshot'); }; ================================================ FILE: lib/api/web-element/commands/uncheck.js ================================================ /** * 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)').uncheck(); * }, * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('input[type=checkbox]:checked)').uncheck(); * }, * } * * @since 3.7.0 * @method uncheck * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).uncheck() * @returns {ScopedWebElement} */ module.exports.command = function () { return this.runQueuedCommand('uncheckElement'); }; ================================================ FILE: lib/api/web-element/commands/update.js ================================================ /** * Sends some text to an element. Can be used to set the value of a form element or to send a sequence of key strokes to an element. Any UTF-8 character may be specified. * *
update also clears the existing value of the element by calling the clear() beforehand.
* * An object map with available keys and their respective UTF-8 characters, as defined on [W3C WebDriver draft spec](https://www.w3.org/TR/webdriver/#character-types), is loaded onto the main Nightwatch instance as `browser.Keys`. * * 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=text]').update('nightwatch', browser.Keys.ENTER); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('input[type=text]').update('nightwatch', browser.Keys.ENTER); * } * } * * @since 3.0.0 * @method update * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).update(characters, ...keys) * @param {...string} keys * @link /#element-send-keys * @returns {ScopedWebElement} */ module.exports.command = function (...args) { const keys = args.reduce((prev, key) => { const keyList = Array.isArray(key) ? key : [key]; prev.push(...keyList); return prev; }, []); return this.runQueuedCommand('setElementValue', { args: [keys] }); }; ================================================ FILE: lib/api/web-element/commands/upload.js ================================================ /** * Uploads file to an element using absolute file path. * * 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('#myFile').upload('/path/file.pdf'); * }, * * async demoTestAsync(browser: NightwatchAPI): Promise { * await browser.element('#myFile').upload('/path/file.pdf'); * } * } * * @since 3.0.0 * @method upload * @memberof ScopedWebElement * @instance * @syntax browser.element(selector).upload(filePath) * @param {string} file The file path to upload. * @tutorial www.selenium.dev/documentation/en/remote_webdriver/remote_webdriver_client/ * @returns {ScopedWebElement} */ module.exports.command = function(file) { return this.runQueuedCommand('uploadFile', { args: [file] }); }; ================================================ FILE: lib/api/web-element/element-locator.js ================================================ const {By, RelativeBy} = require('selenium-webdriver'); const Utils = require('../../utils'); const Element = require('../../element/index.js'); const Locator = require('../../element/locator.js'); const NightwatchLocator = require('../../element/locator-factory'); class ElementLocator { constructor(selector, options = {}) { this.index = ElementLocator.getOrDefault(selector, ['index', '__index'], 0); this.timeout = ElementLocator.getOrDefault(selector, 'timeout', options.waitForConditionTimeout); this.retryInterval = ElementLocator.getOrDefault(selector, 'retryInterval', options.waitForConditionPollInterval); this.locateStrategy = ElementLocator.getLocateStrategy(selector, options.locateStrategy); this.abortOnFailure = ElementLocator.getOrDefault(selector, 'abortOnFailure', true); this.suppressNotFoundErrors = ElementLocator.getOrDefault(selector, 'suppressNotFoundErrors', false); this.condition = ElementLocator.getCondition(selector, this.locateStrategy); } static create(selector, nightwatchInstance = {}) { return new ElementLocator(selector, { waitForConditionTimeout: nightwatchInstance.settings.globals.waitForConditionTimeout, waitForConditionPollInterval: nightwatchInstance.settings.globals.waitForConditionPollInterval, locateStrategy: nightwatchInstance.locateStrategy }); } static getOrDefault(obj, props, defaultValue) { if (!Utils.isObject(obj)) { return defaultValue; } const propArray = Array.isArray(props) ? props : [props]; for (const prop of propArray) { // eslint-disable-next-line no-prototype-builtins if (obj.hasOwnProperty(prop) && Utils.isDefined(obj[prop])) { return obj[prop]; } } return defaultValue; } static getLocateStrategy(element, strategy) { if (element instanceof By) { return element.using; } if (Utils.isString(element)) { return strategy; } return ElementLocator.getOrDefault(element, 'locateStrategy', strategy); } static isElementDescriptor(selector) { if (!Utils.isObject(selector)) { return false; } if (selector.__nightwatchScopedWebElement__) { return true; } // eslint-disable-next-line no-prototype-builtins return ['selector', 'xpath', 'css'].some(prop => selector.hasOwnProperty(prop)); } static getSelectorValue(descriptor) { if (descriptor.selector instanceof By || descriptor.selector instanceof RelativeBy) { return descriptor.selector; } if (descriptor.xpath) { return By.xpath(descriptor.xpath); } if (descriptor.css) { return By.css(descriptor.css); } return null; } static getCondition(element, strategy) { const locateStrategy = ElementLocator.getLocateStrategy(element, strategy); if (element instanceof By || element instanceof RelativeBy) { return element; } if (element instanceof Element) { if (element.usingRecursion) { return element; } return NightwatchLocator.create(element); } if (!ElementLocator.isElementDescriptor(element)) { return By[Locator.AVAILABLE_LOCATORS[locateStrategy]](element); } const selector = ElementLocator.getSelectorValue(element); if (selector) { return selector; } return By[Locator.AVAILABLE_LOCATORS[locateStrategy]](element.selector); } } module.exports.ScopedElementLocator = ElementLocator; ================================================ FILE: lib/api/web-element/element-value.js ================================================ module.exports.create = function createScopedValue(value, nightwatchInstance) { class ScopedValue { constructor(value) { this.value = Promise.resolve(value); } then(onFulfilled, onRejected) { return this.value.then(onFulfilled, onRejected); } map(callback) { return createScopedValue(this.then(callback), nightwatchInstance); } } return new ScopedValue(value); }; ================================================ FILE: lib/api/web-element/factory.js ================================================ const ScopedWebElements = require('./scoped-elements.js'); const ScopedWebElement = require('./scoped-element.js'); const ScopedElementAssertions = require('./assert/element-assertions.js'); const WaitUntil = require('./waitUntil.js'); const Element = require('../../element'); const ScopedValueAssertions = require('./assert/value-assertions.js'); const ScopedValue = require('./element-value.js'); const throwError = (message, commandName) => { const error = new Error(`Error in ${commandName}(): ${message}`); return error; }; const createScopedWebElement = function(selector, parentElement, nightwatchInstance) { const instance = new ScopedWebElement(selector, parentElement, nightwatchInstance); const exported = selector ? Element.createFromSelector(selector) : {}; const methodNames = ScopedWebElement.getMethodNames(); methodNames.forEach(({commandName: methodName, originalCommandName}) => { const fn = { [methodName]: function(...args) { return instance.executeMethod(exported, originalCommandName || methodName, ...args); } }; Object.defineProperty(exported, methodName, { value: fn[methodName], writable: false, enumerable: !originalCommandName, configurable: true }); }); Object.defineProperty(exported, 'assert', { get() { return ScopedElementAssertions.create(instance, { negated: false, nightwatchInstance }); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'waitUntil', { value(actionOrOptions, opts = {}) { return createScopedWebElement(new Promise(function (resolve, reject) { const args = Object.assign({}, opts); if (typeof actionOrOptions === 'string') { args.action = actionOrOptions; } else if (actionOrOptions && typeof actionOrOptions === 'object') { Object.assign(args, actionOrOptions); } const waitUntil = new WaitUntil(instance, { selector, nightwatchInstance, ...args }); return waitUntil.wait().then(element => resolve(element)); }.bind(instance)), parentElement, nightwatchInstance); } }); Object.defineProperty(exported, 'then', { value(onFulfilled, onRejected) { return instance.then(onFulfilled, onRejected); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'webElement', { get() { return instance.webElement; }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'runQueuedCommand', { value(...args) { return instance.runQueuedCommand(...args); }, enumerable: false, configurable: true }); Object.defineProperty(exported, 'createRootElementCommand', { value(...args) { return instance.createRootElementCommand(...args); }, enumerable: false, configurable: false }); Object.defineProperty(exported, 'runQueuedCommandScoped', { value(commandName, ...args) { const node = instance.createNode(commandName, args); return createScopedValue(node, nightwatchInstance); }, enumerable: false, configurable: true }); Object.defineProperty(exported, 'queueAction', { get() { return instance.queueAction.bind(instance); }, enumerable: false, configurable: true }); Object.defineProperty(exported, 'createScopedElement', { value(selector, parentElement = false) { return createScopedWebElement.call( instance, selector, parentElement ? parentElement : null, instance.nightwatchInstance ); }, enumerable: false, configurable: false }); Object.defineProperty(exported, 'createScopedElements', { value(selector, {commandName, parentElement = false} = {}) { return createScopedWebElements.call(instance, commandName, selector, parentElement ? parentElement : null, instance.nightwatchInstance); }, enumerable: false, configurable: false }); return exported; }; const createScopedWebElements = function(commandName, selector, parentElement, nightwatchInstance) { const instance = new ScopedWebElements(selector, parentElement, nightwatchInstance); const createAction = () => function({args}) { const elms = args[0]; return elms.then(elements => elements.map(el => createScopedWebElement(el, parentElement, nightwatchInstance))); }; const node = this.queueAction({name: commandName, args: [instance], createAction, namespace: 'element'}); node.printArgs = function() { return `{ ${selector} }`; }; const exported = {}; Object.defineProperty(exported, 'then', { value(onFulfilled, onRejected) { return instance.then(onFulfilled, onRejected); }, enumerable: false, configurable: true }); Object.defineProperty(exported, 'count', { value() { return createScopedValue({ deferred: { promise: instance.then((elements) => elements.length) } }, nightwatchInstance); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'nth', { value(index = 0) { return createScopedWebElement(new Promise(function (resolve, reject) { this.then((elements) => { if (elements.length === 0) { return reject(throwError(`No elements found for selector: ${selector}`, 'nth')); } if (index < 0 || index >= elements.length) { return reject(throwError(`Index ${index} out of bounds for selector: ${selector}`, 'nth')); } resolve(elements[index]); }); }.bind(instance)), parentElement, nightwatchInstance); }, enumerable: true, configurable: true }); // Object.defineProperty(exported, 'assert', { // get() { // return new ScopedElementsAssertions(instance, { // negated: false, // nightwatchInstance // }); // }, // enumerable: false, // configurable: true // }); return exported; }; const createScopedValue = function(node, nightwatchInstance) { const instance = ScopedValue.create(node.deferred.promise, nightwatchInstance); const exported = {}; Object.defineProperty(exported, 'then', { value(onFulfilled, onRejected) { return instance.then(onFulfilled, onRejected); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'map', { value(callback) { return instance.map(callback); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'assert', { get() { return ScopedValueAssertions.create(instance, { negated: false, nightwatchInstance }); }, enumerable: true, configurable: true }); Object.defineProperty(exported, 'value', { get() { return instance.value; }, enumerable: true, configurable: false }); return exported; }; module.exports.createScopedWebElements = createScopedWebElements; module.exports.create = createScopedWebElement; module.exports.createScopedValue = createScopedValue; ================================================ FILE: lib/api/web-element/index.js ================================================ const {WebElement} = require('selenium-webdriver'); const Factory = require('./factory.js'); module.exports.active = function(nightwatchInstance) { const {transportActions} = nightwatchInstance; const node = nightwatchInstance.queue.add(function findActiveElement() { return transportActions.getActiveElement(); }); const instance = new WebElement(nightwatchInstance.transport.driver, node.deferred.promise); return Factory.create(instance, null, nightwatchInstance); }; module.exports.root = function(nightwatchInstance) { return Factory.create(null, null, nightwatchInstance); }; module.exports.createScopedWebElements = Factory.createScopedWebElements; module.exports.create = Factory.create; ================================================ FILE: lib/api/web-element/scoped-element.js ================================================ const fs = require('fs'); const path = require('path'); const {until, WebElement, WebElementPromise, Condition} = require('selenium-webdriver'); const {ShadowRoot} = require('selenium-webdriver/lib/webdriver'); const {Logger, isFunction, createPromise} = require('../../utils/'); const {WEB_ELEMENT_ID} = require('../../transport/selenium-webdriver/session.js'); const {ScopedElementLocator} = require('./element-locator.js'); class ScopedWebElement { static get methodAliases() { return { 'find': ['findElement', 'get'], 'findAll': ['findElements', 'getAll'], 'findByText': ['getByText'], 'findByRole': ['getByRole'], 'findByPlaceholderText': ['getByPlaceholderText'], 'findByLabelText': ['getByLabelText'], 'findByAltText': ['getByAltText'], 'findAllByText': ['getAllByText'], 'findAllByRole': ['getAllByRole'], 'findAllByPlaceholderText': ['getAllByPlaceholderText'], 'findAllByAltText': ['getAllByAltText'], 'getRect': ['getSize', 'getLocation', 'rect'], 'getAttribute': ['attr', 'attribute'], 'getProperty': ['property', 'prop'], 'getText': ['text'], 'getTagName': ['tagName'], 'getAccessibleName': ['accessibleName', 'getComputedLabel'], 'getCssProperty': ['css', 'getCssValue'], 'getAriaRole': ['ariaRole', 'getComputedRole'], 'isVisible': ['isDisplayed'] }; } static getMethodNames() { const methodsDir = path.resolve(__dirname, 'commands'); const methodFiles = fs.readdirSync(methodsDir); return methodFiles .map(fileName => path.parse(fileName).name) .flatMap(commandName => { const aliases = ScopedWebElement.methodAliases[commandName]; const commandNames = [{commandName}]; if (aliases) { commandNames.push(...aliases.map(commandNameAlias => ({commandName: commandNameAlias, originalCommandName: commandName}))); } return commandNames; }) .filter(({commandName: fileName}) => !fileName.startsWith('_')); } get driver() { return this.nightwatchInstance.transport.driver; } get reporter() { return this.nightwatchInstance.reporter; } get queue() { return this.nightwatchInstance.queue; } get __nightwatchScopedWebElement__() { return true; } get suppressNotFoundErrors() { return this._suppressNotFoundErrors; } constructor(selector = 'html', parentElement, nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; this.parentScopedElement = parentElement; this.webElement = this.createWebElementPromise({ selector, parentElement }); } async waitUntilElementsLocated(selector) { const {timeout, condition, retryInterval} = ScopedElementLocator.create(selector, this.nightwatchInstance); const webElements = await this.driver.wait(until.elementsLocated(condition), timeout, null, retryInterval); if (webElements.length === 0) { return null; } return webElements[0]; } async locateElements({parentElement, selector, timeout, retryInterval}) { const createLocateElement = () => { return parentElement && parentElement.webElement ? function(locator) { return new Condition('for at least one element to be located ' + locator, function (driver) { return parentElement.webElement .then(function (webElement) { // also takes into consideration if `webElement` // resolves to a shadow root. return webElement.findElements(locator); }) .then(function (elements) { return elements.length > 0 ? elements : null; }); }); } : until.elementsLocated; }; const locateFn = createLocateElement(); const webElements = await this.driver.wait(locateFn(selector), timeout, null, retryInterval); return webElements; } async findElementUsingRecursion({parentElement, recursiveElement, timeout, retryInterval}) { const allElements = Array.isArray(recursiveElement.selector) ? recursiveElement.selector.slice() : [ recursiveElement ]; let result = null; while (allElements.length > 0) { const nextElement = allElements.shift(); if (result) { parentElement = {webElement: Promise.resolve(result)}; } const {condition, index} = ScopedElementLocator.create(nextElement, this.nightwatchInstance); result = await this.findElement({parentElement, selector: condition, index, timeout, retryInterval}); if (!result) { break; } } return result; } async findElement({parentElement, selector, index, timeout, retryInterval}) { const webElements = await this.locateElements({parentElement, selector, timeout, retryInterval}); if (webElements.length === 0) { const err = new Error(`Cannot find element with "${selector}" selector in ${timeout} milliseconds.`); this.reporter.registerTestError(err); return; } if (index > webElements.length) { const err = new Error(`The index "${index}" is out of bounds for selector "${selector}".`); this.reporter.registerTestError(err); return; } return webElements[index]; } async findElementAction({parentElement, condition, index, timeout, retryInterval, abortOnFailure}) { const createAction = () => async ({args}) => { if ((args[0] instanceof Promise) && !args[0]['@nightwatch_element']) { args[0] = await args[0]; } const parentElement = args[0]; if (parentElement?.webElement && await parentElement.webElement === null) { return null; } try { if (condition.usingRecursion) { return await this.findElementUsingRecursion({parentElement, recursiveElement: condition, timeout, retryInterval}); } return await this.findElement({parentElement, selector: condition, index, timeout, retryInterval}); } catch (error) { const narrowedError = createNarrowedError({error, condition, timeout}); if ( this.suppressNotFoundErrors && narrowedError.name === 'NoSuchElementError' ) { return null; } Logger.error(narrowedError); if (abortOnFailure) { this.reporter.registerTestError(narrowedError); // TODO: find a way to reject here without unhandled promise rejection // reject(narrowedError); } return null; } }; const node = this.queueAction({name: 'find', createAction, namespace: 'element', args: [parentElement]}); node.printArgs = function() { if (condition.selector) { return `{ ${condition.selector} }`; } return `{ ${condition} }`; }; return node.deferred.promise; } createWebElementPromise({selector, parentElement}) { if (!selector) { return null; } // eslint-disable-next-line no-async-promise-executor return new WebElementPromise(this.driver, new Promise(async (resolve, reject) => { if (selector instanceof Promise) { try { selector = await selector; } catch (error) { return reject(error); } } if (!selector) { return resolve(null); } if (selector instanceof WebElement) { return resolve(selector); } if (selector instanceof ShadowRoot) { return resolve(selector); } if (selector[WEB_ELEMENT_ID] && isFunction(selector.getId)) { const webElement = new WebElement(this.driver, selector.getId()); return resolve(webElement); } const {index, timeout, condition, retryInterval, abortOnFailure, suppressNotFoundErrors} = ScopedElementLocator.create(selector, this.nightwatchInstance); if (condition instanceof WebElement || condition instanceof WebElementPromise) { return resolve(condition); } if (suppressNotFoundErrors) { this._suppressNotFoundErrors = true; } const webElement = await this.findElementAction({ parentElement, condition, index, timeout, retryInterval, abortOnFailure }); resolve(webElement); })); } /** * Keeps in sync operation sequence produced by methods. */ waitFor(promise, {isRoot = false} = {}) { if (isRoot) { this.webElement = promise; } else { this.webElement = new WebElementPromise(this.driver, this.then(async (element) => { await promise; return element; })); } return this; } createRootElementCommand(name, context, args) { const result = context[name](...args, {isRoot: true}); return result; } createNode(commandName, args) { if (args[0]?.suppressNotFoundErrors) { this._suppressNotFoundErrors = true; } const createAction = (actions, webElement) => function () { if (isFunction(commandName)) { return commandName(webElement, ...args); } return actions[commandName](webElement, ...args); }; const node = this.queueAction({name: commandName, createAction}); // TODO: check what changes if we keep the original `getResult` instead of below. node.getResult = function(result) { // here, we resolve the node with `result.value` even if the result contains an error and status === -1 // which results in the command result to be `null` in test case as well, while the command actually failed. // Is this expected? To return `null` result in case of failure as well? // eslint-disable-next-line no-prototype-builtins return result.hasOwnProperty('value') ? result.value : result; }; return node; } runQueuedCommand(commandName, {args = [], namespace, isRoot = false, name} = {}) { const node = this.createNode(commandName, args, namespace); if (name) { node.name = name; } return this.waitFor(node.deferred.promise, {isRoot}); } queueAction({name, createAction, namespace = 'element()', args = [], rejectPromise = true} = {}) { const opts = { args, context: null, deferred: createPromise(), commandFn: createAction.call(this, this.nightwatchInstance.transportActions, this.webElement), namespace, rejectPromise, commandName: name }; return this.queue.add(opts); } executeMethod(context, methodName, ...args) { const methodPath = path.resolve(__dirname, 'commands', `${methodName}.js`); const methodFunc = require(methodPath); return methodFunc.command.call(context, ...args); } then(onFulfilled, onRejected) { return this.webElement.then(onFulfilled, onRejected); } } function createNarrowedError({error, condition, timeout}) { if (error.name === 'TimeoutError') { const err = new Error(`Timed out while waiting for element "${condition}" to be present for ${timeout} milliseconds.`); err.name = 'NoSuchElementError'; return err; } error.message = `Error occurred while trying to locate element "${condition}": ${error.message || 'unknown error'}`; return error; } module.exports = ScopedWebElement; ================================================ FILE: lib/api/web-element/scoped-elements.js ================================================ const {until} = require('selenium-webdriver'); const {Logger, createPromise} = require('../../utils'); const {ScopedElementLocator} = require('./element-locator.js'); class ScopedElements { constructor(selector, parentScopedElement, nightwatchInstance) { this.parentScopedElement = parentScopedElement; this.nightwatchInstance = nightwatchInstance; // eslint-disable-next-line no-async-promise-executor this.webElements = new Promise(async (resolve, reject) => { if (selector instanceof Promise) { try { selector = await selector; } catch (error) { return reject(error); } } if (Array.isArray(selector)) { resolve(selector); } else { try { const webElements = await this.findElementsAction(selector); resolve(webElements); } catch (error) { resolve([]); } } }); } async findElementsUsingRecursion({parentElement, recursiveElement, timeout, retryInterval}) { const allElements = Array.isArray(recursiveElement.selector) ? recursiveElement.selector.slice() : [ recursiveElement ]; let result = []; while (allElements.length > 0) { const nextElement = allElements.shift(); if (result.length) { parentElement = {webElement: result[0]}; } const {condition} = ScopedElementLocator.create(nextElement, this.nightwatchInstance); result = await this.findElements({parentElement, selector: condition, timeout, retryInterval}); if (!result || result.length === 0) { result = []; break; } } return result; } async findElements({parentElement, selector, timeout, retryInterval}) { let webElements; if (parentElement && parentElement.webElement) { const parentWebElement = await parentElement.webElement; webElements = await parentWebElement.findElements(selector); } else { webElements = await this.nightwatchInstance.transport.driver.wait(until.elementsLocated(selector), timeout, null, retryInterval); } return webElements; } async findElementsAction(descriptor) { const { timeout, condition, retryInterval, abortOnFailure, suppressNotFoundErrors } = ScopedElementLocator.create(descriptor, this.nightwatchInstance); const commandFn = async ({args}) => { const parentElement = args[0]; try { if (condition.usingRecursion) { return await this.findElementsUsingRecursion({parentElement, recursiveElement: condition, timeout, retryInterval}); } return await this.findElements({parentElement, selector: condition, timeout, retryInterval}); } catch (error) { if (suppressNotFoundErrors) { return []; } const narrowedError = error.name === 'TimeoutError' ? new Error(`No elements with selector "${condition}" found for ${timeout} milliseconds.`) : error; Logger.error(narrowedError); if (abortOnFailure) { this.nightwatchInstance.reporter.registerTestError(narrowedError); throw narrowedError; } return []; } }; const node = this.queueAction({name: 'findAll', commandFn, namespace: 'element', args: [this.parentScopedElement]}); node.printArgs = function() { if (descriptor.selector) { return `{ ${descriptor.selector} }`; } return `{ ${descriptor} }`; }; return node.deferred.promise; } queueAction({name, commandFn, namespace = 'element()', args = [], rejectPromise = true} = {}) { const opts = { args, context: null, deferred: createPromise(), commandFn, namespace, rejectPromise, commandName: name }; return this.nightwatchInstance.queue.add(opts); } then(onFulfilled, onRejected) { return this.webElements.then(onFulfilled, onRejected); } } module.exports = ScopedElements; ================================================ FILE: lib/api/web-element/waitUntil.js ================================================ const {until, Condition} = require('selenium-webdriver'); const {AssertionRunner} = require('../../assertion'); const {isDefined, format} = require('../../utils'); const {ScopedElementLocator} = require('./element-locator'); const {NoSuchElementError} = require('../../element/locator'); const mapToSeleniumFunction = { 'selected': until.elementIsSelected, 'not.selected': until.elementIsNotSelected, 'visible': until.elementIsVisible, 'not.visible': until.elementIsNotVisible, 'enabled': until.elementIsEnabled, 'not.enabled': until.elementIsDisabled, 'disabled': until.elementIsDisabled, 'present': (webElement, scopedElementLocator) => until.elementLocated(scopedElementLocator.condition), 'not.present': (webElement, scopedElementLocator) => new Condition('until elmenet is not present', (driver) => driver.findElements(scopedElementLocator.condition).then(elements => elements.length ? false : true)) }; /** * Waits a given time in milliseconds (default 5000ms) for an element to be in the action state provided before performing any other commands or assertions. * If the element fails to be in the action state withing the given time, the test fails. You can change this by setting `abortOnFailure` to `false`. * * You can change the polling interval by defining a `waitForConditionPollInterval` property (in milliseconds) in as a global property in your `nightwatch.conf.js` or in your external globals file. * Similarly, the default timeout can be specified as a global `waitForConditionTimeout` property (in milliseconds). * * @example * describe('demo Test', function() { * it ('wait for container', async function(browser){ * // with default implicit timeout of 5000ms (can be overwritten in settings under 'globals.waitForConditionTimeout') * await browser.element.find('#index-container').waitUntil('visible'); * * // with explicit timeout (in milliseconds) * await browser.element.find('#index-container').waitUntil('visible', {timeout: 1000}); * * // continue if failed * await browser.element.find('#index-container').waitUntil('visible', {timeout: 1000, abortOnFailure: false}); * * // with negative assertion * await browser.element.find('#index-container').waitUntil('not.visible'); * * // with xpath as the locate strategy * await browser.element.find(by.xpath('//*[@id="index-container"]')).waitUntil('visible', {message: 'The index container is found.'}); * * // with custom message * await browser.element.find('#index-container').waitUntil('visible', {message: 'The index container is found.'}); * }); * * * it('page object demo Test', async function (browser) { * const nightwatchPage = browser.page.nightwatch(); * * nightwatchPage * .navigate() * .assert.titleContains('Nightwatch.js'); * * await nightwatchPage.element.find('@featuresList').waitUntil('visible'); * }); * }); * * @method waitUntil * @syntax .waitUntil(action, {timeout, retryInterval, message, abortOnFailure}); * @param {string} action The action state. Should be one of the following: selected, not.selected, visible, not.visible, enabled, disabled, present, not.present * @param {number} [timeout] The total number of milliseconds to wait before failing. Can also be set using 'globals.waitForConditionTimeout' under settings. * @param {number} [retryInterval] The number of milliseconds to wait between retries. You can use this only if you also specify the time parameter. Can also be set using 'globals.waitForConditionPollInterval' under settings. * @param {string} [message] Optional message to be shown in the output. The message supports two placeholders: %s for current selector and %d for the time (e.g. Element %s was not in the page for %d ms). * @param {boolean} [abortOnFailure=abortOnAssertionFailure] By the default if the element is not found the test will fail. Set this to false if you wish for the test to continue even if the assertion fails. To set this globally you can define a property `abortOnAssertionFailure` in your globals. */ class WaitUntil { constructor(scopedElement, {action, timeout, retryInterval, message, nightwatchInstance, selector, abortOnFailure}) { this.scopedElement = scopedElement; this.conditionFn = mapToSeleniumFunction[action]; this.nightwatchInstance = nightwatchInstance; this.action = action; this.message = message; this.selector = selector; this.timeout = timeout || this.nightwatchInstance.settings.globals.waitForConditionTimeout; this.retryInterval = retryInterval || this.nightwatchInstance.settings.globals.waitForConditionPollInterval; this.abortOnFailure = isDefined(abortOnFailure) ? abortOnFailure : this.nightwatchInstance.settings.globals.abortOnAssertionFailure; this.scopedElementLocator = ScopedElementLocator.create(selector, nightwatchInstance); } createNode() { const createAction = (actions, webElementPromise) => async function() { let result; try { if (!this.conditionFn) { throw new Error(`Invalid action ${this.action} for element.waitUntil command. Possible actions: ${Object.keys(mapToSeleniumFunction).toString()}`); } const webElement = await webElementPromise; if (!webElement && !['present', 'not.present'].includes(this.action)) { throw new NoSuchElementError({element: this.scopedElementLocator, abortOnFailure: this.abortOnFailure}); } const elapsedTime = new Date().getTime() - node.startTime; const commandResult = await this.scopedElement.driver.wait(this.conditionFn(webElementPromise, this.scopedElementLocator), this.timeout, this.message, this.retryInterval, ()=>{}); result = await this.pass(commandResult, elapsedTime).catch(err => err); return result; } catch (err) { const elapsedTime = new Date().getTime() - node.startTime; const actual = err instanceof NoSuchElementError ? 'not present' : null; result = await this.fail(err, actual, elapsedTime).catch(err => err); return result; } finally { node.deferred.resolve(result); } }.bind(this); const node = this.scopedElement.queueAction({name: 'waitUntil', createAction}); return node; } async wait() { const assertApi = this.nightwatchInstance.api.assert; try { const node = this.createNode(); return this.scopedElement.waitFor(node.deferred.promise); } catch (err) { assertApi.ok(false, err.message); } } formatMsg(message, timeMs) { const defaultMsg = this.message || message; return format(defaultMsg, this.selector, timeMs); } assert({result, passed, err, message, elapsedTime}) { const {reporter} = this.scopedElement; const runner = new AssertionRunner({abortOnFailure: this.abortOnFailure, passed, err, message, reporter, elapsedTime}); return runner.run(result); } pass(result, elapsedTime) { const sliceAction = this.action.split('.'); const expected = sliceAction[0] === 'not' ? `not ${sliceAction[1]}` : this.action; const message = this.formatMsg(`Element <%s> was ${expected} in %d milliseconds`, elapsedTime); return this.assert({ result, passed: true, err: { expected }, elapsedTime, message }); } fail(result, actual, elapsedTime) { const sliceAction = this.action.split('.'); actual = actual ? actual : sliceAction[0] === 'not' ? sliceAction[1] : `not ${this.action}`; const expected = sliceAction[0] === 'not' ? `not ${sliceAction[1]}` : this.action; const message = this.formatMsg(`Timed out while waiting for element <%s> to be ${expected} for %d milliseconds`, this.timeout); return this.assert({ result, passed: false, message, err: { actual, expected }, elapsedTime }); } } module.exports = WaitUntil; ================================================ FILE: lib/assertion/assertion-error.js ================================================ const AssertionError = require('assertion-error'); class NightwatchAssertError extends AssertionError { constructor(message) { super(message); this.name = 'NightwatchAssertError'; } } module.exports = NightwatchAssertError; ================================================ FILE: lib/assertion/assertion-runner.js ================================================ const NightwatchAssertion = require('./assertion.js'); const Utils = require('../utils/'); const {Logger} = Utils; module.exports = class AssertionRunner { /** * This is deprecated. No longer possible to check the httpStatusCode of the command. * * @deprecated * @param result * @returns {boolean|boolean|*} */ static isServerError(result = {}) { if (!result) { return false; } const {status, error, httpStatusCode} = result; return (status === -1 && error && httpStatusCode && httpStatusCode.toString().startsWith('5')); } /** * @param opts = {passed, err, calleeFn, message, stackTrace, abortOnFailure, reporter, addExpected = true} */ constructor(opts = {}) { this.opts = opts; this.create(); } create() { const {passed, err, calleeFn = null, message = '', abortOnFailure = true, stackTrace = '', showTrace, link, help} = this.opts; this.assertion = new NightwatchAssertion(message); if (Utils.isDefined(showTrace)) { this.assertion.showTrace = showTrace; } if (Utils.isDefined(help)) { this.assertion.help = help; } if (Utils.isDefined(link)) { this.assertion.link = link; } this.assertion.failure = !passed && !err.actual; this.assertion.expected = err.expected; this.assertion.actual = err.actual; this.assertion.passed = passed; this.assertion.stackTrace = stackTrace || calleeFn && calleeFn.stackTrace; this.assertion.calleeFn = calleeFn; this.assertion.abortOnFailure = abortOnFailure; this.assertion.stackTraceTitle = true; } async run(commandResult = {value: null}) { const {reporter} = this.opts; let assertResult; let isError = false; if (commandResult && AssertionRunner.isServerError(commandResult)) { const errorMessage = commandResult.error || 'Unknown server error'; this.assertion.passed = false; this.assertion.actual = `Server Error: ${errorMessage}`; } try { await this.assertion.assert(); reporter.registerPassed(this.assertion.message); } catch (e) { const {error} = this.assertion; error.abortOnFailure = this.opts.abortOnFailure; isError = true; const {addExpected = true, elapsedTime} = this.opts; this.assertion.buildStackTrace().setMessage(addExpected, elapsedTime).setFailedMessage(); Logger.error(error); reporter.registerFailed(error); assertResult = error; } reporter.logAssertResult(this.assertion.getAssertResult()); if (isError) { return Promise.reject(assertResult); } const promiseResult = Object.assign({}, commandResult); promiseResult.returned = 1; return Promise.resolve(promiseResult); } }; ================================================ FILE: lib/assertion/assertion.js ================================================ const NightwatchAssertError = require('./assertion-error.js'); const Utils = require('../utils'); const {Logger, stringifyObject} = Utils; class NightwatchAssertion { static getExpectedMessage({expected, actual}) { const expectedMsg = Logger.colors.green(stringifyObject(expected)); const receivedMsg = Logger.colors.red(stringifyObject(actual)); return ` - expected ${expectedMsg} but got: ${receivedMsg}`; } constructor(message) { this.__err = new NightwatchAssertError(message); this.__stackTraceTitle = message; this.__abortOnFailure = true; this.failureMessage = ''; this.message = message; this.calleeFn = undefined; this.actual = undefined; this.expected = undefined; this.stackTrace = undefined; this.passed = undefined; this.failure = false; } get error() { return this.__err; } set stackTraceTitle(val) { this.__stackTraceTitle = val ? null : `AssertionError: ${this.message}`; } set showTrace(value) { this.__err.showTrace = value; } set link(value) { this.__err.link = value; } set help(value) { this.__err.help = value; } get stackTraceTitle() { return this.__stackTraceTitle; } set abortOnFailure(value) { this.__abortOnFailure = value; this.__err.abortOnFailure = value; } get abortOnFailure() { return this.__abortOnFailure; } buildStackTrace() { const stackTrace = this.stackTrace || this.captureStackTrace(this.calleeFn); const sections = stackTrace.split('\n'); sections.shift(); if (this.stackTraceTitle) { sections.unshift(this.stackTraceTitle); } this.error.stack = Utils.stackTraceFilter(sections); return this; } setFailedMessage() { if (this.expected !== undefined && this.actual !== undefined) { this.failureMessage = `Expected "${this.expected}" but got: "${this.actual}"`; } return this; } setMessage(needsExpected = true, elapsedTime) { const {actual, expected} = this; if (expected !== undefined && actual !== undefined && needsExpected) { this.message += NightwatchAssertion.getExpectedMessage({ actual, expected }) + ' ' + Logger.colors.stack_trace(`(${elapsedTime}ms)`); } this.error.message = this.message; return this; } getAssertResult() { const result = { name: this.error.name, message: this.message, stackTrace: this.passed ? '' : this.error.stack, fullMsg: this.message, failure: this.failureMessage !== '' ? this.failureMessage : this.failure }; if (Utils.isDefined(this.error.link)) { result.link = this.error.link; } if (Utils.isDefined(this.error.help)) { result.help = this.error.help; } return result; } captureStackTrace(calleeFn) { Error.captureStackTrace(this.error, calleeFn); return this.error.stack; } assert() { return this.passed ? Promise.resolve() : Promise.reject(); } } module.exports = NightwatchAssertion; ================================================ FILE: lib/assertion/index.js ================================================ const assertion = require('./assertion.js'); const AssertionRunner = require('./assertion-runner.js'); const AssertionError = require('./assertion-error.js'); module.exports = assertion; module.exports.AssertionRunner = AssertionRunner; module.exports.AssertionError = AssertionError; ================================================ FILE: lib/core/asynctree.js ================================================ const EventEmitter = require('events'); const Utils = require('../utils'); const TreeNode = require('./treenode.js'); const {Logger} = Utils; const Debuggability = require('../utils/debuggability.js'); class AsyncTree extends EventEmitter{ get started() { return this.rootNode.started; } get rootNode() { return this.__rootNode__; } get inProgress() { return this.currentNode.started && !this.currentNode.done; } constructor({compatMode, foreignRunner, cucumberRunner, mochaRunner}) { super(); this.compatMode = compatMode; this.foreignRunner = foreignRunner; this.cucumberRunner = cucumberRunner; this.mochaRunner = mochaRunner; this.createRootNode(); this.returnError = null; } addNode(node) { this.currentNode.childNodes.push(node); } createRootNode() { this.__rootNode__ = new TreeNode({ name: '__root__', parent: null }); this.currentNode = this.rootNode; } async traverse(err) { this.rootNode.started = true; const childNode = AsyncTree.getNextChildNode(this.currentNode); if (childNode) { const result = await this.runChildNode(childNode); if (result instanceof Error && result.namespace === 'verify') { return null; } return result; } if (this.currentNode.childNodes.length > 0 && this.currentNode.needsPromise) { this.currentNode.deferred.resolve(); } else if (this.currentNode.isRootNode) { const result = await this.done(err); return result; } return await this.walkUp(err); } async walkUp(err) { if (this.currentNode.childNodes.length > 0 && !this.currentNode.done) { // if the current node has childNodes that have not finished yet return this.currentNode.deferred.promise; } if (!this.currentNode.done) { // if the current node hasn't finished yet return err; } this.currentNode = this.currentNode.parent; return await this.traverse(err); } otherChildNodesInProgress(node) { const parentNode = node.parent; if (!parentNode || parentNode.isRootNode) { return false; } return parentNode.childNodes.filter(item => item !== node && !item.done).length > 0; } shouldAvoidParentNodeResolution(node) { const parentNode = node.parent; if (!parentNode || parentNode.isRootNode) { return true; } const result = parentNode.options?.avoidPrematureParentNodeResolution; return Boolean(result); } shouldRejectNodePromise(err, abortOnFailure, node = this.currentNode) { const rejectNodeOnAbortFailure = node.options?.rejectNodeOnAbortFailure; if ((err.isExpect || node.namespace === 'assert' || (abortOnFailure && rejectNodeOnAbortFailure)) && node.isES6Async) { return true; } if (this.cucumberRunner) { return err.waitFor; } return node.rejectPromise || err.rejectPromise; } shouldRejectParentNodePromise(err, node = this.currentNode) { const {parent} = node; if (!parent || this.mochaRunner) { return false; } return err.name !== 'NightwatchAssertError' && !err.isExpect && !parent.isRootNode && parent.isES6Async; } async runChildNode(node) { this.currentNode = node; this.currentNode.started = true; Logger.log(`\n ${Logger.colors.green('→')} Running command: ${Logger.colors.green(node.fullName)}${AsyncTree.printArguments(node)}`); this.emit('asynctree:command:start', {node}); const result = await node.run(); let abortOnFailure = false; let err; if (result instanceof Error) { err = node.handleError(result); err.namespace = node.namespace; abortOnFailure = err.abortOnFailure || Utils.isUndefined(err.abortOnFailure); if (this.foreignRunner) { err.stack = err.stack.split('\n').slice(1).join('\n'); } if (this.shouldRejectNodePromise(err, abortOnFailure, node)) { node.reject(err); } else { node.resolve(err); } if (this.shouldRejectParentNodePromise(err, node)) { node.parent.reject(err); } } else { node.resolveValue = result; this.resolveNode(node, result); } Logger.log(`${Logger.colors.green('→')} Completed command: ${Logger.colors.green(node.fullName)}` + `${AsyncTree.printArguments(node)} (${node.elapsedTime}ms)`); this.emit('asynctree:command:finished', {node, result}); if (abortOnFailure) { this.empty(); this.createRootNode(); this.returnError = err; // this is to make assertions return the proper errors this.emit('asynctree:finished', this); return err; } if (Debuggability.stepOverAndPause && node.parent.isRootNode && node.fullName !== 'pause') { this.currentNode.context.api.pause(); // Whether to stop after performing next step will be decided // in the next "paused" prompt. Debuggability.stepOverAndPause = false; } return await this.traverse(err); } resolveNode(node, result, times = 0) { if (times === 5) { return; } if (!node.isRootNode) { node.resolve(result); } setTimeout(() => { const stillInProgress = this.otherChildNodesInProgress(node); const avoidPrematureParentNodeResolution = this.shouldAvoidParentNodeResolution(node); if (node.context && node.context instanceof EventEmitter) { node.context.emit('complete'); } const isAssertion = node.parent && (node.parent.namespace === 'assert' || node.parent.namespace === 'verify'); if (node.parent && !node.parent.isRootNode && !isAssertion && !stillInProgress && !avoidPrematureParentNodeResolution) { node.done = true; this.resolveNode(node.parent, result, ++times); } }, 10); } async done(err = null) { // `this.currentTestCaseResult` represents the return value of the // currently executing `it` test case or hook. // We do not want to clear the tree if the test case is still running. // In case of an error, the tree will be cleared in the `runChildNode` // method itself. if (this.currentTestCaseResult instanceof Promise && !this.currentTestCaseResult.settled) { return err; } this.emit('asynctree:finished', this); this.empty(); this.createRootNode(); // Test case is complete, reset Debuggability properties. Debuggability.reset(); return err; } empty() { this.rootNode.childNodes = []; return this; } reset() { this.rootNode.started = false; this.rootNode.done = false; this.currentNode = this.rootNode; return this; } /////////////////////////////////////////////////////////////////////////// // STATIC /////////////////////////////////////////////////////////////////////////// static getNextChildNode(node) { let childNode; for (let i = 0; i < node.childNodes.length; i++) { if (!node.childNodes[i].started) { return node.childNodes[i]; } if (node.childNodes[i].childNodes.length > 0) { childNode = node.childNodes[i]; } } return false; } static get argFilters() { return { setValue(...args) { if (args.length === 4) { args[2] = ''; } else if (args.length === 3) { args[1] = ''; } return args; } }; } static printArguments(node) { let args = node.args ? node.args.slice(0) : []; if (AsyncTree.argFilters[node.name]) { args = AsyncTree.argFilters[node.name](...args); } const argsContent = args.map(function(arg) { if (arg === null || arg === undefined) { return arg; } switch (typeof arg) { case 'function': return '[Function]'; case 'string': return node.redact ? '**********' : `'${arg}'`; case 'object': { if (node.printArgs) { return node.printArgs(); } const keys = Object.keys(arg); return `{${keys.length < 10 ? keys.join(', ') : (keys.slice(0, 10).join(', ') + '...')}}`; } default: return arg.toString(); } }).join(', '); return Logger.colors.cyan(` (${argsContent})`); } } module.exports = AsyncTree; ================================================ FILE: lib/core/client.js ================================================ const EventEmitter = require('events'); const {Key, Capabilities, Browser} = require('selenium-webdriver'); const lodashMerge = require('lodash/merge'); const HttpRequest = require('../http/request.js'); const Utils = require('../utils'); const Settings = require('../settings/settings.js'); const CommandQueue = require('./queue.js'); const Transport = require('../transport'); const Element = require('../element'); const ApiLoader = require('../api'); const ElementGlobal = require('../api/_loaders/element-global.js'); const Factory = require('../transport/factory.js'); const {isAndroid, isIos} = require('../utils/mobile'); const namespacedApi = require('../core/namespaced-api.js'); const cdp = require('../transport/selenium-webdriver/cdp.js'); const {LocateStrategy, Locator} = Element; const {Logger, isUndefined, isDefined, isObject, isFunction, isSafari, isChrome} = Utils; class NightwatchAPI { get WEBDRIVER_ELEMENT_ID() { return Transport.WEB_ELEMENT_ID; } get browserName() { if (this.capabilities && this.capabilities.browserName) { return this.capabilities.browserName; } if (this.desiredCapabilities instanceof Capabilities) { return this.desiredCapabilities.getBrowserName(); } return this.desiredCapabilities.browserName; } get platformName() { if (this.capabilities && this.capabilities.platformName) { return this.capabilities.platformName; } if (this.desiredCapabilities instanceof Capabilities) { return this.desiredCapabilities.getPlatform(); } return this.desiredCapabilities.platformName; } toString() { return 'Nightwatch API'; } constructor(sessionId, settings) { // session returned capabilities this.capabilities = {}; this.currentTest = null; // requested capabilities this.desiredCapabilities = settings.capabilities instanceof Capabilities ? settings.capabilities : settings.desiredCapabilities; this.sessionId = sessionId; this.options = settings; this.globals = settings.globals; } __isBrowserName(browser, alternateName) { const lowerCaseBrowserName = this.browserName && this.browserName.toLowerCase(); const browserNames = [this.browserName, lowerCaseBrowserName]; if (alternateName) { alternateName = Array.isArray(alternateName) ? alternateName : [alternateName]; return [browser, ...alternateName].some(name => browserNames.includes(name) ); } return browserNames.includes(browser); } __isPlatformName(platform) { if (typeof this.platformName === 'undefined') { return false; } return this.platformName.toLowerCase() === platform.toLowerCase(); } isIOS() { return isIos(this.desiredCapabilities); } isAndroid() { return isAndroid(this.desiredCapabilities); } isMobile() { return this.isIOS() || this.isAndroid(); } isSafari() { return isSafari(this.desiredCapabilities); } isChrome() { return isChrome(this.desiredCapabilities); } isFirefox() { return this.__isBrowserName(Browser.FIREFOX); } isEdge() { return this.__isBrowserName(Browser.EDGE, ['edge', 'msedge']); } isInternetExplorer() { return this.__isBrowserName(Browser.INTERNET_EXPLORER); } isOpera() { return this.capabilities.browserName === Browser.OPERA; } isAppiumClient() { if (this.options.selenium && this.options.selenium.use_appium) { return true; } // Handle BrowserStack case // (BrowserStack always returns platformName in capabilities) const isMobile = this.__isPlatformName('android') || this.__isPlatformName('ios'); if (Factory.usingBrowserstack(this.options) && isMobile) { return true; } return false; } } class NightwatchClient extends EventEmitter { static create(settings, argv) { const client = new NightwatchClient(settings, argv); if (!client.settings.disable_global_apis) { Object.defineProperty(global, 'browser', { configurable: true, get: function() { if (client) { return client.api; } return null; } }); } // clear namespaced api for (const namespace of Object.getOwnPropertyNames(namespacedApi)) { if (namespacedApi[namespace].__isProxy) { namespacedApi[namespace] = {}; } else if (Utils.isObject(namespacedApi[namespace])) { for (const key of Object.getOwnPropertyNames(namespacedApi[namespace])) { delete namespacedApi[namespace][key]; } } } return client; } constructor(userSettings = {}, argv = {}) { super(); this.setMaxListeners(0); this.settings = Settings.fromClient(userSettings, argv); Logger.setOptions(this.settings); this.isES6AsyncTestcase = false; this.isES6AsyncCommand = false; // backwards compatibility this.options = this.settings; this.__sessionId = null; this.__argv = argv; this.__locateStrategy = null; this.__transport = null; this.createCommandQueue(); this.__elementLocator = new Locator(this); this.__reporter = { logFailedAssertion(err) { }, registerTestError(err) { Logger.error(err); } }; this.__overridableCommands = new Set(); this.__api = new NightwatchAPI(this.sessionId, this.settings); this.__api.createElement = (locator, options = {}) => { return ElementGlobal.element({locator, client: this, options}); }; this .setLaunchUrl() .setScreenshotOptions() .setConfigLocateStrategy() .setLocateStrategy() .setSessionOptions() .createApis(); } get overridableCommands() { return this.__overridableCommands; } get api() { return this.__api; } get argv() { return this.__argv; } get queue() { return this.__commandQueue; } get locateStrategy() { return this.__locateStrategy; } get configLocateStrategy() { return this.__configLocateStrategy; } get sessionId() { return this.getSessionId(); } get usingCucumber() { if (!this.settings.test_runner) { return false; } return this.settings.test_runner.type === 'cucumber'; } getSessionId() { return this.__sessionId; } set sessionId(val) { this.__sessionId = val; } get transport() { return this.__transport; } get transportActions() { const actions = this.transport.Actions; const api = this.api; return new Proxy(actions, { get(target, name) { return function (...args) { let callback; let method; let sessionId = api.sessionId; const lastArg = args[args.length - 1]; const isLastArgFunction = Utils.isFunction(lastArg); if (isLastArgFunction) { callback = args.pop(); } else if (args.length === 0 || !isLastArgFunction) { callback = function(result) {return result}; } const definition = { args }; if (Array.isArray(args[0]) && Utils.isString(lastArg)) { sessionId = lastArg; definition.args = args[0]; } if (Utils.isString(name)) { if (name in target.session) { // actions that require the current session method = target.session[name]; definition.sessionId = api.sessionId || sessionId; } else { method = target[name]; } //return method(definition).then((result) => Utils.makePromise(callback, api, [result])); return method(definition) .then(async (result) => { if (result && result.error && ((result.error instanceof TypeError) || (result.error instanceof SyntaxError))) { throw result.error; } const newResult = await Utils.makePromise(callback, api, [result]); if (Utils.isUndefined(newResult)) { return result; } return newResult; }) .catch(err => { Logger.error(err); callback(err); }); } if (typeof name == 'symbol') { // this is in case of console.log(this.transportActions) const util = require('util'); const result = Object.keys(target).reduce((prev, key) => { if (key === 'session') { return prev; } prev[key] = target[key]; return prev; }, {}); Object.assign(result, target.session); const sorted = Object.keys(result).sort().reduce((prev, key) => { prev[key] = result[key]; return prev; }, {}); return util.inspect(sorted); } return Promise.resolve(); }; } }); } get elementLocator() { return this.__elementLocator; } get httpOpts() { return this.__httpOpts; } get startSessionEnabled() { return this.settings.start_session; } get unitTestingMode() { return this.settings.unit_tests_mode; } screenshotsEnabled() { return isObject(this.settings.screenshots) ? (this.settings.screenshots && this.options.screenshots.enabled === true) : false; } get reporter() { return this.__reporter || {}; } get client() { const {settings, api, locateStrategy, reporter, sessionId, elementLocator} = this; return { options: settings, settings, api, locateStrategy, reporter, sessionId, elementLocator }; } createApis() { this.setApiProperty('page', {}); this.setApiProperty('assert', {}); this.setApiProperty('verify', {}); if (!this.unitTestingMode) { this.setApiProperty('ensure', {}); this.setApiProperty('chrome', {}); this.setApiProperty('firefox', {}); } Object.defineProperty(this.__api, 'driver', { configurable: true, enumerable: true, get: function() { return this.transport && this.transport.driver; }.bind(this) }); this.setApiMethod('actions', (opts) => { return this.transport.driver.actions(opts); }); } ////////////////////////////////////////////////////////////////////////////////////////// // Setters ////////////////////////////////////////////////////////////////////////////////////////// setApiProperty(key, value) { if (Utils.isFunction(value)) { Object.defineProperty(this.__api, key, { get: value }); } else { this.__api[key] = value; } return this; } setApiOption(key, value) { this.__api.options[key] = value; return this; } /** * * @param key * @param args * @return {NightwatchClient} */ setApiMethod(key, ...args) { let fn; let context = this.__api; if (args.length === 1) { fn = args[0]; } else if (args.length === 2) { const namespace = typeof args[0] == 'string' ? context[args[0]] : args[0]; if (namespace) { context = namespace; } fn = args[1]; } if (!fn) { throw new Error('Method must be a declared.'); } context[key] = fn; return this; } setNamespacedApiMethod(key, ...args) { if (args.length < 1 || args.length > 2) { throw new Error('Invalid number of arguments passed.'); } const fn = args.pop(); if (!fn) { throw new Error('Method must be a declared.'); } const context = (typeof args[0] === 'string' ? namespacedApi[args[0]] : args[0]) || namespacedApi; context[key] = fn; return this; } /** * * @param {string} key * @param {string|object} namespace * @return {boolean} */ isApiMethodDefined(key, namespace) { let api = this.__api; if (namespace) { api = typeof namespace == 'string' ? api[namespace] : namespace; if (api === undefined) { return false; } } return api[key] !== undefined; } setReporter(reporter) { this.__reporter = reporter; return this; } ////////////////////////////////////////////////////////////////////////////////////////// // Options ////////////////////////////////////////////////////////////////////////////////////////// /** * @deprecated */ endSessionOnFail(val) { if (arguments.length === 0) { return this.settings.end_session_on_fail; } this.settings.end_session_on_fail = val; } setLaunchUrl() { let value = this.settings.baseUrl || this.settings.launchUrl || this.settings.launch_url || null; // For e2e and component testing on android emulator if (value && !this.settings.desiredCapabilities.real_mobile && this.settings.desiredCapabilities.avd) { value = value.replace('localhost', '10.0.2.2').replace('127.0.0.1', '10.0.2.2'); } this .setApiProperty('baseUrl', value) .setApiProperty('launchUrl', value) .setApiProperty('launch_url', value); return this; } setScreenshotOptions() { const {screenshots} = this.settings; if (this.screenshotsEnabled()) { this.setApiProperty('screenshotsPath', screenshots.path) .setApiOption('screenshotsPath', screenshots.path); } return this; } setConfigLocateStrategy() { this.__configLocateStrategy = this.settings.use_xpath ? LocateStrategy.XPATH : LocateStrategy.CSS_SELECTOR; return this; } setLocateStrategy(strategy = null) { if (strategy && LocateStrategy.isValid(strategy)) { this.__locateStrategy = strategy; return this; } this.__locateStrategy = this.configLocateStrategy; return this; } get initialCapabilities() { if (isObject(this.settings.capabilities) && Object.keys(this.settings.capabilities).length > 0) { return this.settings.capabilities; } return this.settings.desiredCapabilities; } setInitialCapabilities(value) { if (isObject(this.settings.capabilities) && Object.keys(this.settings.capabilities).length > 0) { this.settings.capabilities = value; } else { this.settings.desiredCapabilities = value; } } setSessionOptions() { this.setApiOption('desiredCapabilities', this.initialCapabilities); return this; } mergeCapabilities(props = {}) { if (isFunction(props)) { this.setInitialCapabilities(props); return; } lodashMerge(this.initialCapabilities, props); this.setSessionOptions(); } setHttpOptions() { this.settings.webdriver = this.settings.webdriver || {}; if (isUndefined(this.settings.webdriver.port)) { this.settings.webdriver.port = this.transport.defaultPort; } if (isUndefined(this.settings.webdriver.default_path_prefix)) { this.settings.webdriver.default_path_prefix = this.transport.defaultPathPrefix; } if (this.settings.testWorkersEnabled && this.settings.webdriver.start_process) { // when running in parallel with test workers, the port needs to be randomly assigned this.settings.webdriver.port = undefined; } const { port, host, timeout_options = {}, ssl = false, keep_alive, proxy, default_path_prefix, username, access_key, internal_server_error_retry_interval } = this.settings.webdriver; if (port) { this.httpOpts.setPort(port); } if (host) { this.httpOpts.setHost(host); } this.httpOpts.useSSL(ssl); this.httpOpts.setKeepAlive(keep_alive); if (isDefined(proxy)) { this.httpOpts.setProxy(proxy); } if (isDefined(timeout_options.timeout)) { this.httpOpts.setTimeout(timeout_options.timeout); } if (isDefined(timeout_options.retry_attempts)) { this.httpOpts.setRetryAttempts(timeout_options.retry_attempts); } if (isDefined(internal_server_error_retry_interval)) { this.httpOpts.setInternalServerRetryIntervel(internal_server_error_retry_interval); } if (isDefined(default_path_prefix)) { this.httpOpts.setDefaultPathPrefix(default_path_prefix); } if (username && isDefined(access_key)) { this .setApiOption('username', username) .setApiOption('accessKey', access_key); this.httpOpts.setCredentials({ username: username, key: access_key }); } HttpRequest.globalSettings = this.httpOpts.settings; return this; } ////////////////////////////////////////////////////////////////////////////////////////// // Initialize the APIs ////////////////////////////////////////////////////////////////////////////////////////// async initialize(loadNightwatchApis = true) { this.loadKeyCodes(); if (loadNightwatchApis) { return this.loadNightwatchApis(); } return this; } setCurrentTest() { this.setApiProperty('currentTest', () => this.reporter.currentTest); return this; } loadKeyCodes() { this.setApiProperty('Keys', Key); return this; } async loadNightwatchApis() { await ApiLoader.init(this); const assertApi = Object.assign({}, this.__api.assert); const verifyApi = Object.assign({}, this.__api.verify); if (!this.unitTestingMode) { const ensureApi = Object.assign({}, this.__api.ensure); this.__api.ensure = ApiLoader.makeAssertProxy(ensureApi); } this.__api.assert = ApiLoader.makeAssertProxy(assertApi); this.__api.verify = ApiLoader.makeAssertProxy(verifyApi); // Add proxies for namespaced API namespacedApi.assert = ApiLoader.makeAssertProxy(Object.assign({}, namespacedApi.assert)); namespacedApi.verify = ApiLoader.makeAssertProxy(Object.assign({}, namespacedApi.verify)); return this; } createTransport() { const HttpOptions = require('../http/options.js'); this.__httpOpts = HttpOptions.global; this.__transport = Transport.create(this); this.setHttpOptions(); return this; } createCommandQueue() { const {type: runner} = this.settings.test_runner; this.__commandQueue = new CommandQueue({ compatMode: this.settings.backwards_compatibility_mode, foreignRunner: runner !== 'default', mochaRunner: runner === 'mocha', cucumberRunner: runner === 'cucumber' }); } ////////////////////////////////////////////////////////////////////////////////////////// // Session ////////////////////////////////////////////////////////////////////////////////////////// /** * @return {Promise} */ createSession({argv, moduleKey = '', reuseBrowser = false} = {}) { if (!this.startSessionEnabled) { return Promise.resolve(); } return this.transport.createSession({argv, moduleKey, reuseBrowser}) .then(data => { this.sessionId = data.sessionId; this.setApiProperty('sessionId', data.sessionId); this.setApiProperty('capabilities', data.capabilities); Logger.info(`Received session with ID: ${data.sessionId}\n`); // Reset cdp connection every time a new webdriver session is created. cdp.resetConnection(); this.emit('nightwatch:session.create', data); return data; }); } /** * @deprecated * @return {Promise} */ startSession() { return this.createSession(); } } module.exports = NightwatchClient; ================================================ FILE: lib/core/namespaced-api.js ================================================ const namespacedApi = { browser: {}, app: {}, appium: {}, alerts: {}, cookies: {}, document: {}, window: {}, chrome: {}, firefox: {}, assert: {}, verify: {}, expect: function() {}, element: function() {} }; module.exports = namespacedApi; ================================================ FILE: lib/core/queue.js ================================================ const EventEmitter = require('events'); const AsyncTree = require('./asynctree.js'); const Utils = require('../utils'); const Node = require('./treenode.js'); class CommandQueue extends EventEmitter { constructor({compatMode = false, foreignRunner = false, cucumberRunner = false, mochaRunner = false} = {}) { super(); this.isDone = false; this.compatMode = compatMode; this.tree = new AsyncTree({compatMode, foreignRunner, cucumberRunner, mochaRunner}); this.scheduleTimeoutId = null; } get currentNode() { return this.tree.currentNode; } get started() { return this.tree.rootNode.started; } shouldStartQueue() { const childNodes = this.currentNode.childNodes; const allChildNodesDone = childNodes.every(function(node) { return node.done; }); return this.currentNode.started && allChildNodesDone; } add(command) { const {commandName, commandFn, context = {}, args, stackTrace, namespace, options = {}, deferred, isES6Async, rejectPromise} = command; const {compatMode} = this; const parentContext = this.currentNode.context; if (!this.deferred) { this.deferred = Utils.createPromise(); } const node = new Node({ name: commandName, parent: this.currentNode, namespace, stackTrace, deferred, isES6Async, rejectPromise, compatMode, addedInsideCallback: parentContext && parentContext.addedInsideCallback }); if (this.currentNode.instance && this.currentNode.instance.isES6AsyncCommand) { node.isES6Async = true; } node.setCommand(commandFn, context, args, options); const initialChildNode = this.shouldStartQueue(); if (context && context.module && context.module.autoInvoke) { return node; } this.tree.addNode(node); if (this.currentNode.done || !this.currentNode.started || initialChildNode) { this.scheduleTraverse(); } return node; } scheduleTraverse() { if (this.scheduleTimeoutId) { clearTimeout(this.scheduleTimeoutId); } this.scheduleTimeoutId = setTimeout(() => this.traverse(), 0); } clearScheduled() { if (this.scheduleTimeoutId) { clearTimeout(this.scheduleTimeoutId); } } empty() { this.tree.empty(); return this; } reset() { this.tree.reset(); return this; } traverse() { this.tree .traverse() .catch(err => { return err; }) .then(err => { const args = []; if (err instanceof Error) { //Logger.error(err); args.push(err); } this.done.apply(this, args); }); return this; } get inProgress() { return this.tree.rootNode.childNodes.length > 0; } done(err) { if (this.tree.rootNode.childNodes.length > 0) { return this; } err = err || this.tree.returnError; this.emit('queue:finished', err); // when using third-party test runners (e.g. cucumber), sometimes the previous error is not cleared this.tree.returnError = null; if (this.deferred) { this.deferred.resolve(err); this.deferred = null; } } waitForCompletion() { if (this.deferred) { return this.deferred.promise; } return Promise.resolve(); } run(currentTestCaseResult) { this.tree.currentTestCaseResult = currentTestCaseResult; if (this.tree.started) { return this; } if (!this.deferred) { this.deferred = Utils.createPromise(); } this.scheduleTraverse(); return this.deferred.promise; } } module.exports = CommandQueue; ================================================ FILE: lib/core/treenode.js ================================================ const EventEmitter = require('events'); const Utils = require('../utils'); const Debuggability = require('../utils/debuggability'); class TreeNode { get command() { return this.__command; } get context() { return this.__context; } get args() { return this.__args; } get instance() { return this.__instance; } get redact() { return !!this.options.redact; } get isTraceable() { return this.options && this.options.isTraceable; } get fullName() { if (!this.namespace || !Utils.isString(this.namespace)) { return this.name; } return `${this.namespace}.${this.name}`; } get isRootNode() { return this.name === '__root__'; } /** * * @param {function} commandFn * @param {object} context * @param {Array} args * @param {object} options */ setCommand(commandFn, context, args, options = {}) { this.__command = commandFn; this.__context = context; this.__args = args; this.options = options; } /** * @param {{name, namespace, stackTrace, parent, deferred}} opts */ constructor({name, parent, namespace, stackTrace, deferred, isES6Async, compatMode, rejectPromise, addedInsideCallback}) { this.__command = null; this.__context = null; this.__args = null; this.__instance = null; // if we have an ES6 async/await testcase, there will be a deferred object containing the promise this.deferred = deferred || Utils.createPromise(); this.isES6Async = deferred !== undefined && (isES6Async || parent.isES6Async); this.name = name || '__root__'; this.namespace = namespace; if (Utils.isFunction(this.namespace)) { this.namespace = this.namespace(); } this.stackTrace = stackTrace; this.parent = parent; this.compatMode = compatMode; this.childNodes = []; this.started = false; this.rejectPromise = rejectPromise; this.done = false; this.startTime = null; this.promiseSettled = false; this.addedInsideCallback = addedInsideCallback; } getResult(result) { if (!this.compatMode && Utils.isObject(result) && !Utils.isUndefined(result.status) && !Utils.isUndefined(result.value)) { return result.value; } return result; } resolve(result) { if (this.promiseSettled || !this.deferred) { return; } this.promiseSettled = true; this.deferred.resolve(this.getResult(result)); } reject(reason) { if (this.promiseSettled || !this.deferred) { return; } this.promiseSettled = true; this.deferred.reject(reason); } run() { return this.runCommand(); } handleError(err) { // Set abortOnFailure to false when command is being executed in debugMode if (Debuggability.debugMode) { err.abortOnFailure = false; } else { err.abortOnFailure = err.abortOnFailure || err.abortOnFailure === undefined; } const errorName = err.name !== 'Error' ? `[${err.name}] ` : ''; const originalError = `${errorName}${err.message}`; if (this.stackTrace && Utils.shouldReplaceStack(err)) { err.stack = this.stackTrace; } if (err.name !== 'NightwatchAssertError' && err.name !== 'NightwatchMountError') { err.message = `Error while running "${this.fullName}" command: ${originalError}`; } return err; } async runCommand() { this.started = true; this.startTime = new Date().getTime(); let result; try { result = await this.execute(); } catch (err) { result = err; } this.done = true; this.endTime = new Date().getTime(); this.elapsedTime = this.endTime - this.startTime; return result; } invokeCommand() { const {args, stackTrace, options, command} = this; command.stackTrace = stackTrace; this.__instance = command.call(this.context, {args, stackTrace, options}); return this.__instance; } execute(autoInvoke = false) { const commandFn = this.command; if (!commandFn) { return Promise.resolve(); } if (autoInvoke) { let result; try { result = this.invokeCommand(); } catch (err) { result = err; } return result; } return new Promise((resolve) => { this.invokeCommand(); if (this.instance instanceof Promise) { this.instance .then(result => resolve(result)) .catch(err => resolve(err)); return; } this.handleCommandResult((result) => { resolve(result); }, (err) => { resolve(err); }); }); } handleCommandResult(resolveFn, rejectFn) { let commandResult; if (this.instance instanceof EventEmitter) { commandResult = this.instance; if (this.instance.needsPromise) { this.needsPromise = true; // this change was done because the only way function-styled custom commands could be resolved // is if they contain another NW API command, which could call `node.context.emit('complete')` // inside `asynctree.js > resolveNode` method for the custom command node. commandResult = this.context; } } else if (this.context instanceof EventEmitter) { // Chai assertions commandResult = this.context; // this is for when the command is not emitting an event itself, but has child nodes that may do, // so when all the child nodes will finish, the parent will also finish this.needsPromise = true; } if (!commandResult) { rejectFn(new Error('Commands must either return an EventEmitter which emits a "complete" event or a Promise.')); return; } const completeHandler = result => { commandResult.removeListener('error', errorHandler); resolveFn(result); }; const errorHandler = (err, abortOnFailure) => { err.abortOnFailure = abortOnFailure || abortOnFailure === undefined; commandResult.removeListener('complete', completeHandler); rejectFn(err); }; commandResult.once('complete', completeHandler); commandResult.once('error', errorHandler); } } module.exports = TreeNode; ================================================ FILE: lib/element/appium-locator.js ================================================ const {By, RelativeBy} = require('selenium-webdriver'); const LocateElement = require('./locator.js'); const LocateStrategy = require('./strategy.js'); class AppiumLocator extends LocateElement { /** * @param {object|string} element * @return {By|RelativeBy} */ static create(element) { if (!element) { throw new Error(`Error while trying to locate element: missing element definition; got: "${element}".`); } const byInstance = LocateElement.locateInstanceOfBy(element); if (byInstance !== null) { return byInstance; } const elementInstance = LocateElement.createElementInstance(element); LocateStrategy.validate(elementInstance.locateStrategy); return new By(elementInstance.locateStrategy, elementInstance.selector); } } module.exports = AppiumLocator; ================================================ FILE: lib/element/command.js ================================================ const EventEmitter = require('events'); const {WebElement} = require('selenium-webdriver'); const Utils = require('../utils'); const LocateStrategy = require('./strategy.js'); const Element = require('./index.js'); const {NoSuchElementError} = require('./locator.js'); const {Logger, PeriodicPromise, isDefined} = Utils; class ElementCommand extends EventEmitter { static isSelectorObject(obj) { return Utils.isObject(obj) && obj.selector; } static get ELEMENT_COMMAND_ARGS() { return 3; } get api() { return this.nightwatchInstance.api; } get client() { return this.nightwatchInstance; } get settings() { return this.nightwatchInstance.settings; } get transport() { return this.nightwatchInstance.transport; } get desiredCapabilities() { return this.nightwatchInstance.session.desiredCapabilities; } get reporter() { return this.nightwatchInstance.reporter; } get commandName() { return this.__commandName; } get defaultUsing() { return this.nightwatchInstance.locateStrategy || LocateStrategy.getDefault(); } get strategy() { return this.__strategy; } get elementLocator() { return this.nightwatchInstance.elementLocator; } get rescheduleInterval() { return this.__rescheduleIntervalMs; } get ms() { return this.__timeoutMs; } get retryOnSuccess() { return false; } get retryOnValidActionResult() { return false; } get callback() { return this.__callback; } get element() { return this.__element; } get selector() { return this.__selector; } get extraArgsCount() { return null; } get abortOnFailure() { return this.__abortOnFailure; } set abortOnFailure(value) { this.__abortOnFailure = Utils.convertBoolean(value); } get suppressNotFoundErrors() { return this.__suppressNotFoundErrors; } set suppressNotFoundErrors(value) { this.__suppressNotFoundErrors = Utils.convertBoolean(value); } /** * @override * @returns {*} */ protocolAction() { return Promise.resolve(); } isResultSuccess(result) { return this.transport.isResultSuccess(result) && result.value !== null; } protocolActionHandler() { return Promise.resolve(); } constructor({nightwatchInstance, commandName, args, requireValidation = true} = {}) { super(); this.nightwatchInstance = nightwatchInstance || this.client; //custom commands don't get the nightwatchInstance this.__commandName = commandName || this.commandFileName; this.transport.registerLastError(null); this.setStrategy(); this.suppressNotFoundErrors = false; this.abortOnFailure = this.api.globals.abortOnAssertionFailure; this.throwOnMultipleElementsReturned = this.api.globals.throwOnMultipleElementsReturned; this.elementId = null; this.args = Array.isArray(args) && args.slice(0) || this.commandArgs || []; this.setMilliseconds(); this.setRescheduleInterval(); this.setArguments(requireValidation); this.setOptionsFromSelector(); this.createElement(); this.setupExecutor(); this.setupActions(); } setupExecutor() { this.executor = new PeriodicPromise({ rescheduleInterval: this.rescheduleInterval, timeout: this.ms }); } setupActions() { } command() { return this.executor.run(); } /** * @override * @param [response] * @returns {*} */ elementFound(response) { if (response) { if (this.isSeleniumWebElement(response.value)) { this.webElement = response.value; } else if (isDefined(response.value)) { this.elementId = this.transport.getElementId(response.value); } else { this.elementId = response; } } return this.protocolAction(); } isSeleniumWebElement(value) { return (value instanceof WebElement); } /** * @override * @param err * @returns {ElementCommand} */ elementNotFound(err) { return this.complete(err, err.result); } elementLocateError(err) { if (err instanceof NoSuchElementError) { return this.elementNotFound(err); } Logger.error(err); return this.complete(err, {}); } async findElement({returnSingleElement = true, cacheElementId = true} = {}) { const {element, commandName, WebdriverElementId} = this; try { if (WebdriverElementId) { return { value: this.transport.toElement(WebdriverElementId), status: 0, result: {} }; } return await this.elementLocator.findElement({element, commandName, returnSingleElement, cacheElementId}); } catch (err) { if (err.name === 'ReferenceError' || err.name === 'TypeError') { throw err; } throw this.noSuchElementError(err); } } executeProtocolAction(actionName, args = []) { const {sessionId} = this; let {elementId, webElement, selector} = this; if (webElement) { elementId = webElement; } else if (selector instanceof WebElement) { elementId = selector; } return this.transport.executeProtocolAction({actionName, args: [elementId, ...args], sessionId}); } async complete(err, response) { try { await this.callback.call(this.api, response, this); if (err instanceof Error) { return err; } return response; } catch (cbErr) { return cbErr; } } validateArgsCount(requireValidation = true) { if (!requireValidation) { return this; } //////////////////////////////////////////////////////////////////////////// // if there is an element command such as getText, getAttribute etc., verify if the // expected number of args have been supplied //////////////////////////////////////////////////////////////////////////// if (this.args.length && !Utils.isFunction(this.args[this.args.length - 1])) { this.args.push(function() {}); } if (this.extraArgsCount !== null) { let expectedArgs = ElementCommand.ELEMENT_COMMAND_ARGS + this.extraArgsCount; let passedArgs = this.args.length; let rejectPromise; if (this.client.isES6AsyncTestcase) { expectedArgs = expectedArgs - 1; passedArgs = passedArgs - 1; rejectPromise = true; } if (passedArgs < expectedArgs - 1 || passedArgs > expectedArgs) { const error = new Error(`${this.commandName} method expects ${(expectedArgs - 1)} ` + `(or ${expectedArgs} if using implicit "css selector" strategy) arguments - ${passedArgs} given.`); error.rejectPromise = rejectPromise; throw error; } if (Utils.isString(this.args[0]) && (expectedArgs === passedArgs || ElementCommand.isSelectorObject(this.args[1]))) { this.setStrategyFromArgs(); } } } setStrategyFromArgs() { const strategy = this.args.shift(); LocateStrategy.validate(strategy, this.commandName); this.setStrategy(strategy); } setArguments(requireValidation) { this.validateArgsCount(requireValidation); this.__selector = this.args.shift(); this.setOutputMessage(); this.setCallback(); return this; } setOptionsFromSelector() { if (Utils.isObject(this.selector)) { const { abortOnFailure, retryInterval, message, timeout, locateStrategy, suppressNotFoundErrors, retryAction } = this.selector; if (!Utils.isUndefined(abortOnFailure)) { this.abortOnFailure = abortOnFailure; } if (!Utils.isUndefined(suppressNotFoundErrors)) { this.suppressNotFoundErrors = suppressNotFoundErrors; } if (!Utils.isUndefined(retryInterval)) { this.setRescheduleInterval(retryInterval); } if (message) { this.message = message; } if (locateStrategy) { this.setStrategy(locateStrategy); } if (!Utils.isUndefined(timeout)) { this.setMilliseconds(timeout); } if (!Utils.isUndefined(retryAction)) { this.__retryOnFailure = retryAction; } const {WebdriverElementId} = this.selector; if (WebdriverElementId) { this.WebdriverElementId = WebdriverElementId; } } return this; } setStrategy(val = this.defaultUsing) { this.__strategy = val; return this; } setOutputMessage() { this.message = null; if (this.args.length > 0) { if (Utils.isString(this.args[this.args.length - 1])) { this.message = this.args.pop(); } } return this; } setCallback() { let callback = null; if (this.args.length && Utils.isFunction(this.args[this.args.length - 1])) { callback = this.args.pop(); } this.__callback = callback || function() {}; return this; } /** * @returns {ElementCommand} */ createElement() { this.__element = Element.createFromSelector(this.selector, this.strategy); return this; } /** * Set the time in milliseconds to wait for the condition, accepting a given value or a globally defined default * * @param {number} [timeoutMs] */ setMilliseconds(timeoutMs = this.api.globals.waitForConditionTimeout) { try { Utils.enforceType(timeoutMs, Utils.PrimitiveTypes.NUMBER); } catch (err) { throw new Error('waitForElement expects second parameter to have a global default (waitForConditionTimeout) ' + 'to be specified if not passed as the second parameter'); } this.__timeoutMs = timeoutMs; return this; } setRescheduleInterval(intervalMs = this.api.globals.waitForConditionPollInterval) { Utils.enforceType(intervalMs, Utils.PrimitiveTypes.NUMBER); this.__rescheduleIntervalMs = intervalMs; return this; } noSuchElementError(err) { const {element, ms, abortOnFailure} = this; const {retries} = err; return new NoSuchElementError({element, ms, abortOnFailure, retries}); } } module.exports = ElementCommand; ================================================ FILE: lib/element/index.js ================================================ const {WebElement, By, RelativeBy} = require('selenium-webdriver'); const LocateStrategy = require('./strategy.js'); const Utils = require('../utils'); class Element { static get defaultProps() { return [ 'name', 'parent', 'selector', 'locateStrategy', 'index', 'abortOnFailure', 'suppressNotFoundErrors', 'timeout', 'retryInterval', 'message', 'pseudoSelector' ]; } /** * Class that all elements subclass from. * * @param {Object} definition User-defined element options defined in page object. * @param {Object} [options] Additional options to be given to the element. * @constructor */ constructor(definition, options = {}) { this.name = options.name; this.setProperties(definition, options); this.parent = options.parent; this.resolvedElement = null; } get index() { return parseInt(this.__index, 10); } set index(val) { this.__index = val; } get indexDisplay() { return isNaN(this.index) ? '' : `[${this.index}]`; } get usingRecursion() { return this.locateStrategy === LocateStrategy.Recursion; } set selector(val) { this.__selector = val; } get selector() { if (Utils.isString(this.__selector) && this.pseudoSelector && !this.__selector.includes(':')) { return `${this.__selector}:${this.pseudoSelector}`; } return this.__selector; } setProperties(definition, options) { if (definition.webElement instanceof WebElement) { this.webElement = definition.webElement; } else if (definition.webElementId) { this.webElementId = definition.webElementId; } else if (!definition.selector) { throw new Error(`No selector property for ${getDescription(this)}. Instead found properties: ` + Object .keys(definition) .filter(hasValidValueIn(definition)) .join(', ') ); } this.__index = definition.index; this.__selector = definition.selector; const locateStrategyFromParent = options.parent ? options.parent.client.configLocateStrategy : undefined; this.locateStrategy = definition.locateStrategy || options.locateStrategy || locateStrategyFromParent || LocateStrategy.getDefault(); this.pseudoSelector = null; if (!Utils.isUndefined(definition.abortOnFailure)) { this.abortOnFailure = Utils.convertBoolean(definition.abortOnFailure); } if (!Utils.isUndefined(definition.suppressNotFoundErrors)) { this.suppressNotFoundErrors = Utils.convertBoolean(definition.suppressNotFoundErrors); } if (!Utils.isUndefined(definition.retryInterval)) { this.retryInterval = definition.retryInterval; } if (definition.message) { this.message = definition.message; } if (!Utils.isUndefined(definition.timeout)) { this.timeout = definition.timeout; } } getId() { return this.resolvedElement; } setResolvedElement(value) { this.resolvedElement = value; } toString() { if (Array.isArray(this.selector)) { // recursive return this.selector.join(','); } if (!this.name) { // inline (not defined in page or section) if (this.selector) { return this.selector; } if (this.webElement instanceof WebElement) { return `web element{${this.resolvedElement}}`; } return 'unknown selector'; } let classType = this.constructor.name; let prefix = this.constructor === Element ? '@' : ''; return `${classType} [name=${prefix}${this.name}${this.indexDisplay}]`; } /** * Determines whether or not the element contains an @ element reference * for its selector. * * @returns {boolean} True if the selector is an element reference starting with an * @ symbol, false if it does not. */ hasElementSelector() { return String(this.selector).charAt(0) === '@'; } /** * If the element object requires a recursive lookup to resolve its * selector, a new element containing the recursive lookup values * is created and returned. * * @returns {Object} A new Element object containing the element and its * parents with a recursive lookup strategy for resolving the element * if one is needed. If not, null is returned. */ getRecursiveLookupElement() { let lookupList = getAncestorsWithElement(this); if (lookupList.length > 1) { return new Element({ selector: lookupList, locateStrategy: LocateStrategy.Recursion, abortOnFailure: this.abortOnFailure, timeout: this.timeout, retryInterval: this.retryInterval, message: this.message }); } return null; } /** * Copies selector properties to the first object from the second if the first * object has undefined or null values for any of those properties. * * @param {Object} target The object to assign values to. * @param {Object} source The object to capture values from. */ static copyDefaults(target, source) { Element.defaultProps.forEach(function(prop) { if (target[prop] === undefined || target[prop] === null || isNaN(target[prop]) && !isNaN(source[prop])) { target[prop] = source[prop]; } }); } /** * Parses the value/selector parameter of an element command creating a * new Element instance with the values it contains, if any. The standard * format for this is a selector string, but additional, complex formats * in the form of an array or object are also supported. * * @param {string|Object|Element} value Selector value to parse into an Element. * @param {string} [using] The using/locateStrategy to use if the selector * doesn't provide one of its own. */ static createFromSelector(value, using) { if (!value) { throw new Error(`Invalid selector value specified "${value}"`); } if ((value instanceof Element) && value.selector) { if (!value.locateStrategy) { value.locateStrategy = using; } return value; } if (value instanceof Promise) { value['@nightwatch_element'] = true; } if (value['@nightwatch_element']) { return value; } let definition; let options; if ((value instanceof Element) && (value.webElement instanceof WebElement)) { return value; } if (WebElement.isId(value)) { definition = { webElementId: value }; } else if (value instanceof WebElement) { definition = { webElement: value }; } else if (value instanceof RelativeBy) { definition = { selector: value }; } else if (value instanceof By) { definition = { selector: value.value, locateStrategy: value.using }; } else { options = { locateStrategy: using }; if (using !== LocateStrategy.Recursion && Utils.isObject(value)) { definition = value; } else { definition = { selector: value }; } } return new Element(definition, options); } /** * Returns true when an elements() request is needed to capture * the result of the Element definition. When false, it means the * Element targets the first result the selector match meaning an * element() (single match only) result can be used. * * @param {Element} element The Element instance to check to see if * it will apply filtering. */ static requiresFiltering(element) { return !isNaN(element.index); } /** * Filters an elements() results array to a more specific set based * on an Element object's definition. * * @param {Element} element The Element instance to check to see if * it will apply filtering. * @param {Array} resultElements Array of WebElement JSON objects * returned from a call to elements() or elementIdElements(). * @return {Array} A filtered version of the elements array or, if * the filter failed (no matches found) null. */ static applyFiltering(element, resultElements) { if (Element.requiresFiltering(element)) { let foundElem = resultElements[element.index]; return foundElem ? [foundElem] : null; // null = not found } return resultElements; } static isElementObject(element) { return (element instanceof Element) || (element instanceof WebElement) || (element instanceof By) || WebElement.isId(element); } } /** * Array filter method removing elements that may be defined (in) but * do not have a recognizable value. */ function hasValidValueIn(definition) { return function(key) { return definition[key] !== undefined && definition[key] !== null; }; } /** * Retrieves an array of ancestors of the supplied element. The last element in the array is the element object itself * * @param {Object} element The element * @returns {Array} */ function getAncestorsWithElement(element) { let elements = []; function addElement(e) { elements.unshift(e); if (e.parent && e.parent.selector) { addElement(e.parent); } } addElement(element); return elements; } /** * Gets a simple description of the element based on whether or not * it is used as an inline selector in a command call or a definition * within a page object or section. */ function getDescription(instance) { if (instance.name) { let classType = instance.constructor.name.toLowerCase(); // Element, Section or any other subclass's name return `${classType} "${instance.name}"`; } return 'selector object'; } module.exports = Element; module.exports.LocateStrategy = LocateStrategy; module.exports.Command = require('./command.js'); module.exports.Locator = require('./locator.js'); ================================================ FILE: lib/element/locate/elements-by-recursion.js ================================================ const RecursiveLookupBase = require('./recursive-lookup.js'); /** * Search for multiple elements on the page, starting with the first element of the array, * and where each element in the passed array is nested under the previous one. * The located element will be returned as a WebElement JSON object. * * @param {Array} elements An array of ancestor element objects containing selector and locateStrategy properties * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol */ class MultipleElementsByRecursion extends RecursiveLookupBase { get transportAction() { return 'locateMultipleElementsByElementId'; } get commandName() { return 'elementIdElements'; } locateElements({element, returnSingleElement}) { this.allElements = Array.isArray(element.selector) ? element.selector.slice() : [ element ]; const nextElement = this.getNextElement(); this.elementLocator .findElement({element: nextElement, cacheElementId: false, returnSingleElement: this.shouldReturnSingleElement(returnSingleElement)}) .then(result => { if (result.value === null) { this.deferred.resolve(result); } else { this.recursiveElementLookup({result, returnSingleElement}); } }); return this.deferred.promise; } } module.exports = MultipleElementsByRecursion; ================================================ FILE: lib/element/locate/recursive-lookup.js ================================================ const EventEmitter = require('events'); const Utils = require('../../utils'); /** * Search for an element on the page, starting with the first element of the array, and where each element in the passed array is nested under the previous one. The located element will be returned as a WebElement JSON object. * * @param {Array} elements An array of ancestor element objects containing selector and locateStrategy properties * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol */ class RecursiveLookupBase extends EventEmitter { constructor(nightwatchInstance) { super(); this.elementLocator = nightwatchInstance.elementLocator; this.transport = nightwatchInstance.transport; this.createPromise(); } getNextElement() { return this.allElements.shift(); } createPromise() { this.deferred = Utils.createPromise(); return this; } /** * In case we need a collection of elements, we only need to retrieve it if it's the last request from * the recursive locate chain * @param {Boolean} returnSingleElement * @return {Boolean} */ shouldReturnSingleElement(returnSingleElement) { if (!returnSingleElement && this.allElements.length > 0) { return true; } return returnSingleElement; } recursiveElementLookup({result, returnSingleElement}) { let element = this.getNextElement(); if (element) { this.elementLocator.findElement({ id: this.transport.getElementId(Utils.isDefined(result.value) ? result.value : result), element, transportAction: this.transportAction, commandName: this.commandName, returnSingleElement: this.shouldReturnSingleElement(returnSingleElement), cacheElementId: false }) .then(result => { this.recursiveElementLookup({ result, returnSingleElement }); }) .catch(result => { this.deferred.reject(result); }); } else { if (result.error instanceof Error) { this.deferred.reject(result.error); return; } this.deferred.resolve(result); } } /** * * @param {string|object} result * @param {function} callback * @return {Promise} */ complete(result, callback) { callback(result); this.emit('complete'); return result; } } module.exports = RecursiveLookupBase; ================================================ FILE: lib/element/locate/single-element-by-recursion.js ================================================ const RecursiveLookupBase = require('./recursive-lookup.js'); /** * Search for an element on the page, starting with the first element of the array, and where each element in the passed array is nested under the previous one. The located element will be returned as a WebElement JSON object. * * @param {Array} elements An array of ancestor element objects containing selector and locateStrategy properties * @param {function} [callback] Optional callback function to be called when the command finishes. * @api protocol */ class SingleElementByRecursion extends RecursiveLookupBase { get transportAction() { return 'locateSingleElementByElementId'; } get commandName() { return 'elementIdElement'; } /** * * @param {Array} elements * @return {Promise} */ locateElement(elements) { this.allElements = elements.slice(); const element = this.getNextElement(); this.elementLocator .findElement({element}) .then(result => { this.recursiveElementLookup({result}); }); return this.deferred.promise; } } module.exports = SingleElementByRecursion; ================================================ FILE: lib/element/locator-factory.js ================================================ const {By, RelativeBy} = require('selenium-webdriver'); const Locator = require('./locator.js'); const AppiumLocator = require('./appium-locator.js'); class NightwatchLocator { /** * @param {object|string} element * @param {boolean} isAppiumClient * @return {By|RelativeBy} */ static create(element, isAppiumClient) { if (isAppiumClient) { return AppiumLocator.create(element); } return Locator.create(element); } } module.exports = NightwatchLocator; ================================================ FILE: lib/element/locator.js ================================================ const {By, RelativeBy, until} = require('selenium-webdriver'); const Element = require('./index.js'); const ElementsByRecursion = require('./locate/elements-by-recursion.js'); const SingleElementByRecursion = require('./locate/single-element-by-recursion.js'); const AVAILABLE_LOCATORS = { 'css selector': 'css', 'id': 'id', 'link text': 'linkText', 'name': 'name', 'partial link text': 'partialLinkText', 'tag name': 'tagName', 'xpath': 'xpath', 'className': 'className' }; class LocateElement { /** * @param {object|string} element * @return {By|RelativeBy} */ static create(element) { if (!element) { throw new Error(`Error while trying to locate element: missing element definition; got: "${element}".`); } const byInstance = LocateElement.locateInstanceOfBy(element); if (byInstance !== null) { return byInstance; } const elementInstance = LocateElement.createElementInstance(element); return By[AVAILABLE_LOCATORS[elementInstance.locateStrategy]](elementInstance.selector); } /** * @param {object} element * @return {By|RelativeBy|null} */ static locateInstanceOfBy(element) { if (element instanceof By) { return element; } if (element.by instanceof By) { return element.by; } if (element.value instanceof RelativeBy) { return element.value; } return null; } /** * @param {object|string} element * @return {Element} */ static createElementInstance(element) { if (typeof element != 'object' && typeof element != 'string') { throw new Error(`Invalid element definition type; expected string or object, but got: ${typeof element}.`); } let selector; let strategy; if (typeof element == 'object' && element.value && element.using) { selector = element.value; strategy = element.using; } else { selector = element; } return Element.createFromSelector(selector, strategy); } get api() { return this.nightwatchInstance.api; } get reporter() { return this.nightwatchInstance.reporter; } get settings() { return this.nightwatchInstance.settings; } get transport() { return this.nightwatchInstance.transport; } get desiredCapabilities() { return this.nightwatchInstance.session.desiredCapabilities; } constructor(nightwatchInstance) { this.nightwatchInstance = nightwatchInstance; } resolveElementRecursively({element}) { return this.findElement({element}).then(response => { const firstElement = element.selector[element.selector.length - 1]; firstElement.resolvedElement = response; const {selector, locateStrategy, name} = firstElement; const {status, value} = response; const WebdriverElementId = response && response.WebdriverElementId || null; const result = { selector, locateStrategy, name, WebdriverElementId, response: { status, value } }; if (!isNaN(firstElement.index)) { result.index = firstElement.index; } return result; }); } /** * Finds a single element by using the multiple locate elements, which instead of throwing an error returns an empty array * * @param {Element} element * @param {String} commandName * @param {String} id * @param {String} transportAction * @param {Boolean} [returnSingleElement] * @param {Boolean} [cacheElementId] * @return {Promise} */ async findElement({element, commandName, id, transportAction = 'locateMultipleElements', returnSingleElement = true, cacheElementId = true}) { if (element && (element.webElement || element.webElementId)) { const elementId = await (element.webElement || element.webElementId).getId(); element.setResolvedElement(elementId); return { value: this.transport.toElement(elementId), status: 0 }; } if (element.resolvedElement && cacheElementId) { return Promise.resolve({ value: this.transport.toElement(element.resolvedElement), status: 0 }); } if (element.usingRecursion) { if (transportAction === 'locateSingleElement') { return this.findSingleElementUsingRecursion(element); } return this.findElementsUsingRecursion({element, returnSingleElement}); } const result = await this.executeProtocolAction({element, commandName, id, transportAction, cacheElementId}); return this.handleLocateElement({result, element, returnSingleElement}); } locateMultipleElements(element) { const commandName = 'locateMultipleElements'; const transportAction = 'locateMultipleElements'; return this.executeProtocolAction({element, commandName, transportAction}); } handleLocateElement({result, element, returnSingleElement}) { const {status = 0, error} = result || {}; let {value} = result; if (error instanceof Error) { return { error, status: -1, value }; } let WebdriverElementId; const elementResult = this.transport.resolveElement(result, element, true); if (elementResult) { WebdriverElementId = this.transport.getElementId(elementResult); element.setResolvedElement(WebdriverElementId); } if (returnSingleElement) { value = elementResult; } return { value, status, WebdriverElementId }; } /** * @returns Promise({{ * value, * status, * result, * now * }}) */ executeProtocolAction(opts = {}) { const {id, element, transportAction, commandName, cacheElementId} = opts; const args = { id }; if (element instanceof By) { args.by = element; } else { args.using = element.locateStrategy; args.value = element.selector; } return this.sendElementsAction({transportAction, args, element, cacheElementId}) .catch(err => { if (this.transport.invalidSessionError(err)) { return new Error(this.transport.getErrorMessage(err)); } throw err; }) .then(result => { if (this.transport.isResultSuccess(result) && Element.requiresFiltering(element)) { return this.filterElements(element, result); } return result; }) // Catch errors from filterElements and from invalid session .catch(error => { return { value: null, status: -1, error }; }); } async sendElementsAction({transportAction, args, element, cacheElementId} = {}) { if (cacheElementId && transportAction === 'locateMultipleElements' && args && !(args.value instanceof RelativeBy)) { const timeout = element.timeout || this.settings.globals.waitForConditionTimeout; const retryInterval = element.retryInterval || this.settings.globals.waitForConditionPollInterval; try { const results = await this.transport.driver.wait(until.elementsLocated(args), timeout, null, retryInterval); const {elementKey} = this.transport; const value = await Promise.all(results.map(async webElement => { const elementId = await webElement.getId(); return {[elementKey]: elementId}; })); return { status: 0, value }; } catch (err) { if (err.name !== 'TimeoutError') { throw err; } throw new NoSuchElementError({element, ms: timeout}); } } return this.transport.executeProtocolAction(transportAction, args); } /** * Selects a subset of elements if the result requires filtering. * * @param {Element} element * @param {object} result * @return {*} */ filterElements(element, result) { let filtered = Element.applyFiltering(element, result.value); if (filtered) { result.value = filtered; return result; } const errorResult = this.transport.getElementNotFoundResult(result); throw new Error(`Element ${element.toString()} not found.${errorResult.message ? (' ' + errorResult.message) : ''}`); } /** * @param {Element} element * @param {Boolean} returnSingleElement * @return {Promise} */ findElementsUsingRecursion({element, returnSingleElement = true}) { let recursion = new ElementsByRecursion(this.nightwatchInstance); return recursion.locateElements({element, returnSingleElement}); } findSingleElementUsingRecursion(element) { let recursion = new SingleElementByRecursion(this.nightwatchInstance); return recursion.locateElement(element.selector); } } class NoSuchElementError extends Error { constructor({element, ms, abortOnFailure, retries}) { super(); this.selector = element.selector; this.abortOnFailure = abortOnFailure; this.name = 'NoSuchElementError'; this.retries = retries; this.strategy = element.locateStrategy; this.message = `Timed out while waiting for element "${this.selector}" with "${this.strategy}" to be present for ${ms} milliseconds.`; } } module.exports = LocateElement; module.exports.NoSuchElementError = NoSuchElementError; module.exports.AVAILABLE_LOCATORS = AVAILABLE_LOCATORS; ================================================ FILE: lib/element/strategy.js ================================================ const __RECURSION__ = 'recursion'; class LocateStrategy { static get Strategies() { return { ID: 'id', CSS_SELECTOR: 'css selector', LINK_TEXT: 'link text', PARTIAL_LINK_TEXT: 'partial link text', TAG_NAME: 'tag name', XPATH: 'xpath', NAME: 'name', CLASS_NAME: 'class name', // Appium-specific strategies ACCESSIBILITY_ID: 'accessibility id', ANDROID_UIAUTOMATOR: '-android uiautomator', IOS_PREDICATE_STRING: '-ios predicate string', IOS_CLASS_CHAIN: '-ios class chain' }; } static isValid(strategy) { return Object.keys(LocateStrategy.Strategies).some(key => { return String(strategy).toLocaleLowerCase() === LocateStrategy.Strategies[key]; }); } static getList() { return Object.keys(LocateStrategy.Strategies).map(k => LocateStrategy.Strategies[k]).join(', '); } static get XPATH() { return LocateStrategy.Strategies.XPATH; } static get CSS_SELECTOR() { return LocateStrategy.Strategies.CSS_SELECTOR; } static getDefault() { return LocateStrategy.CSS_SELECTOR; } static validate(strategy, apiMethod) { if (!LocateStrategy.isValid(strategy)) { const list = LocateStrategy.getList(); const forApiMethod = apiMethod ? ` for .${apiMethod}()` : ''; throw new Error(`Provided locating strategy "${strategy}" is not supported${forApiMethod}. It must be one of the following: ${list}.`); } } static get Recursion() { return __RECURSION__; } } module.exports = LocateStrategy; ================================================ FILE: lib/http/auth.js ================================================ const HttpUtil = require('./http.js'); class Auth { constructor(httpRequest) { this.httpRequest = httpRequest; } basic(user, pass) { return `Basic ${Auth.toBase64(`${user}:${pass}`)}`; } addAuth(user, pass) { if (user && pass) { this.httpRequest.setHeader(HttpUtil.Headers.AUTHORIZATION, this.basic(user, pass)); } } static toBase64(str) { return Buffer.from(str).toString('base64'); } } module.exports = Auth; ================================================ FILE: lib/http/formatter.js ================================================ const {Logger} = require('../utils'); const jsonRegex = new RegExp('[\\u007f-\\uffff]', 'g'); class ResponseFormatter { /** * Built in JSON.stringify() will return unicode characters that require UTF-8 encoding on the wire. * This function will replace unicode characters with their escaped (ASCII-safe) equivalents to support * the keys sending command. * * @param {object} s * @returns {string} */ static jsonStringify(s) { try { const json = JSON.stringify(s); if (json) { return json.replace(jsonRegex, function jsonRegexReplace(c) { return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }); } return json; } catch (err) { Logger.error(err); return ''; } } static stripUnknownChars(str) { const x = []; let i = 0; const length = str.length; for (i; i < length; i++) { if (str.charCodeAt(i)) { x.push(str.charAt(i)); } } return x.join(''); } static formatHostname(hostname, port, useSSL) { const isLocalHost = ['127.0.0.1', 'localhost'].indexOf(hostname) > -1; const protocol = useSSL ? 'https://' : 'http://'; const isPortDefault = [80, 443].indexOf(port) > -1; if (isLocalHost) { return ''; } return protocol + hostname + (!isPortDefault && (':' + port) || ''); } } module.exports = ResponseFormatter; ================================================ FILE: lib/http/http.js ================================================ const ContentTypes = { JSON: 'application/json', JSON_WITH_CHARSET: 'application/json; charset=utf-8', MULTIPART_FORM_DATA: 'multipart/form-data' }; const Headers = { ACCEPT: 'accept', CONTENT_TYPE: 'content-type', CONTENT_LENGTH: 'content-length', AUTHORIZATION: 'authorization' }; const Http = module.exports = { Method: { POST: 'POST', PUT: 'PUT', DELETE: 'DELETE', GET: 'GET' }, StatusCode: { MOVED_PERMANENTLY: 301, MOVED_TEMPORARILY: 302, SEE_OTHER: 303, NOT_MODIFIED: 304, TEMPORARY_REDIRECT: 307 }, isRedirect(statusCode) { return [ Http.StatusCode.MOVED_PERMANENTLY, Http.StatusCode.MOVED_TEMPORARILY, Http.StatusCode.SEE_OTHER, Http.StatusCode.NOT_MODIFIED ].indexOf(statusCode) > -1; }, needsContentLengthHeader(requestMethod) { return [ Http.Method.POST, Http.Method.PUT, Http.Method.DELETE ].indexOf(requestMethod) > -1; }, Headers: Headers, ContentTypes: ContentTypes }; ================================================ FILE: lib/http/options.js ================================================ const Settings = { SELENIUM_HOST: 'host', SELENIUM_PORT: 'port', CREDENTIALS: 'credentials', PROXY: 'proxy', USE_SSL: 'use_ssl', KEEP_ALIVE: 'keep_alive', DEFAULT_PATH: 'default_path', TIMEOUT: 'timeout', RETRY_ATTEMPTS: 'retry_attempts', INTERNAL_SERVER_ERROR_RETRY_INTERVAL: 'internal_server_error_retry_interval' }; class HttpOptions { #settings; get settings() { return this.#settings; } constructor() { this.#settings = {}; } #updateSetting(key, value) { this.#settings[key] = value; return this; } setPort(port) { this.#updateSetting(Settings.SELENIUM_PORT, port); } setHost(value) { this.#updateSetting(Settings.SELENIUM_HOST, value); } useSSL(value) { this.#updateSetting(Settings.USE_SSL, value); } setKeepAlive(value) { this.#updateSetting(Settings.KEEP_ALIVE, value); } setCredentials(credentials) { this.#updateSetting(Settings.CREDENTIALS, credentials); } setProxy(proxy) { this.#updateSetting(Settings.PROXY, proxy); } setDefaultPathPrefix(path) { this.#updateSetting(Settings.DEFAULT_PATH, path); } setTimeout(timeout) { this.#updateSetting(Settings.TIMEOUT, timeout); } setRetryAttempts(retryAttempts) { this.#updateSetting(Settings.RETRY_ATTEMPTS, retryAttempts); } setInternalServerRetryIntervel(retryInterval) { this.#updateSetting(Settings.INTERNAL_SERVER_ERROR_RETRY_INTERVAL, retryInterval); } } module.exports = { global: new HttpOptions(), HttpOptions }; ================================================ FILE: lib/http/request.js ================================================ const EventEmitter = require('events'); const http = require('http'); const https = require('https'); const dns = require('dns'); const path = require('path'); const {Key} = require('selenium-webdriver'); const HttpUtil = require('./http.js'); const HttpOptions = require('./options.js'); const Auth = require('./auth.js'); const Formatter = require('./formatter.js'); const HttpResponse = require('./response.js'); const Utils = require('./../utils'); const {Logger, isString} = Utils; const {DEFAULT_RUNNER_EVENTS: {LogCreated}, NightwatchEventHub} = require('../runner/eventHub.js'); // To handle Node v17 issue. Refer https://github.com/nodejs/node/issues/40702 for details. if (dns.setDefaultResultOrder && (typeof dns.setDefaultResultOrder === 'function')) { dns.setDefaultResultOrder('ipv4first'); } const __defaultSettings__ = { credentials: null, use_ssl: false, proxy: null, timeout: 60000, retry_attempts: 0, internal_server_error_retry_interval: 1000 }; let __globalSettings__ = null; let __httpKeepAliveAgent__ = null; class HttpRequest extends EventEmitter { static get USER_AGENT() { const version = require('../../package.json').version; const platform = ({darwin: 'mac', win32: 'windows'}[process.platform]) || 'linux'; return `nightwatch.js/${version} (${platform})`; } constructor(opts) { super(); this.__settings = null; this.isAborted = false; this.httpRequest = null; this.httpResponse = null; this.retryCount = 0; this.addtOpts = opts.addtOpts || {}; this.auth = null; this.setHttpOpts(); this.setOptions(opts); } static set globalSettings(val) { __globalSettings__ = val; } static resetHttpKeepAliveAgents() { __httpKeepAliveAgent__ = null; } static getAgent({secure, keepAliveMsecs, maxSockets}) { const expectedProtocol = secure ? 'https:' : 'http:'; if (__httpKeepAliveAgent__ && __httpKeepAliveAgent__.protocol === expectedProtocol) { return __httpKeepAliveAgent__; } const protocol = secure ? https : http; // might also consider storing http and https user agents separately // and use the same agent for the entirety of test instead of creating // a new user-agent every time the protocol changes. // 1st commit: https://github.com/nightwatchjs/nightwatch/pull/3748 return __httpKeepAliveAgent__ = new protocol.Agent({ keepAlive: true, keepAliveMsecs, maxSockets }); } static get globalSettings() { return __globalSettings__ || HttpOptions.global.settings; } static updateGlobalSettings(settings = {}) { Object.assign(__globalSettings__, settings); } setHttpOpts() { this.__settings = Object.assign({}, __defaultSettings__, HttpRequest.globalSettings); } get httpOpts() { return this.__settings; } get socket() { return this.httpRequest.socket; } get elapsedTime() { return this.httpResponse.elapsedTime; } get statusCode() { return this.httpResponse.res.statusCode; } setOptions(options) { this.setPathPrefix(options); const {method} = options; if (options.data && !(method === 'POST' || method === 'PUT' || method === 'PATCH')) { options.data = ''; } if (options.multiPartFormData) { this.multiPartFormData = options.multiPartFormData; this.formBoundary = `----NightwatchFormBoundary${Math.random().toString(16).slice(2)}`; this.setFormData(this.formBoundary); } else { this.setData(options); } this.params = options.data; this.contentLength = this.data.length; this.use_ssl = this.httpOpts.use_ssl || options.use_ssl; this.reqOptions = this.createHttpOptions(options); this.hostname = Formatter.formatHostname(this.reqOptions.host, this.reqOptions.port, this.use_ssl); this.retryAttempts = this.httpOpts.retry_attempts; return this; } setData(options) { if (!options.data) { this.data = ''; return this; } this.data = Formatter.jsonStringify(options.data) || ''; return this; } setPathPrefix(options) { const pathContainsPrefix = options.path && options.path.includes(this.httpOpts.default_path); this.defaultPathPrefix = pathContainsPrefix ? '' : (this.httpOpts.default_path || ''); return this; } addKeepAliveOptions(reqOptions) { if (this.httpOpts.keep_alive) { let keepAliveMsecs = 3000; let enabled = true; if (Utils.isObject(this.httpOpts.keep_alive)) { keepAliveMsecs = Number(this.httpOpts.keep_alive.keepAliveMsecs); enabled = JSON.parse(this.httpOpts.keep_alive.enabled); } if (enabled) { const port = reqOptions.port || this.httpOpts.port; reqOptions.agent = HttpRequest.getAgent({ secure: port === 443, keepAliveMsecs, maxSockets: 1 }); } } } createHttpOptions(options) { const reqOptions = { path: this.defaultPathPrefix + (options.path || ''), host: options.host || this.httpOpts.host, port: options.port || this.httpOpts.port, method: options.method && options.method.toUpperCase() || HttpUtil.Method.GET, headers: { 'User-Agent': HttpRequest.USER_AGENT } }; if (options.url) { const url = new URL(options.url); reqOptions.path = url.pathname; reqOptions.host = url.hostname; if (url.search && reqOptions.method === HttpUtil.Method.GET) { // append search query parameters to path. reqOptions.path += url.search; } } this.auth = options.auth || null; this.addKeepAliveOptions(reqOptions); if (options.sessionId) { reqOptions.path = reqOptions.path.replace(':sessionId', options.sessionId); } return reqOptions; } proxyEvents(originalIssuer, events) { events.forEach(event => { originalIssuer.on(event, (...args) => { args.unshift(event); if (event === 'error' && this.shouldRetryRequest()) { this.isAborted = true; this.socket.unref(); this.retryCount = this.retryCount + 1; this.send(); this.retryAttempts = this.retryAttempts - 1; return; } this.emit.apply(this, args); }); }); } createHttpRequest() { try { const req = (this.use_ssl ? https : http).request(this.reqOptions, response => { this.httpResponse = new HttpResponse(response, this); this.httpResponse.on('complete', this.onRequestComplete.bind(this)); this.proxyEvents(this.httpResponse, ['response', 'error', 'success']); }); this.addAuthorizationIfNeeded(req); return req; } catch (err) { err.message = `Error while trying to create HTTP request for "${this.reqOptions.path}": ${err.message}`; throw err; } } logRequest() { const retryStr = this.retryCount ? ` (retry ${this.retryCount})` : ''; const params = this.params; // Trim long execute script strings from params if (this.reqOptions.path.includes('/execute/') && params.script) { params.script = params.script.substring(0, 200) + `... (${params.script.length} characters)`; params.args = params.args.map(arg => { if (isString(arg) && arg.length > 200) { return arg.substring(0, 200) + `... (${arg.length} characters)`; } return arg; }); } else if (this.reqOptions.method === 'POST' && this.reqOptions.path.endsWith('/value') && params.text.startsWith(Key.NULL)) { params.text = '*******'; params.value = '*******'.split(''); } const content = ` Request ${[this.reqOptions.method, this.hostname + this.reqOptions.path, retryStr + ' '].join(' ')}`; Logger.request(content, params); Logger.info(content, params); this.httpRequest.on('error', err => this.onRequestError(err)); } logError(err) { let message; if (Utils.isErrorObject(err)) { message = Utils.stackTraceFilter(err.stack.split('\n')); } else { message = err.message || err; } Logger.error(` ${[this.reqOptions.method, this.hostname, this.reqOptions.path].join(' ') + (err.code ? ' - ' + err.code : '')}\n${message}`); } onRequestComplete(result, response) { this.logResponse(result); if (this.httpResponse.isRedirect) { let location; try { // eslint-disable-next-line location = require('url').parse(response.headers.location); } catch (ex) { Logger.error(ex); this.emit('error', new Error(`Failed to parse "Location" header for server redirect: ${ex.message}`)); return; } const isAbsoluteUrl = !!location.host; if (isAbsoluteUrl) { this.reqOptions.host = location.hostname; this.reqOptions.port = location.port; } this.reqOptions.path = location.pathname; this.reqOptions.method = 'GET'; this.send(); return; } if (this.httpResponse.isInternalServerError && this.retryAttempts > 0) { this.retryCount = this.retryCount + 1; setTimeout(()=> { this.retryAttempts = this.retryAttempts - 1; this.send(); }, this.httpOpts.internal_server_error_retry_interval); return; } if (NightwatchEventHub.runner !== 'cucumber') { NightwatchEventHub.emit(LogCreated, { httpOutput: Logger.collectCommandOutput() }); } this.emit('complete', result); } logResponse(result) { let base64Data; const shouldSupressData = isString(result.value) && this.addtOpts.suppressBase64Data && result.value.length > 100; if (shouldSupressData) { base64Data = result.value; result.value = `${base64Data.substr(0, 100)}...`; result.suppressBase64Data = true; } let logMethod = this.statusCode.toString().startsWith('5') ? 'error' : 'info'; // selenium server throws 500 errors for elements not found if (this.reqOptions.path.endsWith('/element') && logMethod === 'error') { const {value = ''} = result; const errorMessage = Utils.isObject(value) ? value.message : value; if (errorMessage.startsWith('no such element')) { logMethod = 'info'; } } const content = ` Response ${this.statusCode} ${this.reqOptions.method} ${this.hostname + this.reqOptions.path} (${this.elapsedTime}ms)`; Logger.response(content, result); Logger[logMethod](content, result); if (shouldSupressData) { result.value = base64Data; } } onRequestError(err) { this.logError(err); if (this.shouldRetryRequest(err)) { this.isAborted = true; this.socket.unref(); this.retryCount = this.retryCount + 1; setTimeout(() => { this.send(); this.retryAttempts = this.retryAttempts - 1; }, 100); return; } this.emit('error', err); } shouldRetryRequest(err) { return this.retryAttempts > 0 && isRetryableNetworkError(err); } post() { this.reqOptions.method = HttpUtil.Method.POST; return this.send(); } delete() { this.reqOptions.method = HttpUtil.Method.DELETE; return this.send(); } send() { this.addHeaders().setProxyIfNeeded(); this.startTime = new Date(); this.isAborted = false; const {hostname, data} = this; const {method, path, headers} = this.reqOptions; this.emit('send', { hostname, data, method, path, headers }); this.httpRequest = this.createHttpRequest(); this.logRequest(); this.httpRequest.setTimeout(this.httpOpts.timeout, () => { this.httpRequest.abort(); }); this.httpRequest.write(this.data); this.httpRequest.end(); return this; } addHeaders() { if (this.reqOptions.method === HttpUtil.Method.GET) { this.reqOptions.headers[HttpUtil.Headers.ACCEPT] = HttpUtil.ContentTypes.JSON; } if (this.multiPartFormData) { this.reqOptions.headers[HttpUtil.Headers.CONTENT_TYPE] = `${HttpUtil.ContentTypes.MULTIPART_FORM_DATA}; boundary=${this.formBoundary}`; } else if (this.contentLength > 0) { this.reqOptions.headers[HttpUtil.Headers.CONTENT_TYPE] = HttpUtil.ContentTypes.JSON_WITH_CHARSET; } if (HttpUtil.needsContentLengthHeader(this.reqOptions.method)) { this.reqOptions.headers[HttpUtil.Headers.CONTENT_LENGTH] = this.contentLength; } return this; } setProxyIfNeeded() { if (this.httpOpts.proxy !== null) { try { const ProxyAgent = require('proxy-agent'); const proxyUri = this.httpOpts.proxy; this.reqOptions.agent = new ProxyAgent(proxyUri); } catch (err) { console.error('The proxy-agent module was not found. It can be installed from NPM with:\n\n' + '\tnpm install proxy-agent\n'); process.exit(10); } } return this; } setFormData(boundary) { const crlf = '\r\n'; const delimiter = `${crlf}--${boundary}`; const closeDelimiter = `${delimiter}--`; const bufferArray = []; for (const [fieldName, fieldValue] of Object.entries(this.multiPartFormData)) { // set header let header = `Content-Disposition: form-data; name="${fieldName}"`; if (fieldValue.filePath) { const fileName = path.basename(fieldValue.filePath); header += `; filename="${fileName}"`; } bufferArray.push(Buffer.from(delimiter + crlf + header + crlf + crlf)); // set data const {readFileSync} = require('fs'); if (fieldValue.filePath) { bufferArray.push(readFileSync(fieldValue.filePath)); } else { bufferArray.push(Buffer.from(fieldValue.data)); } } bufferArray.push(Buffer.from(closeDelimiter)); this.data = Buffer.concat(bufferArray); } hasCredentials() { return Utils.isObject(this.httpOpts.credentials) && this.httpOpts.credentials.username; } addAuthorizationIfNeeded(req) { if (this.hasCredentials() || this.auth) { const auth = new Auth(req); if (this.hasCredentials()) { auth.addAuth(this.httpOpts.credentials.username, this.httpOpts.credentials.key); } else if (this.auth) { const {user, pass} = this.auth; if (user && pass) { auth.addAuth(user, pass); } } } return this; } } function isRetryableNetworkError(err) { if (err && err.code) { return ( err.code === 'ECONNABORTED' || err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' || err.code === 'EADDRINUSE' || err.code === 'EPIPE' || err.code === 'ETIMEDOUT' ); } return false; } module.exports = HttpRequest; ================================================ FILE: lib/http/response.js ================================================ const EventEmitter = require('events'); const HttpUtil = require('./http.js'); const Formatter = require('./formatter.js'); const Utils = require('../utils'); class HttpResponse extends EventEmitter { /** * * @param {http.ServerResponse} response * @param {object} request */ constructor(response, request) { super(); this.elapsedTime = null; this.res = response; this.request = request; this.res.setEncoding('utf8'); this.isRedirect = HttpUtil.isRedirect(response.statusCode); this.isInternalServerError = response.statusCode >= 500; this.handle(); } handle() { this.flushResponse(); let screenshotContent; this.res.on('end', () => { this.elapsedTime = new Date() - this.request.startTime; if (this.flushed.length > 0) { this.responseData = this.parseResponseData(this.flushed); if (typeof this.responseData.status != 'undefined') { this.responseData.status = parseInt(this.responseData.status, 10); } } else { // for error we have to send empty response, otherwise selenium-webdriver treats the request as success. this.responseData = (this.res.statusCode >= 400) ? '' : {}; } this.emit('response', this.responseData); if (this.responseData.screenshotContent) { screenshotContent = this.responseData.screenshotContent; delete this.responseData.screenshotContent; } this.emit('complete', this.responseData, this.res); if (this.request.isAborted || this.isRedirect) { return; } if (this.isInternalServerError && this.request.retryAttempts > 0) { return; } this.emit('success', this.responseData, this.res); }); } flushResponse() { this.flushed = ''; this.res.on('data', chunk => { if (this.request.reqOptions.method !== 'HEAD') { this.flushed += chunk; } }); } isResponseJson() { return this.res.headers['content-type'] && this.res.headers['content-type'].indexOf('application/json') > -1; } isServerError() { return this.res.statusCode.toString().startsWith('5'); } parseResponseData(data) { let result; data = data.toString(); data = Formatter.stripUnknownChars(data); try { result = JSON.parse(data); } catch (err) { result = data; } // sometimes the server non-json error responses end up parsed as strings and the error in undetected if (Utils.isString(result) && this.isServerError()) { return { status: -1, error: 'internal server error', value: result }; } if (result.value && result.value.stacktrace) { // this is in order to not pollute verbose logs result.value.stacktrace = ''; } return result; } } module.exports = HttpResponse; ================================================ FILE: lib/index.js ================================================ const {By, Key, Capabilities} = require('selenium-webdriver'); const lodashMerge = require('lodash/merge'); const Utils = require('./utils'); const Settings = require('./settings/settings.js'); const ElementGlobal = require('./api/_loaders/element-global.js'); const NightwatchClient = require('./core/client.js'); const namespacedApi = require('./core/namespaced-api.js'); const {NightwatchEventHub} = require('./runner/eventHub'); const HttpRequest = require('./http/request.js'); const cdp = require('./transport/selenium-webdriver/cdp.js'); const {Logger} = Utils; const Nightwatch = module.exports = {}; /** * New programmatic api added in v2 * * @param {Boolean} headless * @param {Boolean} silent * @param {Boolean} output * @param {Boolean} useAsync * @param {String} env * @param {String} browserName * @param {String} config * @param {number} timeout * @param {Boolean} parallel * @param {Object} globals * @param {Boolean} enable_global_apis * @param {Object} reporter * * @returns {{browser}} */ module.exports.createClient = function({ headless = false, silent = true, output = true, useAsync = true, env = null, timeout = null, parallel = false, reporter = null, browserName = null, globals = {}, devtools = false, debug = false, enable_global_apis = false, config = './nightwatch.json', disable_process_listener = false, test_settings } = {}) { if (browserName && !env) { switch (browserName) { case 'firefox': env = 'firefox'; break; case 'chrome': env = 'chrome'; break; case 'safari': env = 'safari'; break; case 'edge': case 'MicrosoftEdge': env = 'edge'; break; } } const cliRunner = Nightwatch.CliRunner({ config, headless, env, timeout, devtools, debug, parallel, disable_process_listener }); const settings = arguments[0] || {}; const allSettings = Object.keys(settings).reduce((prev, key) => { if (![ 'headless', 'useAsync', 'env', 'timeout', 'parallel', 'reporter', 'devtools', 'debug', 'browserName', 'config' ].includes(key)) { prev[key] = settings[key]; } return prev; }, {}); cliRunner.setup({ ...allSettings, silent, output, globals, always_async_commands: useAsync }); cliRunner.test_settings.disable_global_apis = cliRunner.test_settings.disable_global_apis || !enable_global_apis; //merge settings recieved from testsuite to cli runner settings (this might have changed in hooks) lodashMerge(cliRunner.test_settings, test_settings); const client = Nightwatch.client(cliRunner.test_settings, reporter, cliRunner.argv, true); // TODO: Do we need this (global `element` api will only be available on // Nightwatch after `createClient` is called)? new `element` api is currently // available on Nightwatch as named-export, but it won't be once we migrate // to TypeScript, because then named-exports will be exported directly // instead of first adding them to Nightwatch (default export). // Some context: https://github.com/nightwatchjs/nightwatch/pull/3671 Object.defineProperty(Nightwatch, 'element', { configurable: true, value: function(locator) { return ElementGlobal.element({locator, client}); } }); const exported = { updateCapabilities(value) { return client.mergeCapabilities(value); }, runGlobalBeforeHook() { return cliRunner.globals.runGlobalHook('before', [client.settings]); }, runGlobalAfterHook() { return cliRunner.globals.runGlobalHook('after', [client.settings]); }, launchBrowser({loadNightwatchApis = true} = {}) { const {argv} = cliRunner; return client.initialize(loadNightwatchApis) .then(() => { return client.createSession({argv}); }) .then((data) => { if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) { const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format'); NightwatchFormatter.setCapabilities(data); } return client.api; }) .catch((error) => { if (this.settings.test_runner?.type === 'cucumber' && NightwatchEventHub.isAvailable) { const NightwatchFormatter = require('./runner/test-runners/cucumber/nightwatch-format'); NightwatchFormatter.setCapabilities({ error: error }); } throw error; }); }, async cleanup() { await client.queue.waitForCompletion(); client.queue.empty(); client.queue.reset(); client.queue.removeAllListeners(); Logger.reset(); } }; Object.defineProperties(exported, { settings: { configurable: true, get: function() { return client.settings; } }, transport: { get: function() { return client.transport; } }, nightwatch_client: { configurable: true, get: function() { return client; } } }); return exported; }; /** * @param settings * @param reporter * @param argv * @returns {NightwatchClient} */ Nightwatch.client = function(settings, reporter = null, argv = {}, skipInit = true) { const client = NightwatchClient.create(settings, argv); if (reporter === null) { const SimplifiedReporter = require('./reporter/simplified.js'); reporter = new SimplifiedReporter(settings); } client .createTransport() .setReporter(reporter); if (skipInit) { return client; } return client.initialize(); }; Nightwatch.cli = function(callback) { const ArgvSetup = require('./runner/cli/argv-setup.js'); const {argv} = ArgvSetup; if (argv['list-files']) { argv._source = argv['_'].slice(0); const runner = Nightwatch.CliRunner(argv); return runner.setupAsync() .then(() => runner.getTestsFiles()) // eslint-disable-next-line no-console .then((result) => console.log(JSON.stringify(result))); } if (argv.help) { ArgvSetup.showHelp(); } else if (argv.info) { // eslint-disable-next-line no-console console.log(' Environment Info:'); require('envinfo').run( { System: ['OS', 'CPU'], Binaries: ['Node', 'Yarn', 'npm'], Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'] }, { showNotFound: true, duplicates: true, fullTree: true }// eslint-disable-next-line no-console ).then(console.log); } else if (argv.version) { Utils.printVersionInfo(); } else { if (!Utils.isFunction(callback)) { throw new Error('Supplied callback argument needs to be a function.'); } callback(argv); } }; /** * * @param [testSource] * @param [settings] */ Nightwatch.runTests = async function(testSource, settings) { let argv; if (arguments.length <= 1) { settings = arguments[0] || {}; argv = {}; } else if (Array.isArray(testSource)) { argv = { _source: testSource }; } else if (Utils.isObject(testSource)) { argv = testSource; } else if (Utils.isString(testSource)) { argv = { _source: [testSource] }; } if (argv.source) { argv._source = argv.source; } argv.reporter = argv.reporter || Settings.DEFAULTS.default_reporter; if (!Array.isArray(argv.reporter)) { argv.reporter = [argv.reporter]; } try { const runner = Nightwatch.CliRunner(argv); await runner.setupAsync(settings); return runner.runTests(); } catch (err) { return Promise.reject(err); } }; Nightwatch.CliRunner = function(argv = {}) { const CliRunner = require('./runner/cli/cli.js'); return new CliRunner(argv); }; /** * @param opts * @return {*} */ Nightwatch.initClient = function(opts = {}) { const cliRunner = Nightwatch.CliRunner(); cliRunner.initTestSettings(opts); return Nightwatch.client(cliRunner.settings); }; /** * @deprecated * @param argv * @param done * @param settings * @return {*|CliRunner} */ Nightwatch.runner = function(argv = {}, done = function() {}, settings = {}) { if (argv.source) { argv._source = argv.source; } argv.reporter = Settings.DEFAULTS.default_reporter; const runner = Nightwatch.CliRunner(argv); return runner.setup(settings) .runTests() .catch(err => { runner.processListener.setExitCode(10); return err; }) .then(err => { return done(err); }); }; Object.defineProperty(Nightwatch, 'Logger', { configurable: true, get() { const {logMessage, colors, underline, error, enable, disable, disableColors, isEnabled, inspectObject, request} = Logger; return { spinner(message) { const ora = require('ora'); return ora(message).start(); }, log(message, ...args) { logMessage('LOG', message, args, true); }, info(message, ...args) { logMessage('INFO', message, args, true); }, warn(message, ...args) { logMessage('WARN', message, args, true); }, colors, error, underline, enable, disable, disableColors, isEnabled, inspectObject, request }; } }); Object.defineProperty(Nightwatch, 'by', { configurable: true, get() { return By; } }); function throwIfBrowserNotAvailable() { if (!global.browser) { const err = new TypeError('Nightwatch client is not yet available.'); err.addDetailedErr = true; throw err; } } Object.defineProperty(Nightwatch, 'Key', { configurable: true, get() { return Key; } }); Object.defineProperty(Nightwatch, 'Capabilities', { configurable: true, get() { return Capabilities; } }); // Named exports const { browser, app, appium, alerts, cookies, document, window, chrome, firefox, assert, verify, expect, element } = namespacedApi; // browser and app are overwritten below using `Object.defineProperty()`. module.exports.browser = browser; module.exports.app = app; module.exports.appium = appium; module.exports.alerts = alerts; module.exports.cookies = cookies; module.exports.document = document; module.exports.window = window; module.exports.chrome = chrome; module.exports.firefox = firefox; module.exports.assert = new Proxy(assert, { get(_, name) { return namespacedApi.assert[name]; } }); module.exports.verify = new Proxy(verify, { get(_, name) { return namespacedApi.verify[name]; } }); module.exports.expect = new Proxy(expect, { apply(_, __, args) { throwIfBrowserNotAvailable(); return global.browser.expect(...args); }, get(_, name) { throwIfBrowserNotAvailable(); return global.browser.expect[name]; } }); module.exports.element = new Proxy(element, { apply(_, __, args) { throwIfBrowserNotAvailable(); return global.browser.element(...args); }, get(_, name) { throwIfBrowserNotAvailable(); return global.browser.element[name]; } }); const globalBrowserDescriptor = { configurable: true, get() { throwIfBrowserNotAvailable(); return global.browser; } }; Object.defineProperty(Nightwatch, 'browser', globalBrowserDescriptor); Object.defineProperty(Nightwatch, 'app', globalBrowserDescriptor); // expose some internal modules for direct use module.exports.utils = { HttpRequest, cdp }; ================================================ FILE: lib/page-object/base-object.js ================================================ const Utils = require('../utils'); const Section = require('./section.js'); const Element = require('../element'); class BaseObject { static get WrappedProtocolCommands() { return [ 'elements', 'elementIdElement', 'elementIdElements' ]; } /** * Returns the properties object passed as an argument (or null if no arguments are passed). * If the supplied properties argument is a function, it invokes that function with the page as its context. * * @param {Object} parent * @param {Object|Function} val */ static createProps(parent, val = {}) { return Utils.isFunction(val) ? val.call(parent) : val; } /** * Assigns the `elements` property for a page or section object. * For each object in the passed array, it creates a new element object by instantiating Element with its options * * @param {Object} parent * @param {Object|Array} elements Object or array of objects to become element objects */ static createElements(parent, elements = []) { let elementObjects = {}; if (!Array.isArray(elements)) { elements = [elements]; } elements.forEach(els => { Object.keys(els).forEach(name => { let definition = Utils.isString(els[name]) ? { selector: els[name] } : els[name]; let options = { name, parent }; elementObjects[name] = new Element(definition, options); }); }); return elementObjects; } /** * Assigns the `section` property for a page or section object. * For each object in the passed array, it creates a new section object by instantiating Section with its options * * @param {Object} parent * @param {object} sections */ static createSections(parent, sections = {}) { let sectionObjects = {}; Object.keys(sections).forEach(name => { let definition = sections[name]; let options = { name, parent }; sectionObjects[name] = new Section(definition, options); }); return sectionObjects; } /** * Mixes in the passed functions to the page or section object. * * @param {Page|Section} parent The page object or section instance * @param {Object} commands Array of commands that will be added to the page or section */ static addCommands(parent, commands) { if (Utils.isFunction(commands) || Utils.isObject(commands) && !Array.isArray(commands)) { commands = [commands]; } commands.forEach(m => { if (Utils.isFunction(m)) { BaseObject.addCommandsFromClass(parent, m); } else { Object.keys(m).forEach(k => { try { parent[k] = m[k]; } catch (err) { const error = new Error(`Trying to overwrite page object/section "${parent.name}" method/property "${k}".`); error.detailedErr = `Using ${Object.keys(m).join(', ')}.`; throw error; } }); } }); } static addCommandsFromClass(parent, CommandClass) { const instance = BaseObject.createPageCommandInstance(CommandClass, parent); const methodNames = Utils.getAllClassMethodNames(instance, ['httpRequest']); methodNames.forEach(method => { try { parent[method] = function(...args) { return instance[method].apply(instance, args); }; } catch (err) { const error = new Error(`Trying to overwrite page object/section "${parent.name}" method/property "${method}".`); error.detailedErr = `Using ${methodNames}.`; error.displayed = false; throw error; } }); } static createPageCommandInstance(CommandClass, page) { const {client} = page; class CommandInstance extends CommandClass { get page() { return page; } get section() { return this.page.section; } get api() { return this.client.api; } get browser() { return this.client.api; } get client() { return client; } get transportActions() { if (this.client && this.client.transport) { return this.client && this.client.transportActions; } return {}; } get driver() { if (this.client && this.client.transport && this.client.transport.driver) { return this.client.transport.driver; } return null; } httpRequest(requestOptions) { return this.client.transport.runProtocolAction(requestOptions); } toString() { return CommandClass.toString(); } } return new CommandInstance(); } } module.exports = BaseObject; ================================================ FILE: lib/page-object/command-wrapper.js ================================================ const Element = require('../element'); const Utils = require('../utils'); const ALLOWED_NAMESPACES = [ 'alerts', 'cookies', 'document', 'assert', 'verify', 'expect' ]; function isAllowedNamespace(commandName) { return ALLOWED_NAMESPACES.indexOf(commandName) > -1; } class Command { static get TYPE_ELEMENT() { return 'element'; } static get TYPE_SECTION() { return 'section'; } static isPossibleElementSelector(item, commandName = '') { if (!item) { return false; } if (Array.isArray(item)) { return false; } if (Utils.isObject(item)) { /*eslint no-prototype-builtins: 'warn'*/ return item.hasOwnProperty('selector') && Utils.isString(item.selector); } const Api = require('../api'); const ScopedElementApi = require('../api/_loaders/element-api'); return Utils.isString(item) && (item.startsWith('@') || Api.isElementCommand(commandName) || ScopedElementApi.isScopedElementCommand(commandName)); } static isUserDefinedElementCommand(commandName) { const ApiLoader = require('../api'); return !ApiLoader.getElementsCommandsStrict().includes(commandName); } constructor(parent, commandName, isChaiAssertion, isES6Async = false) { this.parent = parent; this.commandName = commandName; this.isChaiAssertion = isChaiAssertion; this.isES6Async = isES6Async; this.isUserDefined = Command.isUserDefinedElementCommand(commandName); } /** * Creates a closure that enables calling commands and assertions on the page or section. * For all element commands and assertions, it fetches element's selector and locate strategy * For elements nested under sections, it sets 'recursion' as the locate strategy and passes as its first argument to the command an array of its ancestors + self * If the command or assertion is not on an element, it calls it with the untouched passed arguments * * @param {function} commandFn The actual command function * @returns {function} */ createWrapper(commandFn) { const self = this; return function (...args) { if (args.length > 0 && this.__needsRecursion) { // within commands const inputElement = Element.createFromSelector(args[0]); if (self.isUserDefined) { inputElement.container = this.__element; args[0] = inputElement; } else { args[0] = Element.createFromSelector({ locateStrategy: 'recursion', selector: [this.__element, inputElement] }); } } const client = this.client || self.parent && self.parent.client || {}; const result = self.executeCommand(commandFn, args, client); if (self.isChaiAssertion) { return result; } const {isES6AsyncTestcase} = client; if ((result instanceof Promise) && (self.parent.constructor.name === 'Page' || isES6AsyncTestcase)) { // when isES6AsyncTestcase is true, all page and section commands reach here (all commands // return a Promise by default when isES6AsyncTestcase is true). // when isES6AsyncTestcase is false, only those page commands which return promise reach here // (normal API commands do not return Promise when isES6AsyncTestcase is false). Object.assign(result, self.parent); // Add parent prototype methods (like api, client, etc.) to result const parentPrototype = Object.getPrototypeOf(self.parent); Object.getOwnPropertyNames(parentPrototype).forEach((propertyName) => { if (propertyName === 'constructor') { return; } const propertyDescriptor = Object.getOwnPropertyDescriptor(parentPrototype, propertyName); Object.defineProperty(result, propertyName, propertyDescriptor); }); return result; } return self.parent; }; } validate(elementOrSection, strategy, type) { let target = null; let available = null; let typeAvailable = 'elements'; let prefix; let showStrategy = ''; let showAvailable; switch (type) { case Command.TYPE_ELEMENT: target = available = this.parent.elements; prefix = 'Element'; break; case Command.TYPE_SECTION: target = available = this.parent.section; typeAvailable = 'sections'; prefix = 'Section'; break; } let isValid = false; if (elementOrSection in target) { isValid = true; } if (isValid && strategy) { isValid = target[elementOrSection].locateStrategy && target[elementOrSection].locateStrategy === strategy; } if (!isValid) { showAvailable = Object.keys(available); if (strategy) { showStrategy = `[locateStrategy='${strategy}']`; showAvailable = showAvailable.map(item => `${item}[locateStrategy='${target[item].locateStrategy}']`); } throw new Error(`${prefix} "${elementOrSection}${showStrategy}" was not found in "${this.parent.name}". Available ${typeAvailable}: ${showAvailable.join(', ')}`); } } /** * Given an element name, returns that element object * * @param {string} elementName Name of element * @param {string} [strategy] * @returns {Element} The element object */ getElement(elementName, strategy = null) { this.validate(elementName, strategy, Command.TYPE_ELEMENT); return this.parent.elements[elementName]; } /** * Given a section name, returns that section object * * @param {string} sectionName Name of section * @param {string} [strategy] * @returns {Element} The section object */ getSection(sectionName, strategy = null) { this.validate(sectionName, strategy, Command.TYPE_SECTION); return this.parent.section[sectionName]; } getSelectorFromArgs(args) { let selectorArg = args[0]; const isSelector = Command.isPossibleElementSelector(selectorArg, this.commandName); if (isSelector) { // check if both strategy and selector are specified as args const {LocateStrategy} = Utils; const isStrategySpecified = LocateStrategy.isValid(selectorArg); if (isStrategySpecified && Utils.isString(args[1])) { selectorArg = { selector: args[1], locateStrategy: args[0] }; } return selectorArg; } return null; } /** * Identifies element references (@-prefixed selectors) within an argument * list and converts it into an element object with the appropriate * selector or recursion chain of selectors. * * @param {Array} args The argument list to check for an element selector. */ parseElementSelector(args) { const selector = this.getSelectorFromArgs(args); if (!selector) { return; } // currently only support first argument for @-elements let inputElement = Element.createFromSelector(selector); if (inputElement.hasElementSelector()) { const nameSections = inputElement.selector.substring(1).split(':'); const name = nameSections[0]; const pseudoSelector = nameSections[1] || null; // When true, indicates that the selector references a selector within a section rather than an elements definition. // eg: .expect.section('@footer').to.be.visible const isSectionSelector = this.isChaiAssertion && this.commandName === 'section'; const getter = isSectionSelector ? this.getSection : this.getElement; const strategy = Utils.isObject(selector) && selector.locateStrategy || null; const elementOrSection = getter.call(this, name, strategy); elementOrSection.pseudoSelector = pseudoSelector; Element.copyDefaults(inputElement, elementOrSection); inputElement.locateStrategy = elementOrSection.locateStrategy; inputElement.selector = elementOrSection.selector; // force replacement of @-selector inputElement = inputElement.getRecursiveLookupElement() || inputElement; args[0] = inputElement; } else { // if we're calling an element on a section using a css/xpath selector, // then we need to retrieve the element using recursion const Section = require('./section.js'); if (this.parent instanceof Section) { inputElement.parent = this.parent; inputElement = inputElement.getRecursiveLookupElement() || Element.createFromSelector({ locateStrategy: 'recursion', selector: [this.parent, inputElement] }); const ScopedElementApi = require('../api/_loaders/element-api'); if (ScopedElementApi.isScopedElementCommand(this.commandName)) { // only locate the parent sections recursively and then find the main element using // original method and arguments. this.onlyLocateSectionsRecursively = true; } args[0] = inputElement; } } } /** * @param {Function} commandFn * @param {Array} args * @param {Object} context */ executeCommand(commandFn, args, context) { let parseArgs; if (Utils.isObject(args[0]) && Array.isArray(args[0].args)) { parseArgs = args[0].args; } else { parseArgs = args; } this.parseElementSelector(parseArgs); return commandFn.apply(context, args); } } class CommandLoader { /** * Entry point to add commands (elements commands, assertions, etc) to the page or section * * @param {Object} parent The parent page or section * @param {function} commandLoader function that retrieves commands * @returns {null} */ static addWrappedCommands(parent, commandLoader) { const commands = { get '__pageObjectItem__' () { return parent; } }; const wrappedCommands = commandLoader(commands); CommandLoader.applyCommandsToTarget(parent, parent, wrappedCommands); if (parent.assert && parent.verify) { const ApiLoader = require('../api'); parent.assert = ApiLoader.makeAssertProxy(parent.assert); parent.verify = ApiLoader.makeAssertProxy(parent.verify); } } static addWrappedCommandsAsync(parent, commandLoader) { const commands = {}; const wrappedCommands = commandLoader(commands); CommandLoader.applyCommandsToTarget(parent, parent, wrappedCommands); return wrappedCommands; } /** * Adds commands (elements commands, assertions, etc) to the page or section * * @param {Object} parent The parent page or section * @param {Object} target What the command is added to (parent|section or assertion object on parent|section) * @param {Object} commands * @returns {null} */ static applyCommandsToTarget(parent, target, commands) { Object.keys(commands).forEach(function(commandName) { if (isAllowedNamespace(commandName)) { target[commandName] = target[commandName] || {}; const isChaiAssertion = commandName === 'expect'; const namespace = commands[commandName]; Object.keys(namespace).forEach(function(nsCommandName) { target[commandName][nsCommandName] = CommandLoader.addCommand({ target: target[commandName], commandFn: namespace[nsCommandName], commandName: nsCommandName, parent, isChaiAssertion }); }); } else { // don't load namespaces here, otherwise they'll be wrapped by a function. if (Utils.isObject(commands[commandName])) { return; } target[commandName] = CommandLoader.addCommand({ target, commandFn: commands[commandName], commandName, parent, isChaiAssertion: false, isES6Async: Utils.isES6AsyncFn(commands[commandName]) }); } }); } /** * @param parent * @param originalApi * @param targetApi * @param {string} commandName * @returns {function} */ static wrapElementCommand(parent, originalApi, targetApi, commandName) { const originalFn = originalApi[commandName]; const command = new Command(parent, commandName, false); return function(...args) { const origArgs = args.slice(); command.parseElementSelector(args); if (command.onlyLocateSectionsRecursively) { // only happens in case of new scoped element api, when non @-referenced selector is passed // to element api methods for sections. Doing this is not really necessary for `find` and `findAll` // methods but only for testing-library methods (which only accepts string as first argument), // but we do it for them anyways. // transfer n-1 selectors to `element()` and the main selector (origArgs) to the actual method. args[0].selector.pop(); const parentSectionElement = targetApi(args[0]); return parentSectionElement[commandName](...origArgs); } return originalFn.apply(targetApi, args); }; } /** * @param parent * @param api * @param {Array} commands */ static wrapProtocolCommands(parent, api, commands) { commands.forEach(commandName => { api[commandName] = CommandLoader.wrapElementCommand(parent, api, api, commandName); }); } static wrapScopedElementApi(parent, api, elementCommands) { const wrappedElementFn = CommandLoader.wrapElementCommand(parent, api, api, 'element'); elementCommands.forEach(commandName => { let names = commandName; if (!Array.isArray(names)) { names = [names]; } names.forEach(commandName => { wrappedElementFn[commandName] = CommandLoader.wrapElementCommand(parent, api.element, wrappedElementFn, commandName); }); }); api.element = wrappedElementFn; } static addCommand({target, commandFn, commandName, parent, isChaiAssertion, isES6Async = false, overwrite = false}) { if (target[commandName] && !overwrite) { const err = new TypeError(`Error while loading the page object commands: the command "${commandName}" is already defined.`); err.displayed = false; err.showTrace = false; throw err; } const command = new Command(parent, commandName, isChaiAssertion, isES6Async); return command.createWrapper(commandFn); } } module.exports = CommandLoader; ================================================ FILE: lib/page-object/index.js ================================================ const Utils = require('../utils'); const CommandWrapper = require('./command-wrapper.js'); const BaseObject = require('./base-object.js'); const ScopedElementAPILoader = require('../api/_loaders/element-api'); const shouldReturnPromise = function() { return this.client.isES6AsyncTestcase || this.client.isES6AsyncCommand; }; const validateUrl = function(url) { let goToUrl = this.getUrl(url); if (!goToUrl) { goToUrl = this.client.settings.launchUrl; } else if (Utils.isString(goToUrl) && Utils.relativeUrl(goToUrl)) { if (!this.client.settings.launchUrl) { const err = new Error(`Invalid URL ${goToUrl} in "${this.name}" page object. When using relative uris, you must ` + 'define a "baseUrl" in your nightwatch config.'); throw err; } goToUrl = Utils.uriJoin(this.client.settings.launchUrl, goToUrl); } if (goToUrl === null) { const err = new Error(`Invalid URL: You must either add a url property to the "${this.name}" or provide a url as an argument`); throw err; } return goToUrl; }; /** * Class that all pages subclass from * * @param {Object} options Page options defined in page object * @constructor */ class Page extends BaseObject { constructor(options, commandLoader, client) { super(); this.commandLoader = commandLoader; this.__options = options; this.__client = client; this.__api = Object.assign({}, client.api); this.__props = Page.createProps(this, options.props); this.__elements = Page.createElements(this, options.elements); this.__sections = Page.createSections(this, options.sections); const pageCommands = options.commands || []; Page.addCommands(this, pageCommands); CommandWrapper.addWrappedCommands(this, this.commandLoader); CommandWrapper.wrapProtocolCommands(this, this.api, BaseObject.WrappedProtocolCommands); CommandWrapper.wrapScopedElementApi(this, this.api, ScopedElementAPILoader.availableElementCommands); Object.defineProperty(this, 'element', { get() { return this.api.element; }, enumerable: true, configurable: false }); } get api() { return this.__api; } get client() { return this.__client; } get name() { return this.__options.name; } /** * @return {object} */ get props() { return this.__props; } /** * @return {object} */ get elements() { return this.__elements; } /** * @return {object} */ get section() { return this.__sections; } get url() { return this.__options.url; } set url(val) { this.__options.url = val; } /** * Returns the url passed as an argument (or null if no arguments are passed). * If the supplied url is a function, it invokes that function with the page as its context. * * @method getUrl * @param {function|string|object} url * @return {function} */ get getUrl() { /** * @returns {string|null} */ return (url) => { if (Utils.isFunction(url)) { return url.call(this); } if (Utils.isString(url)) { return url; } if (Utils.isObject(url) && this.url) { return Utils.replaceParams(this.url, url); } return null; }; } /** * This command is an alias to url and also a convenience method because when called without any arguments * it performs a call to .url() with passing the value of `url` property on the page object. * Uses `url` protocol command. * * @method navigate * @param {Object} [url=this.url] Url to navigate to. * @param {function} [callback] Optional callback function to be called when the command finishes. * @returns {*} */ get navigate() { return function(url, callback) { let goToUrl; if (typeof url == 'function' && !callback) { callback = url; url = this.url; } if (!url) { url = this.url; } if (!shouldReturnPromise.call(this)) { goToUrl = validateUrl.call(this, url, callback); this.api.url(goToUrl, callback); return this; } const promise = new Promise((resolve, reject) => { (async () => { let goToUrl; try { goToUrl = validateUrl.call(this, url, callback); } catch (err) { reject(err); return; } await this.api.url(goToUrl, callback); resolve(); })(); }); Object.assign(promise, this); return promise; }; } } module.exports = Page; ================================================ FILE: lib/page-object/section.js ================================================ const Element = require('../element'); const CommandWrapper = require('./command-wrapper.js'); const ScopedElementAPILoader = require('../api/_loaders/element-api'); /** * Class that all sections subclass from * * @param {Object} definition User-defined section options defined in page object * @param {Object} options Additional options to be given to the section. * @constructor */ class Section extends Element { get section() { return this.sections; } /** * A shallow copy of the page-object api is sufficient because the wrapping of api commands (e.g. .elements()) is only one-level */ createApiShallowCopy() { return Object.assign({}, this.parent.api); } constructor(definition, options) { super(definition, options); this.client = this.parent.client; this.api = this.createApiShallowCopy(); this.commandLoader = this.parent.commandLoader; const BaseObject = require('./base-object.js'); this.props = BaseObject.createProps(this, definition.props); this.elements = BaseObject.createElements(this, definition.elements); this.sections = BaseObject.createSections(this, definition.sections); BaseObject.addCommands(this, definition.commands || []); CommandWrapper.addWrappedCommands(this, this.commandLoader); CommandWrapper.wrapProtocolCommands(this, this.api, BaseObject.WrappedProtocolCommands); CommandWrapper.wrapScopedElementApi(this, this.api, ScopedElementAPILoader.availableElementCommands); Object.defineProperty(this, 'element', { get() { return this.api.element; }, enumerable: true, configurable: false }); } } module.exports = Section; ================================================ FILE: lib/reporter/axe-report.js ================================================ const CliTable = require('cli-table3'); const Utils = require('../utils'); const {Logger} = Utils; const describeViolations = (violations) => { const aggregate = {}; violations.map(({nodes}, index) => { nodes.forEach(({target, html}) => { const key = JSON.stringify(target) + html; if (aggregate[key]) { aggregate[key].violations = aggregate[key].violations || []; aggregate[key].violations.push(index); } else { aggregate[key] = { target: JSON.stringify(target), html, index: [index] }; } }); }); return Object.values(aggregate).map(({target, html, violations}) => { return {target, html, violations: JSON.stringify(violations)}; }); }; const describePasses = (passes) => { const aggregate = {}; passes.map(({nodes}, index) => { nodes.forEach(({target, html}) => { const key = JSON.stringify(target) + html; aggregate[key] = { target: JSON.stringify(target), html }; }); }); return Object.values(aggregate).map(({target, html}) => { return {target, html}; }); }; module.exports = class AxeReport { static createTable() { const table = new CliTable({ chars: { 'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': '', 'bottom': '═', 'bottom-mid': '', 'bottom-left': '╚', 'bottom-right': '╝' } }); table.push( [ Logger.colors.light_cyan('ID'), Logger.colors.light_cyan('Impact'), Logger.colors.light_cyan('Description'), Logger.colors.light_cyan('Nodes') ], [ Logger.colors.stack_trace('─────────────────────'), Logger.colors.stack_trace('──────────'), '', Logger.colors.stack_trace('──────────') ] ); return table; } constructor(report, {detailedReport = true, includeHtml = true} = {}) { this.report = report; this.detailedReport = detailedReport; this.includeHtml = includeHtml; } hasViolations() { return this.report.violations.length > 0; } printPasses() { const table = new CliTable({ chars: { 'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': '', 'bottom': '═', 'bottom-mid': '', 'bottom-left': '╚', 'bottom-right': '═╝' } }); table.push( [ Logger.colors.light_cyan('Rule'), Logger.colors.light_cyan('Description'), Logger.colors.light_cyan('Nodes') ], [ Logger.colors.stack_trace('─────────────────────'), Logger.colors.stack_trace('──────────'), Logger.colors.stack_trace('───────') ] ); const {passes} = this.report; passes.forEach(({id, description, nodes}) => { table.push([id, description, nodes.length]); }); const nodes = describePasses(passes).map(({target, html}) => { return {target, html}; }); table.push( [ Logger.colors.stack_trace('─────────────────────'), Logger.colors.stack_trace('────────────────────────'), Logger.colors.stack_trace('───────') ], [ Logger.colors.light_cyan('Target'), {colSpan: 2, content: Logger.colors.light_cyan('Html')} ] ); nodes.forEach(({target, html}) => { table.push( [ target, {colSpan: 2, content: Logger.colors.stack_trace(html)} ] ); }); // eslint-disable-next-line no-console console.log(table.toString()); } printViolations(component) { // eslint-disable-next-line no-console console.log('\n' + Logger.colors.light_red(`Accessibility violations for: ${component}`)); const {violations} = this.report; const table = AxeReport.createTable(); violations.forEach(({id, impact, description, nodes}) => { table.push([id, impact, description, nodes.length]); }); // summary if (this.detailedReport) { const nodeViolations = describeViolations(violations).map(({target, html, violations}) => { if (!this.includeHtml) { return { target, violations }; } return {target, html, violations}; }); table.push( [ Logger.colors.stack_trace('─────────────────────'), Logger.colors.stack_trace('──────────'), '', Logger.colors.stack_trace('──────────') ], [{colSpan: 2, content: Logger.colors.light_cyan('Target')}, Logger.colors.light_cyan('Html'), Logger.colors.light_cyan('Violations')] ); nodeViolations.forEach(({target, html, violations}) => { table.push( [{colSpan: 2, content: target}, Logger.colors.stack_trace(html), violations] ); }); table.push([ {colSpan: 4, content: ''} ]); } // eslint-disable-next-line no-console console.log(table.toString()); } }; ================================================ FILE: lib/reporter/base-reporter.js ================================================ const fs = require('fs'); const stripAnsi = require('strip-ansi'); const Utils = require('../utils'); const Results = require('./results.js'); module.exports = class BaseReporter { constructor(results, opts = {}) { this.results = results; this.options = opts; } adaptAssertions(module) { module.completed = Object.keys(module.completed).reduce(function(prev, item) { if ((module.skipped && module.skipped.includes(item))) { return prev; } const testcase = module.completed[item]; const assertions = testcase.assertions; for (let i = 0; i < assertions.length; i++) { assertions[i].message = stripAnsi(assertions[i].message); if (assertions[i].stackTrace) { assertions[i].stackTrace = stripAnsi(assertions[i].stackTrace); assertions[i].stackTrace = Utils.stackTraceFilter(assertions[i].stackTrace.split('\n')); } } if (testcase.failed > 0 && assertions.length === 0 && testcase.lastError) { const stackParts = testcase.stackTrace.split('\n'); testcase.stackTrace = Utils.stackTraceFilter(stackParts); testcase.message = stripAnsi(testcase.lastError.message); } prev[item] = testcase; return prev; }, {}); module.completedSections = Object.keys(module.completedSections).reduce(function(prev, item) { if ((module.skippedAtRuntime && module.skippedAtRuntime.includes(item)) || (module.skippedByUser && module.skippedByUser.includes(item)) ) { return prev; } const testcase = module.completedSections[item]; const commands = testcase.commands; for (let i = 0; i < commands.length; i++) { if (commands[i].status === Results.TEST_FAIL && commands[i].result.stack) { commands[i].result.message = stripAnsi(commands[i].result.message || ''); if (commands[i].result.stack) { commands[i].result.stack = stripAnsi(commands[i].result.stack); commands[i].result.beautifiedStack = Utils.beautifyStackTrace(commands[i].result.stack, true, module.modulePath, false); } } } prev[item] = testcase; return prev; }, {}); } writeReportFile(filename, rendered, shouldCreateFolder, output_folder) { return (shouldCreateFolder ? Utils.createFolder(output_folder) : Promise.resolve()) .then(() => { return new Promise((resolve, reject) => { fs.writeFile(filename, rendered, function(err) { if (err) { return reject(err); } resolve(); }); }); }); } /** * @override */ async writeReport(moduleKey) {} write() { const keys = Object.keys(this.results.modules); const promises = keys.map(moduleKey => { return this.writeReport(moduleKey); }); return Promise.all(promises); } }; ================================================ FILE: lib/reporter/global-reporter.js ================================================ const path = require('path'); const Convert = require('ansi-to-html'); const stripAnsi = require('strip-ansi'); const Concurrency = require('../runner/concurrency'); const DefaultSettings = require('../settings/defaults.js'); const Utils = require('../utils'); const {Logger, isFunction} = Utils; const Results = require('./results.js'); const AxeReport = require('./axe-report.js'); const Summary = require('./summary.js'); const PluginLoader = require('../api/_loaders/plugin.js'); const colors = Logger.colors; module.exports = class GlobalReporter { static get initialReport() { return { passed: 0, failed: 0, errors: 0, skipped: 0, tests: 0, assertions: 0, errmessages: [], errorsPerTest: {}, modules: {}, modulesWithEnv: {}, elapsedTime: 0, startTimestamp: null, endTimestamp: null }; } constructor(reporter = DefaultSettings.default_reporter, settings, {openReport = false, reportFileName = null} = {}) { this.suiteResults = []; this.skippedSuites = 0; this.uncaughtErr = null; this.reportFileName = reportFileName; this.reporterFile = []; if (Array.isArray(reporter)) { // Any subsequent changes in `this.reporterFile` shouldn't lead to changes in // `argv.reporter` provided by the user, or `DefaultSettings.default_reporter`. this.reporterFile = reporter.slice(0); } else if (typeof reporter == 'string') { this.reporterFile = reporter.split(','); } this.settings = settings; this.summary = new Summary(settings); this.openReport = openReport; this.ansiConverter = new Convert(); } registerUncaughtErr(err) { this.uncaughtErr = err; } isDisabled() { return this.settings.output_folder === false; } hasTestFailures() { return this.uncaughtErr || this.suiteResults.some(item => { return item.hasFailures; }); } addTestSuiteResults(testResults, httpOutput) { testResults = testResults || {}; const loggerOutput = httpOutput || Logger.collectOutput(); testResults.httpOutput = loggerOutput.map(item => { return [item[0], this.ansiConverter.toHtml(item[1]), (item[2] ? this.ansiConverter.toHtml(item[2]) : '')]; }); testResults.rawHttpOutput = loggerOutput.map(item => { return [item[0], stripAnsi(item[1] || '').replace(/'/g, '\''), (item[2] ? stripAnsi(item[2]) : '').replace(/'/g, '\'')]; }); this.suiteResults.push(testResults); return this; } setupChildProcessListener(emitter) { if (!Concurrency.isTestWorker()) { emitter.on('message', data => { data = this.settings.use_child_process ? JSON.parse(data) : data; this.addTestSuiteResults(data.results, data.httpOutput); }); } } create(startTime) { const initialReport = GlobalReporter.initialReport; if (this.uncaughtErr) { initialReport.errors++; const errorMessage = Logger.getErrorContent(this.uncaughtErr); initialReport.errmessages.push(errorMessage); } const endTime = new Date().getTime(); this.elapsedTime = endTime - startTime; this.globalResults = Results.createGlobalReport(this.suiteResults, initialReport); this.globalResults.elapsedTime = (this.elapsedTime / 1000).toPrecision(4); this.globalResults.startTimestamp = new Date(startTime).toUTCString(); this.globalResults.endTimestamp = new Date(endTime).toUTCString(); return this; } print() { if (Concurrency.isWorker() || !this.settings.output) { return this; } if (this.hasTestFailures()) { const countMessage = this.getTestsFailedMessage(); const {columns = 100} = process.stdout; // eslint-disable-next-line no-console console.log(colors.light_red('\n' + new Array(Math.max(100, columns - 20)).join('─'))); // eslint-disable-next-line no-console console.log(`\n ️${colors.light_red('TEST FAILURE')} ${colors.stack_trace('(' + Utils.formatElapsedTime(this.elapsedTime) + ')')}${colors.light_red(':')} ${countMessage}\n`); this.summary.print(this.globalResults); // eslint-disable-next-line no-console console.log(''); } else { if (!this.shouldShowSummary()) { return; } const message = this.getTestsPassedMessage(); // eslint-disable-next-line no-console console.log(`\n${message} ${colors.stack_trace('(' + Utils.formatElapsedTime(this.elapsedTime) + ')')}`); } this.printA11yReport(); return this; } loadReporter() { const allAvailableReporters = this.reporterFile.slice(0); return Promise.all(allAvailableReporters.map((reporter) => this.loadFile(reporter))); } printA11yReport() { Object.keys(this.globalResults.modules).forEach(testSuite => { Object.keys(this.globalResults.modules[testSuite].completed).forEach(key => { const testcase = this.globalResults.modules[testSuite].completed[key]; if (testcase.a11y) { const a11yReport = new AxeReport(testcase.a11y); if (testcase.a11y.verbose || a11yReport.hasViolations()) { // eslint-disable-next-line no-console console.log('\n' + Logger.colors.green(`Accessibility report for: ${testcase.a11y.component}`)); } if (testcase.a11y.verbose) { a11yReport.printPasses(key); } if (a11yReport.hasViolations()) { a11yReport.printViolations(testcase.a11y.component); } } }); }); } async loadFile(reporterFile) { const builtInReporterFileName = path.join(__dirname, 'reporters', reporterFile + '.js'); const fileExists = await Utils.fileExists(builtInReporterFileName); if (!fileExists) { return this.loadCustomReporter(reporterFile); } return require(builtInReporterFileName); } async loadCustomReporter(reporterFile) { let customReporterModuleExists; let reporter; try { reporter = require(reporterFile); customReporterModuleExists = true; } catch (err) { customReporterModuleExists = false; } const customReporterExists = await Utils.fileExists(reporterFile); if (!customReporterExists && !customReporterModuleExists) { throw new Error(`The custom reporter "${reporterFile}" cannot be resolved.`); } if (!customReporterModuleExists) { reporter = require(path.resolve(reporterFile)); } if (Utils.isFunction(reporter.write)) { return reporter; } throw new Error(`The custom reporter "${reporterFile}" must have a public ".write(results, options, [callback])" method defined which should return a Promise.`); } async writeReportToFile(globalResults) { if (this.isDisabled()) { return; } try { await Utils.createFolder(this.settings.output_folder); const reporters = await this.loadReporter(); return Promise.all( reporters.map(reporterFile => this.writeReport(reporterFile, globalResults)) ); } catch (err) { const error = new Error('An error occurred while trying to save the report file'); error.detailedErr = err.message; throw error; } } async writeReport(reporter, globalResults) { const {globals, output_folder, start_session, folder_format, reporter_options: {filename_format}} = this.settings; const needsCallback = reporter.write.length === 3; const options = { filename_prefix: this.settings.report_prefix, report_filename: this.reportFileName, output_folder, globals, start_session, reporter, openReport: this.openReport, filename_format, folder_format }; if (needsCallback) { // backwards compatibility return new Promise((resolve, reject) => { reporter.write(globalResults, options, function (err) { if (err) { return reject(err); } resolve(); }); }); } return await reporter.write(globalResults, options); } shouldShowSummary() { const modules = Object.keys(this.globalResults.modules); if (modules.length > 1) { return true; } if (modules.length <= 0) { return false; } return Object.keys(this.globalResults.modules[modules[0]].completed).length > 1; } hasAssertionCount() { const testsCounts = this.getTotalTestsCount(); return !this.settings.unit_tests_mode && testsCounts > 0 && this.globalResults.assertions > 0; } getTotalTestsCount() { return Object.keys(this.globalResults.modules) .reduce((count, moduleKey) => { const module = this.globalResults.modules[moduleKey]; return count + module.tests; }, 0); } getTestsFailedMessage() { const hasCount = this.hasAssertionCount(); const totalTests = this.getTotalTestsCount(); let errorsMsg = ''; const failedMsg = hasCount ? 'assertions' : 'tests'; let passedMsg = colors.green(this.globalResults.passed) + ' passed'; // assertions passed const skippedSuitesCount = this.skippedSuites > 0 ? `\n - ${colors.light_red(this.skippedSuites)} other test suites were aborted; ` : ''; if (!hasCount) { const passedCount = Math.max(0, totalTests - this.globalResults.failed - this.globalResults.errors); // testcases passed passedMsg = '\n - ' + colors.green(passedCount + '/' + (totalTests > 0 ? totalTests : 'NA')) + ' tests passed'; } let skipped = ''; if (this.globalResults.skipped) { skipped = `\n - ${colors.cyan(this.globalResults.skipped)} skipped`; } const globalErrors = this.globalResults.errors; if (globalErrors) { const suffix = globalErrors > 1 ? 's' : ''; errorsMsg += `\n - ${colors.red(globalErrors)} error${suffix} during execution;`; } const failedCountMsg = `\n - ${colors.red(this.globalResults.failed)} ${failedMsg} failed;`; return `${errorsMsg} ${skippedSuitesCount}${failedCountMsg} ${passedMsg}${skipped}`; } getTestsPassedMessage() { const hasCount = this.hasAssertionCount(); let message; let count; if (hasCount) { count = this.globalResults.passed; message = colors.green(` ✨ PASSED. ${count} ${count > 1 ? 'total assertions' : ' assertion'}`); } else { count = this.getTotalTestsCount(); message = `${colors.green(' ✨ PASSED. ' + count)} tests`; } return message; } getPluginReporters() { const plugins = this.settings.plugins || []; const reporters = plugins.reduce((prev, pluginName) => { const plugin = PluginLoader.load(pluginName); if (isFunction(plugin.instance?.reporter)) { prev.push(plugin.instance); } else if (plugin.globals?.reporter && isFunction(plugin.globals.reporter)) { prev.push({ reporter: plugin.globals.reporter, settings: plugin.globals.settings || {} }); } return prev; }, []); return reporters; } getTimeoutValueMs(customReporter) { // global timeout for all custom reporters – set by user if (this.settings.custom_reporter_timeout) { return this.settings.custom_reporter_timeout; } // timeout for a specific custom reporter – set by plugin if (customReporter.settings?.timeoutMs) { return customReporter.settings.timeoutMs; } // default timeout when nothing is set – set by nightwatch return this.settings.globals.customReporterCallbackTimeout; } callReporterFn(reporter, globalResults, {callbackTimeoutId, resolve, reject}) { try { if (reporter.length === 2) { const reporterFnAsync = Utils.makeFnAsync(2, reporter, this.settings.globals); reporterFnAsync.call(this.settings.globals, globalResults, function () { clearTimeout(callbackTimeoutId); resolve(); }); } else { const promise = reporter.call(this.settings.globals, globalResults); if (promise instanceof Promise) { promise .then(() => { clearTimeout(callbackTimeoutId); resolve(); }) .catch(err => { clearTimeout(callbackTimeoutId); reject(err); }); } else { clearTimeout(callbackTimeoutId); resolve(); } } } catch (err) { clearTimeout(callbackTimeoutId); reject(err); } } runCustomGlobalReporter(globalResults) { const pluginReporters = this.getPluginReporters(); let customReporters; if (Utils.isFunction(this.settings.globals.reporter)) { customReporters = [{ reporter: this.settings.globals.reporter, settings: { timeoutMs: this.settings.globals.customReporterCallbackTimeout } }]; } else if (Array.isArray(this.settings.globals.reporter)) { customReporters = this.settings.globals.reporter.map(reporter => { if (Utils.isFunction(reporter)) { return { reporter, settings: { timeoutMs: this.settings.globals.customReporterCallbackTimeout } }; } return reporter; }); } customReporters = customReporters.concat(pluginReporters); const results = customReporters.map(customReporter => { return new Promise((resolve, reject) => { const callbackTimeoutId = setTimeout(() => { const reporterName = customReporter.reporterName ? `"${customReporter.reporterName}" ` : ''; const error = new Error(`Timeout while waiting for the custom reporter ${reporterName}to finish.`); error.help = [ 'Make sure the custom reporter calls the "done" callback when finished.', 'If the reporter is async, make sure the async operation is completed before the timeout is reached.', 'You can extend the timeout by defining the "custom_reporter_timeout" config setting in your nightwatch config file.' ]; error.link = 'See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-reporters.html for more details.'; reject(error); }, this.getTimeoutValueMs(customReporter)); const {reporter} = customReporter; this.callReporterFn(reporter, globalResults, {callbackTimeoutId, resolve, reject}); }); }); return Promise.all(results); } save() { if (Concurrency.isWorker()) { return Promise.resolve(); } return Promise.all([ this.runCustomGlobalReporter(this.globalResults), this.writeReportToFile(this.globalResults) ]); } }; ================================================ FILE: lib/reporter/index.js ================================================ const path = require('path'); const Utils = require('../utils'); const TestResults = require('./results.js'); const SimplifiedReporter = require('./simplified.js'); const {Logger, Screenshots} = Utils; const {colors} = Logger; const analyticsCollector = require('../utils/analytics.js'); class Reporter extends SimplifiedReporter { static printAssertions(testcase) { testcase.assertions.forEach(function(a) { if (a.failure !== false) { const message = a.stackTrace.split('\n'); message.unshift(a.fullMsg); Utils.showStackTrace(message.join('\n')); } }); } /** * * @param {Array} tests * @param {SuiteRetries} suiteRetries * @param {Object} settings * @param {Object} addOpts */ constructor({settings, tests, suiteRetries, addOpts = {}, skippedTests, allScreenedTests}) { super(settings); this.suiteRetries = suiteRetries; this.suiteName = addOpts.suiteName; this.testResults = new TestResults(tests, addOpts, settings, skippedTests, allScreenedTests); this.currentContext = null; this.__printA11Report = false; this.reporter = addOpts.repoter; this.isMobile = addOpts.isMobile; this.testResults.initCurrentTest({ module: addOpts.moduleKey, testName: '', group: addOpts.groupName }); }; /** * This is the exported property on the Nightwatch api object, which is passed on to tests * * @return {null|object} */ get currentTest() { return this.testResults.currentTest; } get currentSection() { return this.testResults.currentSection; } get printA11Report() { return this.__printA11Report; } /** * @param {TestCase} testcase * @param {Context} context */ setCurrentTest(testcase, context) { // called for every test case (not for hooks) this.currentContext = context; this.setCurrentSection(testcase); this.testResults.setCurrentTest(testcase); } setCurrentSection(testcase) { // called for all global hooks, testsuite before/after hooks, // and for all test cases but NOT for before_each/after_each hooks // separately (considered part of the test case itself) this.testResults.setCurrentSection(testcase); } markHookRun(hookName) { this.testResults.markHookRun(hookName); } unmarkHookRun(hookName) { this.testResults.unmarkHookRun(hookName); } setAxeResults(results) { this.currentTest.results.a11y = results; } printA11yReport() { this.__printA11Report = true; } resetCurrentTestName() { this.testResults.resetCurrentTestName(); } get unitTestsMode() { return this.currentContext ? this.currentContext.unitTestsMode : this.settings.unit_tests_mode; } get currentTestCasePassed() { return this.testResults.currentTestCasePassed; } get allTestsPassed() { return this.testResults.testsPassed(); } /** * @param {Error} err * * @return {boolean} */ shouldIncrementTotalCount(err) { const {currentTest} = this; const currentTestName = this.testResults.getCurrentTestName(); const shouldRetryTestcase = currentTest && this.suiteRetries && this.suiteRetries.shouldRetryTest(currentTestName); let incrementTotalCount = err.incrementErrorCount || Utils.isUndefined(err.incrementErrorCount); if (err.incrementErrorsNo || shouldRetryTestcase) { incrementTotalCount = false; } return incrementTotalCount; } setFileNamePrefix(prefix) { this.testResults.reportPrefix = prefix; } setSessionInfo(data) { this.testResults.sessionCapabilities = data.capabilities; this.testResults.sessionId = data.sessionId; } setElapsedTime() { this.testResults.setElapsedTime(); } setTestSectionElapsedTime() { this.testResults.setTestSectionElapsedTime(); } setTestStatus() { this.testResults.setTestStatus(); } collectTestSectionOutput() { this.testResults.collectTestSectionOutput(); } testSuiteFinished() { this.testResults.setTotalElapsedTime(); } exportResults() { const results = this.testResults.export; if (this.printA11Report) { results.printA11Report = true; } return results; } //////////////////////////////////////////////////////////// // Results logging //////////////////////////////////////////////////////////// logTestCase(testName) { if (this.settings.live_output || !this.settings.parallel_mode) { // eslint-disable-next-line no-console console.log(`${(!this.settings.silent ? '\n\n' : '')}\n Running ${colors.green(testName)}${colors.stack_trace(':')}`); const {columns = 100} = process.stdout; // eslint-disable-next-line no-console console.log(colors.stack_trace(new Array(Math.max(100, Math.floor(columns / 2))).join('─'))); //} } else { // eslint-disable-next-line no-console console.log(''); // eslint-disable-next-line no-console console.log(` – ${colors.green(testName)}\n`); } } /** * @param {Object} result */ logAssertResult(result) { this.testResults.logAssertion(result); } /** * @param {TheeNode} node * @param {Object} result */ logCommandResult({node, result}) { try { let commandResult = result; const isSuccess = result === undefined || result == null || result.passed || !((result instanceof Error) || (result.error instanceof Error) || result.status === -1 ); // Use only necessary values if (result) { const {status, message, showDiff, name, abortOnFailure, stack, beautifiedStack} = result; commandResult = { status, message, showDiff, name, abortOnFailure, stack, beautifiedStack }; if (this.settings.reporter_options.save_command_result_value) { if (Utils.isString(result.value) && result.value.length > 500) { const filepath = path.join(this.settings.output_folder || '', 'tmp', this.testResults.uuid, `${node.fullName}_${Date.now()}.txt`); commandResult.valuePath = filepath; Utils.writeToFile(filepath, result.value); } else { commandResult.value = result.value; } } } if (this.shouldLogCommand(node)) { this.testResults.logCommand({ name: node.fullName, args: Reporter.stringifyArgs(node.args), startTime: node.startTime, endTime: node.endTime, elapsedTime: node.elapsedTime, status: isSuccess ? TestResults.TEST_PASS : TestResults.TEST_FAIL, result: commandResult }); } } catch (e) { // ignore exceptions } } static stringifyArgs(args) { if (Array.isArray(args)) { return args.map((arg) => { let stringifiedArg; try { stringifiedArg = JSON.stringify(arg); } catch { stringifiedArg = arg && arg.toString(); } return stringifiedArg; }); } return args; } shouldLogCommand(node) { if (!node.parent) { //root node return false; } if (node.parent.isRootNode) { return true; } if (node.addedInsideCallback) { return true; } return false; } registerPassed(message) { Logger.logDetailedMessage(` ${colors.green(Utils.symbols.ok)} ${message}`); this.testResults.incrementPassedCount(); } registerFailed(err) { const incrementTotal = this.shouldIncrementTotalCount(err); this.testResults .setLastError(err, {incrementTotal}) .incrementFailedCount(incrementTotal); } registerTestError(err) { if (err.registered) { return; } analyticsCollector.collectErrorEvent(err); super.registerTestError(err); const incrementTotal = this.shouldIncrementTotalCount(err); this.testResults .setLastError(err, {incrementTotal, addToErrArray: true}) .incrementErrorCount(incrementTotal); } /** * Subtracts the number of passed assertions from the total assertions count */ resetCurrentTestPassedCount() { const assertionsCount = this.testResults.currentTestResult.passed; this.testResults.subtractPassedCount(assertionsCount); } printTestResult() { let ok = false; if (this.testResults.currentTestCasePassed) { ok = true; } const elapsedTime = this.testResults.currentTestElapsedTime; const currentTestResult = this.testResults.currentTestResult; const Concurrency = require('../runner/concurrency'); const isWorker = Concurrency.isWorker(); if (isWorker || !this.settings.detailed_output || this.unitTestsMode) { this.printSimplifiedTestResult(ok, elapsedTime, isWorker); return; } if (ok && currentTestResult.passed > 0) { Logger.logDetailedMessage(`\n ✨ ${colors.green('PASSED.')} ${colors.green(currentTestResult.passed)} assertions. (${Utils.formatElapsedTime(elapsedTime, true)})`); } else if (ok && currentTestResult.passed === 0) { if (this.settings.start_session) { Logger.logDetailedMessage(colors.green('No assertions ran.\n'), 'warn'); } } else { const failureMsg = this.getFailureMessage(); Logger.logDetailedMessage(`\n ${colors.red('FAILED:')} ${failureMsg} (${Utils.formatElapsedTime(elapsedTime, true)})`); } } /** * @param {boolean} ok * @param {number} elapsedTime * @param {boolean} isWorker */ printSimplifiedTestResult(ok, elapsedTime, isWorker) { const {currentTest} = this; const result = [colors[ok ? 'green' : 'red'](Utils.symbols[ok ? 'ok' : 'fail'])]; if (!this.unitTestsMode) { if (isWorker) { result.push(colors.bgBlack.white.bold(this.settings.testEnv)); } result.push(colors.cyan('[' + this.suiteName + ']')); } const testName = currentTest.name; result.push(ok ? testName : colors.red(testName)); if (elapsedTime > 20) { result.push(colors.yellow.bold('(' + Utils.formatElapsedTime(elapsedTime, true) + ')')); } // eslint-disable-next-line no-console console.log(result.join(' ')); if (ok || !currentTest) { return; } const {results} = currentTest; if (this.unitTestsMode && results.lastError) { Logger.error(results.lastError); } else { Reporter.printAssertions(results); } } getFailureMessage() { const failureMsg = []; const currentTestResult = this.testResults.currentTestResult; if (currentTestResult.failed > 0){ failureMsg.push(`${colors.red(currentTestResult.failed)} assertions failed`); } if (currentTestResult.errors > 0) { failureMsg.push(`${colors.red(currentTestResult.errors)} errors`); } if (currentTestResult.passed > 0) { failureMsg.push(`${colors.green(currentTestResult.passed)} passed`); } if (currentTestResult.skipped > 0) { failureMsg.push(`${colors.blue(currentTestResult.skipped)} skipped`); } return failureMsg.join(', ').replace(/,([^,]*)$/g, function(p0, p1) { return ` and ${p1}`; }); } //////////////////////////////////////////////////////////// // Screenshots //////////////////////////////////////////////////////////// /** * @deprecated only used by JSONWire * @param result * @param screenshotContent */ saveErrorScreenshot(result, screenshotContent) { if (this.settings.screenshots.on_error && screenshotContent) { const {currentTest} = this; const prefix = `${currentTest.module}/${currentTest.name}`; const fileName = Screenshots.getFileName(prefix, true, this.settings.screenshots.path); // FIXME: make this async / handle callback Screenshots.writeScreenshotToFile(fileName, screenshotContent); this.testResults.logScreenshotFile(fileName); } } } module.exports = Reporter; module.exports.Simplified = require('./simplified.js'); module.exports.GlobalReporter = require('./global-reporter.js'); ================================================ FILE: lib/reporter/reporters/html.js ================================================ const open = require('open'); const path = require('path'); const lodashPick = require('lodash/pick'); const HtmlReact = require('@nightwatch/html-reporter-template'); const AnsiConverter = require('ansi-to-html'); const Utils = require('../../utils'); const BaseReporter = require('../base-reporter.js'); const {Logger} = Utils; class HtmlReporter extends BaseReporter { static get hookNames(){ return ['__before_hook', '__after_hook', '__global_beforeEach_hook', '__global_afterEach_hook']; } static get ansiConverter(){ return new AnsiConverter(); } openReporter(fileName) { return open(fileName) .catch(err => { Logger.error('Error opening the report: ', err.message); }); } getFolderPrefix() { let folderPrefix = ''; const {folder_format} = this.options; if (folder_format) { if (typeof folder_format === 'function') { folderPrefix = folder_format(this.results); } else if (typeof folder_format === 'string') { folderPrefix = folder_format; } } return folderPrefix; } getFileName() { let fileName = 'index'; const {filename_format} = this.options; if (filename_format) { if (typeof filename_format === 'function') { fileName = filename_format(this.results); } else if (typeof filename_format === 'string') { fileName = filename_format; } } return fileName; } computePassedAndFailedCounts(module) { const result = {passedCount: 0, failedCount: 0}; if (!module || !module.completedSections) { return result; } for (const sectionName of Object.keys(module.completedSections)) { if (HtmlReporter.hookNames.includes(sectionName)) { continue; } const section = module.completedSections[sectionName]; if (section.status === 'pass') { result.passedCount += 1; } else { result.failedCount += 1; } } return result; } createInitialResult(module) { const {passedCount, failedCount} = this.computePassedAndFailedCounts(module); const sessionCapabilities = module.sessionCapabilities || {}; return { metadata: { platformName: sessionCapabilities.platformName, device: 'desktop', browserName: sessionCapabilities.browserName, browserVersion: sessionCapabilities.browserVersion, executionMode: 'local' }, stats: { passed: passedCount, failed: failedCount, skipped: module.skippedCount, total: passedCount + failedCount + module.skippedCount, time: module.timeMs }, modules: {} }; } aggregateEnvironments(testEnv, moduleKey, module) { const moduleName = module.name ? `${module.name} (${moduleKey})` : moduleKey; if (!this.environments[testEnv]) { this.environments[testEnv] = this.createInitialResult(module); } else { const {passedCount, failedCount} = this.computePassedAndFailedCounts(module); const sessionCapabilities = module.sessionCapabilities || {}; this.environments[testEnv].stats.passed += passedCount; this.environments[testEnv].stats.failed += failedCount; this.environments[testEnv].stats.skipped += module.skippedCount; this.environments[testEnv].stats.total += passedCount + failedCount + module.skippedCount; this.environments[testEnv].stats.time += module.timeMs || this.environments[testEnv].stats.time; this.environments[testEnv].metadata.platformName = this.environments[testEnv].metadata.platformName || sessionCapabilities.platformName; this.environments[testEnv].metadata.browserName = this.environments[testEnv].metadata.browserName || sessionCapabilities.browserName; this.environments[testEnv].metadata.browserVersion = this.environments[testEnv].metadata.browserVersion || sessionCapabilities.browserVersion; } this.environments[testEnv].modules[moduleName] = this.adaptModule(module); } aggregateStats() { const startTime = new Date(this.results.startTimestamp).getTime(); const endTime = new Date(this.results.endTimestamp).getTime(); const stats = { total: 0, passed: 0, failed: 0, skipped: 0, time: endTime - startTime }; for (const envName of Object.keys(this.environments)) { const env = this.environments[envName]; stats.passed += env.stats.passed; stats.failed += env.stats.failed; stats.skipped += env.stats.skipped; stats.total += env.stats.total; } return stats; } getGlobalMetadata() { return { date: new Date() }; } adaptModule(module) { // Pick only the necessary fields const result = lodashPick(module, [ 'completedSections', 'rawHttpOutput', 'assertionsCount', 'lastError', 'skipped', 'time', 'timeMs', 'errmessages', 'testsCount', 'skippedCount', 'failedCount', 'errorsCount', 'passedCount', 'group', 'modulePath', 'startTimestamp', 'endTimestamp', 'sessionCapabilities', 'sessionId', 'projectName', 'buildName', 'testEnv', 'isMobile', 'status', 'seleniumLog', 'tests', 'failures', 'errors' ]); // Convert to absolute path for (const sectionName of Object.keys(result.completedSections)) { const section = result.completedSections[sectionName]; for (const command of section.commands) { const {output_folder} = this.options; const destFolder = path.join(output_folder, this.getFolderPrefix(), 'nightwatch-html-report'); if (command.screenshot) { command.screenshot = path.relative(destFolder, command.screenshot); } if (command.domSnapshot && command.domSnapshot.snapshotFilePath) { command.domSnapshot.snapshotFilePath = path.resolve(process.cwd(), command.domSnapshot.snapshotFilePath); } } } // Add skipped tests by user in completed section module.skippedByUser && module.skippedByUser.forEach(skippedTestName => { module.completedSections[skippedTestName] = { status: 'skip', runtimeFailure: false }; }); // Add skipped tests at runtime in completed section module.skippedAtRuntime && module.skippedAtRuntime.forEach(skippedTestName => { module.completedSections[skippedTestName] = { status: 'skip', runtimeFailure: true }; }); return result; } adaptResults() { const {modulesWithEnv} = this.results; this.environments = {}; Object.keys(modulesWithEnv).forEach((env) => { const modules = modulesWithEnv[env]; // Make module paths absolute this.results.modulesWithEnv[env] = Object.keys(modules).reduce((prev, value) => { this.results.modulesWithEnv[env][value].modulePath = this.results.modulesWithEnv[env][value].modulePath.replace(process.cwd(), ''); prev[value] = this.results.modulesWithEnv[env][value]; return prev; }, {}); Object.keys(modules).forEach((moduleKey) => { const module = modules[moduleKey]; this.adaptAssertions(module); this.aggregateEnvironments(env, moduleKey, module); }); }); return { environments: this.environments, stats: this.aggregateStats(), metadata: this.getGlobalMetadata() }; } async writeReport() { const results = this.adaptResults(); const {output_folder, openReport: shouldOpenReport} = this.options; const destFolder = path.join(output_folder, this.getFolderPrefix(), 'nightwatch-html-report'); const fileName = `${this.getFileName()}.html`; const filePath = path.join(destFolder, `${this.getFileName()}.html`); const jsonString = JSON.stringify(results); HtmlReact.writeNightwatchHTMLReport(destFolder, fileName, jsonString); Logger.logDetailedMessage(Logger.colors.stack_trace(` Wrote HTML report file to: ${path.resolve(filePath)}` + '\n'), 'info'); if (shouldOpenReport) { return this.openReporter(filePath); } } } module.exports = (function() { return { write(results, options, callback) { const reporter = new HtmlReporter(results, options); reporter.writeReport() .then(_ => { callback(); }) .catch(err => { Logger.error(err); callback(err); }); }, adaptResults(results, options) { const reporter = new HtmlReporter(results, options); return reporter.adaptResults(); } }; })(); ================================================ FILE: lib/reporter/reporters/json.js ================================================ const stripAnsi = require('strip-ansi'); const path = require('path'); const Utils = require('../../utils'); const {Logger} = Utils; const BaseReporter = require('../base-reporter.js'); class JsonReporter extends BaseReporter { writeReport(moduleKey, data = {}) { const module = this.results.modules[moduleKey]; const pathParts = moduleKey.split(path.sep); const moduleName = pathParts.pop(); let output_folder = this.options.output_folder; let shouldCreateFolder = false; this.adaptAssertions(module); if (pathParts.length) { output_folder = path.join(output_folder, pathParts.join(path.sep)); shouldCreateFolder = true; } const filename = this.options.report_filename || path.join(output_folder, `${module.reportPrefix}${moduleName}.json`); const httpOutput = Logger.getOutput().map(item => { return [item[0], stripAnsi(item[1]), item[2] || '']; }); const report = { report: module, name: moduleName, httpOutput, systemerr: this.results.errmessages.map(item => stripAnsi(item)).join('\n') }; return this.writeReportFile(filename, JSON.stringify(report), shouldCreateFolder, output_folder) .then(_ => { Logger.info(Logger.colors.stack_trace(`Wrote JSON report file to: ${path.resolve(filename)}`)); }); } } module.exports = (function() { return { write(results, options) { const envs = Object.keys(results.modulesWithEnv); const promises = envs.map(env => { const envResult = {...results, modules: results.modulesWithEnv[env]}; const reporter = new JsonReporter(envResult, options); return reporter.write(); }); return Promise.all(promises); } }; })(); ================================================ FILE: lib/reporter/reporters/junit.js ================================================ /** * https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd */ const fs = require('fs'); const stripAnsi = require('strip-ansi'); const path = require('path'); const ejs = require('ejs'); const Utils = require('../../utils'); const BaseReporter = require('../base-reporter.js'); const {Logger} = Utils; let __tmplData__ = ''; class JUnitReporter extends BaseReporter { static get templateFile() { return path.join(__dirname, 'junit.xml.ejs'); } static set tmplData(val) { __tmplData__ = val; } static get tmplData() { return __tmplData__; } static loadTemplate() { return new Promise((resolve, reject) => { if (JUnitReporter.tmplData) { return resolve(JUnitReporter.tmplData); } fs.readFile(JUnitReporter.templateFile, (err, data) => { if (err) { return reject(err); } JUnitReporter.tmplData = data.toString(); resolve(JUnitReporter.tmplData); }); }); } writeReport(moduleKey, data) { const module = this.results.modules[moduleKey]; const pathParts = moduleKey.split(path.sep); const moduleName = pathParts.pop(); let suiteKey = moduleName; let className = moduleName; if (module.name !== Utils.getTestSuiteName(moduleKey)) { // module.name is not generated from moduleKey className = module.name; } let output_folder = this.options.output_folder; let shouldCreateFolder = false; this.adaptAssertions(module); if (pathParts.length) { output_folder = path.join(output_folder, pathParts.join(path.sep)); suiteKey = pathParts.join('.') + '.' + moduleName; if (className === moduleName) { className = suiteKey; } shouldCreateFolder = true; } const filename = path.join(output_folder, `${module.reportPrefix}${moduleName}.xml`); const report = { module, moduleName, className, suiteKey, systemerr: this.results.errmessages.map(item => stripAnsi(item)).join('\n') }; let rendered = ejs.render(data, report); rendered = Utils.stripControlChars(rendered); return this.writeReportFile(filename, rendered, shouldCreateFolder, output_folder) .then(_ => { Logger.info(Logger.colors.stack_trace(`Wrote XML report file to: ${path.resolve(filename)}`)); }); } write() { const keys = Object.keys(this.results.modules); return JUnitReporter.loadTemplate() .then(data => { const promises = keys.map(moduleKey => { return this.writeReport(moduleKey, data); }); return Promise.all(promises); }); } } module.exports = (function() { return { write(results, options, callback) { const envs = Object.keys(results.modulesWithEnv); const promises = envs.map(env => { const envResult = {...results, modules: results.modulesWithEnv[env]}; const reporter = new JUnitReporter(envResult, options); return reporter.write(); }); Promise.all(promises) .then(_ => { callback(); }) .catch(err => { Logger.error(err); callback(err); }); } }; })(); ================================================ FILE: lib/reporter/reporters/junit.xml.ejs ================================================ <% for (var item in module.completed) { var testcase = module.completed[item]; var assertions = testcase.assertions %> <% for (var i = 0; i < assertions.length; i++) { %><% if (assertions[i].failure) { %> <%= assertions[i].stackTrace %><% } %> <% if (assertions[i].screenshots && assertions[i].screenshots.length > 0) { %><% for (var j = 0; j < assertions[i].screenshots.length; j++) { %>[[ATTACHMENT|<%= assertions[i].screenshots[j] %>]]<% } %><% } %> <% } if (assertions.length === 0 && testcase.failed) { %> <%= testcase.stackTrace %><% } if (testcase.errors > 0 && testcase.stackTrace) { %> message="<%= testcase.lastError.message %>" <% } %>type="error"> ]]> <% } %> <% } %> <% if (systemerr != '') { %> <%= systemerr %> <% } %> <% if (module.lastError && Object.keys(module.completed).length === 0) { %> ]]> <% } %> <% if (module.skippedAtRuntime && (module.skippedAtRuntime.length > 0)) { %> <% for (var j = 0; j < module.skippedAtRuntime.length; j++) { %> <% } %> <% } %> ================================================ FILE: lib/reporter/reporters/minimalJson.js ================================================ const lodashPick = require('lodash/pick'); const path = require('path'); const Utils = require('../../utils'); const {Logger} = Utils; const BaseReporter = require('../base-reporter.js'); class MinimalJsonReporter extends BaseReporter { adaptModule(module) { // Pick only the necessary fields const result = lodashPick(module, [ 'modulePath', 'status' ]); return result; } adaptResults() { const {modules} = this.results; this.modules = {}; Object.keys(modules).forEach((moduleKey) => { this.modules[moduleKey] = this.adaptModule(modules[moduleKey]); }); return this.modules; } writeReport() { const results = this.adaptResults(); const {output_folder} = this.options; const filename = path.join(output_folder, 'minimal_report.json'); const shouldCreateFolder = !Utils.dirExistsSync(output_folder); const report = { modules: results }; return this.writeReportFile(filename, JSON.stringify(report), shouldCreateFolder, output_folder) .then(_ => { Logger.info(Logger.colors.stack_trace(`Wrote Rerun Json report file to: ${path.resolve(filename)}`)); // Setting env varaible with minimalJsonReporter path. // Next time user runs nightwatch with rerun functionality, json reporter can be read from this process.env.NIGHTWATCH_RERUN_REPORT_FILE = filename; process.env.NEW_FOO = filename; }); } } function write(results, options, callback) { const reporter = new MinimalJsonReporter(results, options); reporter.writeReport() .then(_ => { callback(); }) .catch(err => { Logger.error(err); callback(err); }); } module.exports = { MinimalJsonReporter, write }; ================================================ FILE: lib/reporter/results.js ================================================ const lodashMerge = require('lodash/merge'); const uuid = require('uuid'); const Utils = require('../utils'); const {Logger} = Utils; // ALL THE NON-STATIC METHODS IN `Results` CLASS // REPRESENT SUITE LEVEL RESULTS. module.exports = class Results { static get TEST_FAIL() { return 'fail'; } static get TEST_PASS() { return 'pass'; } static get TEST_SKIP() { return 'skip'; } constructor(tests = [], opts, settings, skippedTests = [], allScreenedTests = []) { this.skippedByUser = skippedTests; this.skippedAtRuntime = tests.slice(0); this.testcases = {}; this.testSections = {}; this.testSectionHook = {}; this.isHookRunning = false; this.suiteName = opts.suiteName; this.moduleKey = opts.moduleKey; this.modulePath = opts.modulePath; this.groupName = opts.groupName; this.reportPrefix = opts.reportPrefix; this.testEnv = settings.testEnv; this.isMobile = opts.isMobile; this.globalStartTime = new Date().getTime(); this.startTimestamp = this.globalStartTime; this.endTimestamp = this.globalStartTime; this.currentTestName = ''; this.currentSectionName = ''; this.__currentTest = null; // __initialResult is updated whenever there is no testcase result // to update, so that results are not lost. this.__initialResult = { errors: 0, failed: 0, passed: 0, assertions: [], commands: [], tests: 0 }; // Adding sessionInfo to reporter const {capabilities = {}, desiredCapabilities = {}} = settings; this.sessionCapabilities = capabilities || desiredCapabilities; this.sessionId = ''; this.projectName = capabilities.projectName || desiredCapabilities.projectName || ''; this.buildName = capabilities.buildName || desiredCapabilities.buildName || ''; const {webdriver = {}} = settings; this.host = webdriver.host || ''; this.name = opts.suiteName || ''; this.tags = opts.tags || []; this.__retryTest = false; this.__uuid = uuid.v4(); this.initCount(allScreenedTests); } markHookRun(hookName) { // called for beforeEach, testcase, and afterEach (with the same name as `hookName`) // all three above are considered to be hooks during test case execution. this.isHookRunning = true; this.currentTestCaseHookName = hookName; // `testName` below would take values like: `Demo test ecosia.org__beforeEach`, // `Demo test ecosia.org__testcase`, and `Demo test ecosia.org__afterEach` // as `currentSectionName` would represent the test case name. this.testSectionHook = this.createTestCaseResults({testName: `${this.currentSectionName}__${hookName}`}); // reset test hooks output Logger.collectTestHooksOutput(); } unmarkHookRun() { // called for beforeEach, testcase, and afterEach during test case execution. this.isHookRunning = false; if (!this.currentTestCaseHookName) { throw new Error('Hook run not started yet'); } // below currentSection would contain the result data for the complete // test case run, including beforeEach & afterEach. // Ex. for `Demo test ecosia.org` test case. const currentSection = this.currentSection; // hookdata would contain the result data for the individual // beforeEach/testcase/afterEach hook run inside the current // section (test case). const hookdata = this.testSectionHook; const startTime = hookdata.startTimestamp; const endTime = new Date().getTime(); const elapsedTime = endTime - startTime; hookdata.endTimestamp = endTime; hookdata.time = (elapsedTime / 1000).toPrecision(4); hookdata.timeMs = elapsedTime; hookdata.httpOutput = Logger.collectTestHooksOutput(); if (hookdata.errors > 0 || hookdata.failed > 0) { hookdata.status = Results.TEST_FAIL; } currentSection[this.currentTestCaseHookName] = hookdata; } get initialResult() { return this.__initialResult; } // currentTest --> current or most recently run test case get currentTestResult() { // `this.currentTest.name` is only set after first test case is run. // before that, `this.currentTest.name` is '', so `this.initialResult` // is returned by this getter. const testName = this.currentTest.name; return this.getTestResult(testName, {returnFullResult: true}); } get uuid() { return this.__uuid; } /** * @param {TestCase} value */ set currentTest(value) { this.__currentTest = value; } getTestResult(testName, {returnFullResult = false} = {}) { if (!testName || !this.testcases[testName]) { return this.initialResult; } const currentTest = this.testcases[testName]; if (returnFullResult) { currentTest.steps = this.skippedAtRuntime; currentTest.stackTrace = this.stackTrace; // adds details of all test cases ran till now to the current test case result. currentTest.testcases = Object.keys(this.testcases).reduce((prev, key) => { prev[key] = Object.keys(this.testcases[key]).reduce((prevVal, prop) => { if (prop !== 'testcases') { prevVal[prop] = this.testcases[key][prop]; } if (prop === 'assertions') { prevVal.tests = this.testcases[key].assertions; } return prevVal; }, {}); return prev; }, {}); } return currentTest; } getTestSection(testName) { if (!testName || !this.testSections[testName]) { return this.initialResult; } return this.testSections[testName]; } /** * @return {TestCase} */ getCurrentTest() { // returns the current/most recent testcase object (not testcase result) return this.__currentTest; } get currentTest() { // returns the current/most recent testcase object (not testcase result) if (!this.__currentTest) { return null; } const name = this.__currentTest.testName; return { name, module: this.moduleKey, group: this.groupName, results: this.getTestResult(name, {returnFullResult: true}), timestamp: this.timestamp }; } get currentSection() { // returns the current/most recently run test section result. // this will only return `this.initialResult` if called before the global // beforeEach hook is run because we never reset `this.currentSectionName`. return this.getTestSection(this.currentSectionName); } testSectionHasFailures(testSection) { return testSection.errors > 0 || testSection.failed > 0; } //////////////////////////////////////////////////////////// // Counters //////////////////////////////////////////////////////////// get passedCount() { return this.__passedCount; } get failedCount() { return this.__failedCount; } get errorsCount() { return this.__errorsCount; } get skippedCount() { return this.__skippedCount; } get timestamp() { return this.__timestamp; } get testsCount() { return this.__testsCount; } get lastError() { return this.__lastError; } get stackTrace() { return (this.__lastError && this.__lastError instanceof Error) ? this.__lastError.stack : ''; } get currentTestElapsedTime() { return this.currentTestResult.timeMs; } get currentTestCasePassed() { return this.currentTestResult.errors === 0 && this.currentTestResult.failed === 0; } get currentTestCaseHasFailures() { // 'currentTestCase' represents current or most recently run test case. return this.currentTestResult.errors > 0 || this.currentTestResult.failed > 0; } /** * Exports the complete results for the entire testsuite * * @return {object} */ get export() { let lastError = null; const {moduleKey, reportPrefix} = this; const suiteResults = { moduleKey, hasFailures: !this.testsPassed(), results: { uuid: this.uuid, reportPrefix, // if no this.currentTestName is present at the time (which happens // before any test cases started running or after all test cases are // done running), all executed assertions are saved to `this.initialResult`. assertionsCount: this.initialResult.assertions.length } }; Object.keys(this.testcases).forEach(key => { const testcase = this.testcases[key]; if (testcase.lastError) { //lastError = testcase.lastError; } suiteResults.results.assertionsCount += testcase.assertions.length; }); if (!lastError && this.lastError) { lastError = this.lastError; } suiteResults.results.lastError = lastError; suiteResults.results.skippedAtRuntime = this.skippedAtRuntime; suiteResults.results.skippedByUser = this.skippedByUser; suiteResults.results.skipped = [...this.skippedAtRuntime, ...this.skippedByUser]; suiteResults.results.time = this.time; suiteResults.results.timeMs = this.timeMs; suiteResults.results.completed = this.testcases; suiteResults.results.completedSections = this.testSections; suiteResults.results.errmessages = this.errmessages; suiteResults.results.testsCount = this.testsCount; suiteResults.results.skippedCount = this.skippedCount; suiteResults.results.failedCount = this.failedCount; suiteResults.results.errorsCount = this.errorsCount; suiteResults.results.passedCount = this.passedCount; suiteResults.results.group = this.groupName; suiteResults.results.modulePath = this.modulePath; suiteResults.results.startTimestamp = new Date(this.startTimestamp).toUTCString(); suiteResults.results.endTimestamp = new Date(this.endTimestamp).toUTCString(); suiteResults.results.sessionCapabilities = this.sessionCapabilities; suiteResults.results.sessionId = this.sessionId; suiteResults.results.projectName = this.projectName; suiteResults.results.buildName = this.buildName; suiteResults.results.testEnv = this.testEnv; suiteResults.results.isMobile = this.isMobile; suiteResults.results.status = this.getTestStatus(); suiteResults.results.seleniumLog = this.seleniumLog; suiteResults.results.host = this.host; suiteResults.results.name = this.name; suiteResults.results.tags = this.tags; // Backwards compat suiteResults.results.tests = this.testsCount; suiteResults.results.failures = this.failedCount; suiteResults.results.errors = this.errorsCount; suiteResults.results.group = this.groupName; return suiteResults; } initCurrentTest(testcase) { this.currentTest = testcase; return this; } resetCurrentTestName() { // called after all test cases in a test suite are executed. // FIXME: in v2, this needs to be this.__currentTest.testName, but it isn't desirable now because will make // client.currentTest.name empty in the after() hook and potentially introduce breaking changes //this.__currentTest.testName = ''; this.currentTestName = ''; } resetCurrentSectionName() { // never called, which helps `this.currentSection` always return // a valid test section result. this.currentSectionName = ''; } getCurrentTestName() { // FIXME: see resetCurrentTestName() const {testName} = this.getCurrentTest() || {}; return this.currentTestName; } set retryTest(value) { this.__retryTest = value; } get retryTest() { return this.__retryTest; } /** * @param {TestCase} testcase * @return {Object} */ createTestCaseResults(testcase) { const result = { time: 0, assertions: [], commands: [], passed: 0, errors: 0, failed: 0, retries: testcase.retriesCount, skipped: 0, tests: 0, status: Results.TEST_PASS, startTimestamp: new Date().getTime(), httpOutput: [] }; if (this.retryTest && this.testSections[testcase.testName]) { const retryTestData = this.testSections[testcase.testName]; result['retryTestData'] = [retryTestData]; if (retryTestData['retryTestData']) { result['retryTestData'].push(...retryTestData['retryTestData']); delete retryTestData['retryTestData']; } this.retryTest = false; } return result; } resetLastError() { this.__lastError = null; } setLastError(err, {incrementTotal, addToErrArray = false} = {}) { const testName = this.getCurrentTestName(); this.__lastError = err; if (testName && this.testcases[testName]) { this.testcases[testName].lastError = err; if (addToErrArray) { this.testcases[testName].errorsPerTest = this.testcases[testName].errorsPerTest || []; this.testcases[testName].errorsPerTest.push(err.message); } } const detailedLogging = err.detailedLogging || Utils.isUndefined(err.detailedLogging); if ((!testName || addToErrArray) && incrementTotal && detailedLogging) { const errorMessage = Logger.getErrorContent(err, this.modulePath); this.errmessages.push(errorMessage); } return this; } /** * @param {Boolean} incrementTotal * @return {Results} */ incrementErrorCount(incrementTotal = true) { if (incrementTotal) { this.__errorsCount++; } const result = this.getTestResult(this.currentTestName); result.errors++; // also increment errors count for the current section this.currentSection.errors++; if (this.isHookRunning) { // A test case's beforeEach/testcase/afterEach hook is running, // not a before/after hook or global beforeEach/afterEach hook. this.testSectionHook.errors++; } return this; } /** * @param {Boolean} incrementTotal * @return {Results} */ incrementFailedCount(incrementTotal = true) { if (incrementTotal) { this.__failedCount++; } const result = this.getTestResult(this.currentTestName); result.failed++; // also increment failed count for the current section this.currentSection.failed++; if (this.isHookRunning) { this.testSectionHook.failed++; } return this; } incrementPassedCount() { this.__passedCount++; const result = this.getTestResult(this.currentTestName); result.passed++; // also increment passed count for the current section this.currentSection.passed++; if (this.isHookRunning) { this.testSectionHook.passed++; } return this; } subtractPassedCount(count = 0) { this.__passedCount = this.__passedCount - count; return this; } /** * @param {Object} assertion * @return {module.Results} */ logAssertion(assertion) { const result = this.getTestResult(this.currentTestName); result.assertions.push(assertion); // backwards compatibility result.tests++; return this; } logCommand(command) { const result = this.currentSection; result.commands.push(command); if (this.isHookRunning) { // A test case's beforeEach/testcase/afterEach hook is running, // not a before/after hook or global beforeEach/afterEach hook. this.testSectionHook.commands.push(command); } return this; } initCount(allScreenedTests) { this.__passedCount = 0; this.__failedCount = 0; this.__errorsCount = 0; this.__skippedCount = allScreenedTests.length; this.__testsCount = 0; this.__timestamp = new Date().toUTCString(); this.errmessages = []; this.time = 0; } setElapsedTime() { const currentTest = this.getCurrentTest(); const startTime = currentTest ? currentTest.startTime : this.globalStartTime; const endTime = new Date().getTime(); const elapsedTime = endTime - startTime; this.endTimestamp = endTime; if (currentTest) { this.currentTestResult.time = (elapsedTime / 1000).toPrecision(4); this.currentTestResult.timeMs = elapsedTime; this.currentTestResult.startTimestamp = new Date(startTime).toUTCString(); this.currentTestResult.endTimestamp = new Date(endTime).toUTCString(); } return this; } setTestSectionElapsedTime() { const currentSection = this.currentSection; const startTime = currentSection?.startTimestamp || this.globalStartTime; const endTime = new Date().getTime(); const elapsedTime = endTime - startTime; this.endTimestamp = endTime; this.time += elapsedTime; if (currentSection) { currentSection.time = (elapsedTime / 1000).toPrecision(4); currentSection.timeMs = elapsedTime; currentSection.startTimestamp = startTime; currentSection.endTimestamp = endTime; } return this; } setTestStatus() { // run for all test cases and hooks (except for [before|after]_each hooks) // ^ [before|after]_each hooks are considered part of the test case itself const currentTest = this.getCurrentTest(); if (!currentTest) { return; } if (this.currentTestCaseHasFailures) { this.currentTestResult.status = Results.TEST_FAIL; } else { this.currentTestResult.status = Results.TEST_PASS; } // also set status for the current section const currentSection = this.currentSection; if (this.testSectionHasFailures(currentSection)) { currentSection.status = Results.TEST_FAIL; } else { currentSection.status = Results.TEST_PASS; } } collectTestSectionOutput() { const currentSection = this.currentSection; if (!currentSection) { return; } currentSection.httpOutput = Logger.collectTestSectionOutput(); } setTotalElapsedTime() { this.timeMs = this.time; this.time = (this.time / 1000).toPrecision(4); return this; } /** * Sets the currently running testcase * * @param {TestCase} testcase * @return {Results} */ setCurrentTest(testcase) { // we set this during `runCurrentTest` method call in `testsuite/index.js` // only called for test cases (not for hooks) this.currentTest = testcase; const testName = testcase.testName; this.currentTestName = testName; this.testcases[testName] = this.createTestCaseResults(testcase); const index = this.skippedAtRuntime.indexOf(testName); if (index > -1) { this.skippedAtRuntime.splice(index, 1); this.__skippedCount -= 1; } this.__testsCount++; return this; } setCurrentSection(testcase) { // set for all tests and hooks (except [before|after]_each hooks) // ^ [before|after]_each hooks are considered part of the test case itself this.currentSectionName = testcase.testName; this.testSections[testcase.testName] = this.createTestCaseResults(testcase); } setSeleniumLogFile(outputFilePath) { this.seleniumLog = outputFilePath; } logScreenshotFile(screenshotFile) { const assertions = this.currentTestResult.assertions; const lastAssertion = assertions[assertions.length - 1]; if (lastAssertion) { lastAssertion.screenshots = lastAssertion.screenshots || []; lastAssertion.screenshots.push(screenshotFile); } } testsPassed() { return this.failedCount === 0 && this.errorsCount === 0; } getTestStatus() { // returns suite level test status. if (this.failedCount > 0 || this.errorsCount > 0) { return Results.TEST_FAIL; } if (this.passedCount > 0) { return Results.TEST_PASS; } return Results.TEST_SKIP; } /** * Appends data in current testSections data. * * @param {Object} data */ appendTestResult(data) { lodashMerge(this.testSections, data); } /** * Combines all the individual test suite reports into a global one * * @param {Array} suiteResultsArr * @param {Object} initialReport * @return {Object} */ static createGlobalReport(suiteResultsArr, initialReport) { return suiteResultsArr.reduce((prev, item) => { const results = item.results; if (results.lastError) { prev.lastError = results.lastError; } // Accumulating stats prev.passed += results.passedCount; prev.failed += results.failedCount; prev.errors += results.errorsCount; prev.skipped += results.skippedCount; prev.assertions += results.assertionsCount; // Accumulating error messages if (Array.isArray(results.errmessages) && results.errmessages.length > 0) { prev.errmessages = prev.errmessages.concat(results.errmessages); } results.httpOutput = item.httpOutput || []; results.rawHttpOutput = item.rawHttpOutput || []; prev.modules[item.moduleKey] = results; prev.modulesWithEnv[item.results.testEnv] = prev.modulesWithEnv[item.results.testEnv] || {}; prev.modulesWithEnv[item.results.testEnv][item.moduleKey] = results; return prev; }, initialReport); } get eventDataToEmit() { const {testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host} = this; return { envelope: this.testSections, metadata: { testEnv, sessionCapabilities, sessionId, tags, modulePath, name, host } }; } }; ================================================ FILE: lib/reporter/simplified.js ================================================ const Utils = require('../utils'); const {Logger} = Utils; class SimplifiedReporter { static logError(err) { if (!Utils.isErrorObject(err)) { if (Utils.isObject(err)) { err = Object.keys(err).length > 0 ? JSON.stringify(err) : ''; } err = new Error(err.message || err); } Logger.error(err); } get currentTestCase() { return null; } /** * @param {Object} settings */ constructor(settings) { this.settings = settings; this.currentContext = null; } logAssertResult(test) { return this; } /** * @param {Object} result */ logCommandResult(node, result) { return this; } logFailedAssertion(err) { Logger.logDetailedMessage(` ${Logger.colors.red(Utils.symbols.fail)} ${err.message}`); let sections = err.stack.split('\n'); Logger.logDetailedMessage(`${Logger.colors.stack_trace(sections.join('\n'))} \n`); } registerPassed(message) { Logger.logDetailedMessage(`${Logger.colors.green(Utils.symbols.ok)} ${message}`); } registerFailed(err) { return this; } registerTestError(err) {} saveErrorScreenshot(result, screenshotContent) {} } module.exports = SimplifiedReporter; ================================================ FILE: lib/reporter/summary.js ================================================ const Utils = require('../utils'); const {Logger} = Utils; const {colors} = Logger; module.exports = class Summary { static failed(test) { return test.failures > 0 || test.failed > 0 || test.errors > 0; } static getTestcaseHeader(testcase, testcaseName) { const triesCount = testcase.retries > 0 ? ` x ${testcase.retries + 1}` : ''; const triesContent = testcase.retries > 0 ? ` - ${triesCount} tries` : ''; const title = `\n ${colors.light_cyan('–')} ${colors.light_cyan(testcaseName)} `; const details = (testcase.timeMs ? colors.stack_trace(`(${Utils.formatElapsedTime(testcase.timeMs)}${triesCount})`) : '') + triesContent; return `${title}${details}\n`; } /** * @param {object} testSuite * @param {string} testSuiteName * @param {number} index * @param {boolean} startSessionEnabled * @return {[string]} */ static getFailedSuiteContent({testSuite, testSuiteName, index = 0, startSessionEnabled = true}) { const testcases = Object.keys(testSuite.completed); const initial = [colors.red(` ${Utils.symbols.fail} ${index + 1}) ${testSuiteName}`)]; return testcases.reduce((prev, name) => { const testcase = testSuite.completed[name]; if (Summary.failed(testcase)) { prev.push(Summary.getTestcaseHeader(testcase, name)); let content; if (Summary.shouldPrintAssertions(testcase) && startSessionEnabled) { content = Logger.getFailedAssertions(testcase.assertions, testSuite.modulePath); } else if (testcase.lastError) { content = ' ' + Logger.getErrorContent(testcase.lastError, testSuite.modulePath); } if (content) { prev.push(content); testSuite.globalErrorRegister.push(content); } } else if (testcase.lastError) { prev.push(Logger.getErrorContent(testcase.lastError, testSuite.modulePath)); } return prev; }, initial); } /** * @param {Array} content */ static printSuite(content) { content.forEach(line => { if (Utils.isObject(line) && line.stacktrace) { Utils.showStackTrace(line.content); } else { // eslint-disable-next-line no-console console.log(line); } }); } /** * @param {object} testSuite */ static printErrors(testSuite) { if (Array.isArray(testSuite.errmessages)) { testSuite.errmessages = testSuite.errmessages.reduce((prev, val) => { if (testSuite.globalErrorRegister && !testSuite.globalErrorRegister.includes(val)) { testSuite.globalErrorRegister.push(val); prev.push(val); } return prev; }, []); if (testSuite.errmessages.length > 0) { // eslint-disable-next-line no-console console.log(colors.stack_trace('\n - OTHER ERRORS:')); } testSuite.errmessages.forEach(function(errorMessage, index) { // eslint-disable-next-line no-console Logger.error(errorMessage); }); } } /** * @param {object} testSuite */ static printSkipped(testSuite) { if (testSuite.skippedAtRuntime.length > 0) { // eslint-disable-next-line no-console console.log(colors.cyan(' SKIPPED (at runtime):')); testSuite.skippedAtRuntime.forEach(function(testcase) { // eslint-disable-next-line no-console console.log(` - ${testcase}`); }); } if (testSuite.skippedByUser.length > 0) { // eslint-disable-next-line no-console console.log(colors.cyan(' SKIPPED (by user):')); testSuite.skippedByUser.forEach(function(testcase) { // eslint-disable-next-line no-console console.log(` - ${testcase}`); }); } } constructor(settings) { this.settings = settings; } static shouldPrintAssertions(testcase) { return testcase.assertions.length > 0; } print(globalResults) { const testSuites = Object.keys(globalResults.modules); const testSuitesCount = testSuites.length; let failedIndex = 0; testSuites.forEach((testSuiteName, index) => { const testSuite = globalResults.modules[testSuiteName]; if (Summary.failed(testSuite)) { testSuite.globalErrorRegister = []; Summary.printSuite(Summary.getFailedSuiteContent({ testSuite, index: failedIndex++, testSuiteName, startSessionEnabled: this.settings.start_session })); Summary.printErrors(testSuite); Summary.printSkipped(testSuite); } }); if (testSuites.length === 0) { Summary.printErrors(globalResults); } } }; ================================================ FILE: lib/runner/androidEmulator.js ================================================ const {getSdkRootFromEnv, AndroidBinaryError, requireMobileHelper} = require('../utils/mobile.js'); const {killEmulatorWithoutWait, getAlreadyRunningAvd, launchAVD, getPlatformName} = requireMobileHelper(); module.exports = class AndroidServer { constructor (AVD) { this.sdkRoot = getSdkRootFromEnv(); this.avd = AVD || 'nightwatch-android-11'; this.emulatorId = ''; this.emulatorAlreadyRunning = false; } async killEmulator() { return killEmulatorWithoutWait(this.sdkRoot, getPlatformName(), this.emulatorId); } async launchEmulator() { try { const emulatorId = await getAlreadyRunningAvd(this.sdkRoot, getPlatformName(), this.avd); this.emulatorAlreadyRunning = !!emulatorId; this.emulatorId = emulatorId || await launchAVD(this.sdkRoot, getPlatformName(), this.avd); if (this.emulatorId === null) { throw new Error('Failed to launch AVD inside Android Emulator'); } } catch (err) { throw new AndroidBinaryError(err.message, 'emulator'); } } }; ================================================ FILE: lib/runner/cli/argv-setup.js ================================================ const path = require('path'); const minimist = require('minimist'); const DefaultSettings = require('../../settings/defaults.js'); const {Logger} = require('../../utils'); module.exports = new (function () { /** * Based on https://github.com/substack/node-optimist by * James Halliday (mail@substack.net), which has been deprecated */ class ArgvSetup { get argv() { const argv = minimist(this.__processArgs, this.options); argv.$0 = this.$0; if (this.demanded._ && argv._.length < this.demanded._) { this.fail(`Not enough non-option arguments: got ${argv._.length}, need at least ${this.demanded._}`); } const missing = []; Object.keys(this.demanded).forEach(function (key) { if (!argv[key]) { missing.push(key); } }); if (missing.length) { this.fail('Missing required arguments: ' + missing.join(', ')); } return argv; } constructor(processArgs, cwd = process.cwd()) { this.options = { alias: {}, default: {} }; this.__processArgs = processArgs; this.usage = null; this.demanded = {}; this.descriptions = {}; this.$0 = process.argv.slice(0, 2) .map(function (x) { const b = rebase(cwd, x); return x.match(/^\//) && b.length < x.length ? b : x; }).join(' '); if (process.env._ !== undefined && process.argv[1] === process.env._) { this.$0 = process.env._.replace(path.dirname(process.execPath) + '/', ''); } } addDefault(key, value) { if (typeof key === 'object') { Object.keys(key).forEach((k) => { this.addDefault(k, key[k]); }); } else { this.options.default[key] = value; } return this; } isDefault(option, value) { return this.options.default[option] && this.options.default[option] === value; } getDefault(option) { return this.options.default[option]; } alias(x, y) { if (typeof x === 'object') { Object.keys(x).forEach((key) => { this.alias(key, x[key]); }); } else { this.options.alias[x] = (this.options.alias[x] || []).concat(y); } return this; } demand(keys) { if (typeof keys == 'number') { if (!this.demanded._) { this.demanded._ = 0; } this.demanded._ += keys; } else if (Array.isArray(keys)) { keys.forEach((key) => { this.demand(key); }); } else { this.demanded[keys] = true; } return this; } showUsage(msg) { this.usage = msg; return this; } describe(key, desc, groupName) { if (typeof key === 'object') { Object.keys(key).forEach((k) => { this.describe(k, key[k], groupName); }); } else { this.descriptions[key] = {desc, groupName}; } return this; } option(key, opt) { if (typeof key === 'object') { Object.keys(key).forEach((k) => { this.option(k, key[k]); }); } else { if (opt.alias) { this.alias(key, opt.alias); } if (opt.demand) { this.demand(key); } if (typeof opt.defaults !== 'undefined') { this.addDefault(key, opt.defaults); } const desc = opt.describe || opt.description || opt.desc; if (desc) { this.describe(key, desc, opt.group); } } return this; } showHelp(fn) { if (!fn) { fn = console.error; } fn(this.help()); } help() { const keys = Object.keys(this.descriptions); const groups = Object.keys(this.descriptions).reduce((prev, key) => { const group = this.descriptions[key].groupName || 'Main options'; prev[group] = prev[group] || []; prev[group].push(key); return prev; }, {}); const help = []; if (this.usage) { help.unshift(this.usage.replace(/\$0/g, this.$0), ''); } const switches = keys.reduce((acc, key) => { acc[key] = [key].concat(this.options.alias[key] || []) .map(function (sw) { return (sw.length > 1 ? '--' : '-') + sw; }) .join(', '); return acc; }, {}); const switchlen = longest(Object.keys(switches).map(function (s) { return switches[s] || ''; })); const desclen = longest(Object.keys(this.descriptions).map((d) => { return this.descriptions[d].desc || ''; })); Object.keys(groups).forEach((groupName, index) => { const content = ['\n']; content.push(Logger.colors.brown(groupName + ':')); groups[groupName].forEach(key => { const kswitch = switches[key]; let desc = this.descriptions[key].desc || ''; const spadding = new Array(Math.max(switchlen - kswitch.length + 3, 0)).join('.'); const dpadding = new Array(Math.max(desclen - desc.length + 1, 0)).join(' '); if (dpadding.length > 0) { desc += dpadding; } const prelude = ' ' + (kswitch) + ' ' + Logger.colors.stack_trace(spadding); const extra = [ this.demanded[key] ? '[required]' : null, this.options.default[key] !== undefined ? '[default: ' + JSON.stringify(this.options.default[key]) + ']' : null ].filter(Boolean).join(' '); const body = [desc, extra].filter(Boolean).join(' '); content.push(prelude + ' ' + Logger.colors.stack_trace(body)); }); help.push(content.join('\n')); }); help.push('\n'); return help.join(''); } fail(msg) { if (msg) { console.error(Logger.colors.red(msg) + '\n'); } this.showHelp(); process.exit(1); } setup() { // CLI definitions this.option('source', { string: true }); // $ nightwatch -e // $ nightwatch --env saucelabs this.option('env', { description: 'Specify the testing environment to use.', alias: 'e', defaults: 'default' }); // $ nightwatch -c // $ nightwatch --config this.option('config', { demand: true, description: 'Path to configuration file; nightwatch.conf.js or nightwatch.json are read by default if present.', alias: 'c', defaults: './nightwatch.json' }); // $ nightwatch -t // $ nightwatch --test this.option('test', { description: 'Runs a single test.', alias: 't' }); // $ nightwatch --testcase this.option('testcase', { description: 'Used only together with --test. Runs the specified testcase from the current suite/module.' }); // $ nightwatch --mocha this.option('mocha', { description: 'Set the test runner to use Mocha.' }); /* // $ nightwatch --chrome this.option('chrome', { group: 'Browsers', description: 'Run tests in Google Chrome browser' }); // $ nightwatch --firefox this.option('firefox', { group: 'Browsers', description: 'Run tests in Mozilla Firefox browser' }); // $ nightwatch --safari this.option('safari', { group: 'Browsers', description: 'Run tests in Apple Safari' }); // $ nightwatch --edge this.option('edge', { group: 'Browsers', description: 'Run tests in Microsoft Edge' }); */ // $ nightwatch -g // $ nightwatch --group this.option('group', { group: 'Tags & filtering', description: 'Runs a group of tests (i.e. a folder)', alias: 'g' }); // $ nightwatch -s // $ nightwatch --skipgroup this.option('skipgroup', { group: 'Tags & filtering', description: 'Skips one or several (comma separated) group of tests.', alias: 's' }); // $ nightwatch -f // $ nightwatch --filter this.option('filter', { group: 'Tags & filtering', description: 'Specify a filter (glob expression) as the file name format to use when loading the files.', alias: 'f' }); // $ nightwatch -a // $ nightwatch --tag this.option('tag', { group: 'Tags & filtering', description: 'Only run tests with the given tag.', alias: 'a' }); // $ nightwatch --skiptags this.option('skiptags', { group: 'Tags & filtering', description: 'Skips tests that have the specified tag or tags (comma separated).' }); // $ nightwatch --grep this.option('grep', { group: 'Test Filters – Mocha only', description: 'Only run tests matching this string or regexp.' }); // $ nightwatch --fgrep this.option('fgrep', { group: 'Test Filters – Mocha only', description: 'Only run tests containing this string.' }); // $ nightwatch --invert this.option('invert', { group: 'Test Filters – Mocha only', description: 'Inverts --grep and --fgrep matches.' }); // $ nightwatch --retries this.option('retries', { group: 'Retrying', description: 'Retries failed or errored testcases up times.' }); // $ nightwatch --suiteRetries this.option('suiteRetries', { group: 'Retrying', description: 'Retries failed or errored testsuites up times.' }); // $ nightwatch --timeout this.option('timeout', { description: 'Set the global timeout for assertion retries before an assertion fails.' }); // $ nightwatch -o // $ nightwatch --output this.option('output', { group: 'Reporting', description: 'Where to save the (JUnit XML) test reports.', alias: 'o' }); // $ nightwatch -r // $ nightwatch --reporter this.option('reporter', { group: 'Reporting', description: 'Name of a predefined reporter (e.g. junit) or path to a custom reporter file to use.', alias: 'r', defaults: DefaultSettings.default_reporter }); // $ nightwatch --trace this.option('trace', { group: 'Reporting', descriptions: 'Record trace information during nightwatch execution.' }); // $ nightwatch --open this.option('open', { group: 'Reporting', description: 'Opens the HTML report generated in the default browser at the end of test run' }); // $ nightwatch --reuse-browser this.option('reuse-browser', { description: 'Use the same browser session to run the individual test suites' }); // $ nightwatch --parallel // this.option('parallel', { // description: 'Enable running the tests in parallel mode, via test workers.' // }); // $ nightwatch --workers=5 this.option('workers', { description: 'Max number of test files running at the same time (default: CPU cores; e.g. workers=4)' }); // $ nightwatch --sequential this.option('serial', { description: 'Executes tests serially (disables parallel mode)' }); // $ nightwatch --headless this.option('headless', { description: 'Launch the browser (Chrome, Edge or Firefox) in headless mode.' }); // $ nightwatch --devtools this.option('devtools', { description: 'Automatically open devtools when launching the browser (Chrome, Edge, or Safari).' }); // $ nightwatch --debug this.option('debug', { group: 'Component Tests', description: 'Automatically pause the test execution after mounting the component and open the Nightwatch debug REPL interface.' }); // $ nightwatch --story this.option('story', { group: 'Component Tests', description: 'Allows to specify which story to run from the current file (when using Storybook or JSX written in component story format)' }); // $ nightwatch --preview this.option('preview', { group: 'Component Tests', description: 'Used to preview a component story/test; automatically pause the test execution after mounting the component.' }); // $ nightwatch --verbose this.option('verbose', { description: 'Displays extended HTTP command logging during the test run.' }); // $ nightwatch --fail-fast this.option('fail-fast', { description: 'Run in "fail-fast" mode: if a test suite cannot be started, the rest will be aborted' }); // $ nightwatch -h // $ nightwatch --help this.option('help', { description: 'Shows this help (pass COLORS=0 env variable to disable colors).', group: 'Info & help', alias: 'h' }); // $ nightwatch --info this.option('info', { description: 'Shows environment info, i.e. OS, cpu, Node.js and installed browsers.', group: 'Info & help' }); // $ nightwatch -v // $ nightwatch --version this.option('version', { alias: 'v', group: 'Info & help', description: 'Shows version information.' }); // $ nightwatch --list-files this.option('list-files', { description: 'Shows list of files present in the project.' }); this.option('deviceId', { description: 'Set the udid of real iOS device' }); this.option('rerun-failed', { description: 'Rerun failed test suites' }); return this; } } function longest(xs) { return Math.max.apply(null, xs.map(x => x.length)); } function rebase(base, dir) { const ds = path.normalize(dir).split('/').slice(1); const bs = path.normalize(base).split('/').slice(1); for (var i = 0; ds[i] && ds[i] === bs[i]; i++) { ; } ds.splice(0, i); bs.splice(0, i); const p = path.normalize(bs.map(function () { return '..'; }).concat(ds).join('/')).replace(/\/$/, '').replace(/^$/, '.'); return p.match(/^[./]/) ? p : './' + p; } let __instance__; if (!__instance__) { __instance__ = new ArgvSetup(process.argv.slice(2)); __instance__.showUsage(`Usage: ${Logger.colors.cyan('$0 [source] [options]')}`).setup(); } return __instance__; })(); ================================================ FILE: lib/runner/cli/cli.js ================================================ const path = require('path'); const lodashCloneDeep = require('lodash/cloneDeep'); const ArgvSetup = require('./argv-setup.js'); const Settings = require('../../settings/settings.js'); const Globals = require('../../testsuite/globals.js'); const Factory = require('../../transport/factory.js'); const Concurrency = require('../concurrency'); const Utils = require('../../utils'); const Runner = require('../runner.js'); const ProcessListener = require('../process-listener.js'); const analyticsCollector = require('../../utils/analytics.js'); const {RealIosDeviceIdError, iosRealDeviceUDID, isRealIos, isMobile, killSimulator, isAndroid} = require('../../utils/mobile.js'); const {Logger, singleSourceFile, isSafari, isLocalhost} = Utils; const NightwatchEvent = require('../eventHub.js'); const {NightwatchEventHub, DEFAULT_RUNNER_EVENTS} = NightwatchEvent; const {GlobalHook} = DEFAULT_RUNNER_EVENTS; class CliRunner { static get CONFIG_FILE_JS() { return './nightwatch.conf.js'; } static get CONFIG_FILE_CJS() { return './nightwatch.conf.cjs'; } static get CONFIG_FILE_TS() { return './nightwatch.conf.ts'; } static createDefaultConfig(destFileName) { // eslint-disable-next-line no-console console.log(Logger.colors.cyan('No config file found in the current working directory, creating nightwatch.conf.js in the current folder...')); const templateFile = path.join(__dirname, 'nightwatch.conf.ejs'); const os = require('os'); const fs = require('fs'); const ejs = require('ejs'); const tplData = fs.readFileSync(templateFile).toString(); let launch_url = 'https://nightwatchjs.org'; const availablePlugins = []; const autoLoadPlugins = [{ 'vite-plugin-nightwatch': { launch_url: 'http://localhost:3000' } }, { '@nightwatch/storybook': { launch_url: 'http://localhost:6006' } }, { '@nightwatch/react': {} }]; autoLoadPlugins.forEach(plugin => { try { const pluginName = Object.keys(plugin)[0]; const pluginPath = Utils.getPluginPath(pluginName); availablePlugins.push(pluginName); if (plugin[pluginName].launch_url) { launch_url = plugin[pluginName].launch_url; } } catch (err) { // plugin is not installed } }); let rendered = ejs.render(tplData, { plugins: (availablePlugins.length > 0) ? JSON.stringify(availablePlugins) : '[]', launch_url, isMacOS: os.platform() === 'darwin' }); rendered = Utils.stripControlChars(rendered); try { fs.writeFileSync(destFileName, rendered, {encoding: 'utf-8'}); return true; } catch (err) { Logger.error(`Failed to save nightwatch.conf.js config file to ${destFileName}. You need to manually create either a nightwatch.json or nightwatch.conf.js configuration file.`); Logger.error(err); return false; } } constructor(argv = {}) { if (argv.source && !argv._source) { argv._source = argv.source; delete argv.source; } if (argv._source && Utils.isString(argv._source)) { argv._source = [argv._source]; } if (argv.firefox) { argv.e = argv.env = 'firefox'; } else if (argv.chrome) { argv.e = argv.env = 'chrome'; } else if (argv.safari) { argv.e = argv.env = 'safari'; } else if (argv.edge) { argv.e = argv.env = 'edge'; } this.argv = argv; this.testRunner = null; this.globals = null; this.testEnv = null; this.testEnvArray = []; if (!argv.disable_process_listener) { this.processListener = new ProcessListener(); } } initTestSettings(userSettings = {}, baseSettings = null, argv = null, testEnv = '', asyncLoading) { this.test_settings = Settings.parse(userSettings, baseSettings, argv, testEnv); this.setMobileOptions(argv); this.setLoggingOptions(); this.setupGlobalHooks(); const result = this.readExternalHooks(asyncLoading); if (result instanceof Promise) { return result.then(() => { this.globalsSetup(argv); }); } this.globalsSetup(argv); return this; } globalsSetup(argv) { this.globals.init(); this.setTimeoutOptions(argv); } setTimeoutOptions(argv) { if (argv.timeout) { const timeout = parseInt(argv.timeout, 10); if (!isNaN(timeout)) { this.test_settings.globals.waitForConditionTimeout = timeout; this.test_settings.globals.retryAssertionTimeout = timeout; } } } setMobileOptions(argv) { const {desiredCapabilities, selenium = {}} = this.test_settings; if (isRealIos(desiredCapabilities) && !selenium.use_appium) { if (argv.deviceId) { this.test_settings.desiredCapabilities['safari:deviceUDID'] = iosRealDeviceUDID(argv.deviceId); } else if (!desiredCapabilities['safari:deviceUDID']) { throw new RealIosDeviceIdError(); } } if (isAndroid(desiredCapabilities) && argv.deviceId && !selenium.use_appium) { if (desiredCapabilities['goog:chromeOptions']) { this.test_settings.desiredCapabilities['goog:chromeOptions'].androidDeviceSerial = argv.deviceId; } else if (desiredCapabilities['moz:firefoxOptions']) { this.test_settings.desiredCapabilities['moz:firefoxOptions'].androidDeviceSerial = argv.deviceId; } } } setupGlobalHooks() { this.globals = new Globals(this.test_settings, this.argv, this.testEnv); } readExternalHooks(asyncLoading = true) { return this.globals.readExternal(asyncLoading); } /** * backwards compatibility * @readonly * @deprecated * @return {*} */ get settings() { return this.baseSettings; } setCurrentTestEnv() { this.testEnv = Utils.isString(this.argv.env) ? this.argv.env : Settings.DEFAULT_ENV; this.testEnvArray = this.testEnv.split(','); if (!this.baseSettings) { return this; } this.availableTestEnvs = Object.keys(this.baseSettings.test_settings).filter(key => { return Utils.isObject(this.baseSettings.test_settings[key]); }); this.validateTestEnvironments(); return this; } setLoggingOptions() { Logger.setOptions(this.test_settings); return this; } /** * @param {object} [settings] * @return {CliRunner} */ async setupAsync(settings) { this.baseSettings = await this.loadConfig(); await this.commonSetup(settings); return this; } /** * Backwords compatibility for runner * @param {object} [settings] * @return {CliRunner} */ setup(settings) { this.baseSettings = this.loadConfig(); this.commonSetup(settings, false); return this; } commonSetup(settings, asyncLoading = true) { this.validateConfig(); this.setCurrentTestEnv(); const result = this.parseTestSettings(settings, asyncLoading); if (asyncLoading) { return result.then(() => this.runnerSetup()); } this.runnerSetup(); return this; } runnerSetup() { this.createTestRunner(); this.setupConcurrency(); this.loadTypescriptTranspiler(); analyticsCollector.updateSettings(this.test_settings); analyticsCollector.updateLogger(Logger); analyticsCollector.collectEvent('nw_test_run', { arg_parallel: this.argv.parallel, browser_name: this.test_settings.desiredCapabilities.browserName, test_workers_enabled: this.test_settings.testWorkersEnabled, use_xpath: this.test_settings.use_xpath, is_bstack: this.test_settings.desiredCapabilities['bstack:options'] !== undefined, test_runner: this.test_settings.test_runner ? this.test_settings.test_runner.type : null }); this.setupEventHub(); } isRegisterEventHandlersCallbackExistsInGlobal() { const {globals} = this.test_settings; const {plugins = []} = this.globals; return Utils.isFunction(globals.registerEventHandlers) || plugins.some(plugin => plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers)); } setupEventHub() { if (this.isRegisterEventHandlersCallbackExistsInGlobal() && !NightwatchEventHub.isAvailable) { NightwatchEventHub.runner = this.testRunner.type; NightwatchEventHub.isAvailable = true; const {globals, output_folder} = this.test_settings; NightwatchEventHub.output_folder = output_folder; const {plugins} = this.globals; if (Utils.isFunction(globals.registerEventHandlers)) { globals.registerEventHandlers(NightwatchEventHub); } if (plugins.length > 0) { plugins.forEach((plugin) => { if (plugin.globals && Utils.isFunction(plugin.globals.registerEventHandlers)) { plugin.globals.registerEventHandlers(NightwatchEventHub); } }); } } } loadTypescriptTranspiler() { const projectTsFilePath = Utils.findTSConfigFile(this.test_settings.tsconfig_path); if (projectTsFilePath !== '' && !this.test_settings.disable_typescript) { Utils.loadTSNode(projectTsFilePath); } } isConfigDefault(configFile, localJsValue = CliRunner.CONFIG_FILE_JS) { return ArgvSetup.isDefault('config', configFile) || path.resolve(configFile) === localJsValue; } getLocalConfigFileName() { let packageInfo; try { packageInfo = require(path.resolve('package.json')); } catch (err) { packageInfo = null; } packageInfo = packageInfo || {}; const usingESM = packageInfo.type === 'module'; const usingTS = Utils.fileExistsSync(CliRunner.CONFIG_FILE_TS); if (usingTS) { return path.resolve(CliRunner.CONFIG_FILE_TS); } if (usingESM) { return path.resolve(CliRunner.CONFIG_FILE_CJS); } return path.resolve(CliRunner.CONFIG_FILE_JS); } loadConfig() { if (!this.argv.config) { return null; } const localJsOrTsValue = this.getLocalConfigFileName(); // use default nightwatch.json file if we haven't received another value if (this.isConfigDefault(this.argv.config, localJsOrTsValue)) { let newConfigCreated = false; const defaultValue = ArgvSetup.getDefault('config'); const hasJsOrTsConfig = Utils.fileExistsSync(localJsOrTsValue); const hasJsonConfig = Utils.fileExistsSync(defaultValue); if (!hasJsOrTsConfig && !hasJsonConfig) { newConfigCreated = CliRunner.createDefaultConfig(localJsOrTsValue); } if (hasJsOrTsConfig || newConfigCreated) { this.argv.config = localJsOrTsValue; } else if (hasJsonConfig) { this.argv.config = path.join(path.resolve('./'), this.argv.config); } } else { this.argv.config = path.resolve(this.argv.config); } return require(this.argv.config); } validateConfig() { // checking if the env passed is valid if (this.baseSettings && !this.baseSettings.test_settings) { this.baseSettings.test_settings = { default: {} }; } return this; } /** * Validates and parses the test settings * @param {object} [settings] * @returns {CliRunner|Promise} */ parseTestSettings(settings = {}, asyncLoading = true) { this.userSettings = lodashCloneDeep(settings); const result = this.initTestSettings(settings, this.baseSettings, this.argv, this.testEnv, asyncLoading); if (result instanceof Promise) { return result; } return this; } runGlobalHook(key, args = [], isParallelHook = false) { let promise; const {globals} = this.test_settings; if (isParallelHook && Concurrency.isWorker() || !isParallelHook && !Concurrency.isWorker()) { const start_time = new Date(); if (Utils.isFunction(globals[key])) { NightwatchEventHub.emit(GlobalHook[key].started, { start_time: start_time }); } promise = this.globals.runGlobalHook(key, args); return promise.finally(() => { if (Utils.isFunction(globals[key])) { NightwatchEventHub.emit(GlobalHook[key].finished, { start_time: start_time, end_time: new Date() }); } }); } return Promise.resolve(); } validateTestEnvironments() { for (let i = 0; i < this.testEnvArray.length; i++) { if (this.testEnvArray[i] === 'default') { continue; } if (!(this.testEnvArray[i] in this.baseSettings.test_settings)) { const error = new Error(`Invalid testing environment specified: ${this.testEnvArray[i]}. \n\n ${Logger.colors.light_cyan('Available environments are:')}\n ${Logger.inspectObject(this.availableTestEnvs)}`); error.showTrace = false; throw error; } } return this; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // Concurrency related /////////////////////////////////////////////////////////////////////////////////////////////////////////// get testWorkersMode() { return this.isTestWorkersEnabled() && !this.testRunner.isTestWorker(); } usingServer(test_settings = {}) { // TODO: selenium_host and seleniumHost are for backwards compatability. // remove these in future versions. return Utils.isObject(test_settings.selenium) || test_settings.selenium_host || test_settings.seleniumHost; } isTestWorkersEnabled() { if (this.testRunner.supportsParallelTestSuiteRun === false) { return false; } const testWorkers = this.test_settings.testWorkersEnabled && !singleSourceFile(this.argv); if (!testWorkers) { if (this.argv.debug) { Logger.info('Disabling parallelism while running in debug mode'); } return false; } for (const env of this.testEnvArray) { const {webdriver = {}} = this.testEnvSettings[env]; const desiredCapabilities = this.testEnvSettings[env].capabilities || this.testEnvSettings[env].desiredCapabilities; if (isMobile(desiredCapabilities) && !this.usingServer(this.testEnvSettings[env])) { if (Concurrency.isWorker()) { Logger.info('Disabling parallelism while running tests on mobile platform'); } return false; } if (isSafari(desiredCapabilities) && isLocalhost(webdriver)) { this.isSafariEnvPresent = true; if (Concurrency.isMasterProcess()) { // eslint-disable-next-line no-console console.warn('Running tests in parallel is not supported in Safari. Tests will run in serial mode.'); } return false; } } return true; } parallelMode() { return this.testEnvArray.length > 1 || this.testWorkersMode; } setupConcurrency() { if (this.testRunner?.type === Runner.MOCHA_RUNNER) { this.test_settings.use_child_process = true; } this.concurrency = new Concurrency(this.test_settings, this.argv, this.isTestWorkersEnabled()); return this; } isConcurrencyEnabled() { return this.testRunner.supportsConcurrency && this.parallelMode(); } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // Test runner related /////////////////////////////////////////////////////////////////////////////////////////////////////////// executeTestRunner(modules) { return this.testRunner.run(modules); } createTestRunner() { this.testRunner = Runner.create(this.test_settings, this.argv, { globalHooks: this.globals ? this.globals.hooks : null, globalsInstance: this.globals }); if (this.processListener) { this.processListener.setTestRunner(this.testRunner); } return this; } get testEnvSettings() { if (this.__testEnvSettings) { return this.__testEnvSettings; } this.__testEnvSettings = {}; for (const env of this.testEnvArray) { this.__testEnvSettings[env] = Settings.parse(this.userSettings, this.baseSettings, this.argv, env); } return this.__testEnvSettings; } async getTestsFiles() { const modules = {}; for (const env of this.testEnvArray) { modules[env] = await Runner.readTestSource(this.testEnvSettings[env], this.argv); } return modules; } /** * * @param [done] * @return {*} */ runTests(done = null) { return this.runGlobalHook('before', [this.test_settings, this.testEnvSettings]) .then(_ => { return this.runGlobalHook('beforeChildProcess', [this.test_settings], true); }) .then(_ => { return this.getTestsFiles(); }) .then(modules => { if (!this.testRunner) { const error = new Error(`Test runner '${this.test_settings.test_runner.type}' is not known.`); error.showTrace = false; error.detailedErr = `\n Verify "test_runner" settings: \n ${Logger.inspectObject(this.test_settings.test_runner)}`; throw error; } let promise = Promise.resolve(); if (this.test_settings.selenium && this.test_settings.selenium.start_process) { promise = Factory.createSeleniumService(this); } const {real_mobile, avd} = this.test_settings.desiredCapabilities; if (!real_mobile && avd) { const AndroidServer = require('../androidEmulator.js'); this.androidServer = new AndroidServer(avd); promise = promise.then(() => this.androidServer.launchEmulator()); } return promise.then(() => { if (this.isConcurrencyEnabled()) { return this.testRunner.runConcurrent(this.testEnvArray, modules, this.isTestWorkersEnabled(), this.isSafariEnvPresent) .then(exitCode => { if (exitCode > 0) { this.processListener.setExitCode(exitCode); } }); } return this.executeTestRunner(modules[this.testEnv]); }); }) .catch(err => { if (err.detailedErr) { err.data = err.detailedErr; } if (!err.sessionCreate && !err.displayed) { Logger.error(err); if (err.data) { Logger.warn(' ' + err.data); } // eslint-disable-next-line no-console console.log(''); } err.displayed = true; return err; }) .then(errorOrFailed => { if (this.seleniumService) { // stop the Selenium Server if running const {service} = this.seleniumService; if (service && service.kill) { // Give the selenium server some time to close down its browser drivers return new Promise((resolve, reject) => { setTimeout(() => { service.kill() .catch(err => { Logger.error(err); }) .then(() => this.seleniumService.stop()) .then(() => resolve()); }, 100); }).then(() => { return errorOrFailed; }); } } return errorOrFailed; }) .then(errorOrFailed => { if (errorOrFailed instanceof Error || errorOrFailed === true) { try { this.processListener.setExitCode(5); } catch (e) { // eslint-disable-next-line no-console console.error(e); } } return this.runGlobalHook('afterChildProcess', [], true) .then(_ => { return this.runGlobalHook('after'); }) .then(result => { return errorOrFailed; }); }) .catch(err => { // eslint-disable-next-line no-console console.log(''); try { this.processListener.setExitCode(5); } catch (e) { // eslint-disable-next-line no-console console.error(e); } return err; }) .then(errorOrFailed => { if (typeof done == 'function' && !Concurrency.isWorker()) { if (errorOrFailed instanceof Error) { return done(errorOrFailed); } return done(); } if (errorOrFailed instanceof Error) { throw errorOrFailed; } if (this.androidServer && !this.androidServer.emulatorAlreadyRunning) { this.androidServer.killEmulator(); } if (this.test_settings.deviceUDID && !isRealIos(this.test_settings.desiredCapabilities)) { killSimulator(this.test_settings.deviceUDID); } analyticsCollector.__flush(); return errorOrFailed; }); } } module.exports = CliRunner; ================================================ FILE: lib/runner/cli/nightwatch.conf.ejs ================================================ // // Refer to the online docs for more details: // https://nightwatchjs.org/guide/configuration/nightwatch-configuration-file.html // // _ _ _ _ _ _ _ // | \ | |(_) | | | | | | | | // | \| | _ __ _ | |__ | |_ __ __ __ _ | |_ ___ | |__ // | . ` || | / _` || '_ \ | __|\ \ /\ / / / _` || __| / __|| '_ \ // | |\ || || (_| || | | || |_ \ V V / | (_| || |_ | (__ | | | | // \_| \_/|_| \__, ||_| |_| \__| \_/\_/ \__,_| \__| \___||_| |_| // __/ | // |___/ // module.exports = { // An array of folders (excluding subfolders) where your tests are located; // if this is not specified, the test source must be passed as the second argument to the test runner. src_folders: [], // See https://nightwatchjs.org/guide/concepts/page-object-model.html page_objects_path: ['node_modules/nightwatch/examples/pages/'], // See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html custom_commands_path: ['node_modules/nightwatch/examples/custom-commands/'], // See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-assertions.html custom_assertions_path: '', // See https://nightwatchjs.org/guide/extending-nightwatch/adding-plugins.html <% if (plugins) { %>plugins: <%- plugins %>, <% } %> // See https://nightwatchjs.org/guide/concepts/test-globals.html#external-test-globals globals_path : '', // Set this to true to disable bounding boxes on terminal output. Useful when running in some CI environments. disable_output_boxes: false, webdriver: {}, test_workers: { enabled: true, workers: 'auto' }, test_settings: { default: { disable_error_log: false, launch_url: '<%- launch_url %>', screenshots: { enabled: false, path: 'screens', on_failure: true }, desiredCapabilities: { browserName : 'firefox' }, webdriver: { start_process: true, server_path: '' } }, <% if (isMacOS) { %>safari: { desiredCapabilities : { browserName : 'safari', alwaysMatch: { acceptInsecureCerts: false } }, webdriver: { start_process: true, server_path: '' } },<% } %> firefox: { desiredCapabilities : { browserName : 'firefox', acceptInsecureCerts: true, 'moz:firefoxOptions': { args: [ // '-headless', // '-verbose' ] } }, webdriver: { start_process: true, server_path: '', cli_args: [ // very verbose geckodriver logs // '-vv' ] } }, chrome: { desiredCapabilities : { browserName : 'chrome', 'goog:chromeOptions' : { // More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/ // // w3c:false tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78) w3c: true, args: [ //'--no-sandbox', //'--ignore-certificate-errors', //'--allow-insecure-localhost', //'--headless' ] } }, webdriver: { start_process: true, server_path: '', cli_args: [ // '--verbose' ] } }, edge: { desiredCapabilities : { browserName : 'MicrosoftEdge', 'ms:edgeOptions' : { w3c: true, // More info on EdgeDriver: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options args: [ //'--headless' ] } }, webdriver: { start_process: true, // Download msedgedriver from https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/ // and set the location below: server_path: '', cli_args: [ // '--verbose' ] } }, ////////////////////////////////////////////////////////////////////////////////// // Configuration for when using cucumber-js (https://cucumber.io) | // | // It uses the bundled examples inside the nightwatch examples folder; feel free | // to adapt this to your own project needs | ////////////////////////////////////////////////////////////////////////////////// 'cucumber-js': { src_folders: ['examples/cucumber-js/features/step_definitions'], test_runner: { // set cucumber as the runner type: 'cucumber', // define cucumber specific options options: { //set the feature path feature_path: 'node_modules/nightwatch/examples/cucumber-js/*/*.feature', // start the webdriver session automatically (enabled by default) // auto_start_session: true // use parallel execution in Cucumber // workers: 2 // set number of workers to use (can also be defined in the cli as --workers=2 } } }, ////////////////////////////////////////////////////////////////////////////////// // Configuration for when using the browserstack.com cloud service | // | // Please set the username and access key by setting the environment variables: | // - BROWSERSTACK_USERNAME | // - BROWSERSTACK_ACCESS_KEY | // .env files are supported | ////////////////////////////////////////////////////////////////////////////////// browserstack: { selenium: { host: 'hub.browserstack.com', port: 443 }, // More info on configuring capabilities can be found on: // https://www.browserstack.com/automate/capabilities?tag=selenium-4 desiredCapabilities: { 'bstack:options' : { userName: '${BROWSERSTACK_USERNAME}', accessKey: '${BROWSERSTACK_ACCESS_KEY}', } }, disable_error_log: true, webdriver: { timeout_options: { timeout: 60000, retry_attempts: 3 }, keep_alive: true, start_process: false } }, 'browserstack.local': { extends: 'browserstack', desiredCapabilities: { 'browserstack.local': true } }, 'browserstack.chrome': { extends: 'browserstack', desiredCapabilities: { browserName: 'chrome', chromeOptions : { w3c: true } } }, 'browserstack.firefox': { extends: 'browserstack', desiredCapabilities: { browserName: 'firefox' } }, 'browserstack.ie': { extends: 'browserstack', desiredCapabilities: { browserName: 'internet explorer', browserVersion: '11.0' } }, 'browserstack.safari': { extends: 'browserstack', desiredCapabilities: { browserName: 'safari' } }, 'browserstack.local_chrome': { extends: 'browserstack.local', desiredCapabilities: { browserName: 'chrome' } }, 'browserstack.local_firefox': { extends: 'browserstack.local', desiredCapabilities: { browserName: 'firefox' } }, ////////////////////////////////////////////////////////////////////////////////// // Configuration for when using the SauceLabs cloud service | // | // Please set the username and access key by setting the environment variables: | // - SAUCE_USERNAME | // - SAUCE_ACCESS_KEY | ////////////////////////////////////////////////////////////////////////////////// saucelabs: { selenium: { host: 'ondemand.saucelabs.com', port: 443 }, // More info on configuring capabilities can be found on: // https://docs.saucelabs.com/dev/test-configuration-options/ desiredCapabilities: { 'sauce:options' : { username: '${SAUCE_USERNAME}', accessKey: '${SAUCE_ACCESS_KEY}', screenResolution: '1280x1024' // https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/#--region // region: 'us-west-1' // https://docs.saucelabs.com/dev/test-configuration-options/#tunnelidentifier // parentTunnel: '', // tunnelIdentifier: '', } }, disable_error_log: false, webdriver: { start_process: false } }, 'saucelabs.chrome': { extends: 'saucelabs', desiredCapabilities: { browserName: 'chrome', browserVersion: 'latest', javascriptEnabled: true, acceptSslCerts: true, timeZone: 'London', chromeOptions : { w3c: true } } }, 'saucelabs.firefox': { extends: 'saucelabs', desiredCapabilities: { browserName: 'firefox', browserVersion: 'latest', javascriptEnabled: true, acceptSslCerts: true, timeZone: 'London' } }, ////////////////////////////////////////////////////////////////////////////////// // Configuration for when using the Selenium service, either locally or remote, | // like Selenium Grid | ////////////////////////////////////////////////////////////////////////////////// selenium_server: { // Selenium Server is running locally and is managed by Nightwatch // Install the NPM package @nightwatch/selenium-server or download the selenium server jar file from https://github.com/SeleniumHQ/selenium/releases/, e.g.: selenium-server-4.1.1.jar selenium: { start_process: true, port: 4444, server_path: '', // Leave empty if @nightwatch/selenium-server is installed command: 'standalone', // Selenium 4 only cli_args: { //'webdriver.gecko.driver': '', //'webdriver.chrome.driver': '' } }, webdriver: { start_process: false, default_path_prefix: '/wd/hub' } }, 'selenium.chrome': { extends: 'selenium_server', desiredCapabilities: { browserName: 'chrome', chromeOptions : { w3c: true } } }, 'selenium.firefox': { extends: 'selenium_server', desiredCapabilities: { browserName: 'firefox', 'moz:firefoxOptions': { args: [ // '-headless', // '-verbose' ] } } } } }; ================================================ FILE: lib/runner/concurrency/child-process.js ================================================ const child_process = require('child_process'); const EventEmitter = require('events'); const boxen = require('boxen'); const {Logger, isObject, symbols} = require('../../utils'); const ProcessListener = require('../../runner/process-listener.js'); let prevIndex = 0; class ChildProcess extends EventEmitter { static get defaultStartDelay() { return 10; } static get prevIndex() { return prevIndex; } static set prevIndex(val) { prevIndex = val; } constructor(environment, index, env_output, settings, args) { super(); this.settings = settings; this.env_output = env_output || []; this.mainModule = process.mainModule.filename; this.index = index; this.itemKey = this.getChildProcessEnvKey(environment); this.startDelay = settings.parallel_process_delay || ChildProcess.defaultStartDelay; this.environment = environment; this.child = null; this.globalExitCode = 0; this.env_label = ''; this.args = args || []; } setLabel(label) { this.env_itemKey = label; this.env_label = this.settings.disable_colors ? ` ${label} ` : Logger.colors.bgBlack.yellow.bold(` ${label} `); return this; } printLog(msg) { if (this.settings.output) { // eslint-disable-next-line no-console console.info(msg); } } getChildProcessEnvKey(env) { return `${env}_${this.index + 1}`; } /** * Returns an array of cli arguments to be passed to the child process, * based on the args passed to the main process * @returns {Array} */ getArgs() { const args = []; if (isObject(this.settings.test_workers)) { const {node_options} = this.settings.test_workers; if (node_options === 'auto' || node_options === 'inherit' || node_options === true) { args.push.apply(args, process.execArgv); } else if (Array.isArray(node_options)) { args.push.apply(args, node_options); } } args.push(this.mainModule); args.push.apply(args, this.args); args.push('--parallel-mode'); return args; } writeToStdout(data) { data = data.toString().trim(); const color_pair = this.availColors[this.index % 4]; let output = ''; if (ChildProcess.prevIndex !== this.index) { ChildProcess.prevIndex = this.index; if (this.settings.live_output) { output += '\n'; } } if (this.settings.output && (this.settings.detailed_output || !this.settings.silent)) { let childProcessLabel; if (this.settings.disable_colors) { childProcessLabel = ' ' + this.environment + ' '; } else { childProcessLabel = ''; this.env_label = Logger.colors[color_pair[0]][color_pair[1]](` ${this.environment} `); } const lines = data.split('\n').map(line => { return childProcessLabel + ' ' + line + ' '; }); data = lines.join('\n'); } output += data; if (this.settings.live_output) { process.stdout.write(output + '\n'); } else { this.env_output.push(output); } } run(colors, type) { this.availColors = colors; const cliArgs = this.getArgs(); const env = {}; Object.keys(process.env).forEach(function(key) { env[key] = process.env[key]; }); return new Promise((resolve, reject) => { setTimeout(() => { env.__NIGHTWATCH_PARALLEL_MODE = '1'; env.__NIGHTWATCH_ENV = this.environment; env.__NIGHTWATCH_ENV_KEY = this.itemKey; env.__NIGHTWATCH_ENV_LABEL = this.env_itemKey; env.__NIGHTWATCH_PARALLEL_TYPE = type; this.child = child_process.spawn(process.execPath, cliArgs, { cwd: process.cwd(), encoding: 'utf8', env, stdio: [null, null, null, 'ipc'] }); if (typeof this.child.send == 'function') { this.child.send(JSON.stringify({ type: 'vite', vite_port: this.settings.vite_port })); } const color_pair = this.availColors[this.index % 4]; this.printLog(' Running: ' + Logger.colors[color_pair[0]][color_pair[1]](` ${this.env_itemKey} `)); this.child.stdout.on('data', data => { this.writeToStdout(data); }); this.child.on('message', message => { this.emit('message', message); }); this.child.stderr.on('data', data => { this.writeToStdout(data); }); this.processListener = new ProcessListener(this.child); this.child.on('exit', code => { this.printLog(''); const status = code > 0 ? symbols.fail : symbols.ok; if (this.settings.disable_output_boxes) { // eslint-disable-next-line no-console console.log(`\n${status} ${this.env_label}`, this.env_output.join('\n'), '\n'); } else { // eslint-disable-next-line no-console console.log(boxen(this.env_output.join('\n'), {title: `────────────────── ${status} ${this.env_label}`, padding: 1, borderColor: 'cyan'})); } code = code || this.processListener.exitCode; resolve(code); }); }, this.index * this.startDelay); }); } } module.exports = ChildProcess; ================================================ FILE: lib/runner/concurrency/index.js ================================================ const EventEmitter = require('events'); const Utils = require('../../utils'); const {isObject, isNumber} = Utils; const lodashCloneDeep = require('lodash/cloneDeep'); const ChildProcess = require('./child-process.js'); const WorkerPool = require('./worker-process.js'); const {Logger} = Utils; class Concurrency extends EventEmitter { constructor(settings = {}, argv = {}, isTestWorkerEnabled, isSafariEnvPresent = false) { super(); this.argv = argv; this.settings = lodashCloneDeep(settings); this.useChildProcess = settings.use_child_process; this.childProcessOutput = {}; this.globalExitCode = 0; this.testWorkersEnabled = typeof isTestWorkerEnabled !== 'undefined' ? isTestWorkerEnabled : settings.testWorkersEnabled; this.isSafariEnvPresent = isSafariEnvPresent; } static getChildProcessArgs(envs) { const childProcessArgs = []; let arg; for (let i = 2; i < process.argv.length; i++) { arg = process.argv[i]; if (arg === '-e' || arg === '--env') { i++; } else if (!(arg.startsWith('--env=') || arg.startsWith('--e='))) { childProcessArgs.push(arg); } } return childProcessArgs; } /** * * @param {String} label * @param {Array} args * @param {Array} extraArgs * @param {Number} index * @return {ChildProcess} */ createChildProcess(label, args = [], extraArgs = [], index = 0) { this.childProcessOutput[label] = []; const childArgs = args.slice().concat(extraArgs); const childProcess = new ChildProcess(label, index, this.childProcessOutput[label], this.settings, childArgs); childProcess.on('message', data => { this.emit('message', data); }); return childProcess; } /** * * @param {ChildProcess} childProcess * @param {String} outputLabel * @param {Array} availColors * @param {String} type * @return {Promise} */ runChildProcess(childProcess, outputLabel, availColors, type) { return childProcess.setLabel(outputLabel) .run(availColors, type) .then(exitCode => { if (exitCode > 0) { this.globalExitCode = exitCode; } }); } /** * * @param {Array} envs * @param {Array} [modules] * @return {Promise} */ runChildProcesses(envs, modules) { const availColors = Concurrency.getAvailableColors(); const args = Concurrency.getChildProcessArgs(envs); if (this.testWorkersEnabled) { const jobs = []; if (envs.length > 0) { envs.forEach((env) => { const envModules = modules[env]; const jobList = envModules.map((module) => { return { env, module }; }); jobs.push(...jobList); }); } else { const jobList = modules.map((module) => { return { module }; }); jobs.push(...jobList); } return this.runMultipleTestProcess(jobs, args, availColors); } return this.runProcessTestEnvironment(envs, args, availColors); } /** * * @param {Array} envs * @param {Array} modules */ runMultiple(envs = [], modules) { if (this.useChildProcess) { return this.runChildProcesses(envs, modules) .then(_ => { return this.globalExitCode; }); } return this.runWorkerProcesses(envs, modules) .catch(_ => { this.globalExitCode = 1; }) .then(_ => { return this.globalExitCode; }); } /** * * @param {Array} envs * @param {Array} [modules] * @return {Promise} */ runWorkerProcesses(envs, modules) { const availColors = Concurrency.getAvailableColors(); if (this.testWorkersEnabled) { const jobs = []; if (envs.length > 0) { envs.forEach((env) => { const envModules = modules[env]; const jobList = envModules.map((module) => { return { env, module }; }); jobs.push(...jobList); }); } else { const jobList = modules.map((module) => { return { module }; }); jobs.push(...jobList); } return this.runMultipleTestWorkers(jobs, availColors); } return this.runWorkerTestEnvironments(envs, availColors); } /** * * @param {Array} envs * @param {Array} args * @param {Array} availColors * @return {Promise} */ runProcessTestEnvironment(envs, args, availColors) { return Promise.all(envs.map((environment, index) => { const extraArgs = ['--env', environment]; if (this.isSafariEnvPresent) { extraArgs.push('--serial'); } const childProcess = this.createChildProcess(environment, args, extraArgs, index); return this.runChildProcess(childProcess, environment + ' environment', availColors, 'envs'); })); } /** * * @param {Array} envs * @param {Array} availColors * @return {Promise} */ runWorkerTestEnvironments(envs, availColors) { let maxWorkerCount = this.getTestWorkersCount(); const remaining = envs.length; maxWorkerCount = Math.min(maxWorkerCount, remaining); const workerPool = this.setupWorkerPool(['--parallel-mode'], this.settings, maxWorkerCount); envs.forEach((env) => { const workerArgv = {...this.argv}; workerArgv.env = workerArgv.e = env; workerPool.addTask({ argv: workerArgv, settings: this.settings, label: `${env} environment`, colors: availColors }); }); return new Promise((resolve, reject) => { Promise.allSettled(workerPool.tasks) .then(values => { values.some(({status}) => status === 'rejected') ? reject() : resolve(); }); }); } /** * * @param {Array} modules * @param {Array} args * @param {Array} availColors */ runMultipleTestProcess(modules, args, availColors) { let maxWorkerCount = this.getTestWorkersCount(); let remaining = modules.length; maxWorkerCount = Math.min(maxWorkerCount, remaining); if (this.settings.output) { Logger.info(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`); } return new Promise((resolve, reject) => { Concurrency.buildProcessQueue(maxWorkerCount, modules, (env, modulePath, index, next) => { let outputLabel = Utils.getModuleKey(modulePath, this.settings.src_folders, modules); const flags = ['--test', modulePath, '--test-worker']; if (env) { flags.unshift('--env', env); outputLabel = `${env}: ${outputLabel}`; } const childProcess = this.createChildProcess(outputLabel, args, flags, index); return this.runChildProcess(childProcess, outputLabel, availColors, 'workers') .then(_ => { remaining -= 1; if (remaining > 0) { next(); } else { resolve(); } }); }); }); } /** * * @param {Array} modules * @param {Array} availColors */ runMultipleTestWorkers(modules, availColors) { let maxWorkerCount = this.getTestWorkersCount(); const remaining = modules.length; maxWorkerCount = Math.min(maxWorkerCount, remaining); if (this.settings.output) { Logger.info(`Launching up to ${maxWorkerCount} concurrent test worker processes...\n`); } const workerPool = this.setupWorkerPool(['--test-worker', '--parallel-mode'], this.settings, maxWorkerCount); modules.forEach(({module, env}) => { let outputLabel = Utils.getModuleKey(module, this.settings.src_folders, modules); outputLabel = env ? `${env}: ${outputLabel}` : outputLabel; const workerArgv = {...this.argv}; workerArgv._source = [module]; workerArgv.env = env; workerArgv['test-worker'] = true; return workerPool.addTask({ argv: workerArgv, settings: this.settings, label: outputLabel, colors: availColors }); }); return new Promise((resolve, reject) => { Promise.allSettled(workerPool.tasks) .then(values => { values.some(({status}) => status === 'rejected') ? reject() : resolve(); }); }); } /** * * @param {Array} args * @param {Object} settings * @param {Number} maxWorkerCount */ setupWorkerPool(args, settings, maxWorkerCount) { const workerPool = new WorkerPool(args, settings, maxWorkerCount); workerPool.on('message', data => { this.emit('message', data); }); return workerPool; } /** * @param {String} [label] */ printChildProcessOutput(label) { if (label) { this.childProcessOutput[label] = this.childProcessOutput[label].filter(item => { return item !== ''; }).map(item => { if (item === '\\n') { item = '\n'; } return item; }); this.childProcessOutput[label].forEach(function(output) { process.stdout.write(output + '\n'); }); this.childProcessOutput[label] = []; return; } Object.keys(this.childProcessOutput).forEach(environment => { this.printChildProcessOutput(environment); }); } /** * @return {number} */ getTestWorkersCount() { const {test_workers} = this.settings; let workers = require('os').cpus().length; if (isObject(test_workers) && isNumber(test_workers.workers)) { workers = test_workers.workers; } return workers; } /** * @param {number} concurrency * @param {Array} modules * @param {function} fn */ static buildProcessQueue(maxWorkers, modules, fn) { const queue = modules.slice(0); let workers = 0; let index = 0; const next = function() { workers -= 1; process(); }; const process = function(done = function() {}) { while (workers < maxWorkers) { workers += 1; if (queue.length) { const item = queue.shift(); fn(item.env, item.module, index++, next); } else { done(); } } }; process(); } static getAvailableColors() { const availColorPairs = [ ['bgRed', 'white'], ['bgGreen', 'black'], ['bgBlue', 'white'], ['bgMagenta', 'white'] ]; let currentIndex = availColorPairs.length; let temporaryValue; let randomIndex; // While there remain elements to shuffle... while (0 !== currentIndex) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = availColorPairs[currentIndex]; availColorPairs[currentIndex] = availColorPairs[randomIndex]; availColorPairs[randomIndex] = temporaryValue; } return availColorPairs; } static isWorker() { return process.env.__NIGHTWATCH_PARALLEL_MODE === '1' || WorkerPool.isWorkerThread; } static isTestWorker(argv = {}) { return Concurrency.isWorker() && argv['test-worker']; } static isMasterProcess() { return !Concurrency.isWorker(); } } module.exports = Concurrency; ================================================ FILE: lib/runner/concurrency/task.js ================================================ const Nightwatch = require('../../index'); function runWorkerTask({argv, port1}) { const writeData = (data) => { data = data.toString().trim(); if (data){ port1.postMessage({ type: 'stdout', data: data }); } }; process.stdout.write = writeData; process.stderr.write = writeData; //send reports to main thread using message port process.port = port1; return Nightwatch.runTests(argv, {}); } module.exports = runWorkerTask; ================================================ FILE: lib/runner/concurrency/worker-process.js ================================================ const path = require('path'); const Piscina = require('piscina'); const {isWorkerThread} = Piscina; const EventEmitter = require('events'); const WorkerTask = require('./worker-task.js'); const WORKER_FILE = path.resolve(__dirname, 'task.js'); class WorkerPool extends EventEmitter { static get isWorkerThread() { return isWorkerThread; } get tasks() { return this.__tasks; } set tasks(tasks) { this.__tasks = tasks; } constructor(args, settings, maxWorkerCount) { super(); this.settings = settings; this.piscina = new Piscina({ filename: WORKER_FILE, maxThreads: maxWorkerCount, argv: args, env: { ...process.env, __NIGHTWATCH_PARALLEL_MODE: '1' } }); this.__tasks = []; this.index = 0; } /** * adds a task to running worker queue */ addTask({label, argv, colors} = {}) { const workerTask = new WorkerTask({piscina: this.piscina, index: this.index, label, argv, settings: this.settings}); workerTask.on('message', (data) => { this.emit('message', data); }); this.index++; this.__tasks.push(workerTask.runWorkerTask(colors)); } } module.exports = WorkerPool; ================================================ FILE: lib/runner/concurrency/worker-task.js ================================================ const boxen = require('boxen'); const {MessageChannel} = require('worker_threads'); const {Logger, symbols} = require('../../utils'); const EventEmitter = require('events'); const {isString} = require('../../utils'); let prevIndex = 0; class WorkerTask extends EventEmitter { static get prevIndex() { return prevIndex; } static set prevIndex(val) { prevIndex = val; } constructor({piscina, index, label, settings, argv, task_ouput}) { super(); this.task_output = task_ouput || []; this.startDelay = settings.parallel_process_delay; this.piscina = piscina; this.label = label; this.settings = settings; this.argv = argv; this.index = index; this.task_label = ''; this.env_label = argv.env; } printLog(msg) { if (this.settings.output) { // eslint-disable-next-line no-console console.info(msg); } } setlabel(colorPair) { this.task_label = this.settings.disable_colors ? ` ${this.label} ` : Logger.colors[colorPair[0]][colorPair[1]](` ${this.label} `); } writeToStdOut(data) { let output = ''; if (WorkerTask.prevIndex !== this.index) { WorkerTask.prevIndex = this.index; if (this.settings.live_output) { output += '\n'; } } if (this.settings.output && (this.settings.detailed_ouput || !this.settings.silent)) { const lines = data.split('\n').map(line => this.env_label + ' ' + line + ' '); data = lines.join('\n'); } output += data; if (this.settings.live_output) { process.stdout.write(output + '\n'); } else { this.task_output.push(output); } } async runWorkerTask(colors, type) { this.availColors = colors; const colorPair = this.availColors[this.index % 4]; this.setlabel(colorPair); this.printLog('Running ' + Logger.colors[colorPair[0]][colorPair[1]](` ${this.task_label} `)); const {port1, port2} = new MessageChannel(); port2.onmessage = ({data: result}) => { if (isString(result)) { try { result = JSON.parse(result); // eslint-disable-next-line no-empty } catch (e) {} } switch (result.type) { case 'testsuite_finished': result.itemKey = this.label, this.emit('message', result); break; case 'stdout': this.writeToStdOut(result.data); break; } }; port2.unref(); return new Promise((resolve, reject) => { setTimeout(() => { this.piscina.run({argv: this.argv, port1}, {transferList: [port1]}) .catch(err => err) .then(failures => { if (this.settings.disable_output_boxes){ // eslint-disable-next-line no-console console.log(`${failures ? symbols.fail : symbols.ok} ${this.task_label}\n`, this.task_output.join('\n'), '\n'); } else { // eslint-disable-next-line no-console console.log(boxen(this.task_output.join('\n'), {title: `────────────────── ${failures ? symbols.fail : symbols.ok} ${this.task_label}`, padding: 1, borderColor: 'cyan'})); } //throw error to mark exit-code of the process if (failures) { return reject(new Error()); } resolve(); }); }, this.index * this.startDelay); }); } } module.exports = WorkerTask; ================================================ FILE: lib/runner/eventHub.js ================================================ const fs = require('fs'); const path = require('path'); const EventEmitter = require('events'); const {Logger, createFolder} = require('../utils'); class NightwatchEventHub extends EventEmitter { emit(eventName, data) { if (this.isAvailable) { try { super.emit(eventName, data); } catch (err) { Logger.error(err); } } if (eventName === 'TestFinished' && this.output_folder && this.runner === 'cucumber') { let {output_folder} = this; output_folder = path.join(output_folder, 'cucumber'); const filename = path.join(output_folder, 'cucumber-report.json'); this.writeReportFile(filename, JSON.stringify(data.report, null, 2), true, output_folder) .then(_ => { Logger.info(Logger.colors.stack_trace(`Wrote JSON report file to: ${path.resolve(filename)}`)); }); } } get isAvailable() { return this.eventFnExist; } set isAvailable(eventFnExist) { this.eventFnExist = eventFnExist; } set runner(type) { this.runnerType = type; } get runner() { return this.runnerType; } writeReportFile(filename, rendered, shouldCreateFolder, output_folder) { return (shouldCreateFolder ? createFolder(output_folder) : Promise.resolve()) .then(() => { return new Promise((resolve, reject) => { fs.writeFile(filename, rendered, function(err) { if (err) { return reject(err); } resolve(); }); }); }); } } const instance = new NightwatchEventHub(); module.exports = { NightwatchEventHub: instance, COMMON_EVENTS: { ScreenshotCreated: 'ScreenshotCreated' }, DEFAULT_RUNNER_EVENTS: { GlobalHook: { before: { started: 'GlobalBeforeStarted', finished: 'GlobalBeforeFinished' }, beforeChildProcess: { started: 'GlobalBeforeChildProcessStarted', finished: 'GlobalBeforeChildProcessFinished' }, beforeEach: { started: 'GlobalBeforeEachStarted', finished: 'GlobalBeforeEachFinished' }, afterEach: { started: 'GlobalAfterEachStarted', finished: 'GlobalAfterEachFinished' }, afterChildProcess: { started: 'GlobalAfterChildProcessStarted', finished: 'GlobalAfterChildProcessFinished' }, after: { started: 'GlobalAfterStarted', finished: 'GlobalAfterFinished' } }, TestSuiteHook: { started: 'TestSuiteStarted', finished: 'TestSuiteFinished', before: { started: 'BeforeStarted', finished: 'BeforeFinished' }, beforeEach: { started: 'BeforeEachStarted', finished: 'BeforeEachFinished' }, test: { started: 'TestRunStarted', finished: 'TestRunFinished' }, afterEach: { started: 'AfterEachStarted', finished: 'AfterEachFinished' }, after: { started: 'AfterStarted', finished: 'AfterFinished' } }, LogCreated: 'LogCreated' }, CUCUMBER_RUNNER_EVENTS: { TestStarted: 'TestStarted', TestFinished: 'TestFinished', TestCaseStarted: 'TestCaseStarted', TestCaseFinished: 'TestCaseFinished', TestStepStarted: 'TestStepStarted', TestStepFinished: 'TestStepFinished' } }; ================================================ FILE: lib/runner/folder-walk.js ================================================ const path = require('path'); const minimatch = require('minimatch'); const lodashCloneDeep = require('lodash/cloneDeep'); const Utils = require('../utils'); const FilenameMatcher = require('./matchers/filename.js'); const TagsMatcher = require('./matchers/tags.js'); class Results { constructor() { this.dataArray = []; } getData() { return this.dataArray; } register(sourcePath) { if (!Array.isArray(sourcePath)) { sourcePath = [sourcePath]; } sourcePath.filter(item => { return Utils.isFileNameValid(item); }).forEach(item => { if (this.dataArray.indexOf(item) === -1) { this.dataArray.push(item); } }); } } class Walker { constructor(testSource = [], settings, argv = {}) { if (Utils.isString(testSource)) { testSource = [testSource]; } this.testSource = testSource; this.settings = lodashCloneDeep(settings); this.argv = argv; this.usingMocha = this.settings.test_runner && this.settings.test_runner.type === 'mocha'; this.usingCucumber = this.settings.test_runner?.type === 'cucumber'; this.registerMatchers(); this.results = new Results(); if (!this.argv['launch-url']) { this.validateSource(); } } get promise() { return this.__promise; } get disableTs() { return this.settings.disable_typescript === true; } isComponentTestingMode() { return this.settings.globals.component_tests_mode; } validateSource() { if (this.testSource.length === 0) { const err = new Error('No test source specified, please check "src_folders" config'); err.showTrace = false; if (Array.isArray(this.settings.src_folders) && this.settings.src_folders.length > 0) { const srcFolders = this.settings.src_folders.map(item => `"${item}"`).join(', '); err.message += `; src_folders: ${srcFolders}`; } if (this.argv.group) { const groups = this.argv.group.map(item => `"${item}"`).join(', '); err.message += `; group(s): ${groups}`; } err.detailedErr = 'Run nightwatch with --help to display usage info.'; err.message += '.'; throw err; } } registerMatchers() { const {settings, argv} = this; this.tags = new TagsMatcher(settings); if (settings.exclude) { FilenameMatcher.addMatcher(FilenameMatcher.TYPE_EXCLUDE, {settings, argv}); } if (settings.filter) { FilenameMatcher.addMatcher(FilenameMatcher.TYPE_FILTER, {settings, argv}); } } /** * Apply filters on resolved file names * * @param {Array} list */ applyFilters(list) { if (this.usingCucumber) { return list; } return list.sort().filter(filePath => { let matched = true; if (this.settings.filter && !FilenameMatcher.register.filter.match(filePath)) { matched = false; } if (this.settings.exclude && FilenameMatcher.register.exclude.match(filePath)) { matched = false; } const filename = filePath.split(path.sep).slice(-1)[0]; if (this.settings.filename_filter) { matched = matched && minimatch(filename, this.settings.filename_filter); } return matched; }); } async applyTagFilter(list) { if (!Array.isArray(list)) { return; } if (!this.tags.anyTagsDefined() || this.usingCucumber) { return list; } const matches = await Promise.all(list.map(filePath => { return this.tags.loadModule(filePath); })); return matches.filter(context => { if (!context) { return false; } return this.tags.checkModuleTags(context); }).map(context => context.modulePath); } promiseFn(resolve, reject) { const sourcePath = this.modulePathsCopy.shift(); let fullPath = path.resolve(sourcePath); Utils.checkPath(fullPath) .catch(err => { if (err.code === 'ENOENT') { if (sourcePath.startsWith('examples/')) { // try the examples folder fullPath = path.join(__dirname, '../../', sourcePath); } else { fullPath = path.join(Utils.getConfigFolder(this.argv), sourcePath); } return Utils.checkPath(fullPath, err); } throw err; }) .then(stat => { if (stat && stat.isFile()) { return fullPath; } if (stat && !stat.isDirectory()) { return null; } return this.readFolderDeep(fullPath); }) .then(fullPath => { if (fullPath && Utils.isString(fullPath)) { fullPath = [fullPath]; } if (Array.isArray(fullPath)) { return this.applyFilters(fullPath); } }) .then(fullPath => { return this.applyTagFilter(fullPath); }) .then(fullPath => { if (fullPath) { this.results.register(fullPath); } if (this.modulePathsCopy.length === 0) { resolve(this.results.getData()); } else { this.promiseFn(resolve, reject); } }) .catch(function(err) { reject(err); }); } createPromise() { this.__promise = new Promise(this.promiseFn.bind(this)); return this; } readTestSource() { this.modulePathsCopy = this.testSource.slice(0); this.createPromise(); return this.promise; } readFolderDeep(folderPath) { return Utils.readDir(folderPath) .then(list => { if (list.length === 0) { return null; } const statPromises = list.map(resource => { resource = path.isAbsolute(resource) ? resource : path.join(folderPath, resource); return Utils.checkPath(resource); }); return Promise.all(statPromises) .then(statResults => { return statResults.reduce((prev, value, index) => { prev.push([list[index], value]); return prev; }, []); }); }) .then(promiseResults => { if (!promiseResults) { return null; } return Promise.all(promiseResults.map(item => { let resource = item[0]; const stat = item[1]; if (path.isAbsolute(resource)) { folderPath = path.dirname(resource); resource = path.basename(resource); } if (stat && stat.isDirectory()) { const dirName = path.basename(resource); const isExcluded = FilenameMatcher.isFolderExcluded(resource, this.settings); // prevent loading of files from an excluded folder const isSkipped = this.settings.skipgroup && this.settings.skipgroup.indexOf(dirName) > -1; if (isExcluded || isSkipped) { return null; } return this.readFolderDeep(path.join(folderPath, resource)); } if (this.disableTs && Utils.isTsFile(resource)) { return null; } if (stat && stat.isFile() && Utils.isFileNameValid(resource)) { return path.join(folderPath, resource); } return null; })); }).then(results => { if (!results) { return null; } return Utils.flattenArrayDeep(results, 3); }); } } module.exports = Walker; ================================================ FILE: lib/runner/matchers/filename.js ================================================ const path = require('path'); const fs = require('fs'); const minimatch = require('minimatch'); const MatchRegister = { exclude: {}, filter: {} }; class MatcherExclude { constructor({pattern, settings, argv}) { this.src_folders = settings.src_folders || []; this.argv = argv; this.filterPattern = this.adaptFilterPattern(pattern); } adaptFilterPattern(pattern) { if (!Array.isArray(pattern)) { pattern = [pattern]; } return pattern.map(item => { const pathResolved = path.resolve(item); let srcFolder; if (this.src_folders.length === 1) { // in case there is only one src_folder, check if the pattern is specified // relative to the src_folder srcFolder = this.src_folders[0]; } else if (this.src_folders.length === 0 && Array.isArray(this.argv._source) && (this.argv._source.length > 0)) { srcFolder = this.argv._source[0]; } if (srcFolder) { srcFolder = path.resolve(srcFolder); if (pathResolved.startsWith(srcFolder)) { return pathResolved; } return path.join(srcFolder, item); } return pathResolved; }); } match(fullPath) { return this.filterPattern.some(pattern => fullPath.includes(pattern) || minimatch(fullPath, pattern)); } } class FilenameMatcher { static get TYPE_EXCLUDE() { return 'exclude'; } static get TYPE_FILTER() { return 'filter'; } static create({type, pattern, settings, argv}) { switch (type) { case FilenameMatcher.TYPE_EXCLUDE: case FilenameMatcher.TYPE_FILTER: return new MatcherExclude({pattern, settings, argv}); } } static isFolderExcluded(resource, opts) { if (!opts.exclude) { return false; } return MatchRegister.exclude.filterPattern.some(function(item) { if (item.indexOf(resource) === -1) { return false; } try { // FIXME: make this async return fs.statSync(item).isDirectory(); // eslint-disable-next-line no-empty } catch (err) {} return false; }); } static addMatcher(type, {settings, argv}) { MatchRegister[type] = FilenameMatcher.create({type, pattern: settings[type], argv, settings}); } /** * @return {*} */ static get register() { return MatchRegister; } } module.exports = FilenameMatcher; ================================================ FILE: lib/runner/matchers/tags.js ================================================ const Utils = require('../../utils'); const {Context} = require('../../testsuite'); const {Logger} = Utils; class TagsMatcher { static get SEPARATOR() { return ','; } constructor(settings) { this.settings = settings; this.tagsToIncludeArrays = TagsMatcher.convertFilterTags(settings.tag_filter); this.tagsToSkipArray = TagsMatcher.convertTags(settings.skiptags); this.usingMocha = this.settings.test_runner && this.settings.test_runner.type === 'mocha'; } /** * @param {string} testFilePath - file path of a test * @returns {boolean} true if specified test matches given tag */ async match(testFilePath) { let context; try { context = await this.loadModule(testFilePath); } catch (e) { Logger.error(e); return false; } if (!context) { return false; } if (context.isDisabled()) { return false; } return this.checkModuleTags(context); } async loadModule(modulePath) { const {settings} = this; const context = new Context({modulePath, settings}); context.setReloadModuleCache(); // Defining global browser object to make it available before nightwatch client created. // To avoid errors like browser is not defined if testsuits has tags Object.defineProperty(global, 'browser', { configurable: true, get: function() { return {}; } }); try { if (this.usingMocha) { context.loadTags({usingMocha: true}); } else { await context.init(); } } catch (e) { Logger.error(e); return false; } if (context.isDisabled()) { return false; } return context; } /** * Verify if the current module contains or does not contain specific tags * @returns {boolean} */ checkModuleTags(context) { const tags = context.getTags(); const moduleTags = TagsMatcher.convertTags(tags); // if we passed the --tag argument, check if the module contains the tag (or tags) if (this.hasIncludeTagFilter() && !this.matchesIncludeTagFilter(moduleTags)) { return false; } // if we passed the --skiptags argument, check if the module doesn't contain any of the tags in the skiptags array if (this.hasSkipTagFilter() && !excludesAllTags(moduleTags, this.tagsToSkipArray)) { return false; } return true; } matchesIncludeTagFilter(moduleTags) { return this.tagsToIncludeArrays.some(tagsArray => containsAllTags(moduleTags, tagsArray)); } /** * @returns {boolean} */ hasIncludeTagFilter() { return this.tagsToIncludeArrays.length > 0; } /** * @returns {boolean} */ hasSkipTagFilter() { return this.tagsToSkipArray.length > 0; } /** * @returns {boolean} */ anyTagsDefined() { return this.hasIncludeTagFilter() || this.hasSkipTagFilter(); } /** * @param {String|Number|Array} tags * @return {Array} */ static convertTags(tags) { let tagsArray = Array.isArray(tags) ? tags : []; if (Utils.isString(tags) && tags.length > 0) { tagsArray = tags.split(TagsMatcher.SEPARATOR); } else if (Utils.isNumber(tags)) { tagsArray.push(tags); } // convert individual tags to strings return tagsArray.map(tag => String(tag).toLowerCase()); } /** * @param {Array|String} tags * @returns {*} */ static convertFilterTags(tags) { if (!tags) { return []; } // when multiple --tag arguments are passed (e.g.: --tag a --tag b) // the resulting array is [a, b], otherwise it is [a] const tagsArray = Array.isArray(tags) ? tags : [tags]; // now parse each --tag argument return tagsArray.map(t => TagsMatcher.convertTags(t)); } } /** * Whether a given list of tags contains one particular tag * * @param {Array} moduleTags * @param {String} testTag * @returns {boolean} */ const containsTag = function(moduleTags, testTag) { return moduleTags.includes(testTag); }; /** * Whether a given list of tags does not contain one particular tag * * @param {Array} moduleTags * @param {String} testTag * @returns {boolean} */ const excludesSingleTag = function(moduleTags, testTag) { return !containsTag(moduleTags, testTag); }; /** * Whether a given list of tags does not contain any tags in another list * * @param {Array} moduleTags * @param {Array} tagsArray * @returns {boolean} */ const excludesAllTags = function(moduleTags, tagsArray) { return moduleTags.every(testTag => excludesSingleTag(tagsArray, testTag)); }; /** * Whether a given list of tags contains aall tags in another list * * @param {Array} moduleTags * @param {Array} tagsArray * @returns {boolean} */ const containsAllTags = function(moduleTags, tagsArray) { return tagsArray.every(testTag => containsTag(moduleTags, testTag)); }; module.exports = TagsMatcher; ================================================ FILE: lib/runner/process-listener.js ================================================ const {Logger} = require('../utils'); const analyticsCollector = require('../utils/analytics.js'); module.exports = class { constructor(proc = process) { this.__exitCode = 0; this.testRunner = null; this.process = proc; this.finishCallback = null; this.addExitListener(); this.process.once('uncaughtException', err => { this.uncaught(err); }); this.process.on('unhandledRejection', this.unhandled.bind(this)); } addExitListener() { // when used programmatically, nightwatch will keep adding exit listeners for each test suite const listeners = this.process.listeners('exit'); const isAlreadyAdded = listeners.find(item => { return item.name.includes('exitHandlerNightwatch'); }); if (!isAlreadyAdded) { this.exitHandler = function exitHandlerNightwatch(code) { return this.onExit(code); }.bind(this); this.process.on('exit', this.exitHandler); } } setTestRunner(testRunner) { this.testRunner = testRunner; return this; } setExitCode(code) { this.__exitCode = code; return this; } get exitCode() { return this.__exitCode; } onExit(code) { this.process.exitCode = code || this.exitCode; } unhandled(err) { this.uncaught(err, {type: 'unhandledRejection'}); } getCurrentPromise(err) { if (this.testRunner) { const {currentSuite} = this.testRunner; if (currentSuite && (currentSuite.uncaughtError instanceof Error)) { Logger.error('An additional uncaught error occurred while trying to handle the previous one – ' + err.stack); return; } this.testRunner.registerUncaughtErr(err); if (currentSuite) { currentSuite.emptyQueue(); currentSuite.setUncaughtError(err); } if (this.testRunner.publishReport) { this.testRunner.publishReport = false; this.testRunner.reportResults().then(() => {}).catch(err => console.error(err)); } } } uncaught(err, {type = 'uncaughtException'} = {}) { Logger.setOutputEnabled(true); // force log for uncaught exception Logger.enable(); Logger.error(`${type}: ${err.message}\n${err.stack}`); analyticsCollector.collectErrorEvent(err, true); if (['TimeoutError', 'NoSuchElementError'].includes(err.name) && this.testRunner.type !== 'cucumber') { this.closeProcess(err); if (this.testRunner && this.testRunner.publishReport) { this.testRunner.publishReport = false; this.testRunner.reportResults().catch(() => {}).then(function() {}); } return; } this.getCurrentPromise(err); return this.closeProcess(err); } closeProcess(err) { if (this.finishCallback) { this.finishCallback(err); } this.setExitCode(1).exit(); } exit() { this.process.exit && this.process.exit(this.exitCode); return this; } }; ================================================ FILE: lib/runner/rerunUtil.js ================================================ const {fileExistsSync, Logger} = require('../utils'); const path = require('path'); function getRerunFailedFile(minimal_report_file_path) { const jsonFile = path.resolve(process.env.NIGHTWATCH_RERUN_REPORT_FILE || minimal_report_file_path || ''); if (!fileExistsSync(jsonFile)) { const err = new Error('Unable to find the Json reporter file to rerun failed tests'); err.showTrace = false; err.detailedErr = 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path'; err.help = [ `Try setting ${Logger.colors.cyan('minimal_report_file_path: "JSON-REPORTER-PATH"')} in nightwatch configuration`, `Or, try running: ${Logger.colors.cyan('export NIGHTWATCH_RERUN_REPORT_FILE="JSON-REPORTER-PATH"')}` ]; throw err; } return jsonFile; } function getTestSourceForRerunFailed(settings) { const {reporter_options: {minimal_report_file_path}} = settings; const minimalJsonFile = getRerunFailedFile(minimal_report_file_path); try { const {modules = {}} = require(minimalJsonFile); const testsource = []; Object.keys(modules).forEach(moduleKey => { if (modules[moduleKey] && modules[moduleKey].status === 'fail') { testsource.push(modules[moduleKey].modulePath); } }); if (testsource.length === 0) { const err = new Error('Rerun Failed Tests: No failed tests found to rerun.'); err.noFailedTestFound = true; err.showTrace = false; err.detailedErr = 'Run nightwatch with --help to display usage info.'; throw err; } return testsource; } catch (err) { if (err.noFailedTestFound) { err.message = 'Rerun Failed Tests: Invalid Json reporter.'; err.showTrace = false; err.detailedErr = 'Please set env variable NIGHTWATCH_RERUN_REPORT_FILE with valid Json reporter path.'; } throw err; } } module.exports = { getRerunFailedFile, getTestSourceForRerunFailed }; ================================================ FILE: lib/runner/runner.js ================================================ const TestSource = require('./test-source.js'); const Walker = require('./folder-walk.js'); class Runner { static get NIGHTWATCH_RUNNER() { return 'nightwatch'; } static get DEFAULT_RUNNER() { return 'default'; } static get MOCHA_RUNNER() { return 'mocha'; } static get CUCUMBER_RUNNER() { return 'cucumber'; } static createError(err) { if (err) { switch (err.code) { case 'ENOENT': { const error = new Error('An error occurred while trying to start the test runner:'); error.detailedErr = `[${err.code}] Cannot read source: ${err.syscall} ${err.path}.`; error.showTrace = false; error.displayed = false; throw error; } } return err; } return false; } static checkTestSource(modules, testSource, settings) { // TODO: refactor this into TestSource if (modules && modules.length === 0) { let errorMessage = ['No tests defined! using source folder:', testSource]; let err = new Error(errorMessage.join(' ')); let detailed = []; if (settings.tag_filter && settings.tag_filter.length) { detailed.push(`- using tags filter: ${settings.tag_filter}`); } if (settings.skiptags && settings.skiptags.length) { detailed.push(`- using skiptags filter: ${settings.skiptags}`); } if (settings.filter) { detailed.push(`- using path filter: ${settings.filter}`); } if (settings.exclude) { detailed.push(`- using exclude match: ${settings.exclude}`); } if (detailed.length) { err.detailedErr = detailed.join('\n'); } return err; } return true; } static getTestSource(settings, argv = {}) { const testSource = new TestSource(settings, argv); return new Walker(testSource.getSource(), settings, argv); } static readTestSource(settings, argv = {}) { const walker = Runner.getTestSource(settings, argv); if (argv['launch-url']) { return Promise.resolve([]); } return walker.readTestSource() .catch(err => { throw Runner.createError(err); }) .then(modules => { let error = Runner.checkTestSource(modules, walker.testSource, settings); if (error instanceof Error) { throw error; } return modules; }); } static create(settings, argv, addtOpts) { if (argv.mocha) { settings.test_runner = settings.test_runner || {}; settings.test_runner.type = 'mocha'; } switch (settings.test_runner.type) { case Runner.NIGHTWATCH_RUNNER: case Runner.DEFAULT_RUNNER: { const DefaultRunner = require('./test-runners/default.js'); return new DefaultRunner(settings, argv, addtOpts); } case Runner.MOCHA_RUNNER: { const MochaRunner = require('./test-runners/mocha.js'); return new MochaRunner(settings, argv, addtOpts); } case Runner.CUCUMBER_RUNNER: { const CucumberRunner = require('./test-runners/cucumber'); return new CucumberRunner(settings, argv, addtOpts); } } } } module.exports = Runner; ================================================ FILE: lib/runner/test-runners/cucumber/README.md ================================================ # Using Cucumber.js with Nightwatch 2 Nightwatch 2 brings integrated support for using [Cucumber.js](https://cucumber.io/) directly as an alternative test runner. No other plugins are necessary, other than the [Cucumber library](https://www.npmjs.com/package/@cucumber/cucumber) itself (version 7.3 or higher). Simply run the following in the same project where Nightwatch is also installed: ```sh $ npm i @cucumber/cucumber --save-dev ``` The [examples folder](https://github.com/nightwatchjs/nightwatch/tree/v2/examples/cucumber-js) contains a few example spec files and cucumber features, as well as configuration details. ================================================ FILE: lib/runner/test-runners/cucumber/_setup_cucumber_runner.js ================================================ const Nightwatch = require('../../../index.js'); const {Before, After, setDefaultTimeout} = require('@cucumber/cucumber'); setDefaultTimeout(-1); Before(function({pickle, testCaseStartedId}) { const webdriver = {}; process.env.CUCUMBER_TEST_CASE_STARTED_ID = testCaseStartedId; if (this.parameters['webdriver-host']) { webdriver.host = this.parameters['webdriver-host']; } if (this.parameters['webdriver-port']) { webdriver.port = this.parameters['webdriver-port']; } if (typeof this.parameters['start-process'] != 'undefined') { webdriver.start_process = this.parameters['start-process']; } let persist_globals; if (this.parameters['persist-globals']) { persist_globals = this.parameters['persist-globals']; } const globals = {}; if (this.parameters['retry-interval']) { globals.waitForConditionPollInterval = this.parameters['retry-interval']; } this.client = Nightwatch.createClient({ headless: this.parameters.headless, env: this.parameters.env, timeout: this.parameters.timeout, parallel: !!this.parameters.parallel, output: !this.parameters['disable-output'], enable_global_apis: true, silent: !this.parameters.verbose, always_async_commands: true, webdriver, persist_globals, config: this.parameters.config, test_settings: this.parameters.settings, globals }); if (this.client.settings.sync_test_names) { const {name} = pickle; this.client.updateCapabilities({ name }); } // eslint-disable-next-line console.log('\n'); const {options = {}} = this.client.settings.test_runner; // auto_start_session is true by default if (options.auto_start_session || typeof options.auto_start_session == 'undefined') { return this.client.launchBrowser().then(browser => { this.browser = browser; }); } }); After(async function(testCase) { //send test-case result to cloud provider const {result} = testCase; if (this.client && result) { const error = result.status === 'FAILED' ? new Error(result.message) : null; await this.client.transport.testSuiteFinished(error); } if (this.browser?.sessionId) { await this.browser.quit(); } }); ================================================ FILE: lib/runner/test-runners/cucumber/nightwatch-format.js ================================================ const Utils = require('../../../utils'); const {Logger, SafeJSON, isFunction} = Utils; const {Formatter} = require('@cucumber/cucumber'); const {NightwatchEventHub, CUCUMBER_RUNNER_EVENTS: { TestStarted, TestFinished, TestCaseStarted, TestCaseFinished, TestStepStarted, TestStepFinished }} = require('../../eventHub.js'); module.exports = class NightwatchFormatter extends Formatter { constructor(options) { super(options); this.report = {}; NightwatchFormatter.eventBroadcaster = options.eventBroadcaster; options.eventBroadcaster.on('envelope', (envelope) => { this.reportHandler(envelope); }); } static setCapabilities(data) { data = {...data, testCaseStartedId: process.env.CUCUMBER_TEST_CASE_STARTED_ID}; if (isFunction(process.send)) { process.send({ 'jsonEnvelope': SafeJSON.stringify({ session: { ...data, workerId: process.env.CUCUMBER_WORKER_ID } }), 'POST_SESSION_EVENT': SafeJSON.stringify({ session: { ...data, workerId: process.env.CUCUMBER_WORKER_ID } }) }); } else { NightwatchFormatter.eventBroadcaster?.emit('envelope', {session: data}); } } onSessionCapabilities(envelope) { this.report.session = this.report.session || {}; this.report.session[envelope.testCaseStartedId] = envelope; } onMeta(meta) { this.report.metadata = meta; } onGherkinDocument(gherkinDocument) { this.report.gherkinDocument = [...(this.report.gherkinDocument || []), gherkinDocument]; } onParseError(parseError) { this.report.error = [...(this.report.error || []), parseError]; } onPickle(pickle) { this.report.pickle = [...(this.report.pickle || []), pickle]; } onHook(hook) { this.report.hooks = [...(this.report.hooks || []), hook]; } onSource(source) { this.report.source = source; } onStepDefinition(stepDefinition) { this.report.stepDefinition = [...(this.report.stepDefinition || []), stepDefinition]; } onTestCase(testCase) { this.report.testCases = [...(this.report.testCases || []), testCase]; } onTestCaseFinished(result) { result.httpOutput = Logger.collectTestSectionOutput(); this.report.testCaseFinished = this.report.testCaseFinished || {}; this.report.testCaseFinished[result.testCaseStartedId] = result; NightwatchEventHub.emit(TestCaseFinished, { envelope: result, report: this.report }); } onTestCaseStarted(result) { this.report.testCaseStarted = this.report.testCaseStarted || {}; this.report.testCaseStarted[result.id] = result; NightwatchEventHub.emit(TestCaseStarted, { envelope: result, report: this.report }); } onTestRunFinished(result) { result.httpOutput = Logger.collectOutput(); this.report.testRunFinished = result; NightwatchEventHub.emit(TestFinished, { envelope: result, report: this.report }); } onTestRunStarted(result) { this.report.testRunStarted = result; NightwatchEventHub.emit(TestStarted, { envelope: result, report: this.report }); } onTestStepFinished(result) { result.httpOutput = Logger.collectCommandOutput(); this.report.testStepFinished = this.report.testStepFinished || {}; this.report.testStepFinished[result.testCaseStartedId] = result; NightwatchEventHub.emit(TestStepFinished, { envelope: result, report: this.report }); } onTestStepStarted(result) { this.report.testStepStarted = this.report.testStepStarted || {}; this.report.testStepStarted[result.testCaseStartedId] = result; NightwatchEventHub.emit(TestStepStarted, { envelope: result, report: this.report }); } reportHandler(envelope) { try { if (!NightwatchEventHub.isAvailable) { return; } const handlers = { meta: this.onMeta, gherkinDocument: this.onGherkinDocument, parseError: this.onParseError, pickle: this.onPickle, source: this.onSource, stepDefinition: this.onStepDefinition, testCase: this.onTestCase, hook: this.onHook, testCaseFinished: this.onTestCaseFinished, testCaseStarted: this.onTestCaseStarted, testRunFinished: this.onTestRunFinished, testRunStarted: this.onTestRunStarted, testStepFinished: this.onTestStepFinished, testStepStarted: this.onTestStepStarted, session: this.onSessionCapabilities }; const cucumberEvent = Object.keys(envelope)[0]; if (cucumberEvent && handlers[cucumberEvent]) { handlers[cucumberEvent].call(this, envelope[cucumberEvent]); } } catch (err) { Logger.error(err); } } }; ================================================ FILE: lib/runner/test-runners/cucumber.js ================================================ const path = require('path'); const Runner = require('./default'); const TestSuite = require('../../testsuite'); const {Logger, isString, isDefined} = require('../../utils'); const {NightwatchEventHub} = require('../eventHub.js'); const {getTestSourceForRerunFailed} = require('../rerunUtil.js'); const DefaultSettings = require('../../settings/defaults.js'); class CucumberSuite extends TestSuite { static isSessionCreateError(err) { return [ 'You appear to be executing an install of cucumber', 'Must be locally installed.' ].some(item => err.message.includes(item)); } static createCli(settings) { let CucumberCli; try { CucumberCli = require('@cucumber/cucumber/lib/cli/index').default; } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { const error = new Error('Cucumber needs to be installed as a project dependency.'); error.showTrace = false; error.detailedErr = 'You can install cucumber from NPM using:\n\n ' + Logger.colors.light_green('npm i @cucumber/cucumber'); throw error; } err.showTrace = false; err.sessionCreate = true; throw err; } return CucumberCli; } static get cucumberSetupFile() { return path.join(__dirname, './cucumber/_setup_cucumber_runner.js'); } constructor({modulePath, modules, settings, argv}) { super({modulePath, modules, settings, argv, addtOpts: { globalHooks: {} }}); // force all nightwatch commands to return promise this.settings.always_async_commands = true; const minWorkersCount = 2; const {options = {}} = this.settings.test_runner; if (this.argv.parallel) { const argvParallelCount = Number(this.argv.parallel); this.usingCucumberWorkers = Math.max(argvParallelCount, options.parallel || minWorkersCount); } this.reporter = { testSuiteFinished() {}, allTestsPassed: true }; try { this.createCucumberCli(); } catch (err) { const {message} = err; if (!err.detailedErr) { err.detailedErr = message; err.message = 'An error occurred while trying to initialize the Cucumber runner:'; } err.sessionCreate = true; Logger.error(err); throw err; } } createCucumberCli() { const CucumberCli = CucumberSuite.createCli(this.settings); const argv = this.getCliArgvForCucumber(); this.cucumberCli = new CucumberCli({ argv, env: process.env, cwd: process.cwd(), stdout: process.stdout }); } shouldUseNightwatchFormatter(options) { return (NightwatchEventHub.isAvailable || options.format === 'nightwatch-format' || this.argv.format === 'nightwatch-format' ); } getCliArgvForCucumber() { const specs = this.createInitialRequires(); const {options} = this.settings.test_runner; if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed'])) { options.feature_path = getTestSourceForRerunFailed(this.settings); } if (options.feature_path && !Array.isArray(options.feature_path)) { options.feature_path = [options.feature_path]; } if (this.shouldUseNightwatchFormatter(options)) { options.format = [...(options.format || []), path.join(__dirname, './cucumber/nightwatch-format.js')]; } const {feature_path = ''} = options; const parallelArgs = this.usingCucumberWorkers ? ['--parallel', this.usingCucumberWorkers] : []; const additionalOptions = this.buildArgvValue(['tags', 'retry-tag-filter', 'profile', 'format', 'format-options', 'dry-run', 'fail-fast', ['retry', 'retries'], 'no-strict', 'name']); const extraParams = ['--world-parameters', JSON.stringify({...this.argv, settings: this.settings})]; return [ process.execPath, require.resolve('@cucumber/cucumber') ].concat(feature_path, parallelArgs, specs, additionalOptions, extraParams); } createInitialRequires() { const {options} = this.settings.test_runner; const isESMEnable = options.enable_esm || this.argv['enable-esm']; const importTypeArgument = isESMEnable ? '--import' : '--require'; const initialRequires = [ importTypeArgument, CucumberSuite.cucumberSetupFile ]; if (isESMEnable){ initialRequires.push(...this.buildArgvValue(['import'])); } else { initialRequires.push(...this.buildArgvValue(['require', 'require-module'])); } return this.allModulePaths.reduce((prev, spec) => { prev.push(importTypeArgument, spec); return prev; }, initialRequires); } mergeCliConfigValues(key) { const {options = {}} = this.settings.test_runner; return Array.from(new Set([ ...(Array.isArray(this.argv[key]) ? this.argv[key] : (isString(this.argv[key]) ? [this.argv[key]] : [])), ...(Array.isArray(options[key]) ? options[key] : (isString(options[key]) ? [options[key]] : [])) ])); } buildArgvValue(argNames) { if (isString(argNames)) { argNames = [argNames]; } return argNames.reduce((prev, argName) => { let key = argName; if (Array.isArray(argName) && argName.length === 2) { key = argName[0]; argName = argName[1]; } const {options = {}} = this.settings.test_runner; const allArgv = { ...options, ...this.argv, require: this.mergeCliConfigValues('require'), requireModule: this.mergeCliConfigValues('requireModule'), format: this.mergeCliConfigValues('format'), paths: this.mergeCliConfigValues('paths'), import: this.mergeCliConfigValues('import') }; if ((isDefined(allArgv[argName]) && allArgv[argName] !== '') || (isDefined(allArgv[key]) && allArgv[key] !== '') ) { let argValues = allArgv[argName] || allArgv[key]; if (!Array.isArray(argValues)) { argValues = [argValues]; } argValues.forEach(value => { const args = [`--${key}`]; if (value !== true) { args.push(value); } prev.push(...args); }); } return prev; }, []); } onTestSuiteFinished(result) { const failures = result.success === false; return super.onTestSuiteFinished(failures); } testSuiteFinished() {} async runTestSuite() { let result; try { result = await this.cucumberCli.run(); } catch (err) { if (CucumberSuite.isSessionCreateError(err)) { const {message} = err; err.message = 'An error occurred while trying to start Cucumber CLI:'; err.detailedErr = message; err.sessionCreate = true; err.showTrace = false; Logger.error(err); throw err; } if (err.sessionCreate && !this.usingCucumberWorkers) { throw err; } if (!this.isAssertionError(err)) { Logger.error(err); throw err; } result = err; } return result; } } class CucumberRunnner extends Runner { get supportsConcurrency() { return true; } get supportsParallelTestSuiteRun() { return false; } get type() { return 'cucumber'; } constructor(settings, argv, addtOpts) { super(settings, argv, addtOpts); // Disable HTML Reporter as it is not yet supported in Cucumber. const reporterFile = this.globalReporter.reporterFile; if (reporterFile && reporterFile.includes('html')) { if (reporterFile.toString() !== DefaultSettings.default_reporter.toString()) { // user has specifically asked for HTML report. // eslint-disable-next-line no-console console.warn(Logger.colors.yellow('HTML reporter is not supported with Cucumber runner.')); } const index = reporterFile.indexOf('html'); if (index > -1) { reporterFile.splice(index, 1); } } } hasTestFailures(result) { return result && result.success === false; } createTestSuite({modulePath, modules}) { const {settings, argv, addtOpts} = this; return new CucumberSuite({modulePath, modules, settings, argv, addtOpts}); } async runTests(modules) { const modulePath = modules.slice(0).shift(); this.currentSuite = this.createTestSuite({modulePath, modules}); return await this.currentSuite.runTestSuite(); } } module.exports = CucumberRunnner; ================================================ FILE: lib/runner/test-runners/default.js ================================================ const {GlobalReporter} = require('../../reporter'); const TestSuite = require('../../testsuite'); const Concurrency = require('../concurrency'); const {Logger} = require('../../utils'); class DefaultRunner { get supportsConcurrency() { return true; } get type() { return 'nightwatch'; } constructor(settings, argv, addtOpts) { this.startTime = new Date().getTime(); this.settings = settings; this.argv = argv; this.addtOpts = addtOpts; this.publishReport = true; // in-case of an uncaught exception, the report will not be published this.globalReporter = new GlobalReporter(argv.reporter, settings, { openReport: argv.open, reportFileName: argv['report-filename'] }); } get client() { return this.currentSuite && this.currentSuite.client; } get results() { return this.globalReporter.globalResults; } hasTestFailures() { return this.globalReporter.hasTestFailures(); } registerUncaughtErr(err) { this.globalReporter.registerUncaughtErr(err); } /** * @param {Error} [err] * @return {Promise} */ closeOpenSessions(err) { const inspectorServer = this.addtOpts?.globalsInstance?.inspectorServer; if (inspectorServer) { inspectorServer.closeSocket(); } if (this.client?.sessionId && this.client?.startSessionEnabled) { Logger.info(`Attempting to close session ${this.client.sessionId}...`); const failures = !this.currentSuite.reporter.allTestsPassed; return this.currentSuite.terminate(failures ? 'FAILED' : '', null, true); } return Promise.resolve(); } async createTestSuite({modulePath, modules}) { const {settings, argv, addtOpts} = this; const testSuite = new TestSuite({modulePath, modules, settings, argv, addtOpts}); await testSuite.init(); return testSuite; } async runTestSuite(modulePath, modules) { try { this.currentSuite = await this.createTestSuite({modulePath, modules}); } catch (err) { const Runner = require('../runner.js'); throw Runner.createError(err); } let possibleErr; try { await this.currentSuite.run(); } catch (err) { possibleErr = err; } if (!this.argv['launch-url']) { this.globalReporter.addTestSuiteResults(this.currentSuite.reporter.exportResults()); } if (possibleErr instanceof Error) { throw possibleErr; } } async promiseFn(resolve, reject) { const sourcePath = this.modulePathsCopy.shift(); try { await this.runTestSuite(sourcePath, this.fullPaths); if (this.modulePathsCopy.length === 0) { resolve(); } else { await this.promiseFn(resolve, reject); } } catch (err) { reject(err); } } printGlobalResults() { this.globalReporter.create(this.startTime).print(); return this; } /** * @param {Array} modules * @return {Promise} */ runTests(modules) { this.modulePathsCopy = modules.slice(0); this.fullPaths = modules; return new Promise(this.promiseFn.bind(this)); } /** * @return {Promise} */ async reportResults() { if (!this.isTestWorker()) { this.printGlobalResults(); await this.globalReporter.save(); } } /** * * @param {Array} testEnvArray * @param {Array} modules * @return {Promise} */ async runConcurrent(testEnvArray, modules, isTestWorkerEnabled, isSafariEnvPresent) { this.concurrency = new Concurrency(this.settings, this.argv, isTestWorkerEnabled, isSafariEnvPresent); this.globalReporter.setupChildProcessListener(this.concurrency); const exitCode = await this.concurrency.runMultiple(testEnvArray, modules); await this.reportResults(); return exitCode; } isTestWorker() { return Concurrency.isTestWorker(this.argv); } /** * Main entry-point of the runner * * @return {Promise} */ async run(modules) { let possibleErr; let result; try { result = await this.runTests(modules); } catch (err) { if (this.modulePathsCopy && this.modulePathsCopy.length > 0) { this.globalReporter.skippedSuites = this.modulePathsCopy.length; } possibleErr = err; } await this.closeOpenSessions(); if (this.publishReport) { await this.reportResults(); } if (possibleErr) { throw possibleErr; } return this.hasTestFailures(result); } } module.exports = DefaultRunner; ================================================ FILE: lib/runner/test-runners/mocha/custom-runnable.js ================================================ module.exports = async function(fn, isHook = false) { const self = this; const start = new Date(); const ctx = this.ctx; let finished; if (this.isPending()) { return fn(); } let emitted; if (!isHook) { this._enableTimeouts = false; } // Sometimes the ctx exists, but it is not runnable if (ctx && ctx.runnable) { ctx.runnable(this); } // called multiple times function multiple(err) { if (emitted) { return; } emitted = true; const msg = 'done() called multiple times'; if (err && err.message) { err.message += ` (and Mocha's ${msg})`; self.emit('error', err); } else { self.emit('error', new Error(msg)); } } // finished function done(err) { const ms = self.timeout(); if (self.timedOut) { return; } if (finished) { return multiple(err); } self.clearTimeout(); self.duration = new Date() - start; finished = true; if (!err && self.duration > ms && self._enableTimeouts) { err = new Error('Timeout of ' + ms + 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'); } fn(err); } this.callback = done; this.resetTimeout(); try { const args = [this.parent.client.api]; let userCalled = false; const onFinished = function(err) { if (err instanceof Error) { return done(err); } if (err) { if (Object.prototype.toString.call(err) === '[object Object]') { return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err))); } return done(new Error('done() invoked with non-Error: ' + err)); } done(); }; const {nightwatchSuite} = this.parent; let err = await nightwatchSuite.handleRunnable(this.title, () => { this.parent.nightwatchSuite.reporter.testResults.resetLastError(); if (isHook && this.async === 2) { return new Promise((resolve, reject) => { args.push(function(err) { userCalled = true; setTimeout(function() { if (err instanceof Error) { reject(err); } else { resolve(); } onFinished(err); }, 10); }); this.fn.apply(ctx, args); }); } const result = this.fn.apply(ctx, args); if (result instanceof Promise) { return result; } }); if (err && !(err instanceof Error)) { err = undefined; } if (this.async <= 1) { const {lastError} = this.parent.nightwatchSuite.reporter.testResults; if (!err && lastError) { err = lastError; } done(err); emitted = true; } } catch (err) { done(err); emitted = true; } }; ================================================ FILE: lib/runner/test-runners/mocha/custom-runner.js ================================================ const Mocha = require('mocha'); const {adaptRunnables} = require('./extensions.js'); module.exports = class CustomRunner extends Mocha.Runner { async runSuite(suite, fn) { const isMainSuite = suite.parent && suite.parent.root; const createSession = () => { return new Promise(function(resolve, reject) { if (isMainSuite) { try { if (suite.file) { suite.nightwatchSuite.setModulePath(suite.file); } suite.nightwatchSuite .updateClient() .setupHooks() .startTestSuite() .then(data => resolve(data)) .catch(err => reject(err)); } catch (err) { reject(err); } } else { if (!suite.root && suite.parent.nightwatchSuite) { suite.nightwatchSuite.createClient(suite.parent.nightwatchSuite.client); suite.nightwatchSuite.reporter = suite.parent.nightwatchSuite.reporter; } resolve(false); } }); }; const onSuiteFinished = (err) => { if (!suite.nightwatchSuite) { return fn(err); } suite.nightwatchSuite.onTestSuiteFinished().then(failures => { fn(err); }).catch(suiteError => { console.error(suiteError); suiteError = suiteError || err; fn(suiteError); }); }; try { if (suite.disabled) { // eslint-disable-next-line no-console console.log(`Testsuite "${suite.title}" is disabled, skipping...`); return fn(); } if (suite['@nightwatch_promise']) { await suite['@nightwatch_promise'](); } // create the nightwatch session const sessionInfo = await createSession(); } catch (err) { // an error occurred while trying to create the session this.failures = err; return onSuiteFinished(err); } // run the mocha suite return super.runSuite(suite, onSuiteFinished); } run(fn) { adaptRunnables(this.suite); super.run(fn); } }; ================================================ FILE: lib/runner/test-runners/mocha/extensions.js ================================================ const Utils = require('../../../utils'); const customRunnable = require('./custom-runnable.js'); const nightwatchSuiteUtils = require('./nightwatchSuite.js'); const {Logger} = Utils; module.exports = class Extensions { static adaptRunnables(parent) { Extensions.adaptHooks(parent, ['_afterAll', '_afterEach', '_beforeAll', '_beforeEach']); parent.tests = parent.tests.map(function(test) { test.run = function(...args) { // eslint-disable-next-line no-console console.log(`\n Running ${Logger.colors.green(this.title)}${Logger.colors.stack_trace('...')}`); this.parent.client.isES6AsyncTestcase = Utils.isES6AsyncFn(this.fn); delete this.parent.client.isES6AsyncTestHook; return customRunnable.apply(this, args); }.bind(test); return test; }); if (parent.suites && parent.suites.length > 0) { parent.suites.forEach(function(item) { return (function(_item) { Extensions.adaptRunnables(_item); })(item); }); } } static adaptHooks(suite, hooks) { hooks.forEach(function(hook) { suite[hook].forEach(hookInstance => { const originalRunFn = hookInstance.run; hookInstance.run = function(fn) { const isAsync = Utils.isES6AsyncFn(this.fn); //console.log(` Running ${Logger.colors.stack_trace(this.title + ':')}`); this.parent.client.isES6AsyncTestcase = isAsync; if (this.fn.length === 0 && isAsync) { return originalRunFn.call(this, fn); } return customRunnable.call(this, fn, true); }.bind(hookInstance); }); }); } static augmentTestSuite({suite, runner, argv, settings, addtOpts}) { const attributes = [ 'tags', 'desiredCapabilities', 'endSessionOnFail', 'skipTestcasesOnFail', 'unitTest', 'disabled' ]; Object.defineProperties(suite, attributes.reduce((prev, attribute) => { prev[attribute] = { configurable: true, set: function(value) { this.nightwatchSuite.mochaContext.then(context => { context.setAttribute(`@${attribute}`, value); }); }, get: function() { if (!this.nightwatchSuite || !this.nightwatchSuite.context) { return null; } return this.nightwatchSuite.context.getAttribute(`@${attribute}`); } }; return prev; }, {})); const suiteIndex = Math.max(0, runner.suite.suites.length - 1); const nightwatchSuite = nightwatchSuiteUtils.create({ runner, suite, settings, argv, addtOpts, modulePath: runner.files[suiteIndex] }); suite['@nightwatch_promise'] = function() { return nightwatchSuite.init({suite, nightwatchSuite}); }; if (Utils.isUndefined(suite.timeout())) { suite.timeout(30000); } } }; ================================================ FILE: lib/runner/test-runners/mocha/nightwatchSuite.js ================================================ const TestSuite = require('../../../testsuite'); module.exports.create = function ({suite, settings, argv, modulePath, addtOpts, runner}) { const modules = []; const timeoutFn = suite.timeout; const nightwatchSuite = new TestSuite({ modules, settings, argv, usingMocha: true, addtOpts, modulePath }); Object.defineProperties(suite, { nightwatchSuite: { configurable: true, get: function() { return nightwatchSuite; } }, client: { configurable: true, get: function() { return this.nightwatchSuite.client; } }, isWorker: { configurable: true, get: function() { return runner.isWorker; } }, files: { configurable: true, get: function() { return runner.files; } }, mochaOptions: { configurable: true, get: function() { return runner.options; } } }); Object.defineProperties(suite, { globals: { get: function() { return this.nightwatchSuite.settings.globals; } }, settings: { get: function() { return this.nightwatchSuite.settings; } }, argv: { get: function() { return this.nightwatchSuite.argv; } }, suiteRetries: { value: function(value) { if (typeof value != 'undefined') { this.nightwatchSuite.mochaContext.then(context => { context.setSuiteRetries(value); }); } } }, waitForTimeout: { value: function(value) { if (typeof value == 'undefined') { return this.globals.waitForConditionTimeout; } this.globals.waitForConditionTimeout = value; this.globals.retryAssertionTimeout = value; } }, timeout: { value: function(value) { if (typeof value == 'undefined') { return timeoutFn.call(this); } this.globals.unitTestsTimeout = value; timeoutFn.call(this, value); } }, waitForRetryInterval: { value: function(value) { if (typeof value == 'undefined') { return this.globals.waitForConditionPollInterval; } this.globals.waitForConditionPollInterval = value; } } }); return nightwatchSuite; }; module.exports.init = function({suite, nightwatchSuite}) { return nightwatchSuite.initCommon({ suiteTitle: suite.title }).catch(err => { console.error(err); process.exit(1); }); }; ================================================ FILE: lib/runner/test-runners/mocha.js ================================================ const path = require('path'); const Concurrency = require('../concurrency'); const Utils = require('../../utils'); const {Logger} = Utils; const {augmentTestSuite} = require('./mocha/extensions.js'); class MochaRunner { static MochaNightwatch({argv, settings, addtOpts}) { const Mocha = require('mocha'); const CustomRunner = require('./mocha/custom-runner.js'); return class MochaNightwatch extends Mocha { constructor(mochaOpts, nightwatchSettings) { super(mochaOpts); this.nightwatchSettings = nightwatchSettings; this._runnerClass = CustomRunner; const runner = this; this.suite.on('suite', function (suite) { suite.on('suite', function (childSuite) { augmentTestSuite({suite: childSuite, runner, argv, settings, addtOpts}); }); augmentTestSuite({suite, runner, argv, settings, addtOpts}); }); } }; } get supportsConcurrency() { return true; } get combinedReporterList() { return ['mochawesome', 'html']; } get type() { return 'mocha'; } constructor(settings, argv, addtOpts = {}) { this.startTime = new Date().getTime(); this.settings = settings; this.argvOpts = argv; this.publishReport = false; this.argv = argv; this.processListener = addtOpts.processListener; this.mochaOpts = settings.test_runner.options || {}; if (Utils.isUndefined(this.mochaOpts.timeout)) { this.mochaOpts.timeout = settings.globals.asyncHookTimeout; } if (this.settings.disable_colors) { this.mochaOpts.color = false; } if (this.mochaOpts.reporter === 'junit') { this.mochaOpts.reporter = 'mocha-junit-reporter'; } if (argv.reporter.includes('mochawesome')) { this.mochaOpts.reporter = 'mochawesome'; } else if (argv.reporter.includes('mocha-junit-reporter')) { try { require('mocha-junit-reporter'); } catch (err) { const error = new Error('Nightwatch needs the mocha-junit-reporter package for when using Mocha as a test runner.'); error.detailedErr = 'To install it, please run:\n npm install mocha-junit-reporter --save-dev'; error.showTrace = false; error.displayed = false; throw error; } this.mochaOpts.reporter = 'mocha-junit-reporter'; } if (this.mochaOpts.reporter === 'mochawesome') { try { require('mochawesome'); } catch (err) { const error = new Error('Nightwatch needs the mochawesome package for when using Mocha as a test runner.'); error.detailedErr = 'To install it, please run:\n npm install mochawesome --save-dev'; error.showTrace = false; error.displayed = false; throw error; } } if (argv.grep) { this.mochaOpts.grep = argv.grep; } if (argv['fail-fast']) { this.mochaOpts.bail = true; } if (argv.retries) { this.mochaOpts.retries = argv.retries; } if (argv.fgrep) { this.mochaOpts.fgrep = argv.fgrep; } if (argv.invert) { this.mochaOpts.invert = argv.invert; } this.mochaOpts.reporterOptions = this.mochaOpts.reporterOptions || {}; if (argv.reporter.includes('mocha-junit-reporter')) { this.mochaOpts.reporterOptions.mochaFile = path.join(settings.output_folder, 'test-results.xml'); } if (this.isTestWorker()) { const filePath = this.argv.test; const reportFilename = filePath.substring(filePath.lastIndexOf('/') + 1).split('.')[0]; this.mochaOpts.isWorker = true; if (argv.reporter.includes('mocha-junit-reporter')) { this.mochaOpts.reporterOptions.mochaFile = path.join(settings.output_folder, `${reportFilename}.xml`); } else if (argv.reporter.includes('mochawesome')) { this.mochaOpts.reporterOptions = Object.assign(this.mochaOpts.reporterOptions, { html: false, json: true, quiet: true, reportFilename }); } } if (this.mochaOpts.reporter === 'mochawesome' && !this.mochaOpts.reporterOptions.reportDir) { this.mochaOpts.reporterOptions.reportDir = settings.reportDir || 'html-report'; } const MochaNightwatch = MochaRunner.MochaNightwatch({settings, argv, addtOpts}); this.mocha = new MochaNightwatch(this.mochaOpts, settings); } async runConcurrent(testEnvArray, modules) { this.checkReporterDependencies(); this.concurrency = new Concurrency(this.settings, this.argv); const exitCode = await this.concurrency.runMultiple(testEnvArray, modules); await this.generateCombinedReport(); return exitCode; } checkReporterDependencies() { if (!this.argv.reporter.includes('mochawesome')) { return; } try { require('mochawesome-report-generator'); require('mochawesome-merge'); } catch (err) { const error = new Error('Nightwatch needs some more packages when running in parallel and using the mochawesome reporter:'); error.detailedErr = 'To install dependencies, please run:\n npm install mochawesome-report-generator mochawesome-merge --save-dev'; throw error; } } async generateCombinedReport() { if (!this.argv.reporter.includes('mochawesome')) { return; } const {reportDir} = this.mochaOpts.reporterOptions; const marge = require('mochawesome-report-generator'); const {merge} = require('mochawesome-merge'); const files = reportDir ? `${path.join(reportDir, '*.json')}` : './mochawesome-report/*.json'; const report = await merge({ files: [files] }); return marge.create(report, {}); } isTestWorker() { return this.argv['test-worker']; } closeOpenSessions() { const mochaSuite = this.mocha.suite; const client = mochaSuite && mochaSuite.client; if (client && client.sessionId) { const request = client.transport.createHttpRequest({ path: `/session/${client.sessionId}` }); return new Promise(function(resolve, reject) { request.delete() .on('error', function(err) { resolve(); }) .on('success', function() { resolve(); }); }); } return Promise.resolve(); } registerUncaughtErr(err) { } /** * Main entry-point of the runner * * @return {Promise} */ run(modules) { modules.forEach(module => this.mocha.addFile(module)); return new Promise((resolve, reject) => { this.mocha.run(failures => { if ((failures instanceof Error) && failures.sessionCreate) { Logger.error(failures); process.exit(1); } this.closeOpenSessions() .then(_ => { if (this.mocha.suite && this.mocha.suite.client) { return this.mocha.suite.client.transport.sessionFinished(); } }) .catch(err => { console.error(err.stack); }) .then(_ => { if (failures) { let err; if (failures instanceof Error) { err = failures; } else { err = new Error('Mocha reported test failures.'); err.failures = failures; } return reject(err); } resolve(); }); }); }); } } module.exports = MochaRunner; ================================================ FILE: lib/runner/test-source.js ================================================ const path = require('path'); const fs = require('fs'); const minimatch = require('minimatch'); const Utils = require('../utils'); const Concurrency = require('./concurrency'); const {Logger, validExtensions, jsFileExt, singleSourceFile} = Utils; const {getTestSourceForRerunFailed} = require('./rerunUtil'); class TestSource { static get GroupNameDelimiter() { return ','; } constructor(settings, argv = {}) { this.argv = argv; this.settings = settings; this.src_folders = settings.src_folders || []; } getTestSourceForSingle(targetPath) { let testsource; if (Array.isArray(targetPath)) { targetPath = targetPath[0]; } if (targetPath && path.resolve(targetPath) === targetPath) { testsource = targetPath; } else { testsource = path.join(process.cwd(), targetPath); } const fileExt = path.parse(testsource).ext; if (!validExtensions.includes(fileExt)) { const fileExists = fs.existsSync(testsource); if (fileExists) { const err = new Error(`Cannot load file ${testsource}`); err.detailedErr = `Files with ${fileExt} extension are not supported by Nightwatch.`; err.showTrace = false; throw err; } testsource += jsFileExt; } return testsource; } getRerunFailedFile(minimal_report_file_path) { const jsonFile = path.resolve(process.env.NIGHTWATCH_RERUN_REPORT_FILE || minimal_report_file_path || ''); if (!Utils.fileExistsSync(jsonFile)) { const err = new Error('Unable to find the Json reporter file to rerun failed tests'); err.showTrace = false; err.detailedErr = 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path'; err.help = [ `Try setting ${Logger.colors.cyan('minimal_report_file_path: "JSON-REPORTER-PATH"')} in nightwatch configuration`, `Or, try running: ${Logger.colors.cyan('export NIGHTWATCH_RERUN_REPORT_FILE="JSON-REPORTER-PATH"')}` ]; throw err; } return jsonFile; } getTestSourceForRerunFailed() { const {reporter_options: {minimal_report_file_path}} = this.settings; const minimalJsonFile = this.getRerunFailedFile(minimal_report_file_path); try { const {modules = {}} = require(minimalJsonFile); const testsource = []; Object.keys(modules).forEach(moduleKey => { if (modules[moduleKey] && modules[moduleKey].status === 'fail') { testsource.push(modules[moduleKey].modulePath); } }); if (testsource.length === 0) { const err = new Error('Rerun Failed Tests: No failed tests found to rerun.'); err.noFailedTestFound = true; err.showTrace = false; err.detailedErr = 'Run nightwatch with --help to display usage info.'; throw err; } return testsource; } catch (err) { if (err.noFailedTestFound) { err.message = 'Rerun Failed Tests: Invalid Json reporter.'; err.showTrace = false; err.detailedErr = 'Please set env variable NIGHTWATCH_RERUN_REPORT_FILE with valid Json reporter path.'; } throw err; } } /** * Returns the path where the tests are located * @returns {*} */ getSource() { const {test_runner} = this.settings; if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed']) && Concurrency.isMasterProcess() && test_runner?.type !== 'cucumber') { return getTestSourceForRerunFailed(this.settings); } if (this.src_folders.length === 0 && test_runner?.src_folders && test_runner?.type === 'cucumber') { this.src_folders = test_runner?.src_folders; } if (this.argv['test-worker'] || singleSourceFile(this.argv)) { return this.getTestSourceForSingle(this.argv.test || this.argv._source); } if (this.argv.testcase) { this.argv.testcase = null; Logger.warn('Option --testcase used without --test is ignored.'); } if (Array.isArray(this.argv._source) && (this.argv._source.length > 0)) { return this.argv._source; } if (this.argv.group) { // add support for multiple groups if (Utils.isString(this.argv.group)) { this.argv.group = this.argv.group.split(TestSource.GroupNameDelimiter); } const groupTestsource = this.findGroupPathMultiple(this.argv.group); // If a group does not exist in the multiple src folder case, it is removed // from the test path list. if (this.src_folders.length === 1) { return groupTestsource; } // only when all groups fail to match will there be a run error const testsource = groupTestsource.filter(Utils.dirExistsSync); if (!this.settings.silent) { const ignoredSource = groupTestsource.filter(entry => testsource.indexOf(entry) === -1); if (ignoredSource.length) { Logger.warn('The following group paths were not found and will be excluded from the run:\n - ' + ignoredSource.join('\n - ')); } } return testsource; } this.applySrcFilters(); return this.src_folders; } /** * Apply "exclude" patterns on src_folders, in case more than one was specified */ applySrcFilters() { if (this.src_folders.length < 2) { return this; } if (this.settings.exclude) { const arrExclude = Array.isArray(this.settings.exclude) ? this.settings.exclude : [this.settings.exclude]; const resolvedExclude = arrExclude.map(item => path.resolve(item)); this.src_folders = this.src_folders.filter(item => { const resolvedPath = path.resolve(item); let match = true; resolvedExclude.forEach(function(pattern) { match = !resolvedPath.includes(pattern) && !minimatch(resolvedPath, pattern); }); return match; }); } } /** * Gets test paths from each of the src folders for a single group. * * @param {string} groupName * @return {Array} */ findGroupPath(groupName) { const fullGroupPath = path.resolve(groupName); // for each src folder, append the group to the path // to resolve the full test path return this.src_folders.map(function(srcFolder) { const fullSrcFolder = path.resolve(srcFolder); if (fullGroupPath.indexOf(fullSrcFolder) === 0) { return groupName; } return path.join(srcFolder, groupName); }); } /** * Gets test paths for tests from any number of groups. * * @param {Array} groups */ findGroupPathMultiple(groups) { let paths = []; groups.forEach(groupName => { paths = paths.concat(this.findGroupPath(groupName)); }); return paths; } } module.exports = TestSource; ================================================ FILE: lib/settings/defaults.js ================================================ const path = require('path'); const uuid = require('uuid'); const filename_format = function ({testSuite = '', testCase = '', isError = false, dateObject = new Date()} = {}) { const fileName = []; const dateParts = dateObject.toString().replace(/:/g, '').split(' '); dateParts.shift(); const dateStamp = dateParts.slice(0, 5).join('-'); if (testSuite) { fileName.push(testSuite); } if (testCase) { fileName.push(testCase); } return `${fileName.join(path.sep)}${isError ? '_ERROR' : '_FAILED'}_${dateStamp}.png`; }; const snapshot_file_format = function({testSuite = '', testCase = '', commandName, dateObject = new Date()} = {}) { const fileName = []; const dateParts = dateObject.toString().replace(/:/g, '').split(' '); dateParts.shift(); const dateStamp = dateParts.slice(0, 5).join('-'); if (testSuite) { fileName.push(testSuite); } if (testCase) { fileName.push(testCase); } if (commandName) { fileName.push(commandName); } return `${fileName.join(path.sep)}_${dateStamp}.html`; }; const client_id = uuid.v4(); module.exports = { // Location(s) where custom commands will be loaded from. custom_commands_path: null, // Location(s) where custom assertions will be loaded from. custom_assertions_path: null, // Location(s) where page object files will be loaded from. page_objects_path: null, // An array specifying a list of Nightwatch plugin names that should be used; e.g.: plugins: ['vite-plugin-nightwatch'] plugins: [], // Location of an external globals module which will be loaded and made available to the test as a // property globals on the main client instance. globals_path: null, // Location of the existing TypeScript config file. If not provided Nightwatch will search for "nightwatch/tsconfig.json" or "tsconfig.nightwatch.json". tsconfig_path: '', // An object which will be made available on the main test api, throughout the test execution globals: { // this controls whether to abort the test execution when an assertion failed and skip the rest // it's being used in waitFor commands and expect assertions abortOnAssertionFailure: true, // this controls whether to abort the test execution when an element cannot be located; an error // is logged in all cases, but this also enables skipping the rest of the testcase; // it's being used in element commands such as .click() or .getText() abortOnElementLocateError: false, // this will overwrite the default polling interval (currently 500ms) for waitFor commands // and expect assertions that use retry waitForConditionPollInterval: 500, // default timeout value in milliseconds for waitFor commands and implicit waitFor value for // expect assertions waitForConditionTimeout: 5000, // this will cause waitFor commands on elements to throw an error if multiple // elements are found using the given locate strategy and selector throwOnMultipleElementsReturned: false, // Since v2, the warning if multiple elements are found using the given locate strategy // and selector is not shown by default anymore; set this to false to enable those warnings suppressWarningsOnMultipleElementsReturned: true, // controls the timeout value for async hooks. Expects the done() callback to be invoked within this time // or an error is thrown asyncHookTimeout: 20000, // controls the timeout value for when running async unit tests. Expects the done() callback to be invoked within this time // or an error is thrown unitTestsTimeout: 2000, // controls the timeout value for when executing the global async reporter. Expects the done() callback to be invoked within this time // or an error is thrown customReporterCallbackTimeout: 20000, // Automatically retrying failed assertions - You can tell Nightwatch to automatically retry failed assertions until a given timeout is reached, before the test runner gives up and fails the test. retryAssertionTimeout: 5000, // use the same browser session to run the individual test suites reuseBrowserSession: false, reporter: function(results, cb) {cb(results)}, beforeTestSuite(browser) { return Promise.resolve(); }, afterTestSuite(browser) { return Promise.resolve(); }, beforeTestCase(browser) { return Promise.resolve(); }, afterTestCase(browser) { return Promise.resolve(); }, onBrowserNavigate(browser) { return Promise.resolve(); }, onBrowserQuit(browser) { return Promise.resolve(); } }, // configuration settings for the dotenv module - a zero-dependency module that loads environment variables from a .env file into process.env. More details on https://www.npmjs.com/package/dotenv dotenv: {}, // persist the same globals object between runs or have a (deep) copy of it per each test; // this can be useful when persisting data between test suites is needed, such as a cookie or session information persist_globals: false, reporter_options: { // The location where the JUnit XML and HTML report files will be saved. Set this to false if you want to disable XML reporting output_folder: 'tests_output', //The folder formatting to use while saving HTML report folder_format: null, // The file name formatting to use while saving HTML report filename_format: null, minimal_report_file_path: 'tests_output/minimal_report.json', // Save command result values in Json report (inline or in separate file). save_command_result_value: false }, // A string or array of folders (excluding subfolders) where the tests are located. src_folders: null, // Used when running in parallel to determine if the output should be collected and displayed at the end. live_output: false, // disable support of loading of typescript files for backwards compatibility with test suites disable_typescript: false, // Used to disable colored output in the terminal. disable_colors: false, // Used when running in parallel to specify the delay (in milliseconds) between starting the child processes parallel_process_delay: 10, // An object containing Selenium Server related configuration options selenium: { start_process: false, cli_args: {}, server_path: null, log_path: './logs', port: undefined, check_process_delay: 500, max_status_poll_tries: 15, status_poll_interval: 200 }, // Whether or not to automatically start the Selenium/WebDriver session. If running unit tests, this should be set tot false. start_session: true, // End the session automatically when the test is being terminated, usually after a failed assertion. end_session_on_fail: true, // Skip the remaining test cases from the current test suite, when one test case fails. skip_testcases_on_fail: undefined, // Whether or not to run individual test files in parallel. test_workers: false, /* test_workers: { enabled: true, // automatically compute the number of workers based on CPU cores workers: 'auto', // manually specify the number of workers workers: 4, // pass node arguments to individual workers (all of the process.execArgv) node_options: 'auto', // selectively pass node arguments to individual worker processes node_options: ['--inspect'] }, */ // Specifies which test runner to use: default|mocha test_runner: 'default', // Specifies which implementation to use for Concurrency: child-process|worker-threads use_child_process: false, // Defines options used to connect to the WebDriver/Selenium server webdriver: { start_process: false, cli_args: {}, server_path: null, log_path: './logs', // leave empty to use the test suite name when writing the webdriver server logs log_file_name: '', // Time to wait (in ms) before starting to check the Webdriver server is up and running check_process_delay: 100, // Maximum number of ping status check attempts before returning a timeout error max_status_poll_tries: 10, // Interval (in ms) to use between status ping checks when checking if the Webdriver server is up and running status_poll_interval: 200, // The entire time (in ms) to wait for the Node.js process to be created and running (default is 2 min), including spawning the child process and checking the status process_create_timeout: 120000, host: undefined, port: undefined, ssl: undefined, proxy: undefined, timeout_options: { timeout: 90000, retry_attempts: 2 }, default_path_prefix: undefined, username: undefined, access_key: undefined }, test_settings: { }, // A url which can be used later in the tests as the main url to load. launch_url: '', // set to false if you want to show the extended http traffic command logs from the WebDriver server. silent: true, // Used to disable terminal output completely. output: true, // Set this to false if you'd like to only see the test case name displayed and pass/fail status. detailed_output: true, // Set this to true if you'd like to see timestamps next to the logging output output_timestamp: false, // Set this to true if you'd like to disable bounding boxes on terminal output. Useful when running in some CI environments. disable_output_boxes: false, // Set this to iso if you'd like to see timestamps as ISO strings timestamp_format: '', // Set this to true if you'd like to not display errors during the execution of the test (they are shown at the end always). disable_error_log: false, // By default, API command errors that don't deal with DOM elements (e.g. cookie) are ignored report_command_errors: false, // Ignore network errors (e.g. ECONNRESET errors) report_network_errors: true, // Interactive element commands such as "click" or "setValue" can be retried if an error occurred (such as an "element not interactable" error) element_command_retries: 3, // Take error and failure screenshots during test execution screenshots: { enabled: false, filename_format, path: '', on_error: true, on_failure: true }, trace: { enabled: false, path: '', filename_format: snapshot_file_format }, // Used to enable showing the Base64 image data in the (verbose) log when taking screenshots. log_screenshot_data: false, desiredCapabilities: { browserName: 'firefox' }, // An array of folders or file patterns to be skipped (relative to the main source folder). exclude: null, // Folder or file pattern to be used when loading the tests. Files that don't match this pattern will be ignored. filter: null, // Skip a group of tests (a subfolder); can be a list of comma-separated values (no space) skipgroup: '', sync_test_names: true, // Skip tests by tag name; can be a list of comma-separated values (no space) skiptags: '', // Use xpath as the default locator strategy use_xpath: false, parallel_mode: false, report_prefix: '', unit_testing_mode: false, default_reporter: ['junit', 'json', 'minimalJson', 'html'], // In Nightwatch v1.x, when used with "await" operator, API commands will return the full result object as {value: ``} // whereas in v2, the value is return directly; if using a callback, the behaviour remains unchanged backwards_compatibility_mode: false, // disable the global apis like "browser", "element()", "expect()"; this might be needed if using Nightwatch with third-party libraries disable_global_apis: false, // disable only the global expect disable_global_expect: false, // enable aborting the test run execution when the first test failure occurs; the remaining test suites will be skipped enable_fail_fast: false, always_async_commands: false, enable_v3_element_apis: true, usage_analytics: { enabled: false, log_path: './logs/analytics', client_id } }; ================================================ FILE: lib/settings/settings.js ================================================ const dotenv = require('dotenv'); const path = require('path'); const defaultsDeep = require('lodash/defaultsDeep'); const lodashCloneDeep = require('lodash/cloneDeep'); const lodashMerge = require('lodash/merge'); const CI_Info = require('ci-info'); const Defaults = require('./defaults.js'); const Utils = require('../utils'); const {isObject, isUndefined, isDefined, isNumber, singleSourceFile} = Utils; class Settings { static get DEFAULT_ENV() { return 'default'; } static get DEFAULTS() { return Defaults; } /** * Looks for pattern ${VAR_NAME} in settings * @param {Object} [target] */ static replaceEnvVariables(target) { for (const key in target) { switch (typeof target[key]) { case 'object': Settings.replaceEnvVariables(target[key]); break; case 'string': target[key] = target[key].replace(/\${(\w+)\|?([^}]*)}/, function(match, varName, defaultValue) { return process.env[varName] || defaultValue || '${' + varName + '}'; }); break; } } return this; } static getDefaults() { return lodashCloneDeep(Settings.DEFAULTS); } /** * Called from the cli runner with data from config file * * @param {Object} [settings] additional settings which can be passed when called programmatically * @param {Object} [baseSettings] settings data from nightwatch config file * @param {Object} [argv] cli arguments object * @param {String} [testEnv] current test environment * @returns {Object} */ static parse(settings = {}, baseSettings = {}, argv = {}, testEnv = '') { const instance = new Settings(argv, testEnv); instance.fromConfigFile(baseSettings); instance.inheritFromDefaultEnv(); instance.init(settings); return instance.settings; } /** * Called from Nightwatch main client instance containing either an existing settings object or a new one * * @param {Object} userSettings * @param {Object} argv * @returns {Object} */ static fromClient(userSettings = {}, argv = {}) { const instance = new Settings(argv); instance.init(userSettings); return instance.settings; } static isNightwatchObject(settings) { return isDefined(settings['[[@nightwatch_createdAt]]']); } /** * @deprecated * @param settings */ static setDefaults(settings) { defaultsDeep(settings, Defaults); if (settings.unit_testing_mode) { settings.unit_tests_mode = true; } if (!settings.unit_tests_mode) { settings.skip_testcases_on_fail = settings.skip_testcases_on_fail || isUndefined(settings.skip_testcases_on_fail); } } static isUsingSeleniumServer(settings) { return settings.selenium && settings.selenium.start_process; } get testWorkersEnabled() { const {test_workers} = this.settings; const {serial, debug, parallel} = this.argv; if (serial || debug) { return false; } if (parallel || test_workers === true) { return true; } if (test_workers && test_workers.enabled && (test_workers.workers === 'auto' || test_workers.workers > 1)) { return true; } return false; } /** * @param {Object} [argv] the cli arguments object * @param {String} [testEnv] the current test env */ constructor(argv = {}, testEnv = '') { this.baseSettings = null; this.argv = argv; this.testEnv = testEnv || ''; this.initSettingsObject(); } /** * @param {Object|null} [baseSettings] the raw nightwatch config object * @param baseSettings */ fromConfigFile(baseSettings) { this.baseSettings = baseSettings || {}; this.copyGenericProperties(); } /** * Initialize a new settings object based on the defaults */ initSettingsObject() { this.settings = Settings.getDefaults(); this.settings.testEnv = this.testEnv; } /** * Copy all properties from the config file to this.settings that are located outside any test environment * defined as part of the "test_settings" dictionary; * * This allows to define non-standard properties to the Nightwatch settings object */ copyGenericProperties() { Object.keys(this.baseSettings).forEach(key => { if (key === 'test_settings') { return; } const copyVal = lodashCloneDeep(this.baseSettings[key]); if (isObject(this.settings[key])) { Object.assign(this.settings[key], copyVal); } else { this.settings[key] = copyVal; } }); } isSettingsDefined(settingName) { const {webdriver} = this.settings; if (isObject(webdriver[settingName])) { const values = Object.values(webdriver[settingName]); return values.every(item => isDefined(item)); } return isDefined(webdriver[settingName]); } /** * Tries to set a webdriver setting from a several legacy places if the value is not already set * * @param {String} newSetting the new property name * @param {String|Array} [oldSetting] * @param {Object} [opts] * @returns {Settings} for chaining */ setWebdriverHttpOption(newSetting, oldSetting, opts = {}) { const webdriverOpts = this.settings.webdriver; if (this.isSettingsDefined(newSetting)) { return this; } if (oldSetting === undefined) { oldSetting = [newSetting]; } else if (!Array.isArray(oldSetting)) { oldSetting = [oldSetting]; } for (let i = 0; i < oldSetting.length; i++) { const item = oldSetting[i]; if (isDefined(this.settings[item])) { webdriverOpts[newSetting] = this.settings[item]; return this; } } if (isDefined(opts.defaultValue)) { webdriverOpts[newSetting] = opts.defaultValue; } return this; } isUsingSelenium() { const {selenium} = this.settings; return isObject(selenium); } isSeleniumServerManaged() { return this.isUsingSelenium() && this.settings.selenium.start_process; } /** * Set the connection settings to the Webdriver server and any networking options */ setWebdriverSettings() { // if using selenium server, we read settings from the selenium dictionary if (this.isSeleniumServerManaged()) { lodashMerge(this.settings.webdriver, this.settings.selenium); } else if (this.isUsingSelenium()) { defaultsDeep(this.settings.webdriver, this.settings.selenium); } this .setWebdriverHttpOption('host', ['seleniumHost', 'selenium_host'], {defaultValue: 'localhost'}) .setWebdriverHttpOption('port', ['seleniumPort', 'selenium_port']) .setWebdriverHttpOption('ssl', ['useSsl', 'use_ssl']) .setWebdriverHttpOption('proxy') .setWebdriverHttpOption('start_session') .setWebdriverHttpOption('timeout_options', 'request_timeout_options') .setWebdriverHttpOption('default_path_prefix') .setWebdriverHttpOption('username') .setWebdriverHttpOption('access_key', ['accessKey', 'access_key', 'password']); if (isUndefined(this.settings.webdriver.ssl)) { this.settings.webdriver.ssl = this.settings.webdriver.port === 443; } if (!this.settings.webdriver.host) { this.settings.webdriver.host = this.settings.selenium_host || 'localhost'; } this.setServerUrl(); } /** * Set the webdriver server url which will be used in case the service is not managed by Nightwatch */ setServerUrl() { const protocol = this.settings.webdriver.ssl ? 'https' : 'http'; const {port, host, default_path_prefix = ''} = this.settings.webdriver; this.settings.webdriver.url = `${protocol}://${host}:${port}${default_path_prefix}`; if (isObject(this.settings.selenium)) { this.settings.selenium.url = this.settings.webdriver.url; } } mergeOntoExisting(userSettings = {}) { lodashMerge(this.settings, userSettings); return this; } /** * @returns {Settings} */ adaptSettings() { this.setCliOptions(); this.setScreenshotsOptions(); this.setUnitTestsMode(); this.setParallelMode(); this.setTestRunner(); this.setReporterOptions(); if (typeof this.settings.src_folders == 'string') { this.settings.src_folders = [this.settings.src_folders]; } if (typeof this.settings.skipgroup == 'string' && this.settings.skipgroup.length > 0) { this.settings.skipgroup = this.settings.skipgroup.split(','); } return this; } setReporterOptions() { defaultsDeep(this.settings, this.settings.reporter_options); if (this.argv.trace === true) { this.settings.trace.enabled = true; } } setParallelMode() { const Concurrency = require('../runner/concurrency'); if (Concurrency.isWorker()) { this.settings.parallel_mode = true; } if (isObject(this.settings.test_workers)) { this.settings.test_workers.workers = this.settings.test_workers.workers || 'auto'; } if (this.argv.parallel === true && !this.settings.test_workers) { this.settings.test_workers = true; } else if (isNumber(this.argv.parallel)) { if (!isObject(this.settings.test_workers)) { this.settings.test_workers = { enabled: true }; } this.settings.test_workers.workers = this.argv.parallel || this.settings.test_workers.workers; } else if (isNumber(this.argv.workers)) { if (!isObject(this.settings.test_workers)) { this.settings.test_workers = { enabled: true }; } this.settings.test_workers.workers = this.argv.workers || this.settings.test_workers.workers; } this.settings.testWorkersEnabled = this.testWorkersEnabled && (!singleSourceFile(this.argv) || this.argv['test-worker'] === true); return this; } setTestRunner() { if (Utils.isString(this.settings.test_runner)) { this.settings.test_runner = { type: this.settings.test_runner, options: {} }; } if (!Utils.isObject(this.settings.test_runner)) { throw new Error(`Invalid "test_runner" settings specified; received: ${this.settings.test_runner}`); } return this; } setUnitTestsMode() { const unitTesting = this.settings.unit_tests_mode || this.settings.unit_testing_mode; this.settings.unit_testing_mode = this.settings.unit_tests_mode = unitTesting; if (unitTesting) { this.settings.webdriver.start_process = false; this.settings.webdriver.start_session = false; this.settings.start_session = false; this.settings.detailed_output = false; this.settings.output_timestamp = false; } else { this.settings.skip_testcases_on_fail = this.settings.skip_testcases_on_fail || isUndefined(this.settings.skip_testcases_on_fail); } return this; } inheritFromDefaultEnv() { if (!this.baseSettings.test_settings) { return this; } const defaultEnvSettings = this.baseSettings.test_settings[Settings.DEFAULT_ENV] || {}; lodashMerge(this.settings, defaultEnvSettings); if (!this.testEnv || this.testEnv === Settings.DEFAULT_ENV) { return this; } const testEnvSettings = this.baseSettings.test_settings[this.testEnv] || {}; this.inheritFromSuperEnv(testEnvSettings); defaultsDeep(testEnvSettings, defaultEnvSettings); lodashMerge(this.settings, testEnvSettings); return this; } inheritFromSuperEnv(testEnvSettings) { if (testEnvSettings.extends) { const superEnv = this.baseSettings.test_settings[testEnvSettings.extends] || {}; delete testEnvSettings.extends; defaultsDeep(testEnvSettings, superEnv); return this.inheritFromSuperEnv(testEnvSettings); } return testEnvSettings; } /** * @param settings */ persistGlobals(settings) { if (this.settings.persist_globals === true && isObject(settings.globals)) { defaultsDeep(settings.globals, this.settings.globals); this.settings.globals = settings.globals; } } setScreenshotsOptions() { if (isObject(this.settings.screenshots)) { this.settings.screenshots.path = this.settings.screenshots.path ? path.resolve(this.settings.screenshots.path) : ''; } else { const enabled = this.settings.screenshots === true; this.settings.screenshots = Object.assign({}, Defaults.screenshots, {enabled}); } this.settings.screenshotsPath = this.settings.screenshots.path; return this; } setCliOptions() { if (this.argv.verbose) { this.settings.silent = false; } const cliOverwrites = { output_folder: this.argv.output, filename_filter: this.argv.filter, tag_filter: this.argv.tag, skipgroup: this.argv.skipgroup, skiptags: this.argv.skiptags, enable_fail_fast: this.argv['fail-fast'] }; Object.keys(cliOverwrites).forEach(key => { if (isDefined(cliOverwrites[key]) && cliOverwrites[key] !== null) { this.settings[key] = cliOverwrites[key]; } }); // TODO: add support for overwriting any setting return this; } sortSettings() { const sortedSettings = {}; Object.keys(this.settings).sort().forEach(key => { sortedSettings[key] = this.settings[key]; }); this.settings = sortedSettings; Object.defineProperty(this.settings, '[[@nightwatch_createdAt]]', { value: new Date().valueOf(), enumerable: false, configurable: false, writable: false }); } setBaseUrl() { const value = this.settings.baseUrl || this.settings.base_url || this.settings.launchUrl || this.settings.launch_url || null; if (value) { this.settings.baseUrl = this.settings.base_url = this.settings.launchUrl = this.settings.launch_url = value; } } setColorOutput() { const {isCI, CIRCLE, JENKINS, NETLIFY, TRAVIS, GITLAB, GITHUB_ACTIONS, BUILDKITE} = CI_Info; const coloringSupport = CIRCLE || JENKINS || NETLIFY || TRAVIS || GITLAB || BUILDKITE || GITHUB_ACTIONS; if (isCI && !coloringSupport) { this.settings.disable_colors = true; } } /** * Validates and parses the test settings * * @param {Object} [userSettings] */ init(userSettings = {}) { this.mergeOntoExisting(userSettings); this.setWebdriverSettings(); this.adaptSettings(); this.sortSettings(); this.persistGlobals(userSettings); dotenv.config(this.settings.dotenv); Settings.replaceEnvVariables(this.settings); this.setBaseUrl(); this.setColorOutput(); } } module.exports = Settings; ================================================ FILE: lib/testsuite/context.js ================================================ const path = require('path'); const EventEmitter = require('events'); const Utils = require('../utils'); const ExportsInterface = require('./interfaces/exports.js'); const DescribeInterface = require('./interfaces/describe.js'); class Context extends EventEmitter { static get REPORT_KEY_SEPARATOR() { return path.sep; } get isTestHook() { return false; } constructor({modulePath, settings, argv = {}, attributes = {}}) { super(); this.settings = settings; this.argv = argv; this.__module = null; this.__testsuite = {}; this.__currentRunnable = null; this.__retries = { testcase: null, suite: null }; this.attributes = attributes; this.groupName = ''; if (modulePath) { this.setModulePath(modulePath); } } setModulePath(file) { this.modulePath = file; this.__moduleName = path.parse(this.modulePath).name; this.moduleKey = this.moduleName || ''; } loadTags({usingMocha = false} = {}) { if (!usingMocha) { return; } const context = this; const {Suite} = require('mocha'); class BasicSuite extends Suite { get tags() { return context.attributes['@tags']; } set tags(value) { context.attributes['@tags'] = value; } } global.describe = function(title, definitionFn) { const instance = new BasicSuite(title); definitionFn.call(instance); }; try { this.__module = this.requireModule(this.modulePath); // eslint-disable-next-line no-empty } catch (err) {} } async init({usingMocha = false, suiteTitle = null, client = null} = {}) { this.__currentTestName = this.argv.testcase; this.__testSuiteName = suiteTitle; this.__hooks = []; this.__testcases = []; this.__skippedTestCases = []; this.__allScreenedTests = []; this.__contextBinding = {}; this.__transforms = client ? await client.transforms : []; this.source = this.argv._source || []; if (!usingMocha) { this.createInterface(client); await this.loadModule(); } this.createTestSuite(); return this; } get testSuiteName() { return this.__testSuiteName; } get currentTest() { return this.__currentTestName; } get contextBinding() { return this.__contextBinding; } /** * @returns {boolean} */ get unitTestingMode() { return this.settings.unit_testing_mode || this.isUnitTest(); } /** * @deprecated * @returns {boolean} */ get unitTestsMode() { return this.settings.unit_tests_mode || this.isUnitTest(); } get moduleName() { return this.__moduleName; } get module() { return this.__module; } get testsuite() { return this.__testsuite; } get tests() { return this.__testcases; } set tests(value) { this.__testcases = value; } get skippedTests() { return this.__skippedTestCases; } set skippedTests(value) { this.__skippedTestCases = value; } get allScreenedTests() { return this.__allScreenedTests; } set allScreenedTests(value) { this.__allScreenedTests = value; } get hooks() { return this.__hooks; } get currentRunnable() { return this.__currentRunnable; } get queue() { return this.currentRunnable && this.currentRunnable.queue; } get queueStarted() { return this.queue && this.queue.started; } get retries() { return this.__retries; } setReloadModuleCache(val = true) { this.__reloadModuleCache = val; return this; } shouldReloadModuleCache() { return this.__reloadModuleCache; } get usingBddDescribe() { return this.bddInterface && Utils.isFunction(this.bddInterface.describeFn); } getName() { return this.moduleName; } getSuiteName() { return this.testSuiteName || Utils.getTestSuiteName(this.moduleKey); } setCurrentRunnable(runnable) { this.__currentRunnable = runnable; return this; } createInterface(client) { this.exportsInterface = new ExportsInterface(this); this.bddInterface = new DescribeInterface(this, client); return this; } async loadModule() { this.emit('pre-require', global); this.__module = await this.requireModule(); if (!this.module && !this.bddInterface.describeFn) { throw new Error(`Empty module provided in: "${this.modulePath}".`); } if (this.module) { this.__testsuite = Object.assign(this.__testsuite, this.module); } this.emit('post-require'); this.emit('module-loaded'); return this; } async requireModule(loadJsWithPlugins = false) { const pluginDescriptor = this.__transforms.find(transform => { const {filter} = transform; if (Utils.isFunction(filter)) { return filter(this.modulePath, loadJsWithPlugins); } return filter.test(this.modulePath); }); if (pluginDescriptor) { try { const testContext = await pluginDescriptor.requireTest(this.modulePath, pluginDescriptor, { argv: this.argv, nightwatch_settings: this.settings }); if (testContext && testContext.initialize) { return testContext; } return {}; } catch (err) { const error = new Error(`Error while trying to load ${this.modulePath}`); error.detailedErr = err.message; error.stack = err.stack; throw error; } } try { return Utils.requireModule(this.modulePath); } catch (err) { if ((err instanceof SyntaxError) && !loadJsWithPlugins) { return await this.requireModule(true); } throw err; } } createTestSuite() { if (this.currentTest && this.tests.length === 0) { throw new Error(`"${this.currentTest}" is not a valid testcase in the current test suite.`); } if (this.currentTest && this.tests.length > 1) { this.tests = [this.currentTest]; } this.__moduleKeysCopy = this.tests.slice(0); } addTestSuiteMethod(testName, testFn, context) { this.testsuite[testName] = testFn; this.contextBinding[testName] = context || this.module; } /** * Add test hooks created by describe interface * * @param {string} hookName * @param {Function} hookFn * @param {Object} [context] */ addTestHook(hookName, hookFn, context) { // TODO: warn if hook name already exists this.hooks.push(hookName); this.addTestSuiteMethod(hookName, hookFn, context); } /** * Add testcases created by describe interface * * @param {string} testName * @param {function} testFn * @param {Object} [describeInstance] The instance of the describe function declaration * @param {Boolean=false} [runOnly] If the runner should run only this testcase * @param {Boolean=false} [skipTest] If the testcase should be skipped */ addTestCase({testName, testFn, describeInstance, runOnly, skipTest}) { if (!Utils.isFunction(testFn)) { throw new Error(`The "${testName}" test script must be a function. "${typeof testFn}" given.`); } if (this.allScreenedTests.includes(testName)) { const {Logger} = Utils; const err = new Error( 'An error occurred while loading the testsuite:\n' + `A testcase with name "${testName}" already exists. Testcases must have unique names inside the test suite, ` + 'otherwise testcases with duplicate names might not run at all.\n\n' + 'This testsuite has been disabled, please fix the error to run it again properly.' ); Logger.error(err); this.setAttribute('@disabled', true); } if (!skipTest) { this.tests.push(testName); } else { this.skippedTests.push(testName); } this.addTestSuiteMethod(testName, testFn, describeInstance); if (runOnly) { this.__currentTestName = testName; this.skippedTests = [...this.allScreenedTests]; this.runOnly = true; } else if (this.runOnly) { this.skippedTests.push(testName); } this.allScreenedTests.push(testName); } /** * Create a testsuite using describe interface * * @param {string} describeTitle * @param {Object} describeInstance The instance of the describe function declaration * @param {Boolean=false} [runOnly] If the runner should run only this testsuite */ setDescribeContext({describeTitle, describeInstance, runOnly}) { // if `setDescribeContext` is called twice for the same test suite, // that would mean that there are two `describe()`s in same test suite. if (this.__testSuiteName && describeInstance) { // eslint-disable-next-line no-console console.warn( 'Nightwatch does not support more than one "describe" declarations in a single testsuite.' + ' Using this might give unexpected results.' ); } if (runOnly) { // eslint-disable-next-line no-console console.warn('describe.only() is not supported at the moment.'); } this.__testSuiteName = describeTitle; } setTestcaseRetries(n) { this.retries.testcase = n; } setSuiteRetries(n) { this.retries.suite = n; } //////////////////////////////////////////////////////////////// // Attributes //////////////////////////////////////////////////////////////// isES6Async(testName) { return Utils.isES6AsyncFn(this.testsuite[testName]); } addAttributes(attributes = {}) { Object.assign(this.attributes, attributes); } setAttribute(name, value) { this.attributes[name] = value; } getAttribute(name) { return this.attributes[name]; } isDisabled() { return this.attributes['@disabled'] === true; } isUnitTest() { return this.attributes['@unitTest'] === true; } getSkipTestcasesOnFail() { return this.attributes['@skipTestcasesOnFail']; } getEndSessionOnFail() { return this.attributes['@endSessionOnFail']; } getNameAttr() { return this.attributes['@name']; } getTags() { return this.attributes['@tags']; } getDesiredCapabilities() { return this.attributes['@desiredCapabilities']; } //////////////////////////////////////////////////////////////// // Module calls //////////////////////////////////////////////////////////////// /** * * @param {function} done * @param {Object} api * @param {Number} expectedArgs * @return [] */ getHookFnArgs(done, api, expectedArgs) { if (this.unitTestsMode) { return [done]; } const args = [api]; if (expectedArgs === 2) { args.push(done); } return args; } extendContextWithApi(context, api) { if (('client' in context) && this.modulePath && context.client && !('currentTest' in context.client)) { throw new Error('There is already a .client property defined in: ' + this.modulePath); } context.client = api; } /** * * @param {string} fnName * @param {Object} client * @param {Number} expectedArgs * @param {function} done * @return {*} */ invokeMethod(fnName, client, expectedArgs, done) { const isES6Async = this.isES6Async(fnName); const isTestHook = this.isTestHook; if (client) { client.isES6AsyncTestcase = isES6Async; client.isES6AsyncTestHook = isTestHook ? isES6Async : undefined; } const api = client && client.api || null; let context; if (this.contextBinding && this.contextBinding[fnName]) { context = this.contextBinding[fnName]; } else { context = this.__module; } this.extendContextWithApi(context, api); switch (expectedArgs) { case 2: case 1: return this.callAsync({fnName, api, expectedArgs, done, context, isES6Async, isTestHook}); case 0: { try { const result = this.call(fnName); if (!(result instanceof Promise)) { return done(); } result.then(() => { done(); }).catch(err => { done(err); }); } catch (err) { done(err); } } } } /** * @param {string} fnName * @param {Array} args */ call(fnName, ...args) { const context = this.contextBinding[fnName]; const client = args[0]; if (client) { const isES6Async = this.isES6Async(fnName); client.isES6AsyncTestcase = isES6Async; client.isES6AsyncTestHook = this.isTestHook ? isES6Async : undefined; args[0] = client.api; } const result = this.testsuite[fnName].apply(context, args); if (this.currentRunnable) { this.currentRunnable.currentTestCaseResult = result; } return result; } /** * * @param {string} fnName * @param {Object} api * @param {Number} expectedArgs * @param {function} done * @param {Object} context * @return {*} */ callAsync({fnName, api, expectedArgs = 2, done = function() {}, context}) { const fnAsync = Utils.makeFnAsync(expectedArgs, this.testsuite[fnName], context); const args = this.getHookFnArgs(done, api, expectedArgs); const result = fnAsync.apply(context, args); if (this.currentRunnable) { this.currentRunnable.currentTestCaseResult = result; } return result; } //////////////////////////////////////////////////////////////// // Module keys //////////////////////////////////////////////////////////////// hasHook(key) { return this.hooks.indexOf(key) > -1; } getKey(key) { return this.testsuite[key]; } getNextKey() { if (this.tests.length) { return this.tests.shift(); } return null; } /** * When using retries, the testcases are reset * * @return {Context} */ reset() { this.tests = this.__moduleKeysCopy.slice(); return this; } //////////////////////////////////////////////////////////////// // Reporting //////////////////////////////////////////////////////////////// setReportKey(allModulePaths = []) { if (!this.modulePath) { return; } let parentFolder = this.modulePath.substring(0, this.modulePath.lastIndexOf(path.sep)); const parentFolderName = parentFolder.split(path.sep).pop(); const srcFolders = this.settings.src_folders || this.source || []; let diffInFolder = ''; if (srcFolders.length > 0) { for (let i = 0; i < srcFolders.length; i++) { const srcPathResolved = path.resolve(srcFolders[i]); diffInFolder = this.getDiffFromSourceFolder(srcPathResolved, parentFolder, srcFolders); if (diffInFolder) { this.moduleKey = [diffInFolder, this.moduleKey].join(Context.REPORT_KEY_SEPARATOR); this.groupName = parentFolderName; parentFolder = parentFolder.substring(0, parentFolder.lastIndexOf(path.sep + diffInFolder)); // removing the diffInFolder string from the parent folder break; } } } // in case we're using src_folders and there are more than one, prepend the parent folder name to the report key if (diffInFolder === '' && Array.isArray(this.settings.src_folders) && this.settings.src_folders.length > 1) { this.moduleKey = [parentFolderName, this.moduleKey].join(Context.REPORT_KEY_SEPARATOR); } // in case there are several test files, make sure the report key is unique if (allModulePaths.length > 1) { this.moduleKey = this.checkKeyForUniqueness(allModulePaths, parentFolder); } } shouldCheckIfDirectory() { return !this.settings.src_folders && this.source.length > 1; } checkKeyForUniqueness(allModulePaths, parentFolder) { // removing the current module const modulePathsCopy = allModulePaths.slice(0); const index = modulePathsCopy.indexOf(this.modulePath); if (index > -1) { modulePathsCopy.splice(index, 1); } const modulePathParts = parentFolder.split(path.sep); return this.getUniqueModuleKey(modulePathsCopy, modulePathParts, this.moduleKey); } /** * * @param {string} srcPathResolved * @param {string} moduleParentFolder * @param {Array} source */ getDiffFromSourceFolder(srcPathResolved, moduleParentFolder, source) { const isDirectory = !this.shouldCheckIfDirectory() || Utils.dirExistsSync(srcPathResolved); if (!isDirectory) { return ''; } if (moduleParentFolder.startsWith(srcPathResolved)) { return moduleParentFolder.substring(srcPathResolved.length + 1).split(path.sep).join(Context.REPORT_KEY_SEPARATOR); } return ''; } /** * In case there are multiple sources, compute the moduleKey uniquely * * @param {Array} modulePathsCopy * @param {Array} [modulePathParts] * @param {string} [moduleKey] * @return {string} */ getUniqueModuleKey(modulePathsCopy, modulePathParts = null, moduleKey = '') { if (modulePathParts && modulePathParts.length < 2) { return moduleKey; } const isKeyUnique = !modulePathsCopy.some(item => { const modulePath = path.sep + moduleKey; return item.endsWith(modulePath + Utils.jsFileExt) || item.endsWith(modulePath + Utils.tsFileExt); }); if (isKeyUnique) { return moduleKey; } moduleKey = [modulePathParts.pop(), moduleKey].join(Context.REPORT_KEY_SEPARATOR); return this.getUniqueModuleKey(modulePathsCopy, modulePathParts, moduleKey); } } module.exports = Context; ================================================ FILE: lib/testsuite/globals.js ================================================ const path = require('path'); const lodashCloneDeep = require('lodash/cloneDeep'); const TestHooks = require('./hooks.js'); const Context = require('./context.js'); const Utils = require('../utils'); const {Logger} = Utils; const PluginLoader = require('../api/_loaders/plugin.js'); class GlobalsContext extends Context { throwError(err) { err.detailedErr = err.message; err.message = `cannot read external global file using "${this.settings.globals_path}"`; err.showTrace = false; throw err; } get isTestHook() { return true; } constructor(settings, argv, currentEnv) { super({modulePath: settings.globals_path, settings, argv}); this.currentEnv = currentEnv; this.argv = argv; } get testsuite() { return this.module; } init(asyncLoading) { return this.initModule(() => { if (asyncLoading) { return this.loadModule(); } return this.loadModuleSync(); }, () => { this.externalGlobals = Object.assign({}, this.__module); this.__module = this.settings.globals; this.mergeGlobalsOntoSettings(); this.setCurrentEnv(); }, asyncLoading); } initModule(loadFn, callback, asyncLoading) { if (!this.settings.globals_path) { this.__module = this.settings.globals; this.setCurrentEnv(); if (asyncLoading) { return Promise.resolve(); } return; } const result = loadFn.call(this); if (result instanceof Promise) { return result.then(() => { callback.call(this); }); } callback.call(this); } async loadModule(isRetry = false) { try { this.__module = await Utils.requireModule(this.modulePath); if (!this.__module) { throw new Error(`Empty module provided in: "${this.modulePath}".`); } } catch (err) { if (err.code === 'MODULE_NOT_FOUND' && !isRetry) { this.modulePath = path.join(Utils.getConfigFolder(this.argv), this.settings.globals_path); await this.loadModule(true); return this; } this.throwError(err); } } loadModuleSync(isRetry = false) { try { this.__module = Utils.requireModule(this.modulePath); if (!this.__module) { throw new Error(`Empty module provided in: "${this.modulePath}".`); } } catch (err) { if (err.code === 'MODULE_NOT_FOUND' && !isRetry) { this.modulePath = path.join(Utils.getConfigFolder(this.argv), this.settings.globals_path); this.loadModuleSync(true); return this; } this.throwError(err); } return this; } /** * Merges the contents of the external globals back to the settings object in order for it to be available between * test runs; * * This can be either as it is, so that any changes on the globals are maintained, or as a deep copy * * @return {GlobalsContext} */ mergeGlobalsOntoSettings() { let externalGlobals; if (this.settings.persist_globals) { externalGlobals = this.externalGlobals; } else { // if we already have globals, make a copy of them externalGlobals = lodashCloneDeep(this.externalGlobals); } Object.assign(this.__module, externalGlobals); return this; } extendContextWithApi(context, api) { if (('client' in context) && this.modulePath && context.client && !('currentTest' in context.client)) { throw new Error('There is already a .client property defined in: ' + this.modulePath); } Object.defineProperty(context, 'client', { enumerable: false, configurable: false, writable: true, value: api }); } callAsync({fnName, api, expectedArgs = 2, isTestHook = false, isES6Async, done = function() {}}) { if (isES6Async && isTestHook && expectedArgs === 1) { return this.module[fnName].call(this.module, api) .then(function(result) { return done(result); }) .catch(function(err) { Logger.error(err); return done(err); }); } const fnAsync = Utils.makeFnAsync(expectedArgs, this.module[fnName], this.module); const args = this.getHookFnArgs(done, api, expectedArgs); return fnAsync.apply(this.module, args); } /** * * @param {string} fnName * @param args */ call(fnName, ...args) { const client = args[0]; if (client) { client.isES6AsyncTestcase = this.isES6Async(fnName); args[0] = client.api; } return this.module[fnName].apply(this.module, args); } hasHook(key) { return typeof this.moduleCopy[key] == 'function'; } getKey(key) { return this.moduleCopy[key]; } /** * * @param {function} done * @param {Object} api * @param {Number} expectedArgs * @return [] */ getHookFnArgs(done, api, expectedArgs) { return expectedArgs === 1 ? [done] : [api, done]; } setCurrentEnv() { this.moduleCopy = lodashCloneDeep(this.module); // select globals from the current environment if (this.currentEnv) { /*eslint no-prototype-builtins: 'warn'*/ if (this.currentEnv && this.__module.hasOwnProperty(this.currentEnv)) { Object.assign(this.__module, this.module[this.currentEnv]); } } } } class Globals { constructor(settings, argv, currentEnv = '') { this.settings = settings; this.argv = argv; this.currentEnv = currentEnv; } get globals() { return this.context.module; } loadPlugins() { const definition = this.settings.plugins || []; this.plugins = definition.reduce((prev, pluginName) => { const plugin = PluginLoader.load(pluginName); if (plugin.globals) { prev.push(plugin); } return prev; }, []); } init() { this.mergeWithExisting(); this.setupGlobalHooks(); this.loadPlugins(); } readExternal(asyncLoading) { this.context = new GlobalsContext(this.settings, this.argv, this.currentEnv); return this.context.init(asyncLoading); } /** * Shallow merge with existing globals on the settings object */ mergeWithExisting() { const settingsCopy = lodashCloneDeep(this.settings); delete settingsCopy.globals; try { Object.defineProperty(this.globals, 'settings', { enumerable: false, configurable: false, get() { return settingsCopy; } }); // eslint-disable-next-line } catch (err) {} this.settings.globals = this.globals; } setupGlobalHooks() { this.hooks = new TestHooks(this.context, { isGlobal: true, asyncHookTimeout: this.settings.globals.asyncHookTimeout }); } async runPluginHook(hookName, args) { if (this.plugins.length === 0) { return; } await Promise.all(this.plugins.map(plugin => { if (Utils.isFunction(plugin.globals[hookName])) { return plugin.globals[hookName].apply(this.settings.globals, args); } return Promise.resolve(); })); } async runGlobalHook(key, args = []) { await this.hooks[key].run(); await this.runPluginHook(key, args); } } module.exports = Globals; ================================================ FILE: lib/testsuite/hooks/_basehook.js ================================================ const Utils = require('../../utils'); const {TimedCallback} = Utils; class BaseHook { static get beforeAll() { return 'before'; } static get beforeEach() { return 'beforeEach'; } static get beforeChildProcess() { return 'beforeChildProcess'; } static get afterAll() { return 'after'; } static get afterEach() { return 'afterEach'; } static get afterChildProcess() { return 'afterChildProcess'; } get skipTestcasesOnError() { return false; } constructor(hookName, context, addtOpts = {}) { this.context = context; this.addtOpts = addtOpts; this.key = hookName; this.hookTimeoutId = null; } get isGlobal() { return this.addtOpts.isGlobal || false; } get isUnitTest() { return false; } /** * * @param {Object} client The nightwatch main instance * @param {function} [originalFn] * @return {*} */ run(client = null, originalFn = null) { originalFn = originalFn || this.verifyMethod(); if (originalFn) { const argsCount = originalFn.length; let expectedCount; let runnableDone; return new Promise((resolve, reject) => { try { const timedCallback = this.createCallbackWrapper(resolve, reject, err => { err.help = ['You can increase the hooks timeout by setting "asyncHookTimeout" in the globals config to the required value.']; err.skipTestCases = this.skipTestcasesOnError; runnableDone && runnableDone(err); reject(err); }); const doneFn = timedCallback.getWrapper(); if (!this.isGlobal) { expectedCount = 2; } if (this.context.currentRunnable && argsCount === 2) { // Set the runnable resolve function to be called from the inside the done callback of the hook // - necessary for the case when the callback starts a different async operation runnableDone = this.context.currentRunnable.setDoneCallback(); this.context.currentRunnable.deffered.promise.catch(err => { if (this.hookTimeoutId) { clearTimeout(this.hookTimeoutId); } runnableDone(err); }); } const invocationResult = this.context.invokeMethod(this.key, client, argsCount, result => { if (result instanceof Error) { result.skipTestCases = this.skipTestcasesOnError; } if (this.context.queue && this.context.queue.inProgress) { this.context.queue.once('queue:finished', _ => { runnableDone && runnableDone(result); doneFn(result); }); } else { runnableDone && runnableDone(result); doneFn(result); } }); if (argsCount <= 1 && (invocationResult instanceof Promise)) { invocationResult.catch(err => { return err; }).then(result => { if (result instanceof Error) { result.skipTestCases = this.skipTestcasesOnError; } runnableDone && runnableDone(result); doneFn(result); return result; }); } else if (this.onlyApiArgPassed(argsCount)) { // For global hooks (and unit tests), when we have only one argument, the argument is the "done" callback: // E.g.: (global) afterEach(done) {} // // For normal test hooks, the 1st argument is the 'client' object and the "done" callback is optional, // and thus the callback is called implicitly if it's not passed explicitly, e.g.: after(client) {} this.implicitlyCallDoneCallback(doneFn); } } catch (err) { if (this.hookTimeoutId) { clearTimeout(this.hookTimeoutId); } runnableDone && runnableDone(err); reject(err); } }); } return Promise.resolve(); } verifyMethod() { if (this.context.hasHook(this.key)) { return this.context.getKey(this.key); } return null; } /** * * @param {Function} resolve * @param {Function} reject * @param {Function} timeoutExpired * @return {TimedCallback} */ createCallbackWrapper(resolve, reject, timeoutExpired) { const timedCallback = new TimedCallback(function doneCallback(err) { if (Utils.isErrorObject(err)) { return reject(err); } resolve(); }, ((this.isGlobal && !this.isUnitTest) ? 'global ' : '') + this.key, this.addtOpts.asyncHookTimeout); timedCallback.onTimeoutExpired = timeoutExpired; timedCallback.onTimerStarted = timeoutId => { this.hookTimeoutId = timeoutId; }; return timedCallback; } onlyApiArgPassed(argsCount) { return argsCount === 1 && !this.isGlobal; } startQueueIfNeeded() { if (!this.context.queueStarted) { this.context.queue.run(); } } implicitlyCallDoneCallback(doneFn) { if (this.hookTimeoutId) { clearTimeout(this.hookTimeoutId); } process.nextTick(() => { if (this.context.queue) { this.context.queue.once('queue:finished', _ => { doneFn(); }); this.startQueueIfNeeded(); } }); } } module.exports = BaseHook; ================================================ FILE: lib/testsuite/hooks/afterAll.js ================================================ const BaseHook = require('./_basehook.js'); class AfterAll extends BaseHook { constructor(context, addtOpts) { super(BaseHook.afterAll, context, addtOpts); } } module.exports = AfterAll; ================================================ FILE: lib/testsuite/hooks/afterChildProcess.js ================================================ const BaseHook = require('./_basehook.js'); class AfterChildProcess extends BaseHook { constructor(context, addtOpts) { super(BaseHook.afterChildProcess, context, addtOpts); } } module.exports = AfterChildProcess; ================================================ FILE: lib/testsuite/hooks/afterEach.js ================================================ const BaseHook = require('./_basehook.js'); class AfterEach extends BaseHook { constructor(context, addtOpts) { super(BaseHook.afterEach, context, addtOpts); } } module.exports = AfterEach; ================================================ FILE: lib/testsuite/hooks/beforeAll.js ================================================ const BaseHook = require('./_basehook.js'); class BeforeAll extends BaseHook { get skipTestcasesOnError() { return true; } constructor(context, addtOpts) { super(BaseHook.beforeAll, context, addtOpts); } } module.exports = BeforeAll; ================================================ FILE: lib/testsuite/hooks/beforeChildProcess.js ================================================ const BaseHook = require('./_basehook.js'); class BeforeChildProcess extends BaseHook { constructor(context, addtOpts) { super(BaseHook.beforeChildProcess, context, addtOpts); } } module.exports = BeforeChildProcess; ================================================ FILE: lib/testsuite/hooks/beforeEach.js ================================================ const BaseHook = require('./_basehook.js'); class BeforeAll extends BaseHook { get skipTestcasesOnError() { return true; } constructor(context, addtOpts) { super(BaseHook.beforeEach, context, addtOpts); } } module.exports = BeforeAll; ================================================ FILE: lib/testsuite/hooks.js ================================================ const BaseHook = require('./hooks/_basehook.js'); class TestHooks { static get TEST_HOOKS () { return { [BaseHook.beforeAll]: require('./hooks/beforeAll.js'), [BaseHook.beforeEach]: require('./hooks/beforeEach.js'), [BaseHook.beforeChildProcess]: require('./hooks/beforeChildProcess.js'), [BaseHook.afterEach]: require('./hooks/afterEach.js'), [BaseHook.afterAll]: require('./hooks/afterAll.js'), [BaseHook.afterChildProcess]: require('./hooks/afterChildProcess.js') }; } constructor(context, addtOpts) { this.context = context; Object.keys(TestHooks.TEST_HOOKS).forEach(key => { this[key] = new TestHooks.TEST_HOOKS[key](this.context, addtOpts); }); } } module.exports = TestHooks; ================================================ FILE: lib/testsuite/index.js ================================================ const AssertionError = require('assertion-error'); const {By, Key, locateWith, withTagName} = require('selenium-webdriver'); const Reporter = require('../reporter'); const Context = require('./context.js'); const TestHooks = require('./hooks.js'); const TestCase = require('./testcase.js'); const Runnable = require('./runnable.js'); const Transport = require('../transport/selenium-webdriver'); const NightwatchAssertError = require('../assertion').AssertionError; const SuiteRetries = require('./retries.js'); const NightwatchClient = require('../core/client.js'); const Concurrency = require('../runner/concurrency'); const ElementGlobal = require('../api/_loaders/element-global.js'); const {Logger, Screenshots, Snapshots, alwaysDisplayError, isString, isFunction, SafeJSON} = require('../utils'); const NightwatchInspectorServer = require('./nightwatch-inspector'); const {DEFAULT_RUNNER_EVENTS, NightwatchEventHub} = require('../runner/eventHub'); const {GlobalHook, TestSuiteHook} = DEFAULT_RUNNER_EVENTS; class TestSuite { constructor({modulePath, modules, settings, argv, usingMocha = false, addtOpts = {}}) { this.settings = settings; this.argv = argv; this.modulePath = modulePath; this.allModulePaths = modules; this.testcase = null; this.currentRunnable = null; this.usingMocha = usingMocha; this.reuseBrowser = argv['reuse-browser'] || (settings.globals && settings.globals.reuseBrowserSession); this.globalHooks = addtOpts.globalHooks || {}; if (addtOpts.globalsInstance) { this.globalsInstance = addtOpts.globalsInstance; } this.__reportPrefix = ''; this.mochaContext = new Promise(resolve => { this.mochaContextResolve = resolve; }); } get api() { return this.client.api; } get commandQueue() { if (!this.client) { return null; } return this.client.queue; } get reportPrefix() { return this.__reportPrefix; } get transport() { return this.client.transport; } get skipTestcasesOnFail() { const localDefinedValue = this.context.getSkipTestcasesOnFail(); if (localDefinedValue !== undefined) { return localDefinedValue; } const settingsValueUndefined = this.settings.skip_testcases_on_fail === undefined; if (settingsValueUndefined && this.context.unitTestingMode) { // false by default when running unit tests return false; } // true by default when not running unit tests return settingsValueUndefined || this.settings.skip_testcases_on_fail; } get endSessionOnFail() { const definedValue = this.context.getEndSessionOnFail(); return definedValue === undefined ? this.settings.end_session_on_fail : definedValue; } get isES6Async() { return this.client && (this.client.isES6AsyncTestcase || this.client.isES6AsyncTestHook); } get failFastMode() { return this.argv['fail-fast'] || this.settings.enable_fail_fast; } isComponentTestingMode() { return this.api.globals.component_tests_mode; } isE2EPreviewMode() { return this.argv['launch-url']; } shouldSkipTestsOnFail() { return this.skipTestcasesOnFail && !this.context.unitTestingMode || this.failFastMode; } async initCommon(opts = {}) { if (!this.settings.unit_testing_mode) { this.addPropertiesToGlobalScope(); } await this.initClient(opts); if (this.isE2EPreviewMode()) { this.context = { getDesiredCapabilities() {}, isDisabled() { return false; } }; this.reporter = { registerTestError(err) { Logger.error(err); } }; } else { await this.createContext(opts); } this.mochaContextResolve(this.context); this.setSuiteName(); this.setRetries(); } async init(opts = {}) { await this.initCommon(opts); this.updateClient(); this.setupHooks(); return this; } setModulePath(file) { this.modulePath = file; this.context.modulePath = file; } validateNightwatchInspectorCriteria() { return ( this.argv.debug && Concurrency.isMasterProcess() && this.client.api.isChrome() ); } async initClient({initialize = true} = {}) { const settings = Object.assign({}, this.settings); this.client = NightwatchClient.create(settings, this.argv); if (initialize) { await this.client.initialize(); } if (this.validateNightwatchInspectorCriteria()) { this.globalsInstance.inspectorServer?.closeSocket(); this.globalsInstance.inspectorServer = new NightwatchInspectorServer(this.client); } } updateClient() { // this is necessary because mocha as a different suite setup flow if (!this.settings.disable_global_apis) { Object.defineProperty(global, 'browser', { configurable: true, get: function() { return this.client.api; }.bind(this) }); } if (this.settings.sync_test_names) { this.client.mergeCapabilities({ name: this.suiteName }); } if (this.context.getDesiredCapabilities()) { this.client.mergeCapabilities(this.context.getDesiredCapabilities()); } this.client.createTransport(); if (!this.isE2EPreviewMode()) { this.createReporter(); this.client .setReporter(this.reporter) .setCurrentTest(); } return this; } async retrySuite() { this.suiteRetries.incrementSuiteRetriesCount(); await this.terminate('RETRY_SUITE'); await this.createContext({reloadModuleCache: this.context.usingBddDescribe}); await this.createClient(); this.setupHooks(); return this.run(); } createReporter() { if (!this.context) { throw new Error('Context must be created before creating the reporter.'); } const {suiteRetries, suiteName} = this; const {tests, moduleKey, modulePath, groupName, skippedTests, allScreenedTests} = this.context; this.reporter = new Reporter({ settings: this.client.settings, tests, suiteRetries, addOpts: { reporter: this.argv.reporter, suiteName, moduleKey, modulePath, reportPrefix: '', reportFileName: this.argv['report-filename'], groupName, isMobile: this.client.api.isMobile(), tags: this.context.getTags() }, skippedTests, allScreenedTests }); } async createClient(client = null) { if (client) { this.client = client; return this; } await this.initClient(); this.updateClient(); return this; } async createContext({context = null, reloadModuleCache = false, suiteTitle = null, attributes = {}} = {}) { if (context) { this.context = context; return this; } const {modulePath, settings, argv, client} = this; this.context = new Context({modulePath, settings, argv, attributes}); if (settings.tag_filter && settings.tag_filter.length > 0) { reloadModuleCache = true; } this.context.setReloadModuleCache(reloadModuleCache); await this.context.init({usingMocha: this.usingMocha, suiteTitle, client}); this.context.setReportKey(this.allModulePaths); return this; } addPropertiesToGlobalScope() { if (this.settings.disable_global_apis) { return null; } Object.defineProperty(global, 'app', { configurable: true, get: function() { return global.browser; } }); Object.defineProperty(global, 'by', { configurable: true, get: function() { return By; } }); Object.defineProperty(global, 'By', { configurable: true, get: function() { return By; } }); Object.defineProperty(global, 'locateWith', { configurable: true, get: function() { return locateWith; } }); Object.defineProperty(global, 'withTagName', { configurable: true, get: function() { return withTagName; } }); Object.defineProperty(global, 'Keys', { configurable: true, get: function() { return Key; } }); Object.defineProperty(global, 'element', { configurable: true, value: function(locator, options = {}) { return ElementGlobal.element({locator, testSuite: this, options, client: this.client}); }.bind(this), writable: false }); if (this.settings.disable_global_expect) { return null; } const globalExpect = function(...args) { return global.browser.expect(...args); }; Object.defineProperty(globalExpect, 'element', { value: function(...args) { return global.browser.expect.element(...args); }, writable: false }); Object.defineProperty(globalExpect, 'elements', { value: function(...args) { return global.browser.expect.elements(...args); }, writable: false }); Object.defineProperty(globalExpect, 'component', { value: function(...args) { return global.browser.expect.component(...args); }, writable: false }); Object.defineProperty(globalExpect, 'title', { value: function(...args) { return global.browser.expect.title(...args); }, writable: false }); Object.defineProperty(globalExpect, 'cookie', { value: function(...args) { return global.browser.expect.cookie(...args); }, writable: false }); Object.defineProperty(globalExpect, 'url', { value: function(...args) { return global.browser.expect.url(...args); }, writable: false }); Object.freeze(globalExpect); Object.defineProperty(global, 'expect', { configurable: true, get: function() { return globalExpect; } }); } setUncaughtError(err) { this.uncaughtError = err; } setReportPrefix(data) { this.settings.report_prefix = this.__reportPrefix = ''; if (!data) { return this; } const capabilities = data.capabilities || {}; const browserName = (capabilities.browserName && capabilities.browserName.toUpperCase()) || ''; const browserVersion = capabilities.version || capabilities.browserVersion || ''; const platformVersion = capabilities.platform || capabilities.platformVersion || ''; if (!this.context.unitTestingMode) { this.settings.report_prefix = this.__reportPrefix = `${browserName}_${browserVersion}_${platformVersion}_`.replace(/ /g, '_'); } this.reporter.setFileNamePrefix(this.reportPrefix); return this; } setRetries() { if (this.isE2EPreviewMode()) { return this; } this.suiteRetries = new SuiteRetries({ retries: this.context.retries.testcase || this.argv.retries, suiteRetries: this.context.retries.suite || this.argv.suiteRetries }); return this; } setSuiteName() { if (this.isE2EPreviewMode()) { this.suiteName = 'Preview'; } else { this.suiteName = this.context.getSuiteName(); } return this; } /** * Instantiates the test hooks * * @return {TestSuite} */ setupHooks() { this.hooks = new TestHooks(this.context, { asyncHookTimeout: this.settings.globals.asyncHookTimeout }); return this; } runHook(hookName) { Logger.log(`${Logger.colors.green('→')} Running [${hookName}]:`); if (this.context.hasHook(hookName)) { NightwatchEventHub.emit(TestSuiteHook[hookName].started, this.reporter.testResults.eventDataToEmit); } if (hookName === 'beforeEach' || hookName === 'afterEach') { this.client.reporter.markHookRun(hookName); } if (hookName === 'before' || hookName === 'after') { this.client.reporter.setCurrentSection({testName: `__${hookName}_hook`}); } return this.handleRunnable(hookName, () => { return this.hooks[hookName].run(this.client); }).then(result => { if (this.context.hasHook(hookName)) { NightwatchEventHub.emit(TestSuiteHook[hookName].finished, this.reporter.testResults.eventDataToEmit); } if (hookName === 'beforeEach' || hookName === 'afterEach') { this.client.reporter.unmarkHookRun(hookName); } Logger.log(`${Logger.colors.green('→')} Completed [${hookName}].`); return result; }); } runGlobalHook(hookName) { const {globals} = this.settings; if (globals[hookName]) { NightwatchEventHub.emit(GlobalHook[hookName].started, this.reporter.testResults.eventDataToEmit); } if (!this.globalHooks[hookName]) { return Promise.resolve(); } this.client.reporter.setCurrentSection({testName: `__global_${hookName}_hook`}); return this.handleRunnable(hookName, async () => { if (this.globalsInstance) { await this.globalsInstance.runPluginHook(hookName, [this.settings]); } const result = await this.globalHooks[hookName].run(this.client); if (globals[hookName]) { NightwatchEventHub.emit(GlobalHook[hookName].finished, this.reporter.testResults.eventDataToEmit); } this.onTestSectionFinished(); return result; }); } createSession() { if (this.client.sessionId || this.context.unitTestingMode) { return Promise.resolve(); } const {argv, reuseBrowser} = this; const {moduleKey} = this.context; return this.client.createSession({argv, moduleKey, reuseBrowser}).catch(err => { err.sessionCreate = true; this.reporter.registerTestError(err); throw err; }); } async startTestSuite() { NightwatchEventHub.emit(TestSuiteHook.started, this.reporter.testResults.eventDataToEmit); if (!this.client) { await this.createClient(); this.setupHooks(); } return this.createSession() .then(data => { this.setReportPrefix(data); // Adding session info to report if (data) { this.reporter.setSessionInfo(data); if (data.capabilities && data.capabilities['safari:deviceUDID']) { this.settings.deviceUDID = data.capabilities['safari:deviceUDID']; } } this.commandQueue.tree.on('asynctree:command:finished', this.onCommandFinished.bind(this)); return this.runGlobalHook('beforeEach'); }); } /** * Runs the test suite, including all hooks */ runTestSuite() { if (this.isE2EPreviewMode()) { return this.createSession() .then(_ => { const runnable = new Runnable('preview', _ => { const url = isString(this.argv.preview) ? this.argv.preview : this.api.launchUrl; this.api.url(url).pause(); }); return runnable.run(this.commandQueue); }) .then(_ => this.stopSession()); } return this.startTestSuite() .then(() => this.runHook('before')) .then(result => { this.onTestSectionFinished(); if (result instanceof Error) { Logger.error(result); } return this.runNextTestCase(); }) .catch(err => { if (err.sessionCreate) { throw err; } if (!this.isAssertionError(err) && !this.context.unitTestingMode && !this.failFastMode) { if (!this.shouldSkipTestsOnFail()){ Logger.error(err); } err.displayed = true; } // testcase failed - this catch ensures that the after hook is being called return err; }) .then(possibleError => { this.reporter.resetCurrentTestName(); return this.runHook('after') .catch(err => { // exceptions from after hook if (!possibleError) { possibleError = err; } return err; }) .then(result => { if ((possibleError instanceof Error) && this.failFastMode) { throw possibleError; } this.onTestSectionFinished(); return result; }); }) .catch(err => { // testsuite failed - this catch ensures that the global afterEach will always run return this.terminate('FAILED', err); }) .then((errorOrFailures) => this.onTestSuiteFinished(errorOrFailures)); } onTestSectionFinished() { // called for all test cases and hooks (except for [before|after]_each hooks) // ^ [before|after]_each hooks are considered part of the test case itself this.reporter.setTestStatus(); this.reporter.setTestSectionElapsedTime(); this.reporter.collectTestSectionOutput(); } onTestSuiteFinished(errorOrFailures = false) { this.__snapShot = undefined; return this.runGlobalHook('afterEach') .then(result => { if (!(errorOrFailures instanceof Error)) { return result; } if ((errorOrFailures.sessionCreate && this.failFastMode)) { //throw errorOrFailures; } if (this.failFastMode && !this.shouldRetrySuite(errorOrFailures)) { throw errorOrFailures; } }) .catch(err => { if (err.sessionCreate || this.failFastMode) { throw err; } if (!err.displayed) { Logger.error(err); } // catching errors thrown inside the global afterEach return err; }) .then(failedResult => { if (this.shouldRetrySuite(errorOrFailures)) { return this.retrySuite(); } const failures = errorOrFailures || failedResult || !this.reporter.allTestsPassed; return this.stopSession(failures); }); } onCommandFinished({node, result}) { this.reporter.logCommandResult({node, result}); this.takeSnapshot(node); } async stopSession(failures) { try { await this.terminate(failures ? 'FAILED' : ''); } catch (err) { Logger.error(`Could not stop session in ${this.suiteName}:`); Logger.error(err); } return this.testSuiteFinished(failures); } sendReportToParentWorker() { if (this.settings.use_child_process && typeof process.send === 'function') { process.send(SafeJSON.stringify({ type: 'testsuite_finished', itemKey: process.env.__NIGHTWATCH_ENV_LABEL, results: this.reporter.exportResults(), httpOutput: Logger.collectOutput() })); } else if (process.port && typeof process.port.postMessage === 'function') { process.port.postMessage(SafeJSON.stringify({ type: 'testsuite_finished', results: this.reporter.exportResults(), httpOutput: Logger.collectOutput() })); } } testSuiteFinished(failures) { this.reporter.testSuiteFinished(); this.currentRunnable = null; if (Concurrency.isWorker()) { this.sendReportToParentWorker(); } NightwatchEventHub.emit(TestSuiteHook.finished, this.reporter.testResults.eventDataToEmit); return failures; } async sessionFinished(reason) { let lastError = null; if (reason === 'FAILED' || reason === 'RETRY_SUITE') { lastError = this.reporter.testResults.lastError; } await this.transport.sessionFinished(reason, lastError); } async terminate(reason = 'SIGINT', potentialError = null, endSession = !this.reuseBrowser) { this.resetQueue(); if (!this.client.sessionId || this.context.unitTestingMode) { try { await this.sessionFinished(reason); } catch (err) { return err; } return potentialError; } if (!this.endSessionOnFail && reason === 'FAILED') { // Keep the session open; avoid reusing of same session Transport.driver = null; Transport.driverService = null; return potentialError; } if (endSession) { const runnable = new Runnable('terminate', _ => { if (this.api && isFunction(this.api.end)) { this.api.end(endSession); } }, { isES6Async: this.isES6Async }); return runnable .run(this.commandQueue) .then(async result => { try { await this.sessionFinished(reason); } catch (err) { return err; } return result; }) .then(result => { if ((potentialError instanceof Error) && (potentialError.sessionCreate || this.failFastMode)) { return potentialError; } return result; }); } } setReporterCurrentTest() { // called for every test case (not for hooks) this.reporter.setCurrentTest(this.testcase, this.context); this.client.setCurrentTest(); return this; } /** * Sets the next testcase and starts running it, if there is one * * @return {Promise} */ runNextTestCase() { const nextTestCase = this.context.getNextKey(); if (nextTestCase) { return this.runCurrentTest(nextTestCase); } return Promise.resolve(); } /** * Runs the current testcase, including retries * * @param testName * @return {Promise} */ runCurrentTest(testName) { const {reporter, context, settings} = this; this.testcase = new TestCase(testName, { context, settings, reporter, addtOpts: { retriesCount: this.suiteRetries.testRetriesCount[testName], maxRetries: this.suiteRetries.testMaxRetries } }); this.setReporterCurrentTest().emptyQueue(); return this.createSession() .then(() => this.runHook('beforeEach')) .then(_ => { this.client.reporter.markHookRun('testcase'); NightwatchEventHub.emit(TestSuiteHook.test.started, { ...this.reporter.testResults.eventDataToEmit, testcase: this.reporter.testResults.currentTestName, settings: this.reporter.settings, testCaseData: this.testcase }); return this.handleRunnable(this.testcase.testName, () => this.testcase.run(this.client)); }) .catch(err => { return err; }) .then(possibleError => { this.client.reporter.unmarkHookRun(); NightwatchEventHub.emit(TestSuiteHook.test.finished, { ...this.reporter.testResults.eventDataToEmit, testcase: this.reporter.testResults.currentTestName, settings: this.reporter.settings, testCaseData: this.testcase }); if (this.transport.driverService && this.reporter.testResults) { this.reporter.testResults.setSeleniumLogFile(this.transport.driverService.getSeleniumOutputFilePath()); } // if there was an error in the testcase and skip_testcases_on_fail, we must send it forward, but after we run afterEach and after hooks return this.runHook('afterEach') .then(() => this.testCaseFinished()) .then(() => possibleError); }) .then(possibleError => { if (this.shouldRetryTestCase()) { return this.retryCurrentTestCase(); } if ((possibleError instanceof Error) && this.shouldSkipTestsOnFail()) { throw possibleError; } return this.runNextTestCase(); }); } testCaseFinished() { this.reporter.setElapsedTime(); this.onTestSectionFinished(); if (!this.testcase) { return Promise.resolve(); } return this.reporter.printTestResult(); } shouldRetryTestCase() { return !this.reporter.currentTestCasePassed && this.suiteRetries.shouldRetryTest(this.testcase.testName); } retryCurrentTestCase() { const currentTestName = this.testcase.testName; this.suiteRetries.incrementTestRetriesCount(currentTestName); this.reporter.resetCurrentTestPassedCount(); this.reporter.testResults.retryTest = true; this.commandQueue.clearScheduled(); return this.runCurrentTest(currentTestName); } isScreenshotEnabled() { return this.settings.screenshots.enabled; } shouldTakeScreenshotOnError() { return this.isScreenshotEnabled() && this.settings.screenshots.on_error; } shouldTakeScreenshotOnFailure() { return this.isScreenshotEnabled() && this.settings.screenshots.on_failure; } getScreenshotFilenamePath() { return Screenshots.getFileName({ testSuite: this.api.currentTest.module, testCase: this.api.currentTest.name }, this.settings.screenshots); } takeScreenshot() { const fileNamePath = this.getScreenshotFilenamePath(); const runnable = new Runnable('screenshot', _ => { return new Promise((resolve) => { this.api.saveScreenshot(fileNamePath, (result, err) => { if (!err && this.transport.isResultSuccess(result)) { const assertions = this.api.currentTest.results.assertions || []; const commands = this.reporter.currentSection.commands || []; if (assertions.length > 0) { const currentAssertion = assertions[assertions.length - 1]; const currentCommand = commands[commands.length - 1]; if (currentAssertion) { currentAssertion.screenshots = currentAssertion.screenshots || []; currentAssertion.screenshots.push(fileNamePath); } if (currentCommand) { currentCommand.screenshot = fileNamePath; } } } else { Logger.warn('Error saving screenshot...', err || result); } resolve(); }); }); }); return runnable.run(this.commandQueue); } takeSnapshot(node) { const commands = this.reporter.currentSection.commands || []; const currentCommand = commands[commands.length - 1]; if (this.settings.trace.enabled && node.isTraceable) { const snapShotPath = Snapshots.getFileName({ testSuite: this.api.currentTest.module, commandName: node.fullName, traceSettings: this.settings.trace, output_folder: this.settings.output_folder }); this.__snapShot = new Promise((resolve, reject) => { this.api.saveSnapshot(snapShotPath, (result => { if (currentCommand) { currentCommand.domSnapshot = result; } resolve(result); })); }); } else { if (currentCommand && this.__snapShot) { this.__snapShot.then(prevSnapshot => { currentCommand.domSnapshot = prevSnapshot; }); } } } shouldRetrySuite(failures = false) { if ((failures instanceof Error) && alwaysDisplayError(failures)) { return false; } return (failures || !this.reporter.allTestsPassed) && this.suiteRetries.shouldRetrySuite(); } async executeRunnable(name, fn) { this.currentRunnable = new Runnable(name, fn, { isES6Async: this.isES6Async }); if (!this.context) { return; } this.context.setCurrentRunnable(this.currentRunnable); try { const result = await this.currentRunnable.run(this.commandQueue); return result; } catch (err) { return new Promise((resolve, reject) => { setTimeout(() => { reject(err); }, 50); }); } } async handleRunnable(name, fn) { try { const result = await this.executeRunnable(name, fn); return result; } catch (err) { // if some other error was thrown, jump to the next catch err.name = err.name || ''; if (!this.isAssertionError(err) && err.name !== 'TypeError') { // registering non-assert errors this.reporter.registerTestError(err); if (this.shouldTakeScreenshotOnError()) { await this.takeScreenshot(); } throw err; } // if the assertion error was thrown by another assertion library if (!(err instanceof NightwatchAssertError)) { const failureMessage = `expected "${err.expected}" but got: "${err.actual}"`; if (err.actual !== undefined && err.expected !== undefined) { err.message += ` - ${failureMessage}`; } Logger.error(err); this.reporter.registerFailed(err); this.reporter.logAssertResult({ name: err.name, message: err.message, stackTrace: err.stack, fullMsg: err.message, failure: failureMessage }); } if (this.shouldTakeScreenshotOnFailure()) { await this.takeScreenshot(); } // clearing the queue here to avoid continuing with the rest of the testcase, // unless abortOnFailure is set to false if (!this.isAssertionError(err) || err.abortOnFailure) { this.emptyQueue(); } // set to true inside before/beforeEach hooks if (err.skipTestCases) { throw err; } return err; } } print() { if (this.isE2EPreviewMode()) { Logger.info('Previewing...'); return this; } if (this.settings.output) { let testSuiteDisplay; const retriesCount = this.suiteRetries.suiteRetriesCount; if (this.context.unitTestingMode) { testSuiteDisplay = this.context.moduleName || this.context.moduleKey; } else { testSuiteDisplay = `[${this.suiteName}] Test Suite`; } if (this.settings.test_workers && !this.settings.live_output || this.context.unitTestingMode) { // eslint-disable-next-line no-console console.log(''); } if (retriesCount > 0) { // eslint-disable-next-line no-console console.log('\nRetrying: ', Logger.colors.red(testSuiteDisplay), `(${retriesCount}/${this.suiteRetries.suiteMaxRetries}): `); } else if (this.context.unitTestingMode) { // eslint-disable-next-line no-console (this.context.isDisabled() ? Logger.info : console.log)(Logger.colors.cyan('[' + this.context.moduleKey + ']')); } else { Logger[this.context.isDisabled() ? 'info' : 'logDetailedMessage'](`\n${Logger.colors.cyan(testSuiteDisplay)}`); } if (!this.context.unitTestingMode) { Logger[this.context.isDisabled() ? 'info' : 'logDetailedMessage'](Logger.colors.purple(new Array(Math.min(testSuiteDisplay.length * 2 + 1, 80)).join('─'))); } } return this; } resetQueue() { if (!this.commandQueue) { return this; } this.commandQueue.reset().removeAllListeners(); return this; } emptyQueue() { this.resetQueue(); if (this.commandQueue) { this.commandQueue.empty(); } return this; } /** * * @return {*} */ run() { this.print(); if (this.context.isDisabled()) { // eslint-disable-next-line no-console console.log(Logger.colors.green(`Testsuite "${this.context.moduleName}" is disabled, skipping...`)); // send report even if test is skipped if (Concurrency.isWorker()) { this.sendReportToParentWorker(); } return Promise.resolve(); } return this.runTestSuite(); } isAssertionError(err) { return (err instanceof AssertionError) || err.name.startsWith('AssertionError'); } } module.exports = TestSuite; module.exports.Context = Context; ================================================ FILE: lib/testsuite/interfaces/common.js ================================================ const TestHooks = require('../hooks.js'); module.exports = class CommonInterface { static get TestHooks() { return Object.keys(TestHooks.TEST_HOOKS); } static get DEFAULT_ATTRIBUTES() { return { '@unitTest': false, '@name': undefined, '@endSessionOnFail': undefined, '@skipTestcasesOnFail': undefined, '@disabled': false, '@desiredCapabilities': null, '@tags': null }; } constructor(instance) { this.instance = instance; this.modulePath = instance.modulePath; this.currentTest = instance.currentTest; } createInterface() {} }; ================================================ FILE: lib/testsuite/interfaces/describe.js ================================================ const Utils = require('../../utils'); const Common = require('./common.js'); /* describe('test suite', function() { this.timeout(1000); }); */ class DescribeInstance { constructor({describeTitle, instance, client}) { this['[instance]'] = instance; this['[attributes]'] = {}; this['[client]'] = client; this.define('@name', describeTitle); } ///////////////////////////////////////////////// // Attributes ///////////////////////////////////////////////// get name() { return this['[attributes]']['@name']; } set tags(value) { this.define('@tags', value); } get tags() { return this['[attributes]']['@tags']; } set unitTest(value) { this.define('@unitTest', value); } get unitTest() { return this['[attributes]']['@unitTest']; } set endSessionOnFail(value) { this.define('@endSessionOnFail', value); } get endSessionOnFail() { return this['[attributes]']['@endSessionOnFail']; } set skipTestcasesOnFail(value) { this.define('@skipTestcasesOnFail', value); } get skipTestcasesOnFail() { return this['[attributes]']['@skipTestcasesOnFail']; } set disabled(value) { this.define('@disabled', value); } get disabled() { return this['[attributes]']['@disabled']; } set desiredCapabilities(value) { if (Utils.isObject(value) || Utils.isFunction(value)) { this.define('@desiredCapabilities', value); } } get desiredCapabilities() { return this['[client]'].initialCapabilities; } ///////////////////////////////////////////////// // Getters ///////////////////////////////////////////////// get page() { if (this['[client]'] && this['[client]'].api) { return this['[client]'].api.page; } return null; } get globals() { return this.settings.globals; } get settings() { if (this['[client]']) { return this['[client]'].settings; } return this['[instance]'].settings; } get argv() { return this['[instance]'].argv; } timeout(value) { this.globals.waitForConditionTimeout = value; this.globals.retryAssertionTimeout = value; this.globals.unitTestsTimeout = value; } waitForTimeout(value) { if (typeof value == 'undefined') { return this.globals.waitForConditionTimeout; } return this.timeout(value); } waitForRetryInterval(value) { if (typeof value == 'undefined') { return this.globals.waitForConditionPollInterval; } return this.retryInterval(value); } retryInterval(value) { this.globals.waitForConditionPollInterval = value; } retries(n) { this['[instance]'].setTestcaseRetries(n); } suiteRetries(n) { this['[instance]'].setSuiteRetries(n); } define(name, value) { this['[attributes]'][name] = value; const isAttributeValid = Object.keys(Common.DEFAULT_ATTRIBUTES).includes(name) || Object.keys(Common.DEFAULT_ATTRIBUTES).includes(`@${name}`); if (isAttributeValid) { if (!name.startsWith('@')) { name = `@${name}`; } return this['[instance]'].setAttribute(name, value); } // eslint-disable-next-line no-console console.warn(`Attribute "${name}" is not a valid attribute. Valid attributes are: ${Object.keys(Common.DEFAULT_ATTRIBUTES).join(', ')}.`); } } class Describe extends Common { constructor(instance, client = null) { super(instance); this.describeFn = null; this.client = client; this.describeInstance = null; this.describeTitle = null; this.instance.on('pre-require', (context) => this.createInterface(context)); } createInstance(runOnly) { if (!Utils.isFunction(this.describeFn)) { throw new Error(`The describe/context must be a function. ${typeof this.describeFn} given.`); } const {describeTitle, instance, describeFn, client} = this; this.describeInstance = new DescribeInstance({ describeTitle, instance, client }); const {describeInstance} = this; describeFn.call(describeInstance); this.instance.setDescribeContext({describeTitle, describeInstance, runOnly}); } /** * Adds before, after, beforeEach, afterEach hooks to test suite * * @param context */ addHooks(context) { const hooksContext = Common.TestHooks.reduce((prev, hookName) => { prev[hookName] = hookFn => { this.instance.addTestHook(hookName, hookFn, this.describeInstance); }; return prev; }, {}); Object.assign(context, hooksContext); } addRun(context) { context.run = function() { // TODO: implement }; } createTestsuite({title, describeFn, context, runOnly = false}) { this.describeFn = describeFn; this.describeTitle = title; if (this.describeFn) { this.createInstance(runOnly); const testsuite = require.cache[this.instance.modulePath]; if (testsuite && testsuite.exports) { testsuite.exports['[@nightwatchDescribe]'] = true; } } } addDescribe(context) { context.xmodule = {}; context.describe = context.context = (title, describeFn) => { this.createTestsuite({ context, title, describeFn }); }; context.xdescribe = context.xcontext = context.describe.skip = (title, describeFn) => { this.instance.once('module-loaded', () => { // in case tests have been declared using other interfaces (e.g. exports), // we do not want to disable the suite. if (this.instance.tests.length === 0) { // if no tests are added after all interfaces are loaded, disable the suite. this.instance.setAttribute('@disabled', true); } }); }; context.describe.only = (title, describeFn) => { this.createTestsuite({ title, describeFn, context, runOnly: true }); }; } addTest(context) { context.it = context.specify = context.test = (testName, testFn) => { this.instance.addTestCase({testName, testFn, describeInstance: this.describeInstance}); }; context.xit = context.xspecify = context.xtest = context.it.skip = context.test.skip = (testName) => { this.instance.addTestCase({testName, testFn: function() {}, describeInstance: this.describeInstance, skipTest: true}); }; context.it.only = context.specify.only = context.test.only = (testName, testFn) => { this.instance.addTestCase({testName, testFn, describeInstance: this.describeInstance, runOnly: true}); }; } shouldReloadModuleCache() { if (this.instance.shouldReloadModuleCache()) { return true; } if (require.cache && require.cache[this.instance.modulePath]) { const testsuiteModule = require.cache[this.instance.modulePath]; if (testsuiteModule.exports && testsuiteModule.exports['[@nightwatchDescribe]']) { return true; } } return false; } createInterface(context) { // for suiteRetries to work with describe interface we need to re-require the file and clear the require cache if (this.shouldReloadModuleCache()) { delete require.cache[this.instance.modulePath]; } this.addHooks(context); this.addRun(context); this.addDescribe(context); this.addTest(context); } } module.exports = Describe; ================================================ FILE: lib/testsuite/interfaces/exports.js ================================================ const Common = require('./common.js'); const Utils = require('../../utils'); class Exports extends Common { get module() { return this.instance.module; } constructor(instance) { super(instance); this.moduleKeys = null; this.tests = null; this.hooks = null; this.instance.on('post-require', () => this.createInterface()); } includeTestcase(item) { return !Exports.TestHooks.includes(item) && (!this.currentTest || item === this.currentTest); } reduceKeys(testFn) { return this.moduleKeys .reduce((accumulator, item) => { if (testFn(item)) { accumulator.push(item); } return accumulator; }, []); } addTestCase(testName) { const testFn = this.module[testName]; this.instance.addTestCase({testName, testFn}); } addTestHook(key) { this.instance.addTestHook(key, this.module[key]); } loadHooks() { this.hooks = this.reduceKeys(item => Exports.TestHooks.includes(item)) .map(key => this.addTestHook(key)); } loadTests() { this.tests = this.reduceKeys(item => this.includeTestcase(item)) .map(key => this.addTestCase(key)); } createInterface() { this.moduleKeys = Object.keys(this.module).filter(key => { if (!this.module[key]) { return false; } return Utils.isFunction(this.module[key]); }); this.loadHooks(); this.loadTests(); this.readAttributes(); } getAttribute(attrName) { let value = this.module[attrName]; if (Utils.isUndefined(value)) { value = this.module[attrName.substring(1)]; if (Utils.isUndefined(value)) { value = this.instance.getAttribute(attrName); } } return value; } readAttributes() { const attributes = Object.keys(Common.DEFAULT_ATTRIBUTES).reduce((prev, key) => { const value = this.getAttribute(key); prev[key] = value !== undefined ? value : Common.DEFAULT_ATTRIBUTES[key]; return prev; }, {}); this.instance.addAttributes(attributes); } } module.exports = Exports; ================================================ FILE: lib/testsuite/nightwatch-inspector/index.js ================================================ const vm = require('vm'); const {crxfile} = require('@nightwatch/nightwatch-inspector'); const stripAnsi = require('strip-ansi'); const Debuggability = require('../../utils/debuggability'); const Utils = require('../../utils'); const WebSocket = require('./websocket-server.js'); const {Logger} = Utils; module.exports = class NightwatchInspectorServer extends WebSocket { constructor(client) { super(); this.client = client; this.initSocket(this.playgroundWrapper.bind(this)); this.addExtensionInChromeOption(); } /** * Adds extensions capabilities to the Chrome options */ addExtensionInChromeOption() { const {desiredCapabilities} = this.client.settings; const chromeOptions = desiredCapabilities['goog:chromeOptions']; const {args = []} = chromeOptions || {}; desiredCapabilities['goog:chromeOptions'] = { ...chromeOptions, extensions: [crxfile], args: [...args, '--auto-open-devtools-for-tabs'] }; } ////////////////////////////////////////////////////////////////////////////////////////// // Commands Execution and Response ////////////////////////////////////////////////////////////////////////////////////////// modifyCommandsResult(result, executedCommand, commandList) { let error; if (!result) { result = 'Success'; } else if (Utils.isErrorObject(result) || result.error) { result = result.message; result = stripAnsi(result); error = true; } return JSON.stringify({ result: result, error: error, executedCommand: executedCommand, commandList: commandList }); } getNightwatchCommands() { const {api} = this.client; const {assert, expect, verify, ensure} = api; return [ 'browser', ...[api, assert, expect, verify, ensure].reduce((keys, obj) => keys.concat(Object.keys(obj)), []) ]; } async executeCommands(data) { let result; const context = {browser: this.client.api}; const message = data.toString().replace('await ', ''); if (message === 'commandlist') { const commandList = this.getNightwatchCommands(); return this.modifyCommandsResult(null, message, commandList); } try { vm.createContext(context); Logger.log('Executed from browser : ', message); result = await vm.runInContext(message, context); } catch (err) { result = err; } return this.modifyCommandsResult(result, message); } async playgroundWrapper(data) { const isES6AsyncTestcase = this.client.isES6AsyncTestcase; Debuggability.debugMode = true; this.client.isES6AsyncTestcase = true; const result = await this.executeCommands(data); this.client.isES6AsyncTestcase = isES6AsyncTestcase; Debuggability.debugMode = false; return result; } }; ================================================ FILE: lib/testsuite/nightwatch-inspector/websocket-server.js ================================================ const {WebSocketServer} = require('ws'); const Utils = require('../../utils'); const {Logger} = Utils; module.exports = class Websocket { constructor() { this.portNumber = 10096; } initSocket(cb) { try { this.startServer(cb); } catch (e) { this.handleSocketError(e, cb); } } startServer(cb) { this._wss = new WebSocketServer({host: 'localhost', port: this.portNumber}); this._wss.on('error', (error) => { this.handleSocketError(error, cb); }); this._wss.on('listening', () => { Logger.log(`WebSocket server is listening on port ${this.portNumber}`); }); this._wss.on('connection', (ws) => { ws.on('message', async (data) => { const result = await cb(data); ws.send(result); }); }); } handleSocketError(e, cb) { if (e.code === 'EADDRINUSE') { Logger.warn(`Port ${this.portNumber} is already in use. Trying the next available port.`); this.portNumber++; } else { Logger.error(`Could not start WebSocket server on port ${this.portNumber}: ${e.message}`); } this.initSocket(cb); } get nwsocket() { return this._wss; } closeSocket() { Logger.info(`Attempting to close websocket server running on port ${this.portNumber}...`); this.nwsocket.close(); } }; ================================================ FILE: lib/testsuite/repl.js ================================================ const repl = require('repl'); const {Logger} = require('../utils'); const vm = require('vm'); module.exports = class NightwatchRepl { constructor(config) { this._config = Object.assign({ eval: this._eval.bind(this), useGlobal: false, // so that REPL does not create a separate context to pass to eval. preview: true, timeout: 5500 // timeout for assertions is 5000 ms. }, config); } static introMessage() { return ` DEBUG MODE on... Type any Nightwatch command to execute in the browser in real-time. Try, ${Logger.colors.cyan('browser.navigateTo(\'https://nightwatchjs.org\');')} (To exit, press Ctrl+C twice or type .exit) `; } startServer(context) { vm.createContext(context); this._context = context; // Start REPLServer this._replServer = repl.start(this._config); } _eval(cmd, _, filename, callback) { // Evaluate for output previews. if (/^try { .+ } catch {}$/.test(cmd)) { // if `cmd` represents a method call if (cmd.includes('(')) { return; } return this._outputPreview(cmd, callback); } // A command is already running whose result is awaited. if (this._resultAwaited) { return; } try { const result = vm.runInContext(cmd, this._context); this._handleResult(result, callback); } catch (err) { const errRegex = /^(Unexpected end of input|Unexpected token)/; if (err.name === 'SyntaxError' && errRegex.test(err.message)) { return callback(new repl.Recoverable(err)); } Logger.error(err); callback(); } } async _handleResult(result, callback) { const resultIsPromise = result instanceof Promise || (result && typeof result.then === 'function'); if (!resultIsPromise) { return callback(null, result); } this._resultAwaited = true; let timeoutCalled = false; const timeoutId = setTimeout( () => { this._resultAwaited = false; timeoutCalled = true; Logger.error('Timed out while waiting for response.'); callback(); }, this._config.timeout ); try { const res = await result; if (timeoutCalled) { return; } clearTimeout(timeoutId); this._resultAwaited = false; callback(null, res); } catch (err) { // When the promise is rejected, the error would have // already been logged by Nightwatch. // TODO: Should we close the REPL server here? // Assertions errors would have already been logged if (err.name !== 'NightwatchAssertError') { Logger.error(err); } if (timeoutCalled) { return; } clearTimeout(timeoutId); this._resultAwaited = false; callback(); } } _outputPreview(cmd, callback) { const regex = /^try { (.+) } catch {}$/; const match = cmd.match(regex); if (match) { const actualCmd = match[1]; const cmdArray = actualCmd.split('.'); try { const result = cmdArray.reduce((prevCmd, currCmd) => { return prevCmd[currCmd]; }, this._context); return callback(null, result); } catch (err) { return; } } } onExit(callback) { this._replServer.on('exit', callback); } }; ================================================ FILE: lib/testsuite/retries.js ================================================ class SuiteRetries { constructor({retries = 0, suiteRetries = 0}) { // testcase retries this.testMaxRetries = retries; this.testRetriesCount = {}; // suite retries this.suiteMaxRetries = suiteRetries; this.suiteRetriesCount = 0; } incrementTestRetriesCount(testName) { this.testRetriesCount[testName] = this.testRetriesCount[testName] || 0; this.testRetriesCount[testName]++; } incrementSuiteRetriesCount() { this.suiteRetriesCount++; } shouldRetryTest(testName) { if (this.testMaxRetries === 0) { return false; } this.testRetriesCount[testName] = this.testRetriesCount[testName] || 0; return this.testRetriesCount[testName] < this.testMaxRetries; } shouldRetrySuite() { return this.suiteRetriesCount < this.suiteMaxRetries; } } module.exports = SuiteRetries; ================================================ FILE: lib/testsuite/runnable.js ================================================ class Runnable { constructor(name, runFn, opts = {}) { this.name = name; this.resolved = false; this.deffered = { settled: false, /** * @type {Promise} */ promise: null, /** * @type {Function} */ rejectFn: null, /** * @type {Function} */ resolveFn: null, name }; this.isES6Async = opts.isES6Async; this.createPromise(); this.setRunFn(runFn); } get runFn() { return this.__runFn; } get currentPromise() { if (!this.deffered.promise) { return null; } return { runnable: this.name, resolve: this.deffered.resolveFn, reject: this.deffered.rejectFn, command: this.queue && this.queue.currentNode.name }; } setQueue(queue) { this.queue = queue; this.queue.reset(); return this; } /** * * @param {function} runFn */ setRunFn(runFn) { if (typeof runFn != 'function') { throw new Error(`Runnable must be a function. "${typeof runFn}" given.`); } this.__runFn = runFn; return this; } createPromise() { this.deffered.promise = new Promise((resolve, reject) => { this.deffered.resolveFn = (result) => { if (!this.deffered.settled) { resolve(result); } this.deffered.settled = true; }; this.deffered.rejectFn = (err, force = false) => { if (!this.deffered.settled || force) { reject(err); } this.deffered.settled = true; }; }); return this; } abort(err) { return new Promise((resolve) => { if (this.currentPromise && this.queueInProgress()) { return this.deffered.promise.then(_ => resolve()).catch(_ => resolve()); } resolve(); }).then(_ => { this.currentPromise.reject(err, true); return err; }); } setDoneCallback(cb) { const originalResolve = this.deffered.resolveFn; const originalReject = this.deffered.rejectFn; this.deffered.resolveFn = function() {}; this.deffered.rejectFn = function() {}; return (result) => { if (result instanceof Error) { originalReject(result); } else { originalResolve(result); } }; } run(queue = null) { this.setQueue(queue); let result; try { result = this.runFn(); } catch (err) { this.deffered.rejectFn(err); return this.deffered.promise; } if (result instanceof Promise) { const deferredFn = () => { result .then(res_ => { if (this.queueInProgress()) { return; } this.deffered.resolveFn(res_); }) .catch(err => { this.deffered.rejectFn(err); }); }; if (this.isES6Async) { // the timeout is needed for situations where there is an async function without any await commands setTimeout(() => deferredFn(), 20); // without .catch() here, we can get an unhandledRejection result .catch(err => { this.deffered.rejectFn(err); }); } else { deferredFn(); } } // `this.currentTestCaseResult` represents the return value of the currently // running test case or hook. // in case the runnable is executing something other than a test case/hook, // `this.currentTestCaseResult` will be `undefined`. if (this.currentTestCaseResult instanceof Promise) { this.currentTestCaseResult .catch(() => { // to avoid unhandledRejections // although the test case promises are already handled for rejections // above (`result.catch()`), if we don't use `.catch()` here again, // `.finally` will return a new promise that will be rejected without // any error handling. }) .finally(() => { // mark the promise as settled as a cue to the asynctree so that it // can get cleared out and subsequently call the queue `done` method. this.currentTestCaseResult.settled = true; // sometimes this promise is settled after the last call of // asynctree `done` method, so we need schedule a tree traversal // again to clear out the tree and call the queue `done` method. this.queue.scheduleTraverse(); }); } this.queue.run(this.currentTestCaseResult).then(err => { if (err) { return this.deffered.rejectFn(err); } this.deffered.resolveFn(result); }); return this.deffered.promise; } queueInProgress() { return this.queue.tree.inProgress; } } module.exports = Runnable; ================================================ FILE: lib/testsuite/testcase.js ================================================ const BaseHook = require('./hooks/_basehook.js'); const {Logger} = require('../utils'); class UnitTest extends BaseHook { get isGlobal() { return true; } get isUnitTest() { return true; } verifyMethod() { return this.context.getKey(this.key) || null; } } class TestCase { constructor(testName, {context, settings, reporter, options = {}}) { this.testName = testName; this.context = context; this.reporter = reporter; this.settings = settings; this.retriesCount = options.retriesCount; this.maxRetries = options.maxRetries; this.print(); this.startTime = new Date().getTime(); this.reportKey = `${this.context.moduleKey}/${testName}`; } print() { const {output, detailed_output} = this.settings; if (output && detailed_output && !this.context.unitTestsMode) { const {colors} = Logger; if (this.retriesCount > 0) { // eslint-disable-next-line no-console console.log('Retrying (' + this.retriesCount + '/' + this.maxRetries + '): ', colors.red(this.testName)); } else { this.reporter.logTestCase(this.testName); } } return this; } run(client = null) { this.client = client; try { let result; if (this.context.unitTestsMode) { result = this.runUnitTest(); } else { result = this.context.call(this.testName, this.client); } return result; } catch (err) { return Promise.reject(err); } } runUnitTest() { let unitTest = new UnitTest(this.testName, this.context, { asyncHookTimeout: this.settings.globals.unitTestsTimeout }); return unitTest.run(this.client); } } module.exports = TestCase; ================================================ FILE: lib/transport/errors/index.js ================================================ const ErrorCode = { INSECURE_CERTIFICATE: 'insecure certificate', ELEMENT_CLICK_INTERCEPTED: 'element click intercepted', ELEMENT_IS_NOT_SELECTABLE: 'element not selectable', ELEMENT_IS_NOT_INTERACTABLE: 'element not interactable', INVALID_ARGUMENT: 'invalid argument', INVALID_COOKIE_DOMAIN: 'invalid cookie domain', INVALID_ELEMENT_COORDINATES: 'invalid coordinates', INVALID_ELEMENT_STATE: 'invalid element state', INVALID_SELECTOR: 'invalid selector', NO_SUCH_SESSION: 'invalid session id', JAVASCRIPT_ERROR: 'javascript error', MOVE_TARGET_OUT_OF_BOUNDS: 'move target out of bounds', NO_SUCH_ALERT: 'no such alert', NO_SUCH_COOKIE: 'no such cookie', NO_SUCH_ELEMENT: 'no such element', NO_SUCH_FRAME: 'no such frame', NO_SUCH_WINDOW: 'no such window', SCRIPT_TIMEOUT: 'script timeout', SESSION_NOT_CREATED_EXCEPTION: 'session not created', STALE_ELEMENT_REFERENCE: 'stale element reference', TIMEOUT: 'timeout', UNABLE_TO_SET_COOKIE: 'unable to set cookie', UNABLE_TO_CAPTURE_SCREEN: 'unable to capture screen', UNEXPECTED_ALERT_OPEN: 'unexpected alert open', UNKNOWN_COMMAND: 'unknown command', UNKNOWN_ERROR: 'unknown error', UNKNOWN_METHOD: 'unknown method', UNSUPPORTED_OPERATION: 'unsupported operation' }; const Errors = { [ErrorCode.UNEXPECTED_ALERT_OPEN]: { message: 'A modal dialog was open, blocking this operation.' }, [ErrorCode.SESSION_NOT_CREATED_EXCEPTION]: { message: 'A new session could not be created.' }, [ErrorCode.NO_SUCH_ALERT]: { message: 'An attempt was made to operate on a modal dialog when one was not open.' }, [ErrorCode.INVALID_ELEMENT_STATE]: { message: 'An element command could not be completed because the element is in an invalid state (e.g. attempting to click an element that is no longer attached to the document).' }, [ErrorCode.NO_SUCH_ELEMENT]: { message: 'An element could not be located on the page using the given search parameters.', help: [ 'Please inspect the html before the step', 'Verify if an element with the mentioned selector is present in the DOM tree' ] }, [ErrorCode.JAVASCRIPT_ERROR]: { message: 'An error occurred while executing user supplied JavaScript.' }, [ErrorCode.UNKNOWN_ERROR]: { message: 'An unknown server-side error occurred while processing the command.' }, [ErrorCode.NO_SUCH_COOKIE]: { message: 'No cookie matching the given path name was found amongst the cookies of the current active document.' }, [ErrorCode.INSECURE_CERTIFICATE]: { message: 'The SSL certificate running on this host cannot be validated. ' + 'If you wish to force accepting insecure SSL certificates, set acceptInsecureCerts=true in the ' + 'desiredCapabilities options.' }, [ErrorCode.INVALID_ARGUMENT]: { message: 'The arguments passed to the command are either invalid or malformed.' }, [ErrorCode.INVALID_ARGUMENT]: { message: 'The arguments passed to the command are either invalid or malformed.' }, [ErrorCode.STALE_ELEMENT_REFERENCE]: { message: 'The command failed because the referenced element is no longer attached to the DOM.' }, [ErrorCode.INVALID_COOKIE_DOMAIN]: { message: 'The cookie domain name is not valid for the current page.' }, [ErrorCode.INVALID_ELEMENT_COORDINATES]: { message: 'The coordinates provided to an interactions operation are invalid.' }, [ErrorCode.ELEMENT_CLICK_INTERCEPTED]: { message: 'The element click command could not be completed because another element is receiving the click event.' }, [ErrorCode.TIMEOUT]: { message: 'The operation did not complete before its timeout expired.' }, [ErrorCode.UNABLE_TO_SET_COOKIE]: { message: 'The request to set a cookie\'s value could not be satisfied.' }, [ErrorCode.UNKNOWN_METHOD]: { message: 'The requested command matched a known URL but did not match a method for that URL.' }, [ErrorCode.ELEMENT_IS_NOT_SELECTABLE]: { message: 'The requested element cannot be selected.' }, [ErrorCode.ELEMENT_IS_NOT_INTERACTABLE]: { message: 'The requested element is not pointer or keyboard interactable.' }, [ErrorCode.UNKNOWN_COMMAND]: { message: 'The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.' }, [ErrorCode.UNABLE_TO_CAPTURE_SCREEN]: { message: 'The screen capture failed.' }, [ErrorCode.SCRIPT_TIMEOUT]: { message: 'The script did not complete before its timeout expired.' }, [ErrorCode.NO_SUCH_SESSION]: { message: 'The session is either terminated or not started.' }, [ErrorCode.NO_SUCH_FRAME]: { message: 'The specified frame could not be found.', help: [ 'Please inspect the html before the step.', 'Verify if an iframe with the id is present in the DOM tree.' ] }, [ErrorCode.NO_SUCH_WINDOW]: { message: 'The specified window could not be found.', help: [ 'Print existing window handles by using the command "print driver.window_handles".', 'Verify if the window name you have entered exists in the list.' ] }, [ErrorCode.INVALID_SELECTOR]: { message: 'The supplied argument was an invalid selector (e.g. XPath/CSS).' }, [ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS]: { message: 'The target for mouse interaction is not in the browser\'s viewport and cannot be brought into the viewport.' }, [ErrorCode.UNSUPPORTED_OPERATION]: { message: 'Unsupported operation exception.' } }; const IosSessionErrors = { SessionNotCreatedError: ErrorCode.SESSION_NOT_CREATED_EXCEPTION, UnsupportedOperationError: ErrorCode.UNSUPPORTED_OPERATION }; const SeleniumNightwatchErrorCodeMap = { NoSuchElementError: ErrorCode.NO_SUCH_ELEMENT }; module.exports = { findErrorById(statusCode) { return { status: statusCode, id: statusCode, message: Errors[statusCode].message }; }, getErrorObject(err) { if (err && (err.name in SeleniumNightwatchErrorCodeMap)) { const statusCode = SeleniumNightwatchErrorCodeMap[err.name]; for (const [key, value] of Object.entries(Errors[statusCode])) { err[key] ||= value; } err.id = statusCode; return err; } else if (err && err.name && (err.stack || err.stackTrace)){ return err; } if (err instanceof Error) { return err; } let error; if (typeof err == 'string') { error = new Error(err); } else if (err.error instanceof Error) { return err.error; } error = error || new Error('unknown error'); return error; }, StatusCode: ErrorCode, Response: Errors, IosSessionErrors: IosSessionErrors }; ================================================ FILE: lib/transport/factory.js ================================================ const {Browser, Capabilities} = require('selenium-webdriver'); const BrowsersLowerCase = { chrome: Browser.CHROME, firefox: Browser.FIREFOX, safari: Browser.SAFARI, microsoftedge: Browser.EDGE, msedge: Browser.EDGE, edge: Browser.EDGE, ie: Browser.INTERNET_EXPLORER, 'internet explorer': Browser.INTERNET_EXPLORER }; module.exports = class TransportFactory { static createSeleniumService(cliRunner) { const usingAppium = cliRunner.test_settings.selenium.use_appium; const Server = usingAppium ? require('./selenium-webdriver/appium.js') : require('./selenium-webdriver/selenium.js'); cliRunner.seleniumService = Server.createService(cliRunner.test_settings); cliRunner.test_settings.selenium['[_started]'] = true; return cliRunner.seleniumService.init(); } static usingSeleniumServer(settings) { if (!settings.selenium) { return false; } let {start_process, host} = settings.selenium; if (start_process) { return true; } if (!host) { host = settings.selenium_host || settings.seleniumHost; } return host && !settings.webdriver.start_process; } static adaptWebdriverSettings({settings, usingSeleniumServer = false}) { if (!usingSeleniumServer && !(settings.capabilities instanceof Capabilities)) { settings.capabilities = Object.assign({}, settings.desiredCapabilities); } } static usingBrowserstackTurboScale(settings) { return settings.webdriver.host && (settings.webdriver.host.endsWith('.browserstack-ats.com') || settings.webdriver.host.startsWith('browserstack-turboscale-grid')); } static usingBrowserstack(settings) { return settings.webdriver.host && (settings.webdriver.host.endsWith('.browserstack.com') || TransportFactory.usingBrowserstackTurboScale(settings)); } static getBrowserName(nightwatchInstance) { const {settings, argv = {}} = nightwatchInstance; const capabilities = settings.capabilities || settings.desiredCapabilities; if (capabilities instanceof Capabilities) { return capabilities.getBrowserName(); } let {browserName} = settings.desiredCapabilities; // Allow to use any browserName with Appium and BrowserStack. // Not supporting '--browserName' cli flag for both of these. const usingAppium = TransportFactory.usingSeleniumServer(settings) && settings.selenium.use_appium; const usingBrowserStack = TransportFactory.usingBrowserstack(settings); if (usingAppium || usingBrowserStack) { if (BrowsersLowerCase[browserName && browserName.toLowerCase()]) { browserName = BrowsersLowerCase[browserName.toLowerCase()]; } return browserName; } // for backward compatibility if (browserName === null) { // eslint-disable-next-line no-console console.warn('DEPRECATED: Setting browserName=null for running Appium tests has been deprecated ' + 'and will not be supported in future versions. Set `use_appium` property in `selenium` config to true ' + 'in your Nightwatch configuration file to run Appium tests.'); settings.selenium.use_appium = true; return browserName; } if (argv.chrome) { browserName = Browser.CHROME; } else if (argv.firefox) { browserName = Browser.FIREFOX; } else if (argv.safari) { browserName = Browser.SAFARI; } else if (argv.edge) { browserName = Browser.EDGE; } else if (BrowsersLowerCase[browserName && browserName.toLowerCase()]) { browserName = BrowsersLowerCase[browserName.toLowerCase()]; } else { const didYouMean = require('didyoumean'); const browsersList = Object.values(Browser); const resultMeant = didYouMean(browserName, browsersList); throw new Error(`Unknown browser: "${browserName}"${resultMeant ? ('; did you mean "' + resultMeant + '"?') : ''}`); } return browserName; } static create(nightwatchInstance) { const {settings} = nightwatchInstance; const browserName = TransportFactory.getBrowserName(nightwatchInstance); const usingSeleniumServer = TransportFactory.usingSeleniumServer(settings); TransportFactory.adaptWebdriverSettings({settings, usingSeleniumServer}); return TransportFactory.createWebdriver(nightwatchInstance, {browserName, usingSeleniumServer}); } static createWebdriver(nightwatchInstance, {usingSeleniumServer, browserName}) { if (TransportFactory.usingBrowserstack(nightwatchInstance.settings)) { if (!browserName) { const AppAutomate = require('./selenium-webdriver/browserstack/appAutomate.js'); return new AppAutomate(nightwatchInstance, browserName); } if (TransportFactory.usingBrowserstackTurboScale(nightwatchInstance.settings)) { const AutomateTurboScale = require('./selenium-webdriver/browserstack/automateTurboScale.js'); return new AutomateTurboScale(nightwatchInstance, browserName); } const Automate = require('./selenium-webdriver/browserstack/automate.js'); return new Automate(nightwatchInstance, browserName); } if (usingSeleniumServer) { if (nightwatchInstance.settings.selenium.use_appium) { const Appium = require('./selenium-webdriver/appium.js'); return new Appium(nightwatchInstance, browserName); } const Selenium = require('./selenium-webdriver/selenium.js'); return new Selenium(nightwatchInstance, browserName); } let Driver; switch (browserName) { case Browser.FIREFOX: Driver = require('./selenium-webdriver/firefox.js'); break; case Browser.CHROME: Driver = require('./selenium-webdriver/chrome.js'); break; case Browser.EDGE: Driver = require('./selenium-webdriver/edge.js'); break; case Browser.SAFARI: Driver = require('./selenium-webdriver/safari.js'); break; default: throw new Error(`Unrecognized browser: ${browserName}.`); } return new Driver(nightwatchInstance); } }; ================================================ FILE: lib/transport/index.js ================================================ const EventEmitter = require('events'); const HttpRequest = require('../http/request.js'); const Factory = require('./factory.js'); const {Logger} = require('../utils'); class Transport extends EventEmitter { static get DO_NOT_LOG_ERRORS() { return [ 'Unable to locate element', '{"errorMessage":"Unable to find element', 'no such element' ]; } get Errors() { return require('./errors'); } static create(nightwatchInstance) { return Factory.create(nightwatchInstance); } /** * Called once a test suite has finished executing * @override * @param {Error|Boolean} failures * @returns {Promise} */ async testSuiteFinished(failures) {} /** * Called once a session has closed; a test suite can have multiple test cases and * hence multiple sessions can be started * @override * @returns {Promise} */ async sessionFinished() {} isResultSuccess() { return true; } /** * Resolves the element ID based on the current transport used * * @param {Object} result * @param {Element} element * @param {Boolean} multipleElements * @return {{value: *, status: number}|null} */ resolveElement(result, element, multipleElements) { if (!this.isResultSuccess(result)) { return null; } let {value} = result; if (multipleElements && Array.isArray(value) && value.length > 0) { if (value.length > 1) { let message = `More than one element (${value.length}) found for element <${element.toString()}> with selector: "${element.selector}".`; if (this.settings.globals.throwOnMultipleElementsReturned) { throw new Error(message); } if (!this.settings.globals.suppressWarningsOnMultipleElementsReturned) { Logger.warn(` Warning: ${message} Only the first one will be used.`); } } value = value[0]; } else if (Array.isArray(value) && value.length === 0) { value = null; } return value; } //////////////////////////////////////////////////////////////////// // Legacy Transport related //////////////////////////////////////////////////////////////////// /** * Used when the error object is not parsed to its specific type (such as when using ChromeDriver with w3c:false) * */ getRetryableErrorMessages() { const {StatusCode} = this.Errors; const filtered = Object.keys(StatusCode) .filter(key => [ 'STALE_ELEMENT_REFERENCE', 'ELEMENT_CLICK_INTERCEPTED', 'INVALID_ELEMENT_STATE', 'ELEMENT_IS_NOT_INTERACTABLE' ].includes(key)) .reduce((obj, key) => { return { ...obj, [key]: StatusCode[key] }; }, {}); return Object.values(filtered); } createHttpRequest(requestOptions) { return new HttpRequest(requestOptions); } runProtocolAction(requestOptions) { const request = this.createHttpRequest(requestOptions); return new Promise((resolve, reject) => { request .on('success', (result, response) => { if (result.error && result.status === -1) { const errorResult = this.handleProtocolError(result, response); return reject(errorResult); } resolve(result); }) .on('error', (result, response, screenshotContent) => { const errorResult = this.handleProtocolError(result, response, screenshotContent); reject(errorResult); }) .send(); }); } sendHttpRequest(options) { const request = this.createHttpRequest(options); return new Promise((resolve, reject) => { request .on('success', (result, response) => { resolve(result); }) .on('error', (result, response) => { reject(result); }) .send(); }); } handleProtocolError(result, response = {}) { result = result || {}; const {status = '', value = null, error: errorResult, code = '', message = null} = result; const {statusCode = null} = response; let error; // node.js errors if (code && message) { error = `Error ${code}: ${message}`; } else { // default error message; error = response && response.statusCode === 404 ? 'Unknown command' : (errorResult || 'An unknown error has occurred.'); } if (value && value.message) { error = value.message; } else if (value && value.error && this.Errors.Response[value.error]) { error = this.Errors.Response[value.error].message; } if (!error && errorResult) { error = errorResult; } return { status: -1, value, code, errorStatus: status, error, httpStatusCode: statusCode }; } getElementNotFoundResult(result) { result.status = -1; result.value = []; let errorId = this.Errors.StatusCode && this.Errors.StatusCode.NO_SUCH_ELEMENT; let errorInfo = this.Errors.findErrorById(errorId); if (errorInfo) { result.message = errorInfo.message; result.errorStatus = errorInfo.status; } return result; } shouldRegisterError(result) { let errorMessage = ''; if (result.value && result.value.message) { errorMessage = result.value.message; } else { errorMessage = result.error || result.message; } const shouldIgnore = Transport.DO_NOT_LOG_ERRORS.some(function(item) { return errorMessage.startsWith(item); }); return !shouldIgnore; } } module.exports = Transport; ================================================ FILE: lib/transport/selenium-webdriver/actions.js ================================================ const {error} = require('selenium-webdriver'); const {Logger, isObject, isFunction, isString} = require('../../utils'); const MethodMappings = require('./method-mappings.js'); class TransportActions { get transport() { return this.__transport; } get actions() { return this.__actions; } get compatMode() { return this.transport.settings.backwards_compatibility_mode; } constructor(transport) { this.__transport = transport; this.__actions = {}; this.lastError = null; this.retriesCount = 0; this.MethodMappings = new MethodMappings(transport); } loadActions(methodMappings = this.MethodMappings.methods, target = this.__actions) { Object.keys(methodMappings).forEach(name => { if (!methodMappings[name]) { return; } if (isObject(methodMappings[name])) { this.__actions[name] = this.__actions[name] || {}; this.loadActions(methodMappings[name], this.__actions[name]); } else { target[name] = this.createAction(name, methodMappings[name]); } }); } createAction(name, mapping) { return async (definition) => { const args = definition.args; let result; let promise; try { if (isFunction(mapping)) { promise = Array.isArray(args) ? mapping.apply(this.MethodMappings, args) : mapping.call(this.MethodMappings, args); } if (!(promise instanceof Promise)) { const opts = {}; if (isString(promise)) { opts.path = `/session/${definition.sessionId}${promise}`; } else if (isObject(promise)) { Object.assign(opts, promise); if (isFunction(opts.path)) { opts.path = await opts.path(); } opts.path = `/session/${definition.sessionId}${opts.path}`; } promise = this.transport.runProtocolAction(opts); } result = await promise; return this.makeResult(result); } catch (err) { const error = this.handleError(err, name); return { error, status: -1, value: null }; } }; } makeResult(result) { this.transport.handleErrorResponse(result); if (Array.isArray(result) || !isObject(result)) { return { value: result, status: 0 }; } result.status = result.status || 0; return result; } handleError(err, commandName) { let errorMsg = 'Unknown error'; try { this.transport.handleErrorResponse(err); } catch (error) { if (!error.message) { if (isString(err.value)) { error.message = err.value; } else if (isString(err.error)) { error.message = err.error; } else { error.message = 'Unknown error'; } } err = error; } if (err instanceof Error) { errorMsg = err.message; } else if (err && err.error) { errorMsg = err.error; } const {shouldRegisterError} = this.transport; const {lastError, retriesCount} = this; if (shouldRegisterError(err) && (retriesCount < 1 || lastError && err.name !== lastError.name)) { Logger.error(`Error while running .${commandName}() protocol action: ${errorMsg}\n`); this.lastError = err; } if (this.lastError && err.name === this.lastError.name) { this.retriesCount += 1; } if (err instanceof Error) { err.message = errorMsg; return err; } return new error.WebDriverError(errorMsg); } } module.exports = TransportActions; ================================================ FILE: lib/transport/selenium-webdriver/appium.js ================================================ const AppiumBaseServer = require('./appiumBase.js'); const AppiumServiceBuilder = require('./service-builders/appium.js'); const {iosRealDeviceUDID} = require('../../utils/mobile.js'); class AppiumServer extends AppiumBaseServer { static createService(settings) { const Options = require('./options.js'); const opts = new Options({settings}); opts.updateWebdriverPath(); const appiumService = new AppiumServiceBuilder(settings); const outputFile = settings.webdriver.log_file_name || ''; appiumService.setOutputFile(outputFile); return appiumService; } get defaultBrowser() { return null; } get ServiceBuilder() { return AppiumServiceBuilder; } get defaultServerUrl() { return 'http://127.0.0.1:4723'; } get defaultPort() { return 4723; } get defaultPathPrefix() { return '/wd/hub'; } createSessionOptions(argv) { this.extractAppiumOptions(); // set 'appium:udid' if deviceId present in argv if (argv && argv.deviceId) { const platformName = this.desiredCapabilities.platformName; let udid = argv.deviceId; if (platformName && platformName.toLowerCase() === 'ios') { udid = iosRealDeviceUDID(udid); } this.desiredCapabilities['appium:udid'] = udid; } // if `appium:chromedriverExecutable` is present and left blank, // assign the path of binary from `chromedriver` NPM package to it. if (this.desiredCapabilities['appium:chromedriverExecutable'] === '') { const chromedriver = this.seleniumCapabilities.getChromedriverPath(); if (chromedriver) { this.desiredCapabilities['appium:chromedriverExecutable'] = chromedriver; } } return super.createSessionOptions(argv) || this.desiredCapabilities; } createDriver({options}) { return this.createAppiumDriver({options}); } }; module.exports = AppiumServer; ================================================ FILE: lib/transport/selenium-webdriver/appiumBase.js ================================================ const {WebDriver} = require('selenium-webdriver'); const {Executor} = require('selenium-webdriver/http'); const http = require('selenium-webdriver/http'); const SeleniumServer = require('./selenium.js'); const {isObject} = require('../../utils'); class AppiumBaseServer extends SeleniumServer { extractAppiumOptions() { // break 'appium:options' to individual configs if (isObject(this.desiredCapabilities['appium:options'])) { const appiumOptions = this.desiredCapabilities['appium:options']; for (let key of Object.keys(appiumOptions)) { const value = appiumOptions[key]; if (!key.startsWith('appium:')) { key = `appium:${key}`; } this.desiredCapabilities[key] = value; } delete this.desiredCapabilities['appium:options']; } } createAppiumDriver({options}) { const httpClient = new http.HttpClient(this.getServerUrl()); return WebDriver.createSession(new Executor(httpClient), options); } }; module.exports = AppiumBaseServer; ================================================ FILE: lib/transport/selenium-webdriver/browserstack/appAutomate.js ================================================ const path = require('path'); const Utils = require('../../../utils'); const BrowserStack = require('./browserstack.js'); const {Logger} = Utils; class AppAutomate extends BrowserStack { get ApiUrl() { return `https://api.browserstack.com/${this.productNamespace}`; } get productNamespace() { return 'app-automate'; } async createSessionOptions() { this.extractAppiumOptions(); const options = this.desiredCapabilities; if (options && (options.appUploadUrl || options.appUploadPath)) { await this.uploadAppToBrowserStack(options); } return options; } async uploadAppToBrowserStack(options) { const {appUploadPath, appUploadUrl} = options; const multiPartFormData = {}; if (appUploadPath) { multiPartFormData['file'] = { filePath: path.resolve(appUploadPath) }; } else if (appUploadUrl) { multiPartFormData['url'] = { data: appUploadUrl }; } if (options['appium:app'] && !options['appium:app'].startsWith('bs://')) { multiPartFormData['custom_id'] = { data: options['appium:app'] }; } // eslint-disable-next-line no-console console.log(Logger.colors.stack_trace(`Uploading app to BrowserStack from '${appUploadPath || appUploadUrl}'...`)); try { const response = await this.sendHttpRequest({ url: 'https://api-cloud.browserstack.com/app-automate/upload', method: 'POST', use_ssl: true, port: 443, auth: { user: this.username, pass: this.accessKey }, multiPartFormData }); if (response.error) { const errMessage = 'App upload to BrowserStack failed. Original error: ' + response.error; throw new Error(errMessage); } if (!response.app_url) { const errMessage = 'App upload was unsuccessful. Got response: ' + response; throw new Error(errMessage); } // eslint-disable-next-line no-console console.log(Logger.colors.green(Utils.symbols.ok), Logger.colors.stack_trace('App upload successful!'), '\n'); if (!response.custom_id) { // custom_id not being used options['appium:app'] = response.app_url; // to display url when test suite is finished this.uploadedAppUrl = response.app_url; } } catch (err) { err.help = []; if (appUploadPath) { err.help.push('Check if you have entered correct file path in \'appUploadPath\' desired capability.'); } else if (appUploadUrl) { err.help.push('Check if you have entered correct publicly available file URL in \'appUploadUrl\' desired capability.'); } if (err.message.includes('BROWSERSTACK_INVALID_CUSTOM_ID')) { err.help.push('Check if \'appium:app\' or \'appium:options\' > app desired capability is correctly set to BrowserStack app url or required custom ID.'); } err.help.push( 'See BrowserStack app-upload docs for more details: https://www.browserstack.com/docs/app-automate/api-reference/appium/apps#upload-an-app', 'More details on setting custom ID for app: https://www.browserstack.com/docs/app-automate/appium/upload-app-define-custom-id' ); Logger.error(err); throw err; } } createDriver({options}) { return this.createAppiumDriver({options}); } } module.exports = AppAutomate; ================================================ FILE: lib/transport/selenium-webdriver/browserstack/automate.js ================================================ const BrowserStack = require('./browserstack.js'); const {Capabilities} = require('selenium-webdriver'); class Automate extends BrowserStack { get ApiUrl() { return `https://api.browserstack.com/${this.productNamespace}`; } get productNamespace() { return 'automate'; } createDriver({options = this.desiredCapabilities}) { if (options instanceof Capabilities) { return super.createDriver({options}); } return this.createAppiumDriver({options}); } } module.exports = Automate; ================================================ FILE: lib/transport/selenium-webdriver/browserstack/automateTurboScale.js ================================================ const stripAnsi = require('strip-ansi'); const Automate = require('./automate.js'); class AutomateTurboScale extends Automate { get buildUrl() { return `https://${this.productNamespace}.browserstack.com/dashboard`; } get ApiUrl() { return 'https://api.browserstack.com/automate-turboscale/v1'; } get productNamespace() { return 'grid'; } get buildsListingApiUrl() { // https://www.browserstack.com/docs/automate-turboscale/api-reference/build#get-build-list return `${this.ApiUrl}/builds`; } findBuildHashId(buildsResponse) { const builds = buildsResponse?.builds; if (!builds || builds.length === 0) { return false; } const currentBuild = builds.find((item) => item.name === this.build); if (currentBuild) { return currentBuild.hashed_id; } } async sendReasonToBrowserstack(isFailure = false, reason = '') { const sessionDetails = await this.sendHttpRequest({ url: `${this.ApiUrl}/sessions/${this.sessionId}`, method: 'GET', use_ssl: true, port: 443, auth: { user: this.username, pass: this.accessKey } }); const status = sessionDetails?.status; if (['passed', 'failed'].includes(status)) { // status has already been set by user return; } reason = stripAnsi(reason); await this.sendHttpRequest({ url: `${this.ApiUrl}/sessions/${this.sessionId}`, method: 'PATCH', use_ssl: true, port: 443, data: { status: isFailure ? 'failed' : 'passed', reason }, auth: { user: this.username, pass: this.accessKey } }); } } module.exports = AutomateTurboScale; ================================================ FILE: lib/transport/selenium-webdriver/browserstack/browserstack.js ================================================ const stripAnsi = require('strip-ansi'); const {Logger} = require('../../../utils'); const AppiumBaseServer = require('../appiumBase.js'); const defaultsDeep = require('lodash/defaultsDeep'); class Browserstack extends AppiumBaseServer { bStackOptions() { return this.settings.desiredCapabilities['bstack:options']; } get buildUrl() { return `https://${this.productNamespace}.browserstack.com`; } get buildsListingApiUrl() { // https://www.browserstack.com/docs/automate/api-reference/selenium/build#get-build-list return `${this.ApiUrl}/builds.json`; } get accessKey() { return this.bStackOptions().accessKey; } get username() { return this.bStackOptions().userName; } get build() { return this.bStackOptions().buildName; } get local() { return this.bStackOptions().local; } constructor(nightwatchInstance, browserName) { super(nightwatchInstance, browserName); this.useLocal = false; this.nightwatchInstance.on('nightwatch:session.create', (data) => { this.sessionId = data.sessionId; this.getBuildId().then(buildId => { if (buildId) { this.buildId = buildId; } }); }); } findBuildHashId(buildsResponse) { if (!buildsResponse || buildsResponse.length === 0) { return false; } const currentBuild = buildsResponse.find((item) => item.automation_build?.name === this.build); if (currentBuild) { return currentBuild.automation_build?.hashed_id; } } adaptSettings() { this.settings.webdriver.start_process = false; this.settings.webdriver.port = Number(this.settings.webdriver.port); const {desiredCapabilities} = this.settings; // checking for legacy-ways for providing config this.settings.desiredCapabilities['bstack:options'] = defaultsDeep(this.settings.desiredCapabilities['bstack:options'], { userName: desiredCapabilities['browserstack.user'], accessKey: desiredCapabilities['browserstack.key'], buildName: desiredCapabilities.build || desiredCapabilities.buildName, local: desiredCapabilities['browserstack.local'], sessionName: desiredCapabilities['name'] }); if (!this.accessKey && process.env.BROWSERSTACK_KEY) { this.settings.desiredCapabilities['bstack:options'].accessKey = process.env.BROWSERSTACK_KEY; } if (!this.username && process.env.BROWSERSTACK_USER) { this.settings.desiredCapabilities['bstack:options'].userName = process.env.BROWSERSTACK_USER; } if (!this.build) { this.settings.desiredCapabilities['bstack:options'].buildName = 'nightwatch-test-build'; } if (this.local) { this.useLocal = true; } } verifySettings() { if (this.settings.webdriver.port !== 443) { // eslint-disable-next-line no-console console.warn(Logger.colors.brown('Using insecure HTTP connection on port 80. Consider using SSL by ' + 'setting port to 443 in your Nightwatch configuration.')); } if (!this.accessKey) { throw new Error('BrowserStack access key is not set. Verify that "browserstack.key" capability is set correctly or ' + 'set BROWSERSTACK_KEY environment variable (.env files are supported).'); } if (!this.username) { throw new Error('BrowserStack username is not set. Verify that "browserstack.user" capability is set correctly or ' + 'set BROWSERSTACK_USER environment variable (.env files are supported).'); } } createSession({argv, moduleKey}) { this.adaptSettings(); this.verifySettings(); return super.createSession({argv, moduleKey}); } async getBuildId() { try { let offset = 0; while (offset <= 100) { const builds = await this.sendHttpRequest({ url: `${this.buildsListingApiUrl}?status=running&limit=20&offset=${offset}`, method: 'GET', use_ssl: true, port: 443, auth: { user: this.username, pass: this.accessKey } }); const buildHashId = this.findBuildHashId(builds); if (buildHashId) { return buildHashId; // Return the matched hashed_id } if (buildHashId === false) { // No builds returned by API, exit the loop break; } offset += 20; } } catch (err) { console.error(err); } } async sendReasonToBrowserstack(isFailure = false, reason = '') { const sessionDetails = await this.sendHttpRequest({ url: `${this.ApiUrl}/sessions/${this.sessionId}.json`, method: 'GET', use_ssl: true, port: 443, auth: { user: this.username, pass: this.accessKey } }); const status = sessionDetails?.automation_session?.status; if (['passed', 'failed'].includes(status)) { // status has already been set by user return; } reason = stripAnsi(reason); await this.sendHttpRequest({ url: `${this.ApiUrl}/sessions/${this.sessionId}.json`, method: 'PUT', use_ssl: true, port: 443, data: { status: isFailure ? 'failed' : 'passed', reason }, auth: { user: this.username, pass: this.accessKey } }); } async sessionFinished(reason, err) { super.sessionFinished(reason); await this.testSuiteFinished(err); } async testSuiteFinished(err) { try { if (this.sessionId) { const reason = err instanceof Error ? `${err.name}: ${err.message}` : ''; await this.sendReasonToBrowserstack(!!err, reason); // eslint-disable-next-line no-console console.log('\n ' + 'See more info, video, & screenshots on Browserstack:\n' + ' ' + Logger.colors.light_cyan(`${this.buildUrl}/builds/${this.buildId}/sessions/${this.sessionId}`)); } if (this.uploadedAppUrl) { // App was uploaded to BrowserStack and custom_id not being used // eslint-disable-next-line no-console console.log('\n ' + Logger.colors.light_cyan( `Please set 'appium:app' capability to '${this.uploadedAppUrl}' to avoid uploading the app again in future runs.` ) + '\n'); } this.sessionId = null; return true; } catch (err) { Logger.error(err); return false; } } } module.exports = Browserstack; ================================================ FILE: lib/transport/selenium-webdriver/cdp.js ================================================ class Cdp { async getConnection(driver, reset = false) { if (!reset && this._connection) { return this._connection; } return this._connection = await driver.createCDPConnection('page'); } resetConnection() { this._connection = undefined; this._networkMocks = undefined; } get networkMocks() { if (this._networkMocks) { return this._networkMocks; } return this._networkMocks = {}; } addNetworkMock(url, response) { this.networkMocks[url] = response; } } module.exports = new Cdp(); ================================================ FILE: lib/transport/selenium-webdriver/chrome.js ================================================ const {Browser} = require('selenium-webdriver'); const SeleniumWebdriver = require('./'); module.exports = class ChromeDriver extends SeleniumWebdriver { get ServiceBuilder() { return require('./service-builders/chrome.js'); } setBuilderOptions({builder, options}) { if (this.driverService) { const {service} = this.driverService; builder .forBrowser(Browser.CHROME) .setChromeService(service); } else { const serverUrl = this.getServerUrl(); builder .usingServer(serverUrl) .withCapabilities(this.initialCapabilities); } return super.setBuilderOptions({builder, options}); } }; ================================================ FILE: lib/transport/selenium-webdriver/edge.js ================================================ const {Browser} = require('selenium-webdriver'); const SeleniumWebdriver = require('./'); module.exports = class ChromeDriver extends SeleniumWebdriver { get ServiceBuilder() { return require('./service-builders/edge.js'); } setBuilderOptions({builder, options}) { if (this.driverService) { const {service} = this.driverService; builder .forBrowser(Browser.EDGE) .setEdgeService(service); } else { const serverUrl = this.getServerUrl(); builder .usingServer(serverUrl) .withCapabilities(this.initialCapabilities); } return super.setBuilderOptions({builder, options}); } }; ================================================ FILE: lib/transport/selenium-webdriver/firefox.js ================================================ const {Browser} = require('selenium-webdriver'); const SeleniumWebdriver = require('./'); module.exports = class GeckoDriver extends SeleniumWebdriver { get ServiceBuilder() { return require('./service-builders/firefox.js'); } setBuilderOptions({builder, options}) { if (this.driverService) { const {service} = this.driverService; builder .forBrowser(Browser.FIREFOX) .setFirefoxService(service); } else { const serverUrl = this.getServerUrl(); builder .usingServer(serverUrl) .withCapabilities(this.initialCapabilities); } return super.setBuilderOptions({builder, options}); } }; ================================================ FILE: lib/transport/selenium-webdriver/httpclient.js ================================================ const HttpRequest = require('../../http/request.js'); module.exports = function(settings, HttpResponse) { // TODO: handle agent and proxy arguments below const url = require('url'); return class HttpClient { constructor(serverUrl, opt_agent, opt_proxy) { this.agent_ = opt_agent || null; // eslint-disable-next-line const options = url.parse(serverUrl); if (!options.hostname) { throw new Error('Invalid URL: ' + serverUrl); } this.proxyOptions_ = opt_proxy ? {} : null; const {hostname: host, pathname: path, protocol} = options; const {log_screenshot_data} = settings; let {port} = options; if (port) { port = Number(port); HttpRequest.updateGlobalSettings({port}); } else { port = protocol === 'https' ? 443 : 80; } this.options = { host, port, path, addtOpts: { suppressBase64Data: !log_screenshot_data }, use_ssl: protocol === 'https:' }; this.errorTimeoutId = null; } /** @override */ send(httpRequest) { const {method, data, path} = httpRequest; const headers = {}; if (httpRequest.headers) { httpRequest.headers.forEach(function (value, name) { headers[name] = value; }); } this.options.headers = headers; this.options.data = data; this.options.path = path; this.options.method = method; const request = new HttpRequest(this.options); return new Promise((resolve, reject) => { request.once('success', (data, response, isRedirect) => { const {statusCode, headers} = response; let body = ''; if (data) { try { body = JSON.stringify(data); } catch (err) { // } } if (data && data.error) { reject(data); } else { const resp = new HttpResponse(statusCode, headers, body); resolve(resp); } }); request.on('error', (err) => { let {message, code} = err; // for connection reset errors, sometimes the error event gets fired multiple times if (this.errorTimeoutId) { clearTimeout(this.errorTimeoutId); } this.errorTimeoutId = setTimeout(() => { if (code) { message = code + ' ' + message; } const error = new Error(message); if (code) { error.code = code; } reject(error); }, 15); }); request.send(); }); } }; }; ================================================ FILE: lib/transport/selenium-webdriver/index.js ================================================ const ora = require('ora'); const {Builder, Browser, error} = require('selenium-webdriver'); const Actions = require('./actions.js'); const SeleniumCapabilities = require('./options.js'); const {Logger, isObject} = require('../../utils'); const {IosSessionNotCreatedError, AndroidConnectionError} = require('../../utils/mobile.js'); const httpClient = require('./httpclient.js'); const Session = require('./session.js'); const BaseTransport = require('../'); const {colors} = Logger; const {isErrorResponse, checkLegacyResponse, throwDecodedError, WebDriverError} = error; const {IosSessionErrors} = require('../errors'); let _driverService = null; let _driver = null; class Transport extends BaseTransport { /** * @param {Builder} builder * @param {Capabilities} options */ static setBuilderOptions({builder, options}) { switch (options.getBrowserName()) { case Browser.CHROME: builder.setChromeOptions(options); break; case Browser.FIREFOX: builder.setFirefoxOptions(options); break; case Browser.SAFARI: builder.setSafariOptions(options); break; case Browser.EDGE: builder.setEdgeOptions(options); break; case Browser.OPERA: // TODO: implement break; case Browser.INTERNET_EXPLORER: builder.setIeOptions(options); break; } } static get driver() { return _driver; } static set driver(value) { _driver = value; } static get driverService() { return _driverService; } static set driverService(value) { _driverService = value; } /** * @override */ get ServiceBuilder() { return null; } get defaultPort() { return this.ServiceBuilder ? this.ServiceBuilder.defaultPort : 4444; } get Actions() { return this.actionsInstance.actions; } get reporter() { return this.nightwatchInstance.reporter; } get api() { return this.nightwatchInstance.api; } get settings() { return this.nightwatchInstance.settings; } get desiredCapabilities() { return this.settings.desiredCapabilities; } get defaultPathPrefix() { return ''; } get outputEnabled() { return this.settings.output; } get usingSeleniumServer() { return this.settings.selenium && this.settings.selenium.start_process; } get shouldStartDriverService() { return this.settings.webdriver.start_process; } get serviceName() { return this.ServiceBuilder.serviceName; } get elementKey() { return this.__elementKey || Session.WEB_ELEMENT_ID; } get initialCapabilities() { return this.seleniumCapabilities.initialCapabilities; } get parallelMode() { return this.settings.testWorkersEnabled; } constructor(nightwatchInstance, {isSelenium = false, browserName} = {}) { super(nightwatchInstance); this.nightwatchInstance = nightwatchInstance; this.browserName = browserName; this.seleniumCapabilities = new SeleniumCapabilities({ settings: this.settings, browserName }); this.createHttpClient(); this.createActions(); } /** * @override */ setBuilderOptions({options, builder}) { Transport.setBuilderOptions({options, builder}); } createActions() { this.actionsInstance = new Actions(this); this.actionsInstance.loadActions(); } createHttpClient() { const http = require('selenium-webdriver/http'); http.HttpClient = httpClient(this.settings, http.Response); } getServerUrl() { if (this.shouldStartDriverService) { return this.defaultServerUrl; } return this.settings.webdriver.url; } //////////////////////////////////////////////////////////////////// // Session related //////////////////////////////////////////////////////////////////// async closeDriver() { if (this.driverService) { try { await this.driverService.stop(); this.driverService = null; this.stopped = true; } catch (err) { Logger.error(err); err.displayed = true; throw err; } } } async sessionFinished(reason) { this.emit('session:finished', reason); await this.closeDriver(); } async createDriverService({options, moduleKey, reuseBrowser = false}) { try { moduleKey = this.settings.webdriver.log_file_name || moduleKey || ''; if (!this.shouldReuseDriverService(reuseBrowser)) { Transport.driverService = new this.ServiceBuilder(this.settings); await Transport.driverService.setOutputFile(reuseBrowser ? 'test' : moduleKey).init(options); } this.driverService = Transport.driverService; } catch (err) { this.showConnectSpinner(colors.red(`Failed to start ${this.serviceName}.`), 'warn'); throw err; } } shouldReuseDriverService(reuseBrowser) { return (Transport.driverService && !Transport.driverService.stopped && reuseBrowser); } async getDriver({options, reuseBrowser = false}) { const value = await this.shouldReuseDriver(reuseBrowser); if (value) { return Transport.driver; } Transport.driver = await this.createDriver({options}); return Transport.driver; } async shouldReuseDriver(reuseBrowser) { if (!reuseBrowser || !Transport.driver) { return false; } try { await Transport.driver.getSession(); return true; } catch (err) { return false; } } /** * @param {Capabilities} options * @returns {Builder} */ createSessionBuilder(options) { const builder = new Builder(); builder.disableEnvironmentOverrides(); this.setBuilderOptions({builder, options}); return builder; } createSessionOptions(argv) { return this.seleniumCapabilities.create(argv); } createDriver({options}) { const builder = this.createSessionBuilder(options); this.builder = builder; return builder.build(); } async createSession({argv, moduleKey, reuseBrowser = false}) { const startTime = new Date(); const {host, port, start_process} = this.settings.webdriver; const portStr = port ? `port ${port}` : 'auto-generated port'; const options = await this.createSessionOptions(argv); if (start_process) { if (this.usingSeleniumServer) { options.showSpinner = (msg) => { this.showConnectSpinner(msg); }; } else { this.showConnectSpinner(`Starting ${this.serviceName} on ${portStr}...\n`); } await this.createDriverService({options, moduleKey, reuseBrowser}); } else { this.showConnectSpinner(`Connecting to ${host} on ${portStr}...\n`); } try { this.driver = await this.getDriver({options, reuseBrowser}); const session = new Session(this.driver); const sessionExports = await session.exported(); const {sessionInfo, sessionId, capabilities, elementKey} = sessionExports; this.__elementKey = elementKey; await this.showConnectInfo({startTime, host, port, start_process, sessionInfo}); return { sessionId, capabilities, host, port }; } catch (err) { const error = this.handleConnectError(err, host, port); this.showConnectSpinner(colors.red(`Failed to connect to ${this.serviceName} on ${host} with ${colors.stack_trace(portStr)}.`), 'warn'); throw error; } } //////////////////////////////////////////////////////////////////// // Output related //////////////////////////////////////////////////////////////////// async showConnectInfo({startTime, port, host, start_process, sessionInfo}) { if (!this.parallelMode) { this.showConnectSpinner(`Connected to ${colors.stack_trace(start_process ? this.serviceName : host)} on port ${colors.stack_trace(port)} ${colors.stack_trace('(' + (new Date() - startTime) + 'ms)')}.`); } if (this.outputEnabled) { const {platform, browserVersion, platformVersion, browserName, appId} = sessionInfo; const appName = appId.split('.').pop() || browserName; const appVersion = browserVersion && ` (${browserVersion})`; const platName = platform.toUpperCase(); const platVersion = platformVersion && ` (${platformVersion})`; // eslint-disable-next-line no-console console.info(` Using: ${colors.light_blue(appName)}${colors.brown(appVersion)} on ${colors.cyan(platName + platVersion)}.\n`); } } showConnectSpinner(msg, method = 'info') { if (!this.outputEnabled || this.parallelMode) { return; } if (this.connectSpinner) { this.connectSpinner[method](msg); } else { this.connectSpinner = ora(msg).start(); } } //////////////////////////////////////////////////////////////////// // Elements related //////////////////////////////////////////////////////////////////// getElementId(resultValue) { return resultValue[this.elementKey]; } toElement(resultValue) { return {[this.elementKey]: resultValue}; } mapWebElementIds(value) { if (Array.isArray(value)) { return value.reduce((prev, item) => { prev.push(this.getElementId(item)); return prev; }, []); } return value; } /** * Helper method * * @param {String} protocolAction * @param {Object} executeArgs * @return {Promise} */ executeProtocolAction(protocolAction, executeArgs) { if (isObject(protocolAction) && protocolAction.actionName) { const {actionName, args, sessionId = this.nightwatchInstance.sessionId} = protocolAction; return this.Actions.session[actionName]({ args, sessionId, sessionRequired: true }); } return this.Actions.session[protocolAction]({ args: executeArgs, sessionId: this.nightwatchInstance.sessionId, sessionRequired: true }); } //////////////////////////////////////////////////////////////////// // Error handling //////////////////////////////////////////////////////////////////// handleErrorResponse(result) { if (isErrorResponse(result)) { // will throw error if w3c response throwDecodedError(result); // will throw error if legacy response checkLegacyResponse(result); } } registerLastError(err, retryCount = 0) { this.lastError = err; this.retriesCount = retryCount; } getErrorMessage(result) { if (result instanceof Error) { return result.message; } return result.value && result.value.message; } handleConnectError(err, host, port) { const errMsg = `An error occurred while creating a new ${this.serviceName} session:`; switch (err.code) { case 'ECONNREFUSED': err.sessionCreate = true; err.message = `${errMsg} Connection refused to ${host}:${port}. If the Webdriver/Selenium service is managed by Nightwatch, check if "start_process" is set to "true".`; break; default: err.message = `${errMsg} [${err.name}] ${err.message}`; } if (!err.detailedErr && this.driverService) { const logPath = this.driverService.getOutputFilePath(); err.detailedErr = ` Verify if ${this.serviceName} is configured correctly; using:\n ${this.driverService.getSettingsFormatted()}\n`; err.extraDetail = (logPath ? `\n More info might be available in the log file: ${logPath}` : `\n Set webdriver.log_path in your Nightwatch config to retrieve more logs from ${this.serviceName}.`); if (err.message.includes('Failed to run adb command') || err.message.includes('no devices online')) { return new AndroidConnectionError(err); } if (IosSessionErrors[err.name] && this.api.isSafari() && this.api.isIOS()) { return new IosSessionNotCreatedError(err, this.desiredCapabilities); } } err.showTrace = false; err.reportShown = true; return err; } isResultSuccess(result = {}) { return !( (result instanceof Error) || (result.error instanceof Error) || result.status === -1 ); } getOutputFilePath() { return this.driverService.getOutputFilePath(); } getErrorResponse(result) { return result instanceof Error ? result : result.error; } staleElementReference(result) { return result instanceof error.StaleElementReferenceError; } elementClickInterceptedError(result) { return result instanceof error.ElementClickInterceptedError; } invalidElementStateError(result) { return result instanceof error.InvalidElementStateError; } elementNotInteractableError(result) { return result instanceof error.ElementNotInteractableError; } invalidWindowReference(result) { return result instanceof error.NoSuchWindowError; } invalidSessionError(result) { return result instanceof error.NoSuchSessionError; } isRetryableElementError(result) { const errorResponse = this.getErrorResponse(result); if (errorResponse instanceof WebDriverError && errorResponse.name === 'WebDriverError') { const errors = this.getRetryableErrorMessages(); return errors.some(item => errorResponse.message.includes(item)); } return ( this.staleElementReference(errorResponse) || this.elementClickInterceptedError(errorResponse) || this.invalidElementStateError(errorResponse) || this.elementNotInteractableError(errorResponse) ); } } module.exports = Transport; ================================================ FILE: lib/transport/selenium-webdriver/method-mappings.js ================================================ const {WebElement, WebDriver, Origin, By, until, Condition, Key} = require('selenium-webdriver'); const {ShadowRoot} = require('selenium-webdriver/lib/webdriver'); const {Locator} = require('../../element'); const NightwatchLocator = require('../../element/locator-factory.js'); const {isString} = require('../../utils'); const fs = require('fs'); const cdp = require('./cdp.js'); module.exports = class MethodMappings { get driver() { return this.transport.driver; } get settings() { return this.transport.settings; } getWebElement(webElementOrString) { if (webElementOrString && (webElementOrString.webElement instanceof WebElement)) { webElementOrString = webElementOrString.webElement; } if (webElementOrString instanceof WebElement) { return webElementOrString; } if (isString(webElementOrString)) { return new WebElement(this.driver, webElementOrString); } throw new Error(`Unknown element: ${webElementOrString}`); } async runScriptForElement(scriptFn, webElementOrId) { const element = this.getWebElement(webElementOrId); const parentElementId = await element.getId(); const {elementKey} = this.transport; return this.driver.executeScript(scriptFn, {[elementKey]: parentElementId}); } async getElementByJs(scriptFn, webElementOrId) { try { const result = await this.runScriptForElement(scriptFn, webElementOrId); if (!result) { return { value: null, status: 0 }; } const {elementKey} = this.transport; const elementId = await result.getId(); const returnValue = { value: { [elementKey]: elementId }, status: 0, elementId }; Object.assign(returnValue.value, { get getId() { return function () { return elementId; }; } }); return returnValue; } catch (error) { if (error.name === 'NoSuchElementError') { return { status: 0, value: null }; } throw error; } } constructor(transport) { this.transport = transport; } executeFn(fn, name, args) { if (!Array.isArray(args)) { args = [args]; } return this.driver[name](fn, ...args); } get methods() { return { /////////////////////////////////////////////////////////// // Session related /////////////////////////////////////////////////////////// async sessionAction(action) { switch (action) { case 'DELETE': { await this.driver.quit(); return { state: 'success', value: null }; } default: return this.driver.getSession(); } }, getSessions() { return '/sessions'; }, getStatus() { return '/status'; }, session: { /////////////////////////////////////////////////////////// // Timeouts /////////////////////////////////////////////////////////// async setTimeoutType(type, value) { await this.driver.manage().setTimeouts({ [type]: value }); return null; }, async setTimeoutsAsyncScript(value) { await this.driver.manage().setTimeouts({script: value}); return null; }, async setTimeoutsImplicitWait(value) { await this.driver.manage().setTimeouts({implicit: value}); return null; }, getTimeouts() { return this.driver.manage().getTimeouts(); }, /////////////////////////////////////////////////////////// // Session log /////////////////////////////////////////////////////////// async getSessionLogTypes() { const value = await this.driver.manage().logs().getAvailableLogTypes(); return { value }; }, async getLogContents(type) { const value = await this.driver.manage().logs().get(type); return { value: value.map(item => { return { level: { value: item.level.value, name: item.level.name }, type: item.type, timestamp: item.timestamp, message: item.message }; }) }; }, /////////////////////////////////////////////////////////// // Navigation /////////////////////////////////////////////////////////// async navigateTo(url) { await this.driver.navigate().to(url); return null; }, async getCurrentUrl() { const url = await this.driver.getCurrentUrl(); return url; }, async navigateBack() { await this.driver.navigate().back(); return null; }, async navigateForward() { await this.driver.navigate().forward(); return null; }, async pageRefresh() { await this.driver.navigate().refresh(); return null; }, async getPageTitle() { const title = await this.driver.getTitle(); return title; }, /////////////////////////////////////////////////////////// // Windows /////////////////////////////////////////////////////////// async switchToWindow(windowHandle) { await this.driver.switchTo().window(windowHandle); return null; }, async closeWindow() { await this.driver.close(); return null; }, /** * @returns {string} */ async getWindowHandle() { const value = await this.driver.getWindowHandle(); return value; }, async getAllWindowHandles() { const value = await this.driver.getAllWindowHandles(); return { value }; }, async getWindowPosition() { const {x, y} = await this.driver.manage().window().getRect(); return { value: { x, y } }; }, async maximizeWindow() { await this.driver.manage().window().maximize(); return null; }, async minimizeWindow() { await this.driver.manage().window().minimize(); return null; }, async fullscreenWindow() { await this.driver.manage().window().fullscreen(); return null; }, async openNewWindow(type = 'tab') { await this.driver.switchTo().newWindow(type); return null; }, async setWindowPosition(x, y) { await this.driver.manage().window().setRect({ x, y }); return { value: null }; }, async getWindowSize(windowHandle) { const value = await this.driver.manage().window().getRect(); // For backward compatibility if (windowHandle !== undefined) { return { value }; } const {width, height} = value; return { value: { width, height } }; }, async setWindowSize(width, height) { await this.driver.manage().window().setRect({ width, height }); return { value: null }; }, async getWindowRect() { const value = await this.driver.manage().window().getRect(); return { value }; }, async setWindowRect(data) { await this.driver.manage().window().setRect(data); return { value: null }; }, /////////////////////////////////////////////////////////// // Frames /////////////////////////////////////////////////////////// async switchToFrame(frameId) { if (frameId === undefined) { frameId = null; } await this.driver.switchTo().frame(frameId); return { value: null }; }, async switchToParentFrame() { await this.driver.switchTo().parentFrame(); return { value: null }; }, /////////////////////////////////////////////////////////// // Elements /////////////////////////////////////////////////////////// async locateSingleElement(element) { const locator = NightwatchLocator.create(element, this.transport.api.isAppiumClient()); const webElement = await this.driver.findElement(locator); const elementId = await webElement.getId(); const {elementKey} = this.transport; return { value: {[elementKey]: elementId} }; }, async locateMultipleElements(element) { const locator = NightwatchLocator.create(element, this.transport.api.isAppiumClient()); const resultValue = await this.driver.findElements(locator); if (Array.isArray(resultValue) && resultValue.length === 0) { return { status: -1, value: [], error: 'no such element', message: `Unable to locate element: ${locator.value} using ${locator.using}` }; } const {elementKey} = this.transport; const value = await Promise.all(resultValue.map(async webElement => { const elementId = await webElement.getId(); return {[elementKey]: elementId}; })); return value; }, elementIdEquals(id, otherId) { return `/element/${id}/equals/${otherId}`; }, async locateSingleElementByElementId({id, using, value}) { const locator = Locator.create({using, value}); const element = await this.getWebElement(id); const webElement = await element.findElement(locator); const elementId = await webElement.getId(); const {elementKey} = this.transport; return { value: {[elementKey]: elementId}, status: 0, elementId }; }, async locateMultipleElementsByElementId({id, using, value}) { const locator = Locator.create({using, value}); const element = await this.getWebElement(id); const resultValue = await element.findElements(locator); const {elementKey} = this.transport; const resultProcessed = await Promise.all(resultValue.map(async webElement => { const elementId = await webElement.getId(); return {[elementKey]: elementId}; })); return resultProcessed; }, async getActiveElement() { const webElement = await this.driver.switchTo().activeElement(); const elementId = await webElement.getId(); return elementId; }, getElementAttribute(webElementOrId, attributeName) { if (this.transport.api.isAppiumClient()) { return `/element/${webElementOrId}/attribute/${attributeName}`; } const element = this.getWebElement(webElementOrId); return element.getAttribute(attributeName); }, async getElementAccessibleName(webElementOrId) { const element = this.getWebElement(webElementOrId); const elementAccessibleName = await element.getAccessibleName(); return elementAccessibleName; }, async getElementAriaRole(webElementOrId) { const element = this.getWebElement(webElementOrId); const elementAriaRole = await element.getAriaRole(); return elementAriaRole; }, async takeElementScreenshot(webElementOrId, scroll) { const element = this.getWebElement(webElementOrId); const screenshotData = await element.takeScreenshot(scroll); return screenshotData; }, async getElementCSSValue(webElementOrId, cssPropertyName) { const element = this.getWebElement(webElementOrId); const elementCssValue = await element.getCssValue(cssPropertyName); return elementCssValue; }, async getElementProperty(webElementOrId, propertyName) { const element = this.getWebElement(webElementOrId); const elementValue = await element.getProperty(propertyName); return { value: elementValue }; }, async isElementActive(webElementOrId) { const element = this.getWebElement(webElementOrId); const elementId = await element.getId(); const currentActiveElementId = await this.methods.session.getActiveElement.call(this); return elementId === currentActiveElementId; }, async setElementProperty(webElementOrId, name, value) { const element = this.getWebElement(webElementOrId); await this.driver.executeScript(function (element, name, value) { element[name] = value; }, element, name, value); }, async setElementAttribute(webElement, attrName, value) { const element = this.getWebElement(webElement); // eslint-disable-next-line /* istanbul ignore next */const fn = function (e, a, v) { try { if (e && typeof e.setAttribute == 'function') { e.setAttribute(a, v); } return true; } catch (err) { return { error: err.message, message: err.name + ': ' + err.message }; } }; const elementId = await element.getId(); const result = await this.driver.executeScript('var passedArgs = Array.prototype.slice.call(arguments,0); ' + 'return (' + fn.toString() + ').apply(window, passedArgs);', {[this.transport.elementKey]: elementId}, attrName, value); return result; }, async getElementTagName(id) { const element = this.getWebElement(id); const elementTagName = await element.getTagName(); return elementTagName; }, async getElementRect(id) { const element = this.getWebElement(id); try { const value = await element.getRect(); return { value }; } catch (error) { error.message = `Unable to get element rect because of: ${error.message}`; return { error, status: -1, value: null }; } }, async getElementText(id) { const element = this.getWebElement(id); const elementText = await element.getText(); return elementText; }, // the value param is compulsory async getElementValue(webElementOrId, value) { const element = this.getWebElement(webElementOrId); const elementValue = await element.getAttribute(value); return elementValue; }, isElementLocationInView(id) { return `/element/${id}/location_in_view`; }, isElementDisplayed(webElementOrId) { if (this.transport.api.isAppiumClient()) { return `/element/${webElementOrId}/displayed`; } const element = this.getWebElement(webElementOrId); return element.isDisplayed(); }, async isElementEnabled(webElementOrId) { const element = this.getWebElement(webElementOrId); const value = await element.isEnabled(); return value; }, async isElementSelected(webElementOrId) { const element = this.getWebElement(webElementOrId); const value = await element.isSelected(); return value; }, async isElementPresent(webElement) { // webElement would be a Promise in case of new Element API. const element = await webElement; return element instanceof WebElement || element instanceof ShadowRoot; }, async clearElementValue(webElementOrId) { const element = this.getWebElement(webElementOrId); await element.clear(); try { const value = await element.getProperty('value'); if (isString(value) && value.length > 0) { const backArr = Array(value.length).fill(Key.BACK_SPACE); await element.sendKeys(...backArr); } } catch { // silent catch } return null; }, setElementValueRedacted(webElementOrId, value) { const modifiedValue = [Key.NULL].concat(value); 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(''); } else { value = String(value); } const element = this.getWebElement(webElementOrId); // clear Element value try { await this.methods.session.clearElementValue.call(this, webElementOrId); } catch (err) { if (err.name !== 'InvalidElementStateError') { throw err; } } await element.sendKeys(value); return null; }, async sendKeysToElement(webElementOrId, value) { if (Array.isArray(value)) { if (value.includes(undefined) || value.includes(null)) { throw TypeError('each key must be a number or string; got ' + value); } value = value.join(''); } const element = this.getWebElement(webElementOrId); await element.sendKeys(value); return null; }, async uploadFile(webElementOrId, filepath) { const remote = require('selenium-webdriver/remote'); this.driver.setFileDetector(new remote.FileDetector()); const element = this.getWebElement(webElementOrId); await element.sendKeys(filepath); return { value: null }; }, async clickElement(webElementOrId) { const element = this.getWebElement(webElementOrId); await element.click(); return null; }, async clickElementWithJS(webElementOrId) { const element = this.getWebElement(webElementOrId); await this.driver.executeScript('arguments[0].click();', element); return null; }, async elementSubmit(webElementOrId) { const element = this.getWebElement(webElementOrId); await element.submit(); return null; }, sendKeys(keys) { return { method: 'POST', path: '/keys', data: { value: keys } }; }, async getFirstElementChild(webElementOrId) { return await this.getElementByJs(function (element) { return element && element.firstElementChild; }, webElementOrId); }, /** * @param {WebElement} webElement */ inspectInDevTools(webElement, content = 'Element') { return this.driver.executeScript(function (element, content) { // eslint-disable-next-line no-console console.log(content + ':', element); }, webElement, content); }, async getLastElementChild(webElementOrId) { return await this.getElementByJs(function (element) { return element && element.lastElementChild; }, webElementOrId); }, async getNextSibling(webElementOrId) { return await this.getElementByJs(function (element) { return element && element.nextElementSibling; }, webElementOrId); }, async getPreviousSibling(webElementOrId) { return await this.getElementByJs(function (element) { return element && element.previousElementSibling; }, webElementOrId); }, async getShadowRoot(webElementOrId) { const element = this.getWebElement(webElementOrId); const shadowRoot = await element.getShadowRoot(); return shadowRoot; }, async elementHasDescendants(webElementOrId) { const count = await this.runScriptForElement(function (element) { return element ? element.childElementCount : null; }, webElementOrId); if (count === null) { throw new Error('No such element: ' + webElementOrId); } return count > 0; }, /////////////////////////////////////////////////////////// // Document Handling /////////////////////////////////////////////////////////// async getPageSource() { const value = await this.driver.getPageSource(); return value; }, async executeScript(script, args) { const value = await this.executeFn(script, 'executeScript', args); return { value }; }, async executeAsyncScript(fn, args) { const value = await this.executeFn(fn, 'executeAsyncScript', args); return { value }; }, /////////////////////////////////////////////////////////// // Cookies /////////////////////////////////////////////////////////// async addCookie(cookie) { await this.driver.manage().addCookie(cookie); return null; }, async deleteCookie(cookieName) { await this.driver.manage().deleteCookie(cookieName); return null; }, async deleteAllCookies() { await this.driver.manage().deleteAllCookies(); return null; }, getCookies() { return this.driver.manage().getCookies(); }, async getCookie(name) { try { const result = await this.driver.manage().getCookie(name); return { value: result }; } catch (err) { if (err.name === 'NoSuchCookieError') { return null; } throw err; } }, /////////////////////////////////////////////////////////// // User Actions /////////////////////////////////////////////////////////// async doubleClick(webElement) { if (webElement) { webElement = this.getWebElement(webElement); } await this.driver.actions({async: true}).doubleClick(webElement).perform(); return null; }, /** * @deprecated */ mouseButtonClick(buttonIndex) { return { method: 'POST', path: '/click', data: { button: buttonIndex } }; }, mouseButtonUp(buttonIndex) { return { method: 'POST', path: '/buttonup', data: { button: buttonIndex } }; }, mouseButtonDown(buttonIndex) { return { method: 'POST', path: '/buttondown', data: { button: buttonIndex } }; }, /** * @param {string|WebElement|null} origin * @param {number} x * @param {number} y * @param {object} [options] * @param {number} [duration] */ async moveTo(origin, x, y, duration, options = {}) { switch (origin) { case Origin.POINTER: break; case Origin.VIEWPORT: break; default: origin = this.getWebElement(origin); } const moveOptions = {origin, x, y}; if (typeof duration != 'undefined') { moveOptions.duration = duration; } await this.driver.actions(options).move(moveOptions).perform(); return null; }, async dragElement(source, destination) { source = this.getWebElement(source); //destination could be webElementId or {x,y} offset if (typeof destination === 'string') { destination = this.getWebElement(destination); } await this.driver.actions({async: true}).dragAndDrop(source, destination).perform(); return null; }, async contextClick(webElementOrId) { const element = this.getWebElement(webElementOrId); await this.driver.actions({async: true}).contextClick(element).perform(); return null; }, async pressAndHold(webElementOrId) { const element = this.getWebElement(webElementOrId); await this.driver.actions({async: true}).move({origin: element}).press().perform(); return null; }, async release(webElementOrId) { await this.driver.actions({async: true}).release().perform(); return null; }, /////////////////////////////////////////////////////////// // User Prompts /////////////////////////////////////////////////////////// async acceptAlert() { await this.driver.switchTo().alert().accept(); return null; }, async dismissAlert() { await this.driver.switchTo().alert().dismiss(); return null; }, getAlertText() { return this.driver.switchTo().alert().getText(); }, async setAlertText(keys) { await this.driver.switchTo().alert().sendKeys(keys); return null; }, /////////////////////////////////////////////////////////// // Screen /////////////////////////////////////////////////////////// async getScreenshot(logBase64Data) { const data = await this.driver.takeScreenshot(); return { value: data, status: 0, suppressBase64Data: !logBase64Data }; }, getScreenOrientation() { return '/orientation'; }, setScreenOrientation(orientation) { return { method: 'POST', path: '/orientation', data: { orientation } }; }, /////////////////////////////////////////////////////////// // Appium /////////////////////////////////////////////////////////// getAvailableContexts() { return '/contexts'; }, getCurrentContext() { return '/context'; }, setCurrentContext(context) { return { method: 'POST', path: '/context', data: { name: context } }; }, startActivity(opts) { return { method: 'POST', path: '/appium/device/start_activity', data: opts }; }, getCurrentActivity() { return '/appium/device/current_activity'; }, getCurrentPackage() { return '/appium/device/current_package'; }, getDeviceGeolocation() { return '/location'; }, setDeviceGeolocation(location) { return { method: 'POST', path: '/location', data: {location} }; }, pressDeviceKeyCode(opts) { return { method: 'POST', path: '/appium/device/press_keycode', data: opts }; }, longPressDeviceKeyCode(opts) { return { method: 'POST', path: '/appium/device/long_press_keycode', data: opts }; }, hideDeviceKeyboard(opts) { return { method: 'POST', path: '/appium/device/hide_keyboard', data: opts }; }, isDeviceKeyboardShown() { return '/appium/device/is_keyboard_shown'; }, resetApp() { return { method: 'POST', path: '/appium/app/reset' }; }, /////////////////////////////////////////////////////////////////////////// // Selenium Webdriver /////////////////////////////////////////////////////////////////////////// async wait(conditionFn, timeMs, message, retryInterval) { const driver = new WebDriver(Promise.resolve()); await driver.wait(conditionFn instanceof Condition ? conditionFn : conditionFn(), timeMs, message, retryInterval); return { value: null }; }, async setNetworkConditions(spec) { await this.driver.setNetworkConditions(spec); return { value: null }; }, waitUntilElementsLocated({condition, timeout, retryInterval}) { return this.driver.wait(until.elementsLocated(condition), timeout, null, retryInterval); }, /////////////////////////////////////////////////////////////////////////// // BiDi apis /////////////////////////////////////////////////////////////////////////// async registerAuth(username, password) { const cdpConnection = await cdp.getConnection(this.driver, true); await this.driver.register(username, password, cdpConnection); return { value: null }; }, async startLogsCapture(userCallback) { const cdpConnection = await cdp.getConnection(this.driver); await this.driver.onLogEvent(cdpConnection, userCallback); return { value: null }; }, async catchJsExceptions(userCallback) { const cdpConnection = await cdp.getConnection(this.driver); await this.driver.onLogException(cdpConnection, userCallback); return { value: null }; }, /////////////////////////////////////////////////////////////////////////// // CDP commands /////////////////////////////////////////////////////////////////////////// async setGeolocation(coordinates) { const cdpConnection = await cdp.getConnection(this.driver); await cdpConnection.execute( 'Emulation.setGeolocationOverride', coordinates ); return { value: null }; }, async clearGeolocation() { const cdpConnection = await cdp.getConnection(this.driver); await cdpConnection.execute( 'Emulation.clearGeolocationOverride', {} ); return { value: null }; }, async setDeviceMetrics(metrics) { const cdpConnection = await cdp.getConnection(this.driver); await cdpConnection.execute( 'Emulation.setDeviceMetricsOverride', metrics ); return { value: null }; }, async interceptNetworkCalls(userCallback) { const cdpConnection = await cdp.getConnection(this.driver); cdpConnection._wsConnection.on('message', (message) => { const params = JSON.parse(message); if (params.method === 'Network.requestWillBeSent') { const requestParams = params['params']; userCallback(requestParams); } }); await cdpConnection.execute( 'Network.enable', {} ); return { value: null }; }, async mockNetworkResponse(urlToIntercept, response) { const cdpConnection = await cdp.getConnection(this.driver); const {status = 200, headers: headersObject, body: bodyPlain = ''} = response; const headers = []; if (headersObject) { for (const [name, value] of Object.entries(headersObject)) { headers.push({name, value}); } } // Convert body to base64 const bodyBase64 = Buffer.from(bodyPlain, 'utf-8').toString('base64'); cdp.addNetworkMock(urlToIntercept, {status, headers, body: bodyBase64}); // Add event listener only the first time. if (Object.keys(cdp.networkMocks).length === 1) { cdpConnection._wsConnection.on('message', (message) => { const params = JSON.parse(message); if (params.method === 'Fetch.requestPaused') { const requestPausedParams = params['params']; const requestUrl = requestPausedParams.request.url; const networkMocks = cdp.networkMocks; if (Object.keys(networkMocks).includes(requestUrl)) { const mockResponse = networkMocks[requestUrl]; cdpConnection.execute('Fetch.fulfillRequest', { requestId: requestPausedParams['requestId'], responseCode: mockResponse.status, responseHeaders: mockResponse.headers, body: mockResponse.body }); } else { cdpConnection.execute('Fetch.continueRequest', { requestId: requestPausedParams['requestId'] }); } } }); } await cdpConnection.execute( 'Fetch.enable', {} ); await cdpConnection.execute( 'Network.setCacheDisabled', {cacheDisabled: true} ); return { value: null }; }, async takeHeapSnapshot(heapSnapshotLocation) { const cdpConnection = await cdp.getConnection(this.driver); const chunks = []; cdpConnection._wsConnection.on('message', (message) => { const params = JSON.parse(message); if (params.method === 'HeapProfiler.addHeapSnapshotChunk') { const chunk = params['params']['chunk']; chunks.push(chunk); } }); await cdpConnection.execute( 'HeapProfiler.enable', {} ); await cdpConnection.execute( 'HeapProfiler.takeHeapSnapshot', {} ); let prevChunks = []; return new Promise((resolve) => { const intervalId = setInterval(() => { if (prevChunks.length !== 0 && prevChunks.length === chunks.length) { resolveAndClearInterval(); } prevChunks = [...chunks]; }, 100); const resolveAndClearInterval = () => { clearInterval(intervalId); const heapSnapshot = chunks.join(''); if (heapSnapshotLocation) { fs.writeFileSync(heapSnapshotLocation, heapSnapshot); } resolve({value: heapSnapshot}); }; }); }, async enablePerformanceMetrics(enable) { // Disable the metrics once even before enabling it. await this.driver.sendAndGetDevToolsCommand('Performance.disable'); if (enable) { await this.driver.sendAndGetDevToolsCommand('Performance.enable'); } return { value: null }; }, async getPerformanceMetrics() { const {metrics: metricsReturned} = await this.driver.sendAndGetDevToolsCommand('Performance.getMetrics'); const metrics = {}; for (const metric of metricsReturned) { metrics[metric.name] = metric.value; } return { value: metrics }; } } }; } }; ================================================ FILE: lib/transport/selenium-webdriver/options.js ================================================ const {Capabilities, Browser} = require('selenium-webdriver'); const Utils = require('../../utils'); module.exports = class SeleniumCapabilities { get initialCapabilities() { return this.__capabilities; } get isChrome() { return this.initialCapabilities.getBrowserName() === Browser.CHROME; } get isSafari() { return this.initialCapabilities.getBrowserName() === Browser.SAFARI; } get isEdge() { return this.initialCapabilities.getBrowserName() === Browser.EDGE; } get isFirefox() { return this.initialCapabilities.getBrowserName() === Browser.FIREFOX; } constructor({settings, browserName}) { this.settings = settings; this.createDesired(); this.__capabilities = this.createInitialCapabilities(); if (browserName) { this.initialCapabilities.setBrowserName(browserName); } } createDesired() { this.desiredCapabilities = this.settings.capabilities || this.settings.desiredCapabilities; // flatten alwaysMatch param from desired Capabilities if (this.desiredCapabilities.alwaysMatch) { Object.assign(this.desiredCapabilities, this.desiredCapabilities.alwaysMatch); delete this.desiredCapabilities['alwaysMatch']; } if (typeof this.desiredCapabilities == 'function') { this.desiredCapabilities = this.desiredCapabilities.call(this.settings); } return this.desiredCapabilities; } /** * Create an initial capabilities instance based on either the capabilities or desiredCapabilities * setting from the nightwatch config * * @returns {Capabilities} */ createInitialCapabilities() { if (this.desiredCapabilities instanceof Capabilities) { return this.desiredCapabilities; } return new Capabilities(this.desiredCapabilities); } alreadyDefinedAs(OptionsClass) { return this.desiredCapabilities instanceof OptionsClass; } create(argv = {}) { this.argv = argv; let options; switch (this.initialCapabilities.getBrowserName()) { case Browser.CHROME: options = this.createChromeOptions(); break; case Browser.FIREFOX: options = this.createFirefoxOptions(); break; case Browser.SAFARI: options = this.createSafariOptions(); break; case Browser.EDGE: options = this.createEdgeOptions(); break; case Browser.OPERA: // TODO: implement break; case Browser.INTERNET_EXPLORER: options = this.createIeOptions(); break; } this .updateWebdriverPath() .addHeadlessOption({options}) .addDevtoolsOption({options}) .addWindowSizeOption({options}) .addProxyOption({options}); return options; } /** * @return {chrome.Options} */ createChromeOptions() { const {Options: ChromeOptions} = require('selenium-webdriver/chrome'); if (this.alreadyDefinedAs(ChromeOptions)) { return this.desiredCapabilities; } // Backward compatibility for 'chromeOptions' if (Utils.isObject(this.desiredCapabilities.chromeOptions)) { this.desiredCapabilities['goog:chromeOptions'] = Object.assign( this.desiredCapabilities.chromeOptions, this.desiredCapabilities['goog:chromeOptions'] ); } const {webdriver} = this.settings; const options = new ChromeOptions(this.desiredCapabilities); if (webdriver.chrome_binary || this.settings.chrome_binary) { options.setChromeBinaryPath(webdriver.chrome_binary || this.settings.chrome_binary); } if (webdriver.chrome_log_file || this.settings.chrome_log_file) { options.setChromeLogFile(webdriver.chrome_log_file || this.settings.chrome_log_file); } if (webdriver.android_chrome) { options.androidChrome(); } return options; } createIeOptions() { const {Options: IeOptions} = require('selenium-webdriver/ie'); if (this.alreadyDefinedAs(IeOptions)){ return this.desiredCapabilities; } const {webdriver} = this.settings; const options = new IeOptions(this.desiredCapabilities); if (webdriver.log_path || this.settings.log_path) { options.setLogFile(webdriver.log_path || this.settings.log_path); } if (webdriver.host || this.settings.host) { options.setHost(webdriver.host || this.settings.host); } return options; } createEdgeOptions() { const {Options: EdgeOptions} = require('selenium-webdriver/edge'); if (this.alreadyDefinedAs(EdgeOptions)) { return this.desiredCapabilities; } const {webdriver} = this.settings; const options = new EdgeOptions(this.desiredCapabilities); if (webdriver.edge_binary || this.settings.edge_binary) { options.setEdgeChromiumBinaryPath(webdriver.edge_binary || this.settings.edge_binary); } if (webdriver.edge_log_file || this.settings.edge_log_file) { options.setBrowserLogFile(webdriver.edge_log_file || this.settings.edge_log_file); } if (webdriver.android_package) { options.androidPackage(webdriver.android_package); } return options; } /** * @return {firefox.Options} */ createFirefoxOptions() { const {Options: FirefoxOptions} = require('selenium-webdriver/firefox'); if (this.alreadyDefinedAs(FirefoxOptions)) { return this.desiredCapabilities; } const {webdriver} = this.settings; const options = new FirefoxOptions(this.desiredCapabilities); if (webdriver.firefox_binary || this.settings.firefox_binary) { options.setBinary(webdriver.firefox_binary || this.settings.firefox_binary); } if (webdriver.firefox_profile || this.settings.firefox_profile) { options.setProfile(webdriver.firefox_profile || this.settings.firefox_profile); } return options; } createSafariOptions() { const {Options: SafariOptions} = require('selenium-webdriver/safari'); if (this.alreadyDefinedAs(SafariOptions)) { return this.desiredCapabilities; } const options = new SafariOptions(this.desiredCapabilities); return options; } usingSeleniumServer() { return this.settings.selenium && this.settings.selenium.start_process; } shouldSetupWebdriver() { return this.settings.webdriver.start_process && !this.settings.webdriver.server_path; } getChromedriverPath() { try { return require('chromedriver').path; } catch (err) { return ''; } } getGeckodriverPath() { try { return require('geckodriver').path; } catch (err) { return ''; } } getAppiumPath() { return require.resolve('appium'); } updateWebdriverPath() { if (this.shouldSetupWebdriver()) { try { if (this.usingSeleniumServer()) { if (this.settings.selenium.use_appium) { this.settings.selenium.server_path = this.settings.webdriver.server_path = this.getAppiumPath(); return this; } this.settings.selenium.server_path = this.settings.webdriver.server_path = require('@nightwatch/selenium-server').path; this.settings.selenium.cli_args = this.settings.selenium.cli_args || {}; const chromeDriver = this.getChromedriverPath(); if (chromeDriver) { this.settings.selenium.cli_args['webdriver.chrome.driver'] = chromeDriver; } const geckoDriver = this.getGeckodriverPath(); if (geckoDriver) { this.settings.selenium.cli_args['webdriver.gecko.driver'] = geckoDriver; } return this; } switch (this.initialCapabilities.getBrowserName()) { case Browser.CHROME: this.settings.webdriver.server_path = this.getChromedriverPath(); break; case Browser.FIREFOX: this.settings.webdriver.server_path = this.getGeckodriverPath(); break; case Browser.SAFARI: this.settings.webdriver.server_path = '/usr/bin/safaridriver'; break; } } catch (err) { this.settings.webdriver.server_path = ''; } } return this; } hasDevtoolsFlag(options) { return this.argv.devtools && (options instanceof Capabilities); } addDevtoolsOption({options}) { if (!this.hasDevtoolsFlag(options)) { return this; } if (this.isChrome) { const {args = []} = options.get('goog:chromeOptions'); const newArg = 'auto-open-devtools-for-tabs'; if (!(args.includes(newArg) || args.includes(`--${newArg}`))) { options.addArguments(newArg); } } else if (this.isSafari) { options.map_.set('safari:automaticInspection', true); } else if (this.isEdge) { const {args = []} = options.get('ms:edgeOptions'); const newArg = 'auto-open-devtools-for-tabs'; if (!(args.includes(newArg) || args.includes(`--${newArg}`))) { options.addArguments(newArg); } } else if (this.isFirefox) { // TODO: implement when available in Firefox } return this; } addHeadlessOption({options}) { if (this.argv.headless && (options instanceof Capabilities) && options.addArguments) { // For other Chromium-based browsers, `options` wouldn't be an instance of Capabilities // because it is never made so above (not officially supported by Selenium). if (this.isChrome || this.isEdge) { options.addArguments('headless=new'); } else if (this.isFirefox) { options.addArguments('-headless'); } } return this; } addWindowSizeOption({options}) { if (this.settings.window_size && (options instanceof Capabilities) && options.windowSize) { options.windowSize(this.settings.window_size); } return this; } addProxyOption({options}) { if (this.initialCapabilities.getProxy() && (options instanceof Capabilities) && options.setProxy) { const proxy = require('selenium-webdriver/proxy'); options.setProxy(proxy.manual(this.initialCapabilities.getProxy())); } } }; ================================================ FILE: lib/transport/selenium-webdriver/safari.js ================================================ const {WebDriver} = require('selenium-webdriver'); const http = require('selenium-webdriver/http'); const SeleniumWebdriver = require('./'); module.exports = class SafariDriverTransport extends SeleniumWebdriver { get ServiceBuilder() { return require('./service-builders/safari.js'); } get defaultServerUrl() { return 'http://127.0.0.1:4444/'; } createDriver({options}) { let service; let serverUrl; if (this.driverService) { service = this.driverService.service.build(); } serverUrl = this.getServerUrl(); return SafariDriver.createSession(options, service, serverUrl); } }; // 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. // Built-in SafariDriver class doesn't support passing the service object class SafariDriver extends WebDriver { static createSession(options, service, serverUrl) { let client; if (service) { client = service.start().then(url => new http.HttpClient(url)); } else { client = Promise.resolve(new http.HttpClient(serverUrl)); } const executor = new http.Executor(client); return super.createSession(executor, options, () => { if (service) { service.kill(); } }); } /** * This function is a no-op as file detectors are not supported by this * implementation. * @override */ setFileDetector() {} } ================================================ FILE: lib/transport/selenium-webdriver/selenium.js ================================================ const DefaultSeleniumDriver = require('./'); const SeleniumServiceBuilder = require('./service-builders/selenium.js'); module.exports = class SeleniumServer extends DefaultSeleniumDriver { /** * Used when running in parallel with start_process=true */ static createService(settings) { const Options = require('./options.js'); const opts = new Options({settings}); opts.updateWebdriverPath(); const seleniumService = new SeleniumServiceBuilder(settings); const outputFile = settings.webdriver.log_file_name || ''; seleniumService.setOutputFile(outputFile); return seleniumService; } get defaultBrowser() { return 'firefox'; } get ServiceBuilder() { return SeleniumServiceBuilder; } get defaultServerUrl() { return 'http://127.0.0.1:4444'; } get defaultPathPrefix() { return '/wd/hub'; } constructor(nightwatchInstance, browserName) { if (nightwatchInstance.settings.selenium && nightwatchInstance.settings.selenium['[_started]']) { nightwatchInstance.settings.selenium.start_process = false; nightwatchInstance.settings.webdriver.start_process = false; } super(nightwatchInstance, {isSelenium: true, browserName}); } async closeDriver() {} async sessionFinished(reason) { this.emit('session:finished', reason); // TODO: refactor this, selenium server management has moved to runner/cli if (this.driverService) { const {service} = this.driverService; if (service && service.kill) { // Give the selenium server some time to close down its browser drivers return new Promise((resolve, reject) => { setTimeout(() => { service.kill() .catch(err => reject(err)) .then(() => this.driverService.stop()) .then(() => resolve()); }, 100); }); } } } setBuilderOptions({builder, options}) { const serverUrl = this.getServerUrl(); builder .usingServer(serverUrl) .withCapabilities(this.initialCapabilities); return super.setBuilderOptions({builder, options}); } }; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/appium.js ================================================ const {SeleniumServer, DriverService} = require('selenium-webdriver/remote'); const {getFreePort} = require('../../../utils'); const BaseService = require('./base-service.js'); class AppiumService extends DriverService { constructor(server_path, opt_options) { const options = opt_options || {}; const {args, default_path_prefix} = options; const port = options.port; if (port !== AppiumServiceBuilder.defaultPort && !args.includes('--port')) { args.unshift('--port', port); } let cmd = 'node'; if (server_path.startsWith('appium')) { cmd = server_path; } else { args.unshift(server_path); } super(cmd, { loopback: options.loopback, port, args, path: default_path_prefix, env: options.env, stdio: options.stdio }); } } class AppiumServiceBuilder extends BaseService { static get serviceName() { return 'Appium Server'; } static get defaultPort() { return 4723; } get npmPackageName() { return 'appium'; } get outputFile() { return this._outputFile + '_appium-server.log'; } get defaultPort() { return AppiumServiceBuilder.defaultPort; } get serviceName() { return 'Appium Server'; } get downloadMessage() { return 'install Appium globally with "npm i -g appium" command, \n and set ' + '"selenium.server_path" config option to "appium".'; } /** * @param {Capabilities} opts * @returns {Promise} */ async createService(opts = {}) { const {port} = this; const options = new SeleniumServer.Options(); options.port = port || await getFreePort(); const {server_path, default_path_prefix = '/wd/hub'} = this.settings.webdriver; const introMsg = `Starting Appium Server on port ${options.port}...`; if (opts.showSpinner) { opts.showSpinner(`${introMsg}\n\n`); } else { // eslint-disable-next-line console.info(introMsg); } // TODO: read the log_path and add it to cliArgs // above TODO is copied from ./selenium.js options.args = this.cliArgs; options.default_path_prefix = default_path_prefix; if (this.hasSinkSupport() && this.needsSinkProcess()) { this.createSinkProcess(); options.stdio = ['pipe', this.process.stdin, this.process.stdin]; } this.service = new AppiumService(server_path, options); return this.service.start(); } } module.exports = AppiumServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/base-service.js ================================================ const path = require('path'); const child_process = require('child_process'); const fs = require('fs'); const Concurrency = require('../../../runner/concurrency'); const {Logger, createFolder} = require('../../../utils'); class BaseService { get outputFile() { return this._outputFile + '_webdriver.log'; } get serviceName() { return 'WebDriver'; } get serviceDownloadUrl() { return ''; } get requiresDriverBinary() { return true; } static get DEFAULT_HOST() { return 'localhost'; } get npmPackageName() { return null; } get downloadMessage() { return `download it from ${this.serviceDownloadUrl}, \nextract the archive and set ` + '"webdriver.server_path" config option to point to the binary file.\n'; } get errorMessages() { let binaryMissing = `${this.serviceName} cannot be found in the current project.`; if (this.npmPackageName) { binaryMissing += '\n\n ' + Logger.colors.yellow.bold(`You can either install ${this.npmPackageName} from NPM with: \n\n npm install ${this.npmPackageName} --save-dev\n\n`) + ' or '; } else { binaryMissing += '\n\n Please '; } binaryMissing += this.downloadMessage; return { binaryMissing }; } get errorOutput() { const errorOut = this.error_out.split('\n'); return errorOut.reduce(function(prev, message) { if (prev.indexOf(message) < 0) { prev.push(message); } return prev; }, []).join('\n '); } get defaultPort() { return undefined; } constructor(settings) { this.settings = settings; this.process = null; this.output = ''; this.error_out = ''; this.cliArgs = []; this.processExited = false; this.hostname = this.settings.webdriver.host || BaseService.DEFAULT_HOST; this.port = this.settings.webdriver.port; if (!this.settings.webdriver.server_path && this.requiresDriverBinary) { throw this.getStartupErrorMessage(this.errorMessages.binaryMissing); } this.exitListener = function exitListenerSink() { return this.stop(); }.bind(this); process.on('exit', this.exitListener); process.on('SIGINT', () => { this.stop().then(_ => { process.exit(0); }); }); } setCliArgs(args) { const {cli_args} = this.settings.webdriver; const cliArgs = Array.isArray(args) ? args : cli_args; if (Array.isArray(cliArgs)) { cliArgs.forEach(item => { if (typeof item == 'string') { this.cliArgs.push(item); } }); } } createSinkProcess() { const exitHandler = this.onExit.bind(this); this.process = child_process.spawn('cat', [], { env: process.env, stdio: ['pipe', 'pipe', 'pipe'] }); this.process.unref(); this.process.stdout.on('data', this.onStdout.bind(this)); this.process.stderr.on('data', this.onStderr.bind(this)); this.process.on('error', this.onError.bind(this)); this.process.on('exit', exitHandler); this.process.on('close', this.onClose.bind(this)); if (this.service) { this.service.setStdio(['pipe', this.process.stdin, this.process.stdin]); } } createErrorMessage(code) { return `${this.serviceName} process exited with code: ${code}`; } /** * @override * @param code */ onExit(code) { if (this.processExited) { return this; } if (code === null || code === undefined) { code = 0; } this.processExited = true; if (code > 0) { const err = this.createError(null, code); err.detailedErr = this.error_out || this.output; } } /** * @override * @param err */ onError(err) { let errMessage; if (err.code === 'ENOENT') { errMessage = `\nAn error occurred while trying to start ${this.serviceName}: cannot resolve path: "${err.path}".`; } Logger.error(errMessage || err); if (err.code === 'ENOENT') { // eslint-disable-next-line no-console console.warn('Please check that the "webdriver.server_path" config property is set correctly.\n'); } process.nextTick(() => this.stop()); } onClose() { Logger.info(`${this.serviceName} process closed.`); } createError(message, code = 1) { if (!message && code) { message = this.createErrorMessage(code); } const err = new Error(message); err.code = code; err.errorOut = this.errorOutput; return err; } getStartupErrorMessage(message) { const err = this.createError(message); const parts = message.split('\n'); const messageLine = parts.shift(); const startUpError = new Error(messageLine); if (parts.length > 0) { startUpError.detailedErr = parts.join('\n'); } if (err.code) { startUpError.code = err.code; } startUpError.showTrace = false; return startUpError; } onStdout(data) { this.output += data.toString(); } onStderr(data) { this.output += data.toString(); this.error_out += data.toString(); } needsSinkProcess() { return !Concurrency.isWorker(); } hasSinkSupport() { return process.platform !== 'win32' || process.env._ && process.env._.startsWith('/usr/'); } /** * @param {Capabilities} options * @returns {Promise} */ async createService(options) { const {default_path_prefix, server_path} = this.settings.webdriver; const {hostname, port} = this; let serverPathLog = ''; this.seleniumLogPath = this.settings.webdriver.log_path; if (server_path) { serverPathLog = ` with server_path=${server_path}`; } Logger.info(`Starting ${this.serviceName}${serverPathLog}...`); if (this.hasSinkSupport() && this.needsSinkProcess()) { await this.createSinkProcess(); } else { this.settings.webdriver.log_path = false; } if (port) { this.service.setPort(port); } if (hostname) { this.service.setHostname(hostname); } if (default_path_prefix) { this.service.setPath(default_path_prefix); } } async init(options = {}) { this.processExited = false; this.stopped = false; this.startTime = new Date(); this.setCliArgs(); try { await this.createService(options); } catch (err) { const {message} = err; err.message = `Unable to create the ${this.serviceName} process: ${message}`; err.detailedErr = '; verify if webdriver is configured correctly; using:\n ' + this.getSettingsFormatted() + '\n'; err.showTrace = false; err.sessionCreate = true; return Promise.reject(err); } } getSettingsFormatted() { const {start_process, server_path, port, host, ssl, default_path_prefix, proxy, cli_args} = this.settings.webdriver; const displaySettings = { start_process, server_path, port, host, ssl, default_path_prefix, proxy, cli_args }; return Logger.inspectObject(displaySettings); } async stop() { if (this.exitListener) { process.removeListener('exit', this.exitListener); } if (this.stopped) { return; } await this.writeLogFile(); if (!this.process || this.process.killed) { return; } try { this.process.kill(); } catch (err) { Logger.error(err); return Promise.reject(err); } } setOutputFile(fileName) { this._outputFile = fileName; return this; } getSeleniumLogPath() { return path.resolve(this.seleniumLogPath || 'logs'); } getLogPath() { const {log_path = 'logs'} = this.settings.webdriver; if (log_path === false) { return null; } return path.resolve(log_path); } getSeleniumOutputFilePath() { const {log_file_name} = this.settings.webdriver; if (log_file_name) { this._outputFile = log_file_name; } return path.join(this.getSeleniumLogPath(), this.outputFile); } getOutputFilePath() { const logPath = this.getLogPath(); if (!logPath) { return null; } const {log_file_name} = this.settings.webdriver; if (log_file_name) { this._outputFile = log_file_name; } return path.join(logPath, this.outputFile); } async writeLogFile() { const logPath = this.getLogPath(); if (!logPath) { return true; } const filePath = this.getOutputFilePath(); const folderPath = path.dirname(filePath); await createFolder(folderPath); return new Promise((resolve, reject) => { fs.writeFile(filePath, this.output, (err) => { if (err) { Logger.error(`Cannot write log file to ${filePath}.`); Logger.warn(err); this.stopped = true; return resolve(); } Logger.info(`Wrote log file to: ${filePath}`); this.stopped = true; resolve(); }); }); } } module.exports = BaseService; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/chrome.js ================================================ const chrome = require('selenium-webdriver/chrome'); const BaseService = require('./base-service.js'); class ChromeServiceBuilder extends BaseService { static get serviceName() { return 'ChromeDriver'; } static get defaultPort() { return 9515; } get npmPackageName() { return 'chromedriver'; } get outputFile() { return this._outputFile + '_chromedriver.log'; } get serviceName() { return ChromeServiceBuilder.serviceName; } get serviceDownloadUrl() { return 'https://sites.google.com/chromium.org/driver/downloads'; } /** * @param {Capabilities} options * @returns {Promise} */ async createService(options) { const {server_path} = this.settings.webdriver; this.service = new chrome.ServiceBuilder(server_path); let enableVerboseLogging = true; if (Array.isArray(this.cliArgs) && this.cliArgs.length > 0) { this.service.addArguments(...this.cliArgs); for (const arg of this.cliArgs) { if (arg === '--silent' || arg.startsWith('--log-level=')) { enableVerboseLogging = false; break; } } } if (enableVerboseLogging) { this.service.enableVerboseLogging(); } if (process.platform !== 'win32') { this.service.enableChromeLogging(); } return super.createService(); } get requiresDriverBinary() { return false; } } module.exports = ChromeServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/edge.js ================================================ const edge = require('selenium-webdriver/edge'); const BaseService = require('./base-service.js'); class EdgeServiceBuilder extends BaseService { static get serviceName() { return 'EdgeDriver'; } static get defaultPort() { return 9514; } get requiresDriverBinary() { return false; } get outputFile() { return this._outputFile + '_msedgedriver.log'; } get serviceName() { return EdgeServiceBuilder.serviceName; } get serviceDownloadUrl() { return 'https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/'; } /** * @param {Capabilities} options * @returns {Promise} */ async createService(options) { const {server_path} = this.settings.webdriver; this.service = new edge.ServiceBuilder(server_path); let enableVerboseLogging = true; if (Array.isArray(this.cliArgs)) { this.service.addArguments(...this.cliArgs); for (const arg of this.cliArgs) { if (arg === '--silent' || arg.startsWith('--log-level=')) { enableVerboseLogging = false; break; } } } if (enableVerboseLogging) { this.service.enableVerboseLogging(); } return super.createService(); } } module.exports = EdgeServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/firefox.js ================================================ const firefox = require('selenium-webdriver/firefox'); const BaseService = require('./base-service.js'); class FirefoxServiceBuilder extends BaseService { static get serviceName() { return 'GeckoDriver'; } static get defaultPort() { return 4444; } get npmPackageName() { return 'geckodriver'; } get outputFile() { return this._outputFile + '_geckodriver.log'; } get serviceName() { return FirefoxServiceBuilder.serviceName; } get serviceDownloadUrl() { return 'https://github.com/mozilla/geckodriver/releases'; } /** * @param {Capabilities} options * @returns {Promise} */ async createService(options) { const {server_path} = this.settings.webdriver; this.service = new firefox.ServiceBuilder(server_path); let enableVerboseLogging = true; if (Array.isArray(this.cliArgs)) { this.service.addArguments(...this.cliArgs); if (this.cliArgs.includes('--log')) { enableVerboseLogging = false; } } if (enableVerboseLogging) { this.service.enableVerboseLogging(true); } return super.createService(); } get requiresDriverBinary() { return false; } } module.exports = FirefoxServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/safari.js ================================================ const {Capabilities} = require('selenium-webdriver'); const safari = require('selenium-webdriver/safari'); const BaseService = require('./base-service.js'); const OPTIONS_CAPABILITY_KEY = 'safari.options'; const TECHNOLOGY_PREVIEW_OPTIONS_KEY = 'technologyPreview'; const SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE = '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver'; function useTechnologyPreview(o) { if (o instanceof Capabilities) { let options = o.get(OPTIONS_CAPABILITY_KEY); return !!(options && options[TECHNOLOGY_PREVIEW_OPTIONS_KEY]); } if (o && typeof o === 'object') { return !!o[TECHNOLOGY_PREVIEW_OPTIONS_KEY]; } return false; } class SafariServiceBuilder extends BaseService { static get serviceName() { return 'SafariDriver'; } get requiresDriverBinary() { return false; } get outputFile() { return this._outputFile + '_safaridriver.log'; } /** * Port will be auto generated and probed by selenium * @returns {number} */ get defaultPort() { return 0; } get serviceName() { return SafariServiceBuilder.serviceName; } /** * @param {Capabilities} options * @returns {Promise} */ async createService(options) { if (useTechnologyPreview(options.get(OPTIONS_CAPABILITY_KEY))) { this.settings.webdriver.server_path = SAFARIDRIVER_TECHNOLOGY_PREVIEW_EXE; } this.service = new safari.ServiceBuilder(this.settings.webdriver.server_path); if (Array.isArray(this.cliArgs)) { this.service.addArguments(...this.cliArgs); } return super.createService(options); } } module.exports = SafariServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/service-builders/selenium.js ================================================ const path = require('path'); const {SeleniumServer, DriverService} = require('selenium-webdriver/remote'); const {isObject, isUndefined, isDefined, getFreePort} = require('../../../utils'); const BaseService = require('./base-service.js'); class SeleniumServer4 extends DriverService { constructor(jar, opt_options) { const options = opt_options || {}; const {command = 'standalone', jvmArgs, args} = options; let combinedArgs = jvmArgs.concat('-jar', jar, command); const port = options.port; if (!args.includes('--port')) { args.unshift('--port', port); } combinedArgs = combinedArgs.concat(args); let java = 'java'; if (process.env['JAVA_HOME']) { java = path.join(process.env['JAVA_HOME'], 'bin/java'); } super(java, { loopback: options.loopback, port, args: combinedArgs, path: '/wd/hub', env: options.env, stdio: options.stdio }); } } class SeleniumServiceBuilder extends BaseService { static get serviceName() { return 'Selenium Server'; } static get defaultPort() { return 4444; } get npmPackageName() { return '@nightwatch/selenium-server'; } get outputFile() { return this._outputFile + '_selenium-server.log'; } get defaultPort() { return SeleniumServiceBuilder.defaultPort; } get serviceName() { return 'Selenium Server'; } get serviceDownloadUrl() { return 'https://selenium.dev/download/'; } get downloadMessage() { return `download the selenium server jar from ${this.serviceDownloadUrl}, \n and set ` + '"selenium.server_path" config option to point to the jar file.'; } setCliArgs() { const {cli_args, jvmArgs} = this.settings.selenium; let cliArgs = jvmArgs || cli_args; if (Array.isArray(cliArgs)) { return super.setCliArgs(cliArgs); } this.jvmArgs = []; if (isObject(cliArgs)) { Object.keys(cliArgs).forEach(key => { if (!isUndefined(cliArgs[key])) { let property = ''; let isJvmArg = !key.startsWith('-'); if (isJvmArg) { property += '-D'; } property += key; if (!cliArgs[key]) { this.cliArgs.unshift(property); return; } if (isJvmArg) { this.jvmArgs.unshift(`${property}=${cliArgs[key]}`); } else { this.cliArgs.unshift(property, cliArgs[key]); } } }); } } usingSelenium4() { const {command, server_path} = this.settings.webdriver; return isDefined(command) && !server_path.includes('selenium-server-standalone-3.'); } /** * @param {Capabilities} opts * @returns {Promise} */ async createService(opts = {}) { const {port} = this; const options = new SeleniumServer.Options(); options.port = port || await getFreePort(); let {server_path, command} = this.settings.webdriver; let commandStr = ''; if (command) { commandStr = ` in ${command} mode `; } let serverPathStr = server_path.split(path.sep).pop(); const introMsg = `Starting Selenium Server [${serverPathStr}] on port ${options.port}${commandStr}...`; if (opts.showSpinner) { opts.showSpinner(`${introMsg}\n\n`); } else { // eslint-disable-next-line console.info(introMsg); } // TODO: read the log_path and add it to cliArgs options.args = this.cliArgs; options.jvmArgs = this.jvmArgs; if (this.hasSinkSupport() && this.needsSinkProcess()) { this.createSinkProcess(); options.stdio = ['pipe', this.process.stdin, this.process.stdin]; } let Constructor = SeleniumServer; if (this.usingSelenium4()) { options.command = command; Constructor = SeleniumServer4; } this.service = new Constructor(server_path, options); return this.service.start(); } } module.exports = SeleniumServiceBuilder; ================================================ FILE: lib/transport/selenium-webdriver/session.js ================================================ module.exports = class Session { static get WEB_ELEMENT_ID () { return 'element-6066-11e4-a52e-4f735466cecf'; } static serializeCapabilities(caps) { const ret = {}; for (const key of caps.keys()) { const cap = caps.get(key); if (cap !== undefined && cap !== null) { ret[key] = cap; } } return ret; } constructor(driver) { this.driver = driver; } async exported() { const session = await this.driver.getSession(); const sessionId = await session.getId(); const sessionCapabilities = await session.getCapabilities(); const platform = sessionCapabilities.getPlatform() || sessionCapabilities.get('platform') || ''; const platformVersion = sessionCapabilities.get('platformVersion') || ''; const browserName = sessionCapabilities.getBrowserName(); const browserVersion = sessionCapabilities.getBrowserVersion() || sessionCapabilities.get('version') || ''; const appId = sessionCapabilities.get('appPackage') || sessionCapabilities.get('bundleId') || ''; const executor = await this.driver.getExecutor(); const elementKey = executor.w3c ? Session.WEB_ELEMENT_ID : 'ELEMENT'; return { sessionId, elementKey, sessionInfo: { platform, platformVersion, browserName, browserVersion, appId }, capabilities: Session.serializeCapabilities(sessionCapabilities) }; } }; ================================================ FILE: lib/utils/addDetailedError.js ================================================ /** * @method addDetailedError * @param {Error} err */ module.exports = function(err) { let detailedErr; if (err instanceof TypeError) { if (err.detailedErr && /browser\..+ is not a function$/.test(err.detailedErr)) { detailedErr = ' ' + err.detailedErr + '\n\n Nightwatch client is not yet available; this often happens when you are attempting to reference the "browser" ' + 'object globally too soon, either in your test or other Nightwatch related files'; } else if (/\.page\..+ is not a function$/.test(err.message)) { detailedErr = '- verify if page objects are setup correctly, check "page_objects_path" in your config'; } else if (err.message.includes('.mountComponent() is already defined.')) { detailedErr = ' - running component tests? loading multiple plugins together is not supported.'; err.help = [ 'check your nightwatch config file (e.g. nightwatch.conf.js) and inspect the line with "plugins": [...]', 'make sure to load only the plugin for the intended framework (e.g. either load @nightwatch/react or @nightwatch/vue, but not both together)' ]; err.link = 'https://nightwatchjs.org/guide/concepts/component-testing.html'; } else if (err.message.includes('browser.mountComponent is not a function')) { detailedErr = ' - writing an ES6 async test case? - keep in mind that commands return a Promise; \n - writing unit tests? - make sure to specify "unit_tests_mode=true" in your config.'; } else if (/\w is not a function$/.test(err.message)) { detailedErr = ' - writing an ES6 async test case? - keep in mind that commands return a Promise; \n - writing unit tests? - make sure to specify "unit_tests_mode=true" in your config.'; } } else if (err instanceof SyntaxError) { const stackParts = err.stack.split('SyntaxError:'); detailedErr = stackParts[0]; let modulePath = err.stack.split('\n')[0]; if (modulePath.includes(':')) { modulePath = modulePath.split(':')[0]; } if (stackParts[1]) { if (detailedErr) { err.stack = ''; } let header = stackParts[1].split('\n')[0]; if (header.trim() === err.message) { header = ''; } else { header = header + ':\n\n '; } detailedErr = header + detailedErr; } if (modulePath.endsWith('.jsx') || modulePath.endsWith('.tsx')) { detailedErr = `\n In order to be able to load JSX files, one of these plugins is needed: - @nightwatch/react - @nightwatch/storybook (only if using Storybook in your project) `; } } if (detailedErr) { err.detailedErr = detailedErr; } }; ================================================ FILE: lib/utils/alwaysDisplayError.js ================================================ module.exports = function(err) { return (err instanceof Error) && [ 'TypeError', 'SyntaxError', 'ReferenceError', 'RangeError' ].includes(err.name); }; ================================================ FILE: lib/utils/analytics.js ================================================ /** Parts of the code are taken from angular-cli code base which is governed by the following license: * https://angular.io/license */ const os = require('os'); const https = require('https'); const uuid = require('uuid'); const fs = require('fs').promises; const path = require('path'); const {execSync} = require('child_process'); const {Logger, VERSION, fileExists} = require('./'); const GA_SERVER_URL = 'https://www.google-analytics.com'; const GA_SERVER_PORT = '443'; const GA_API_KEY = 'XuPojOTwQ6yTO758EV4hBg'; const GA_TRACKING_ID = 'G-DEKPKZSLXS'; const defaultSettings = { enabled: false, log_path: './logs/analytics', client_id: uuid.v4() }; const RESERVED_EVENT_NAMES = ['ad_activeview', 'ad_click', 'ad_exposure', 'ad_impression', 'ad_query', 'adunit_exposure', 'app_clear_data', 'app_install', 'app_update', 'app_remove', 'error', 'first_open', 'first_visit', 'in_app_purchase', 'notification_dismiss', 'notification_foreground', 'notification_open', 'notification_receive', 'os_update', 'screen_view', 'session_start', 'user_engagement']; const ALLOWED_ERRORS = ['Error', 'SyntaxError', 'TypeError', 'ReferenceError', 'WebDriverError', 'TimeoutError', 'NotFoundError', 'NoSuchElementError', 'IosSessionNotCreatedError', 'AndroidConnectionError', 'AndroidBinaryError', 'RealIosDeviceIdError', 'AndroidHomeError']; const RESERVED_EVENT_PARAMETERS = ['firebase_conversion']; const MAX_PARAMETERS_LENGTH = 40; const SYSTEM_LANGUAGE = getLanguage(); /** * See: https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide */ class AnalyticsCollector { constructor() { this.queueLength = 0; this.parameters = {}; this.logger = Logger; this.userAgentString = buildUserAgentString(); this.runId = uuid.v4(); // This identifies unique users and helps us separate out sessions. this.parameters['client_id'] = uuid.v4(); // Prevents google from indexing the events for ad targeting. this.parameters['non_personalized_ads'] = true; } initialize() { if (this.initialized) {return this.initialized} this.initialized = new Promise((resolve) => { // update queueLength fs.stat(this.__getLogFileLocation()) .then((length) => { this.queueLength = length.size / 400; // the actual value doesn't matter resolve(); }) .catch((err)=>{ // ignore error resolve(); }); }); return this.initialized; } updateSettings(settings) { this.settings = settings; if (!this.settings.usage_analytics) { this.settings.usage_analytics = defaultSettings; } this.testEnv = settings.testEnv; // update client_id this.parameters['client_id'] = this.settings.usage_analytics.client_id; } updateLogger(logger) { this.logger = logger; } async collectEvent(name, parameters = {}) { if (!this.settings.usage_analytics.enabled) { return; } await this.initialize(); // Not all shape of events are supported. this.__validateEvent(name, parameters); // Add event timestamp. parameters['event_time'] = new Date().getTime() * 1000; // Add environment information. parameters['env_os'] = this.userAgentString; parameters['env_lang'] = SYSTEM_LANGUAGE; parameters['env_nw_version'] = VERSION.full; parameters['env_node_version'] = `node ${process.version}`; parameters['test_env'] = this.testEnv; parameters['run_id'] = this.runId; return await this.__addToQueue(name, parameters); } async collectErrorEvent(error, isUncaught = false) { if (!error) { return; } await this.initialize(); let errorName = error.name; if (!ALLOWED_ERRORS.includes(errorName)) { errorName = 'UserGeneratedError'; } try { const parameters = { env_os: this.userAgentString, env_nw_version: VERSION.full, env_node_version: `node ${process.version}`, test_env: this.testEnv, err_name: errorName, is_uncaught: isUncaught, run_id: this.runId }; this.__validateEvent('nw_test_err', parameters); await this.__addToQueue('nw_test_err', parameters); if (isUncaught) { await this.__flush(); } } catch (e) { // Ignore } } async __flush() { if (!this.settings.usage_analytics.enabled) { return; } if (this.queueLength === 0) { return; } try { const logfile = this.__getLogFileLocation(); const pendingLogs = await fs.readFile(logfile, 'utf8'); // The below is needed so that if flush is called multiple times, // we don't report the same event multiple times. await fs.truncate(logfile); this.queueLength = 0; const pendingLogsArray = pendingLogs.split('\n'); pendingLogsArray.pop(); const pendingTrackingEvents = pendingLogsArray.map(log => JSON.parse(log)); // Time when the request is sent, expected in micro seconds. const timestamp_micros = new Date().getTime() * 1000; const payload = { ...this.parameters, timestamp_micros, events: pendingTrackingEvents }; await this.__send(payload); } catch (error) { // Failure to report analytics shouldn't crash the system. this.logger.info('Analytics flush error:', error.message); } } async __addToQueue(eventType, parameters) { if (!this.settings.usage_analytics.enabled) { return; } this.queueLength++; const writeLogPromise = await this.__logAnalyticsEvents({name: eventType, params: parameters}); // periodically flush if (this.queueLength > 5) { await this.__flush(); } return writeLogPromise; } async __send(data) { if (!this.settings.usage_analytics.enabled) { return; } this.logger.info('Analytics send event:', data); const path = this.__getGoogleAnalyticsPath(); const serverUrl = new URL(this.settings.usage_analytics.serverUrl || GA_SERVER_URL) ; const serverPort = serverUrl.port || this.settings.usage_analytics.serverPort || GA_SERVER_PORT; const payload = JSON.stringify(data); const request = new https.request({ hostname: serverUrl.hostname, port: serverPort, method: 'POST', path, headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length } }); return new Promise((resolve, reject) => { request.write(payload); request.once('response', (response) => { if (response.statusCode !== 204) { reject( new Error(`Analytics reporting failed with status code: ${response.statusCode}.`) ); } else { resolve(response); } response.on('data', (_) => {}); }); request.on('error', (result) => { new Error(`Failed to send usage metric: ${result.code}.`); reject(result); }); request.end(); }); } __getGoogleAnalyticsPath() { const apiKey = this.settings.usage_analytics.apiKey || GA_API_KEY; const trackingId = this.settings.usage_analytics.trackingId || GA_TRACKING_ID; return `/mp/collect?api_secret=${apiKey}&measurement_id=${trackingId}`; } async __logAnalyticsEvents(data) { const logfile = this.__getLogFileLocation(); const hasAnalyticsLog = await fileExists(logfile); if (!hasAnalyticsLog) { data.params = data.params ? data.params : {}; data.params['first_run'] = true; await fs.mkdir(path.dirname(logfile), {recursive: true}); } const writeFn = hasAnalyticsLog ? fs.appendFile : fs.writeFile; try { await writeFn(logfile, JSON.stringify(data) + '\n'); // Always send data for first runs, this helps us detect CI builds also. if (!hasAnalyticsLog) { await this.__flush(); } } catch (err) { this.logger.warn('Failed to log usage data:', err.message); } } __getLogFileLocation() { const log_path = this.settings.usage_analytics.log_path || './logs/analytics'; return path.resolve(log_path, 'analytics.log'); } __validateEvent(name, parameters) { Object.keys(parameters).forEach(key => { if (parameters[key] === undefined || parameters[key] === null) { parameters[key] = 'undefined'; } }); if (RESERVED_EVENT_NAMES.includes(name)) { throw Error(`Analytics event name ${name} is reserved.`); } Object.keys(parameters).forEach(key => { if (RESERVED_EVENT_PARAMETERS.includes(key)) { throw Error(`Parameter name ${key} is reserved.`); } if (typeof parameters[key] === 'object') { throw Error(`Parameter ${key} is an object. Only string and integer allowed.`); } }); if (parameters.length > MAX_PARAMETERS_LENGTH) { throw Error(`Too many parameters. Maximum allowed is ${MAX_PARAMETERS_LENGTH}.`); } } } /** * Get a language code. */ function getLanguage() { return (process.env.LANG || // Default Unix env variable. process.env.LC_CTYPE || // For C libraries. Sometimes the above isn't set. process.env.LANGSPEC || // For Windows, sometimes this will be set (not always). getWindowsLanguageCode() || '??'); } /** * Attempt to get the Windows Language Code string. */ function getWindowsLanguageCode() { if (!os.platform().startsWith('win')) { return undefined; } try { // This is true on Windows XP, 7, 8 and 10 AFAIK. Would return empty string or fail if it // doesn't work. return execSync('wmic.exe os get locale').toString().trim(); } catch (err) {;} return undefined; } /** * Build a fake User Agent string. This gets sent to Analytics so it shows the proper OS version. */ function buildUserAgentString() { const cpus = os.cpus(); const cpuModel = cpus.length > 1 ? cpus[0].model : 'NA'; return `${os.platform()}/${os.release()}/${cpuModel}`; } // export singleton instance module.exports = new AnalyticsCollector();; ================================================ FILE: lib/utils/beautifyStackTrace.js ================================================ const fs = require('fs'); const stackTraceParser = require('stacktrace-parser'); const AssertionError = require('assertion-error'); const {filterStackTrace} = require('./stackTrace.js'); const alwaysDisplayError = require('./alwaysDisplayError'); const {colors} = require('./chalkColors.js'); /** * Read the User file from the stackTrace and create a string with highlighting the line with error * @param {Error} err */ function beautifyStackTrace(err, errStackPassed = false, modulepath, cli = true) { if ((err instanceof AssertionError) || alwaysDisplayError(err) || errStackPassed) { try { const errorStack = errStackPassed ? err : err.stack; const parsedStacks = stackTraceParser.parse(filterStackTrace(errorStack)); let parsedStack = modulepath ? parsedStacks.find(o => o.file === modulepath) : parsedStacks[0]; if (!parsedStack) { parsedStack = parsedStacks[0]; } const file = fs.readFileSync(parsedStack.file, 'utf-8'); const errorLinesofFile = file.split(/\r?\n/); const formattedStack = formatStackTrace(errorLinesofFile, parsedStack); if (!cli) { return formattedStack; } const desiredLines = formattedStack.codeSnippet.reduce(function(lines, newLine) { const currentLine = newLine.line_number; if (currentLine === formattedStack.error_line_number) { lines += '\n ' + colors.bgRed.white(` ${currentLine} | ${newLine.code} `); } else if (currentLine <= (formattedStack.error_line_number + 2) && currentLine >= (formattedStack.error_line_number - 2)) { lines += `\n ${currentLine} | ${newLine.code}`; } return lines; }, ''); const delimiter = (new Array(parsedStack.file.length + 3).join('–')); return ' ' + parsedStack.file + `:${formattedStack.error_line_number}\n ` + delimiter + desiredLines + '\n ' + delimiter + '\n'; } catch (err) { return ''; } } return ''; } /** * Read the User file from the stackTrace and create a string with highlighting the line with error * @param {Error} err */ function formatStackTrace(errorLinesofFile, parsedStack) { const result = { filePath: parsedStack.file, error_line_number: parsedStack.lineNumber, codeSnippet: [] }; errorLinesofFile.reduce(function(lines, newLine, lineIndex) { const currentLine = lineIndex + 1; if (currentLine <= (parsedStack.lineNumber + 2) && currentLine >= (parsedStack.lineNumber - 2)) { result.codeSnippet.push({ line_number: currentLine, code: newLine }); } }, ''); return result; } module.exports = beautifyStackTrace; ================================================ FILE: lib/utils/browsername.js ================================================ const BrowserName = module.exports = { get CHROME() { return 'chrome'; }, get FIREFOX() { return 'firefox'; }, get SAFARI() { return 'safari'; }, get EDGE() { return 'MicrosoftEdge'; }, get INTERNET_EXPLORER() { return 'internet explorer'; }, get OPERA() { return 'opera'; } }; Object.freeze(BrowserName); ================================================ FILE: lib/utils/chalkColors.js ================================================ const chalk = require('chalk'); class ChalkColors { constructor() { this.instance = new chalk.Instance(); this.origLevel = this.instance.level; this.loadCustomColors(); // for backward compatibility if (process.env.COLORS === '0') { this.disable(); } } loadCustomColors() { const colorsInstance = this.instance; // foreground colors colorsInstance.dark_gray = colorsInstance.black.bold; colorsInstance.light_blue = colorsInstance.blue.bold; colorsInstance.light_green = colorsInstance.green.bold; colorsInstance.light_cyan = colorsInstance.cyan.bold; colorsInstance.light_red = colorsInstance.red.bold; colorsInstance.light_purple = colorsInstance.magenta.bold; colorsInstance.light_gray = colorsInstance.white; colorsInstance.purple = colorsInstance.magenta; colorsInstance.brown = colorsInstance.yellow; colorsInstance.stack_trace = colorsInstance.gray; } get colors() { return this.instance; } get colorsEnabled() { return this.instance.level !== 0; } disable() { this.prevLevel = this.instance.level; this.instance.level = 0; } enable() { this.instance.level = this.prevLevel; } reset() { this.instance.level = this.origLevel; } } module.exports = new ChalkColors(); ================================================ FILE: lib/utils/createPromise.js ================================================ /** * @return {{resolve, reject, promise}} */ module.exports = function createPromise() { const deferred = { resolve: null, reject: null, promise: null }; deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); return deferred; }; ================================================ FILE: lib/utils/debuggability.js ================================================ class Debuggability { static get stepOverAndPause() { return this._stepOverAndPause || false; } static set stepOverAndPause(value) { this._stepOverAndPause = value; } static reset() { this._stepOverAndPause = false; } static get debugMode() { return this._debugMode; } static set debugMode(value) { this._debugMode = value; } } module.exports = Debuggability; ================================================ FILE: lib/utils/getAllClassMethodNames.js ================================================ const reserved = [ 'constructor', 'isPrototypeOf', 'propertyIsEnumerable', 'toString', 'valueOf', 'toLocaleString' ]; const isObjectPrototype = (item) => { return item.constructor.name === 'Object' && Object.getPrototypeOf(item) === null; }; module.exports = function(instance, additionalReserved = []) { const result = new Set(); while (instance && !isObjectPrototype(instance)) { Object.getOwnPropertyNames(instance).forEach(p => { if ((typeof instance[p] == 'function') && !reserved.includes(p) && !additionalReserved.includes(p)) { result.add(p); } }); instance = Object.getPrototypeOf(instance); } return [...result]; }; ================================================ FILE: lib/utils/getFreePort.js ================================================ /** * @method getFreePort * @param host * @returns {Promise} */ module.exports = function(host = 'localhost') { const net = require('net'); return new Promise((resolve, reject) => { const server = net.createServer(); server.on('listening', function () { const serverAddress = server.address(); if (!serverAddress || typeof serverAddress === 'string') { reject(new Error('Unable to get port from server address.')); } else { resolve(serverAddress.port); } server.close(); }); server.on('error', (e) => { let err; if (e.code === 'EADDRINUSE' || e.code === 'EACCES') { err = new Error('Unable to find a free port'); } else { err = e; } reject(err); }); // By providing 0 we let the operative system find an arbitrary port server.listen(0, host); }); }; ================================================ FILE: lib/utils/index.js ================================================ const path = require('path'); const fs = require('fs'); const glob = require('glob'); const lodashMerge = require('lodash/merge'); const {By, Capabilities} = require('selenium-webdriver'); const {inspect} = require('util'); const Logger = require('./logger'); const BrowserName = require('./browsername'); const LocateStrategy = require('./locatestrategy.js'); const PeriodicPromise = require('./periodic-promise.js'); const createPromise = require('./createPromise'); const isErrorObject = require('./isErrorObject'); const alwaysDisplayError = require('./alwaysDisplayError'); const Screenshots = require('./screenshots.js'); const Snapshots = require('./snapshots.js'); const TimedCallback = require('./timed-callback.js'); const getFreePort = require('./getFreePort'); const requireModule = require('./requireModule.js'); const getAllClassMethodNames = require('./getAllClassMethodNames.js'); const VERSION = require('./version.js'); const printVersionInfo = require('./printVersionInfo.js'); const {filterStack, filterStackTrace, showStackTrace, stackTraceFilter, errorToStackTrace} = require('./stackTrace.js'); const beautifyStackTrace = require('./beautifyStackTrace.js'); const SafeJSON = require('./safeStringify.js'); const formatRegExp = /%[sdj%]/g; const testSuiteNameRegxp = /(_|-|\.)*([A-Z]*)/g; const nameSeparatorRegxp = /(\s|\/)/; const PrimitiveTypes = { OBJECT: 'object', FUNCTION: 'function', BOOLEAN: 'boolean', NUMBER: 'number', STRING: 'string', UNDEFINED: 'undefined' }; class Utils { static get tsFileExt() { return '.ts'; } static get jsFileExt() { return '.js'; } static isObject(obj) { return obj !== null && typeof obj == 'object'; } static isFunction(fn) { return typeof fn == PrimitiveTypes.FUNCTION; } static isBoolean(value) { return typeof value == PrimitiveTypes.BOOLEAN; } static isNumber(value) { return typeof value == PrimitiveTypes.NUMBER; } static isString(value) { return typeof value == PrimitiveTypes.STRING; } static isUndefined(value) { return typeof value == PrimitiveTypes.UNDEFINED; } static isDefined(value) { return !Utils.isUndefined(value); } static isES6AsyncFn(fn) { return Utils.isFunction(fn) && fn.constructor.name === 'AsyncFunction'; } static enforceType(value, type) { type = type.toLowerCase(); switch (type) { case PrimitiveTypes.STRING: case PrimitiveTypes.BOOLEAN: case PrimitiveTypes.NUMBER: case PrimitiveTypes.FUNCTION: if (typeof value != type) { throw new Error(`Invalid type ${typeof value} for value "${value}". Expecting "${type}" instead.`); } return; } throw new Error(`Invalid type ${type} for ${value}`); } static convertBoolean(value) { if (Utils.isString(value) && (!value || value === 'false' || value === '0')) { return false; } return Boolean(value); } static get symbols() { let ok = String.fromCharCode(10004); let fail = String.fromCharCode(10006); if (process.platform === 'win32') { ok = '\u221A'; fail = '\u00D7'; } return { ok: ok, fail: fail }; } /** * @param {object|string} definition * @return {object} */ static convertToElementSelector(definition) { const selector = Utils.isString(definition) ? {selector: definition} : definition; return selector; } static isElementGlobal(selector) { return selector.webElementLocator instanceof By; } /** * @param {object} definition * @param {object} [props] * @return {object} */ static setElementSelectorProps(definition, props = {}) { const selector = Utils.convertToElementSelector(definition); if (!selector || Utils.isElementGlobal(selector)) { return selector; } Object.keys(props).forEach(function(key) { selector[key] = props[key]; }); return selector; } static formatElapsedTime(timeMs, includeMs = false) { const seconds = timeMs / 1000; return (seconds < 1 && timeMs + 'ms') || (seconds > 1 && seconds < 60 && (seconds + 's')) || (Math.floor(seconds / 60) + 'm' + ' ' + Math.floor(seconds % 60) + 's' + (includeMs ? (' / ' + timeMs + 'ms') : '')); } /** * Wrap a synchronous function, turning it into an psuedo-async fn with a callback as * the last argument if necessary. `asyncArgCount` is the expected argument * count if `fn` is already asynchronous. * * @deprecated * @param {number} asyncArgCount * @param {function} fn * @param {object} [context] */ static makeFnAsync(asyncArgCount, fn, context) { if (fn.length === asyncArgCount) { return fn; } return function(...args) { const done = args.pop(); context = context || null; fn.apply(context, args); done(); }; } static makePromise(handler, context, args) { const result = Reflect.apply(handler, context, args); if (result instanceof Promise) { return result; } return Promise.resolve(result); } static checkFunction(name, parent) { return parent && (typeof parent[name] == 'function') && parent[name] || false; } static getTestSuiteName(moduleName) { moduleName = moduleName.replace(testSuiteNameRegxp, function(match, $0, $1, offset, string) { if (!match) { return ''; } return (offset > 0 && (string.charAt(offset - 1) !== ' ') ? ' ' : '') + $1; }); const words = moduleName.split(nameSeparatorRegxp).map(function(word, index, matches) { if (word === '/') { return '/'; } return word.charAt(0).toUpperCase() + word.substr(1); }); return words.join(''); } /** * A smaller version of util.format that doesn't support json and * if a placeholder is missing, it is omitted instead of appended * * @param f * @returns {string} */ static format(message, selector, timeMS) { return String(message).replace(formatRegExp, function(exp) { if (exp === '%%') { return '%'; } switch (exp) { case '%s': return String(selector); case '%d': return Number(timeMS); default: return exp; } }); } static getModuleKey(filePath, srcFolders, fullPaths) { const modulePathParts = filePath.split(path.sep); let diffInFolder = ''; let folder = ''; let parentFolder = ''; const moduleName = modulePathParts.pop(); filePath = modulePathParts.join(path.sep); if (srcFolders) { for (let i = 0; i < srcFolders.length; i++) { folder = path.resolve(srcFolders[i]); if (fullPaths.length > 1) { parentFolder = folder.split(path.sep).pop(); } if (filePath.indexOf(folder) === 0) { diffInFolder = filePath.substring(folder.length + 1); break; } } } parentFolder = this.isFileNameValid(parentFolder) ? '' : parentFolder; return path.join(parentFolder, diffInFolder, moduleName); } static getOriginalStackTrace(commandFn) { let originalStackTrace; if (commandFn.stackTrace) { originalStackTrace = commandFn.stackTrace; } else { const err = new Error; Error.captureStackTrace(err, commandFn); originalStackTrace = err.stack; } return originalStackTrace; } // util to replace deprecated fs.existsSync static dirExistsSync(path) { try { return fs.statSync(path).isDirectory(); // eslint-disable-next-line no-empty } catch (e) {} return false; } static fileExistsSync(path) { try { return fs.statSync(path).isFile(); // eslint-disable-next-line no-empty } catch (e) {} return false; } static fileExists(path) { return Utils.checkPath(path) .then(function(stats) { return stats.isFile(); }) .catch(function(err) { return false; }); } static isTsFile(fileName){ return (path.extname(fileName) === Utils.tsFileExt); } static get validExtensions() { return [ Utils.jsFileExt, '.mjs', '.cjs', '.jsx', Utils.tsFileExt, '.cts', '.mts', '.tsx' ]; } static isFileNameValid(fileName) { return Utils.validExtensions.includes(path.extname(fileName)); } static checkPath(source, originalErr = null, followSymlinks = true) { return new Promise(function(resolve, reject) { if (glob.hasMagic(source)) { return resolve(); } fs[followSymlinks ? 'stat' : 'lstat'](source, function(err, stats) { if (err) { return reject(err.code === 'ENOENT' && originalErr || err); } resolve(stats); }); }); } /** * @param {string} source * @return {Promise} */ static isFolder(source) { return Utils.checkPath(source, null, false).then(stats => stats.isDirectory()); } /** * @param {string} source * @return {Promise} */ static readDir(source) { return new Promise(function(resolve, reject) { const callback = function(err, list) { if (err) { return reject(err); } resolve(list); }; glob.hasMagic(source) ? glob(source, callback) : fs.readdir(source, callback); }); } /** * * @param {string} sourcePath * @param {Array} namespace * @param {function} loadFn * @param {function} readSyncFn */ static readFolderRecursively(sourcePath, namespace = [], loadFn, readSyncFn) { let resources; if (glob.hasMagic(sourcePath)) { resources = glob.sync(sourcePath); } else if (Utils.isFunction(readSyncFn)) { const result = readSyncFn(sourcePath); sourcePath = result.sourcePath; resources = result.resources; } else { resources = fs.readdirSync(sourcePath); } resources.sort(); // makes the list predictable resources.forEach(resource => { if (path.isAbsolute(resource)) { sourcePath = path.dirname(resource); resource = path.basename(resource); } const isFolder = fs.lstatSync(path.join(sourcePath, resource)).isDirectory(); if (isFolder) { const pathFolder = path.join(sourcePath, resource); const ns = namespace.slice(0); ns.push(resource); Utils.readFolderRecursively(pathFolder, ns, loadFn); return; } loadFn(sourcePath, resource, namespace); }); } static getPluginPath(pluginName) { return path.resolve(require.resolve(pluginName, { paths: [process.cwd()] })); } static singleSourceFile(argv = {}) { const {test, _source} = argv; if (Utils.isString(test)) { return Utils.fileExistsSync(test); } return Array.isArray(_source) && _source.length === 1 && Utils.fileExistsSync(_source[0]); } static getConfigFolder(argv) { if (!argv || !argv.config) { return ''; } return path.dirname(argv.config); } /** * * @param {Array} arr * @param {number} maxDepth * @param {Boolean} includeEmpty * @returns {Array} */ static flattenArrayDeep(arr, maxDepth = 4, includeEmpty = false) { if (!Array.isArray(arr)) { throw new Error(`Utils.flattenArrayDeep excepts an array to be passed. Received: "${arr === null ? arr : typeof arr}".`); } return (function flatten(currentArray, currentDepth, initialValue = []) { currentDepth = currentDepth + 1; return currentArray.reduce(function(prev, value) { if (Array.isArray(value)) { const result = prev.concat(value); if (Array.isArray(result) && currentDepth <= maxDepth) { return flatten(result, currentDepth); } return result; } currentDepth = 0; if (!includeEmpty && (value === null || value === undefined || value === '')) { return prev; } prev.push(value); return prev; }, initialValue); })(arr, 0); } /** * Strips out all control characters from a string * However, excludes newline and carriage return * * @param {string} input String to remove invisible chars from * @returns {string} Initial input string but without invisible chars */ static stripControlChars(input) { return input && input.replace( // eslint-disable-next-line no-control-regex /[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '' ); } static relativeUrl(url) { return !(url.includes('://')); } static uriJoin(baseUrl, uriPath) { let result = baseUrl; if (baseUrl.endsWith('/')) { result = result.substring(0, result.length - 1); } if (!uriPath.startsWith('/')) { result = result + '/'; } return result + uriPath; } static replaceParams(url, params = {}) { return Object.keys(params).reduce(function(prev, param) { prev = prev.replace(`:${param}`, params[param]); return prev; }, url); } static createFolder(dirPath) { return new Promise((resolve, reject) => { Utils.mkpath(dirPath, function(err) { if (err) { return reject(err); } resolve(); }); }); } /** * Writes content to file. Creates parent folders if the folders do not exist. * @param {string} filePath * @param {string} data */ static writeToFile(filePath, data, encoding = null) { const dir = path.resolve(filePath, '..'); return new Promise((resolve, reject) => { Utils.mkpath(dir, function(err) { if (err) { reject(err); } else { fs.writeFile(filePath, data, encoding, function(err) { if (err) { reject(err); } else { resolve(filePath); } }); } }); }); } static containsMultiple(arrayOrString, valueToFind, separator = ',') { if (typeof valueToFind == 'string') { valueToFind = valueToFind.split(separator); } if (Array.isArray(valueToFind)) { if (valueToFind.length > 1) { return valueToFind.every(item => arrayOrString.includes(item)); } valueToFind = valueToFind[0]; } return arrayOrString.includes(valueToFind); } static shouldReplaceStack(err) { return !alwaysDisplayError(err); } static findTSConfigFile(existingTSConfig) { const projectTsFileLocation1 = path.join(process.cwd(), 'nightwatch', 'tsconfig.json'); const projectTsFileLocation2 = path.join(process.cwd(), 'tsconfig.nightwatch.json'); if (Utils.fileExistsSync(existingTSConfig)) { return existingTSConfig; } if (Utils.fileExistsSync(projectTsFileLocation1)) { return projectTsFileLocation1; } if (Utils.fileExistsSync(projectTsFileLocation2)) { return projectTsFileLocation2; } return ''; } static loadTSNode(projectTsFile) { try { require('ts-node').register({ esm: false, transpileOnly: true, project: projectTsFile, // Always compile and execute .ts files as CommonJS, // even in ESM projects. moduleTypes: { '**/*.ts': 'cjs' } }); } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { const error = new Error(`ts-node needs to be installed as a project dependency. You can install ts-node from NPM using:\n\n ${Logger.colors.light_green('npm i ts-node --save-dev')}`); error.showTrace = false; error.displayed = false; throw error; } err.showTrace = false; throw err; } } static isSafari(desiredCapabilities = {}) { const browserName = desiredCapabilities.browserName || (desiredCapabilities instanceof Capabilities && desiredCapabilities.getBrowserName()); if (browserName && browserName.toLowerCase() === 'safari') { return true; } return false; } static isChrome(desiredCapabilities = {}) { const browserName = desiredCapabilities.browserName || (desiredCapabilities instanceof Capabilities && desiredCapabilities.getBrowserName()); if (browserName && browserName.toLowerCase() === 'chrome') { return true; } return false; } static isLocalhost(webdriver = {}) { const {host} = webdriver; return ['127.0.0.1', 'localhost'].indexOf(host) > -1; } static setFunctionName(fn, name) { Object.defineProperty(fn, 'name', { value: name, writable: false, enumerable: false, configurable: true }); return fn; } static stringifyObject(objects) { const objectString = Utils.isObject(objects) ? inspect(objects) : objects; return `"${objectString}"`; } /** * make all directories in a path, like mkdir -p */ static mkpath(dirpath, mode, callback) { dirpath = path.resolve(dirpath); if (typeof mode === 'function' || typeof mode === 'undefined') { callback = mode; mode = parseInt('0777', 8); if (!callback) { callback = function() {}; } } fs.stat(dirpath, function (err, stats) { if (err) { if (err.code === 'ENOENT') { Utils.mkpath(path.dirname(dirpath), mode, function (err) { if (err) { callback(err); } else { fs.mkdir(dirpath, mode, function (err) { if (!err || err.code === 'EEXIST') { callback(null); } else { callback(err); } }); } }); } else { callback(err); } } else if (stats.isDirectory()) { callback(null); } else { callback(new Error(dirpath + ' exists and is not a directory')); } }); } } lodashMerge(Utils, { PrimitiveTypes, BrowserName, LocateStrategy, Logger, isErrorObject, requireModule, createPromise, getAllClassMethodNames, SafeJSON, filterStack, filterStackTrace, showStackTrace, stackTraceFilter, errorToStackTrace, PeriodicPromise, Screenshots, Snapshots, TimedCallback, getFreePort, VERSION, printVersionInfo, alwaysDisplayError, beautifyStackTrace }); module.exports = Utils; ================================================ FILE: lib/utils/isErrorObject.js ================================================ module.exports = function(err) { return err instanceof Error || Object.prototype.toString.call(err) === '[object Error]'; }; ================================================ FILE: lib/utils/locatestrategy.js ================================================ const __RECURSION__ = 'recursion'; class LocateStrategy { static get Strategies() { return { ID: 'id', CSS_SELECTOR: 'css selector', LINK_TEXT: 'link text', PARTIAL_LINK_TEXT: 'partial link text', TAG_NAME: 'tag name', XPATH: 'xpath', NAME: 'name', CLASS_NAME: 'class name', // Appium-specific strategies ACCESSIBILITY_ID: 'accessibility id', ANDROID_UIAUTOMATOR: '-android uiautomator', IOS_PREDICATE_STRING: '-ios predicate string', IOS_CLASS_CHAIN: '-ios class chain' }; } static isValid(strategy) { return Object.keys(LocateStrategy.Strategies).some(key => { return String(strategy).toLocaleLowerCase() === LocateStrategy.Strategies[key]; }); } static getList() { return Object.keys(LocateStrategy.Strategies).map(k => LocateStrategy.Strategies[k]).join(', '); } static get XPATH() { return LocateStrategy.Strategies.XPATH; } static get CSS_SELECTOR() { return LocateStrategy.Strategies.CSS_SELECTOR; } static getDefault() { return LocateStrategy.CSS_SELECTOR; } static get Recursion() { return __RECURSION__; } } module.exports = LocateStrategy; ================================================ FILE: lib/utils/logger/index.js ================================================ const util = require('util'); const boxen = require('boxen'); const didYouMean = require('didyoumean'); const AssertionError = require('assertion-error'); const lodashEscape = require('lodash/escape'); const LogSettings = require('./log_settings.js'); const chalkColors = require('../chalkColors.js'); const addDetailedError = require('../addDetailedError.js'); const Errors = require('../../transport/errors'); const beautifyStackTrace = require('../beautifyStackTrace.js'); const alwaysDisplayError = require('../alwaysDisplayError.js'); const Severity = { LOG: 'LOG', INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR' }; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } let __instance__; // 26 Feb 16:19:34 function timestamp(d = new Date(), format) { if (format === 'iso') { return d.toISOString(); } const time = [ pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds()) ].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } function inspectObject(obj) { return util.inspect(obj, { showHidden: false, depth: 4, colors: chalkColors.colorsEnabled }) .replace(/^\s{2}/gm, ' ') .replace(/^}$/m, ' }'); } function logObject(obj) { // eslint-disable-next-line no-console console.log(' ', inspectObject(obj)); } function logTimestamp(d) { if (LogSettings.isLogTimestamp()) { return chalkColors.colors.white.bold(timestamp(d, LogSettings.timestampFormat)); } return ''; } function logMessage(type, message, args, alwaysShow) { if (!message || !LogSettings.outputEnabled || !LogSettings.enabled && !alwaysShow) { return; } const colors = chalkColors.colors; let messageStr = ''; let logMethod = 'log'; let prefix; const d = new Date(); const timeIso = d.toISOString(); let timestamp = logTimestamp(d); switch (type) { case Severity.ERROR: prefix = colors.bgBlack.yellow.bold(type); // eslint-disable-next-line no-control-regex messageStr = message.match(/\u001b\[.*?m/) ? message : colors.red.bold(message); logMethod = 'error'; break; case Severity.INFO: prefix = colors.bgBlack.magenta.bold(type); // eslint-disable-next-line no-control-regex messageStr = message.match(/\u001b\[.*?m/) ? message : colors.cyan.bold(message); break; case Severity.LOG: prefix = colors.bgBlack.white.bold(type + ' '); // eslint-disable-next-line no-control-regex messageStr = message.match(/\u001b\[.*?m/) ? message : colors.white.bold(message); break; case Severity.WARN: prefix = colors.bgBlack.green.bold(type); // eslint-disable-next-line no-control-regex messageStr = message.match(/\u001b\[.*?m/) ? message : colors.green.bold(message); logMethod = 'warn'; break; } if (LogSettings.timestampFormat === 'iso') { let severity = type.toUpperCase(); if (severity === Severity.LOG) { severity = Severity.INFO; } timestamp += ` ${severity} nightwatch:`; } // eslint-disable-next-line no-console console[logMethod](timestamp, messageStr); if (args.length > 0) { let inlineArgs = []; args.forEach(function(item) { if (item === undefined) { return; } if (Object.prototype.toString.call(item) === '[object Object]' && Object.keys(item).length > 0) { if (inlineArgs.length) { // eslint-disable-next-line no-console console[logMethod](...inlineArgs); inlineArgs = []; } logObject(item); } else { inlineArgs.push(item); } }); if (inlineArgs.length) { // eslint-disable-next-line no-console console[logMethod](...inlineArgs); inlineArgs = []; } } } function findSuggestion({message} = {}) { if (message && message.includes('is not a function')) { const match = message.replace(' is not a function', '').replace('browser.', ''); if (match) { const suggestion = didYouMean(match, ['element.findAll']); if (suggestion) { return suggestion; } } } return null; } function logRequest(message, params) { if (!message || !LogSettings.htmlReporterEnabled) { return; } const d = new Date(); const timeIso = d.toISOString(); const instance = Logger.getInstance(); instance.output.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]); instance.commandOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]); instance.testSectionOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]); instance.testHooksOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]); } function logError(severity, errOrMessage, args) { const alwaysDisplay = LogSettings.isErrorLogEnabled() && severity === Severity.ERROR; logMessage(severity, errOrMessage, args, alwaysDisplay); } class Logger { static getInstance() { return __instance__; } constructor() { this.colors = chalkColors.colors; this.output = []; this.commandOutput = []; this.testSectionOutput = []; this.testHooksOutput = []; } logMessage(...args) { logMessage(...args); } inspectObject(obj) { return inspectObject(obj); } info(message, ...args) { logMessage('INFO', message, args); } log(message, ...args) { logMessage('LOG', message, args); } warn(message, ...args) { message = this.getErrorContent(message); logError('WARN', message, args); } error(message, ...args) { message = this.getErrorContent(message); logError('ERROR', message, args); } request(message, params) { logRequest(message, params); } response(message, params) { logRequest(message, params); } underline(text) { if (!this.colors) { return text; } return '\u{1b}[4m' + text + '\u{1b}[24m'; } setOptions(settings) { this.setOutputEnabled(settings.output); this.setHtmlReporterEnabled(typeof settings.output_folder == 'string'); this.setDetailedOutput(settings.detailed_output); this.setLogTimestamp(settings.output_timestamp, settings.timestamp_format); this.setErrorLog(settings.disable_error_log); if (settings.disable_colors) { this.disableColors(); } if (settings.silent) { this.disable(); } else { this.enable(); } } disableColors() { chalkColors.disable(); } setHtmlReporterEnabled(value) { LogSettings.htmlReporterEnabled = value; } disable() { LogSettings.disable(); } enable() { LogSettings.enable(); } setOutputEnabled(val = true) { LogSettings.outputEnabled = val; } isOutputEnabled() { return LogSettings.outputEnabled; } isHtmlReporterEnabled() { return LogSettings.htmlReporterEnabled; } setDetailedOutput(val) { LogSettings.detailedOutput = val; } isDetailedOutput() { return LogSettings.outputEnabled && LogSettings.detailedOutput; } setLogTimestamp(val, format) { LogSettings.setLogTimestamp(val, format); } setHttpLogOptions(opts) { LogSettings.setHttpLogOptions(opts); } showRequestData() { return LogSettings.showRequestData; } showResponseHeaders() { return LogSettings.showResponseHeaders; } isLogTimestamp() { return LogSettings.isLogTimestamp(); } isErrorLogEnabled() { return LogSettings.isErrorLogEnabled(); } isEnabled() { return LogSettings.enabled; } setErrorLog(val = false) { LogSettings.disableErrorLog = val; } logDetailedMessage(message, type = 'log') { if (!LogSettings.outputEnabled || !LogSettings.detailedOutput) { return; } // eslint-disable-next-line no-console console[type](message); } getFailedAssertions(assertions, modulepath) { return assertions.reduce((prev, a) => { if (a.failure !== false) { prev.push(` ${this.colors.red.bold('→') + this.getErrorContent(a, modulepath)}`); } return prev; }, []).join('\n'); } isAssertionError(err) { return (err instanceof AssertionError) || err.name === 'AssertionError' || err.name === 'NightwatchAssertError' || err.name === 'NightwatchMountError'; } getErrorContent(error, modulepath) { let errorObj; if (this.isAssertionError(error)) { errorObj = error; } else { errorObj = Errors.getErrorObject(error); } addDetailedError(errorObj, modulepath); const content = []; let errorTitle = ` ${this.colors.light_red(errorObj.name)}`; if (this.isAssertionError(error) || alwaysDisplayError(error)) { errorTitle = ` ${this.colors.red.bold('✖') + errorTitle}`; } const message = []; if (errorObj.message || errorObj.fullMsg) { let errorObjMessage = errorObj.message; // eslint-disable-next-line no-control-regex errorObjMessage = errorObjMessage.match(/\u001b\[.*?m/) ? errorObjMessage : this.colors.red(errorObjMessage); message.push(errorObjMessage); if (errorObj.detailedErr) { message.push(this.colors.light_green(errorObj.detailedErr)); if (errorObj.extraDetail) { message.push(this.colors.light_green(errorObj.extraDetail)); } } } content.push(errorTitle); const showStack = errorObj.showTrace || errorObj.showTrace === undefined; if (!showStack && errorObj.reportShown) { return ' ' + message.join('\n '); } if (!showStack && !this.isAssertionError(error) && !error.help) { return '\n' + boxen(`${message.join('\n')}\n`, {padding: 1, borderColor: 'red'}) + '\n'; } const messageStr = message.join('\n '); content.push(` ${messageStr}`); const suggestion = findSuggestion(error); if (suggestion) { errorObj.help = errorObj.help || []; errorObj.help.unshift(' did you mean: ' + suggestion + '?'); } if (errorObj.help) { content.push(this.colors.brown('\n Try fixing by :')); errorObj.help.forEach((step, index) => { content.push(` ${this.colors.blue(index + 1)}. ${step}`); }); } if (errorObj.link){ content.push(`\n ${this.colors.brown('Read More')} : \n ${this.colors.cyan(errorObj.link)} `); } let stack = error.stack || error.stackTrace; if (stack && showStack) { stack = ' at' + stack.split(/ {4}at/g).slice(1).join(' at'); const beautified = beautifyStackTrace(stack, true, modulepath); if (beautified) { content.push(this.colors.brown('\n Error location:')); content.push(beautified); } if (alwaysDisplayError(errorObj)) { content.push(this.colors.brown(' Stack Trace :')); const coloredStack = stack.split('\n').map((line) => this.colors.stack_trace(line)).join('\n'); content.push(`${coloredStack}\n`); } } if (content.length === 2) { if (content[0].includes('WebDriverError')) { return content.join(this.colors.light_red(':')); } if (content[0].includes('Error') && content[1].includes('Error while')) { const firstLine = content.shift(); content[0] = ' ' + firstLine + content[0]; } } return content.join('\n'); } formatMessage(msg, ...args) { args = args.map(val => { return this.colors.brown(val); }); return util.format(msg, ...args); } } module.exports = new (function() { __instance__ = new Logger(); return Logger.getInstance(); }); const getOutput = module.exports.getOutput = function() { if (!Logger.getInstance()) { return []; } const {output} = Logger.getInstance(); return output.slice(0); }; module.exports.collectOutput = function() { const instance = Logger.getInstance(); if (!instance) { return []; } const output = getOutput(); instance.output = []; return output; }; module.exports.collectTestSectionOutput = function() { const instance = Logger.getInstance(); if (!instance) { return []; } const {testSectionOutput} = instance; instance.testSectionOutput = []; return testSectionOutput; }; module.exports.collectCommandOutput = function() { const instance = Logger.getInstance(); if (!instance) { return []; } const {commandOutput} = instance; instance.commandOutput = []; return commandOutput; }; module.exports.collectTestHooksOutput = function() { const instance = Logger.getInstance(); if (!instance) { return []; } const {testHooksOutput} = instance; instance.testHooksOutput = []; return testHooksOutput; }; module.exports.reset = function() { const instance = Logger.getInstance(); if (!instance) { return; } instance.testSectionOutput = []; instance.output = []; }; ================================================ FILE: lib/utils/logger/log_settings.js ================================================ class LogSettings { #outputEnabled; #showResponseHeaders; #showRequestData; #detailedOutput; #disableErrorLog; #log_timestamp; #timestamp_format; #enabled; #htmlReporterEnabled; constructor() { this.#outputEnabled = true; this.#showResponseHeaders = false; this.#showRequestData = { enabled: true, trimLongScripts: true }, this.#detailedOutput = true; this.#disableErrorLog = false; this.#log_timestamp = false; this.#timestamp_format = null; this.#enabled = true; this.#htmlReporterEnabled = false; } get outputEnabled() { return this.#outputEnabled; } get detailedOutput() { return this.#detailedOutput; } get showRequestData() { return this.#showRequestData; } get enabled() { return this.#enabled; } get showResponseHeaders() { return this.#showResponseHeaders; } get timestampFormat() { return this.#timestamp_format; } set outputEnabled(value) { if (typeof value === 'undefined') { value = true; } this.#outputEnabled = value; } set detailedOutput(value) { this.#detailedOutput = value; } set disableErrorLog(value) { if (typeof value === 'undefined') { value = true; } this.#disableErrorLog = value; } set htmlReporterEnabled(value) { this.#htmlReporterEnabled = value; } get htmlReporterEnabled() { return this.#htmlReporterEnabled; } isLogTimestamp() { return this.#log_timestamp; } isErrorLogEnabled() { return !this.#disableErrorLog; } disable() { this.#enabled = false; } enable() { this.#enabled = true; } setLogTimestamp(val, format) { this.#log_timestamp = val; this.#timestamp_format = format; } setHttpLogOptions({showRequestData, showResponseHeaders}) { this.#showRequestData = showRequestData; this.#showResponseHeaders = showResponseHeaders; } } module.exports = new LogSettings(); ================================================ FILE: lib/utils/mobile.js ================================================ const {execSync} = require('child_process'); const Logger = require('./logger'); const untildify = require('untildify'); const path = require('path'); const semver = require('semver'); /** * Function to require mobile-helper */ function requireMobileHelper() { try { return require('@nightwatch/mobile-helper'); } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { err.message = `@nightwatch/mobile-helper needs to be installed as a project dependency. You can install @nightwatch/mobile-helper from NPM using:\n\n ${Logger.colors.light_green('npm i @nightwatch/mobile-helper --save-dev')}`; } else if (!semver.satisfies(process.version, '>=14.0.0')) { err.message = 'You are using Node ' + process.version + ', but @nightwatch/mobile-helper requires Node >= v14.0.0.\nPlease upgrade your Node version.'; } err.showTrace = false; err.displayed = false; throw err; } } /** * check if target is Android * @param {Object} desiredCapabilities * @returns {Boolean} */ function isAndroid(desiredCapabilities = {}){ const {platformName} = desiredCapabilities; if (platformName && platformName.toLowerCase() === 'android') { return true; } const options = desiredCapabilities['goog:chromeOptions'] || desiredCapabilities['moz:firefoxOptions']; if (options && options.androidPackage) { return true; } return false; }; /** * check if target is iOS Device * @param {Object} desiredCapabilities * @returns {Boolean} */ function isIos(desiredCapabilities = {}) { const {platformName} = desiredCapabilities; if (platformName && platformName.toLowerCase() === 'ios') { return true; } return false; } /** * check if target is Simulator * @param {Object} desiredCapabilities * @returns {Boolean} */ function isSimulator(desiredCapabilities){ if (isIos(desiredCapabilities) && desiredCapabilities['safari:useSimulator'] === true) { return true; } return false; }; /** * check if target is Real iOS Device * @param {Object} desiredCapabilities * @returns {Boolean} */ function isRealIos(desiredCapabilities) { if (isIos(desiredCapabilities) && desiredCapabilities['safari:useSimulator'] !== true) { return true; } return false; }; /** * check if the target is a mobile platform * @param {Object} desiredCapabilities * @returns {Boolean} */ function isMobile(desiredCapabilities){ if (isIos(desiredCapabilities) || isAndroid(desiredCapabilities)) { return true; } return false; }; /** * Check if Real iOS device UDID is correct * @param {String} udid * @returns {String} */ function iosRealDeviceUDID(udid){ if (udid.length > 25) { return udid; } if (udid.length < 24) { throw new Error('Incorrect UDID provided for real iOS device'); } return `${udid.substring(0, 8)}-${udid.substring(9, 25)}`; }; /** * Function to kill iOS Simulator * @param {String} udid */ function killSimulator(udid) { const cmd = `xcrun simctl shutdown ${udid}`; try { execSync(cmd, { stdio: 'pipe' }); } catch (e) { Logger.err(e); } } /** * Function to set and return the ANDROID_HOME * @returns {String} */ function getSdkRootFromEnv() { const androidHome = process.env.ANDROID_HOME; if (androidHome) { return process.env.ANDROID_HOME = path.resolve(untildify(androidHome)); } throw new AndroidHomeError(androidHome); } class AndroidHomeError extends Error { constructor(androidHome) { super(); this.message = 'ANDROID_HOME environment variable is NOT set or is NOT a valid path!'; this.name = 'AndroidHomeError'; this.help = [ `To setup Android requirements, run: ${Logger.colors.cyan('npx @nightwatch/mobile-helper android')}`, `For Android help, run: ${Logger.colors.cyan('npx @nightwatch/mobile-helper android --help \n')}` ]; this.stack = false; this.androidHome = androidHome; } } class RealIosDeviceIdError extends Error { constructor() { super(); this.name = 'RealIosDeviceIdError'; this.message = 'Real Device ID is neither configured nor passed'; this.help = [ `Pass ${Logger.colors.green('deviceId')} in the command (for e.g : ${Logger.colors.cyan('--deviceId 00008030-00024C2C3453402E')})`, `Or pass ${Logger.colors.green('safari:deviceUDID')} capability in config`, `To verify the deviceId run, ${Logger.colors.cyan('system_profiler SPUSBDataType | sed -n \'/iPhone/,/Serial/p\' | grep \'Serial Number:\' | awk -F \': \' \'{print $2}')}`, `For more help, run: ${Logger.colors.cyan('npx @nightwatch/mobile-helper ios')}\n` ]; this.stack = false; } } function getBinaryLocation(binaryName) { const {getBinaryLocation, getPlatformName} = requireMobileHelper(); return getBinaryLocation(getSdkRootFromEnv(), getPlatformName(), binaryName, true); } class AndroidBinaryError extends Error { constructor(message, binaryName) { super(); this.message = message; this.stack = false; this.binaryName = binaryName; } get help() { let help; const binaryLocation = getBinaryLocation(this.binaryName); if (binaryLocation === '') { help = [ `${Logger.colors.cyan(this.binaryName)} binary not found. Run command ${Logger.colors.cyan('npx @nightwatch/mobile-helper android')} to setup the missing requirements.` ]; } else if (this.binaryName === 'emulator') { help = [ `Run ${Logger.colors.cyan('npx @nightwatch/mobile-helper android')} and re-run the test.`, `If it still doesn't work, start the emulator by yourself by running ${Logger.colors.cyan(binaryLocation + ' @' + this.avd)} and then run the test.` ]; } else { help = [ `Run ${Logger.colors.cyan('npx @nightwatch/mobile-helper android')} and re-run the test.` ]; } return help; } } class AndroidConnectionError extends Error { constructor({message, detailedErr, extraDetail}) { super(); this.message = message; this.extraHelp = [detailedErr, extraDetail + '\n']; this.stack = false; } get help() { let binaryLocation = getBinaryLocation('adb'); let help; if (binaryLocation === '') { help = [ `${Logger.colors.cyan('adb')} binary not found. Run command ${Logger.colors.cyan('npx @nightwatch/mobile-helper android')} to setup the missing requirements.` ]; } else { if (binaryLocation === 'PATH') { binaryLocation = 'adb'; } if (this.message.includes('Failed to run adb command')) { help = [ `Run command: ${Logger.colors.cyan(binaryLocation + ' start-server')}`, `If still doesn't work, run "${Logger.colors.green('npx @nightwatch/mobile-helper android')}"` ]; } if (this.message.includes('no devices online')) { help = [ `If testing on real-device, check if device is connected with USB debugging turned on and ${Logger.colors.cyan(binaryLocation + ' devices')} should list the connected device.`, `If testing on emulator, check the Nightwatch configuration to make sure ${Logger.colors.cyan('real_mobile')} is set to ${Logger.colors.cyan('false')} and ${Logger.colors.cyan('avd')} to the name of AVD to launch and test on.` ]; } } return [...help, ...this.extraHelp]; } } class IosSessionNotCreatedError extends Error { constructor({message, name}, desiredCapabilities) { super(); this.message = message; this.name = name; this.stack = false; this.desiredCapabilities = desiredCapabilities; } get help() { let help; if (this.message.includes('session timed out')) { // 'The session timed out while connecting to a Safari instance.' help = [ 'Re-run the test command', `If it doesn't work, try running: ${Logger.colors.cyan('npx @nightwatch/mobile-helper ios')}` ]; } else if (this.message.includes('not find any session hosts') || this.message.includes('Some devices were found')) { // Could not find any session hosts that match the requested capabilities // or some devices were found, but could not be used if (isSimulator(this.desiredCapabilities)) { help = [ `Run command to get device list: ${Logger.colors.cyan('xcrun simctl list devices')}`, `Update the ${Logger.colors.cyan('safari:platformVersion')} and/or ${Logger.colors.cyan('safari:platforName')} in Nightwatch configuration accordingly`, `If it doesn't work, try running: ${Logger.colors.cyan('npx @nightwatch/mobile-helper ios')}` ]; } else { help = [ `Make sure you have passed correct ${Logger.colors.green('deviceId')} in the command (for e.g : ${Logger.colors.cyan('--deviceId 00008030-00024C2C3453402E')})`, `Or pass ${Logger.colors.green('safari:deviceUDID')} capability in config`, `To verify the deviceId run, ${Logger.colors.cyan('system_profiler SPUSBDataType | sed -n \'/iPhone/,/Serial/p\' | grep \'Serial Number:\' | awk -F \': \' \'{print $2}')}`, `For more help, run: ${Logger.colors.cyan('npx @nightwatch/mobile-helper ios')}\n` ]; } } else if (this.desiredCapabilities['safari:deviceUDID']) { help = [ `Verify the UDID of the device set in Nightwatch configuration (look for ${Logger.colors.cyan('safari:deviceUDID')} capability) or pass the correct UDID using ${Logger.colors.cyan('--deviceId')} flag in the test command.`, 'Re-run the test command', `If it doesn't work, try running: ${Logger.colors.cyan('npx @nightwatch/mobile-helper ios')}` ]; if (isRealIos(this.desiredCapabilities)) { help = [ `Check device connection by running command: ${Logger.colors.cyan('system_profiler SPUSBDataType | sed -n \'/iPhone/,/Serial/p\' | grep \'Serial Number:\' | awk -F \': \' \'{print $2}')}`, ...help ]; } } return help; } } module.exports = { isMobile, isIos, isRealIos, isSimulator, isAndroid, iosRealDeviceUDID, killSimulator, getSdkRootFromEnv, RealIosDeviceIdError, AndroidConnectionError, IosSessionNotCreatedError, AndroidBinaryError, AndroidHomeError, requireMobileHelper }; ================================================ FILE: lib/utils/periodic-promise.js ================================================ const EventEmitter = require('events'); const createPromise = require('./createPromise'); class PeriodicPromise extends EventEmitter { get rescheduleInterval() { return this.__rescheduleIntervalMs; } get ms() { return this.__timeoutMs; } get elapsedTime() { return new Date().getTime() - this.startTime; } get queue() { return this.__queue; } constructor({ rescheduleInterval, timeout }) { super(); this.__queue = []; this.__rescheduleIntervalMs = rescheduleInterval; this.__timeoutMs = timeout; this.retries = 0; } queueAction(opts) { this.__queue.push(opts); return this; } async runAction({prevResult, prevQueuePromise}) { if (!this.queue.length) { return null; } const queuePromise = this.queue.shift(); const deferred = createPromise(); try { const result = await this.perform(queuePromise, {prevResult, prevQueuePromise}, deferred); if (this.queue.length) { return await this.runAction({ prevResult: result, prevQueuePromise: queuePromise }); } return result; } catch (err) { if (err.name === 'TypeError' || err.name === 'ReferenceError' || !queuePromise.errorHandler) { throw err; } if (queuePromise.errorHandler) { return queuePromise.errorHandler(err); } } } async perform({ action, validate, isResultStale, successHandler = (r) => Promise.resolve(r), shouldRetryOnError = () => true, retryOnSuccess = false, retryOnFailure = true }, {prevResult, prevQueuePromise}, deferred) { let currentResult; try { // running the current action using the result from the previous action currentResult = await action(prevResult); // if the current action returns a stale reference to the previous result, re-run the previous action if (isResultStale && isResultStale(currentResult)) { const freshResult = await prevQueuePromise.action({cacheElementId: false}); if (freshResult.error instanceof Error) { throw freshResult.error; } currentResult = await action(freshResult); } } catch (err) { currentResult = { status: -1, message: err.message, stack: err.stack }; } const isValidResult = validate(currentResult); if (this.shouldRetry({retryOnSuccess, isValidResult, currentResult, shouldRetryOnError, retryOnFailure})) { this.reschedule(arguments[0], arguments[1], deferred); return deferred.promise; } if (isValidResult) { currentResult = await successHandler(currentResult); deferred.resolve(currentResult); return deferred.promise; } throw this.getError(currentResult); } run() { this.startTime = new Date().getTime(); return this.runAction({}); } reschedule(promise, opts, deferred) { setTimeout(() => { this.retries++; this.perform(promise, opts, deferred).catch(err => { deferred.reject(err); }); }, this.rescheduleInterval); } shouldRetry({retryOnFailure, retryOnSuccess, shouldRetryOnError, isValidResult, currentResult}) { const now = new Date().getTime(); const timePassed = (now - this.startTime) >= this.ms; const retryOnFailureResult = typeof retryOnFailure == 'function' ? retryOnFailure() : retryOnFailure; if (timePassed) { return false; } if (retryOnFailureResult && !isValidResult) { return shouldRetryOnError(currentResult); } const needsRetryOnSuccess = typeof retryOnSuccess == 'function' ? retryOnSuccess(currentResult) : retryOnSuccess; return (needsRetryOnSuccess && isValidResult); } getError(response) { if (response instanceof Error) { return response; } const {startTime, retries} = this; let {message = 'timeout error'} = response; if (response.error) { message = response.error; if (response.value && response.value.message) { message = response.value.message; } else if (message.message) { message = message.message; } } return new TimeoutError({ message, now: Date.now(), startTime, response, retries }); } } class TimeoutError extends Error { constructor({message, now, response, startTime, retries}) { super(message); this.name = 'TimeoutError'; this.now = now; this.response = response; this.startTime = startTime; this.retries = retries; } } module.exports = PeriodicPromise; ================================================ FILE: lib/utils/printVersionInfo.js ================================================ const VERSION = require('./version'); module.exports = function() { // eslint-disable-next-line no-console console.log('\n Nightwatch:'); // eslint-disable-next-line no-console console.log(' version: ' + VERSION.full); // eslint-disable-next-line no-console console.log(' changelog: https://github.com/nightwatchjs/nightwatch/releases/tag/v' + VERSION.full + '\n'); }; ================================================ FILE: lib/utils/requireModule.js ================================================ const mergeDefaultAndNamedExports = (module) => { const _default = module.default || {}; return Object.keys(module).reduce((prev, val) => { if (val !== 'default') { prev[val] = module[val]; } return prev; }, _default); } /** * Requires a module, supporting both CommonJS and ES6 modules. * * @param {string} fullpath - The full path to the module to require. * @returns {any|Promise} - The required module or a promise that resolves to the required module. */ module.exports = function (fullpath) { let exported; try { exported = require(fullpath); } catch (err) { const isEsmSyntaxError = err.message === 'Cannot use import statement outside a module' || err.message.includes('Unexpected token \'export\''); const isMjsFile = fullpath.endsWith('.mjs'); // calling require() on a .mjs file on Node.js < 20 throws ERR_REQUIRE_ESM. // calling require() on a .mjs file on Node.js >= 20 passes through without // any error, but if ts-node is activated, it throws ERR_REQUIRE_ESM in normal // cases (it still relies on the old CommonJS loader) and isEsmSyntaxError if // v8-compile-cache-lib is used (see PR #4437). // Use dynamic import() in both cases. if (err.code === 'ERR_REQUIRE_ESM' || (isMjsFile && isEsmSyntaxError)) { const { pathToFileURL } = require('node:url'); return import(pathToFileURL(fullpath).href).then(mergeDefaultAndNamedExports); } if (isEsmSyntaxError) { err.detailedErr = err.message; err.help = ['Using ES6 import/export syntax? - make sure to specify "type=module" in your package.json or use .mjs extension.']; err.link = 'https://nodejs.org/api/esm.html'; } throw err; } if (exported && Object.prototype.hasOwnProperty.call(exported, 'default')) { const keys = Object.keys(exported); if (keys.length === 1 || (keys.length === 2 && exported.__esModule === true)) { return exported.default; } } if (exported?.__esModule === true) { // ESM module with named exports present (with or without default export). return mergeDefaultAndNamedExports(exported); } return exported; } ================================================ FILE: lib/utils/safeStringify.js ================================================ class SafeStringify { static visit(obj, seen) { if (obj == null || typeof obj !== 'object') { return obj; } if (seen.indexOf(obj) !== -1) { return '[Circular]'; } seen.push(obj); if (typeof obj.toJSON === 'function') { try { const result = this.visit(obj.toJSON(), seen); seen.pop(); return result; } catch (err) { return '[Error]'; } } if (Array.isArray(obj)) { const result = obj.map(val => this.visit(val, seen)); seen.pop(); return result; } const result = Object.keys(obj).reduce((result, prop) => { result[prop] = this.visit(obj[prop], seen); return result; }, {}); seen.pop(); return result; } static safeJSON(obj) { const seen = []; return this.visit(obj, seen); } static stringify(obj) { return JSON.stringify(this.safeJSON(obj)); } } module.exports = SafeStringify; ================================================ FILE: lib/utils/screenshots.js ================================================ const path = require('path'); const fs = require('fs'); const Defaults = require('../settings/defaults.js'); class Screenshots { /** * @param {object} prefix * @param {object} screenshots * @return {string} */ static getFileName({testSuite, testCase, isError = false}, screenshots) { const dateObject = new Date(); let filename; let filename_format; if (typeof screenshots.filename_format == 'function') { filename_format = screenshots.filename_format.bind(screenshots); } else { filename_format = Defaults.screenshots.filename_format; } filename = filename_format({testSuite, testCase, isError, dateObject}); filename = filename.replace(/\s/g, '-').replace(/["']/g, ''); return path.join(screenshots.path, filename); } } module.exports = Screenshots; ================================================ FILE: lib/utils/seleniumAtoms.js ================================================ const getAttribute = 'get-attribute.js'; const isDisplayed = 'is-displayed.js'; const findElements = 'find-elements.js'; const path = require('path'); const SELENIUM_ATOMS_PATH = path.join(path.dirname(require.resolve('selenium-webdriver')), 'lib', 'atoms'); /** * @param {string} module * @return {!Function} */ function requireAtom(module) { try { return require(path.join(SELENIUM_ATOMS_PATH, module)); } catch (ex) { throw Error(`Failed to import atoms module ${module} using Selenium path: ${SELENIUM_ATOMS_PATH}`); } } module.exports = { requireIsDisplayed() { return requireAtom(isDisplayed); } }; ================================================ FILE: lib/utils/snapshots.js ================================================ const path = require('path'); const Defaults = require('../settings/defaults.js'); class Snapshots { /** * @param {object} prefix * @param {object} snapshots * @return {string} */ static getFileName({testSuite, testCase, commandName, traceSettings = Defaults.trace, output_folder = ''}) { const dateObject = new Date(); let filename_format; let filename; if (typeof traceSettings.filename_format == 'function') { filename_format = traceSettings.filename_format.bind(traceSettings); } else { filename_format = Defaults.trace.filename_format; } const base_path = traceSettings.path || `${output_folder}/snapshots`; filename = filename_format({testSuite, testCase, commandName, dateObject}); filename = filename.replace(/\s/g, '-').replace(/["']/g, ''); return path.join(base_path, filename); } } module.exports = Snapshots; ================================================ FILE: lib/utils/stackTrace.js ================================================ const boxen = require('boxen'); const {colors} = require('./chalkColors.js'); const isErrorObject = require('./isErrorObject'); const addDetailedError = require('./addDetailedError.js'); const indentRegex = /^/gm; const stackTraceFilter = function (parts) { const stack = parts.reduce(function(list, line) { if (contains(line, [ 'node_modules', '(node.js:', '(timers.js:', '(events.js:', '(util.js:', '(net.js:', '(internal/process/', 'internal/modules/cjs/loader.js', 'internal/modules/cjs/helpers.js', 'internal/timers.js', '_http_client.js:', 'process._tickCallback', 'node:internal/' ])) { return list; } list.push(line); return list; }, []); return stack.join('\n'); }; const contains = function(str, text) { if (Array.isArray(text)) { for (let i = 0; i < text.length; i++) { if (contains(str, text[i])) { return true; } } } return str.includes(text); }; const filterStack = function(err) { if (err instanceof Error) { const stackTrace = err.stack.split('\n').slice(1); return stackTraceFilter(stackTrace); } return ''; }; const filterStackTrace = function(stackTrace = '') { const sections = stackTrace.split('\n'); return stackTraceFilter(sections); }; const showStackTrace = function (stack) { const parts = stack.split('\n'); const headline = parts.shift(); console.error(colors.red(headline.replace(indentRegex, ' '))); if (parts.length > 0) { const result = stackTraceFilter(parts); console.error(colors.stack_trace(result.replace(indentRegex, ' '))); } }; /** * @method errorToStackTrace * @param {Error} err */ const errorToStackTrace = function(err) { if (!isErrorObject(err)) { err = new Error(err); } addDetailedError(err); let headline = err.message ? `${err.name}: ${err.message}` : err.name; headline = colors.red(headline.replace(indentRegex, ' ')); if (err.detailedErr) { headline += `\n ${colors.light_green(err.detailedErr)}`; if (err.extraDetail) { headline += `\n ${colors.light_green(err.extraDetail)}`; } } const showStack = err.showTrace || err.showTrace === undefined; let stackTrace = ''; if (!showStack && err.reportShown) { return ' ' + headline; } if (!showStack) { return '\n' + boxen(`${headline}\n`, {padding: 1, borderColor: 'red'}) + '\n'; } stackTrace = filterStack(err); stackTrace = '\n' + colors.stack_trace(stackTrace.replace(indentRegex, ' ')); return `${headline}${stackTrace}`; }; module.exports = { errorToStackTrace, stackTraceFilter, filterStack, filterStackTrace, showStackTrace }; ================================================ FILE: lib/utils/timed-callback.js ================================================ class TimeoutError extends Error { constructor(message) { super(message); this.name = 'TimeoutError'; } } class TimedCallback { constructor(callbackFn, name, timeoutMs) { this.callbackFn = callbackFn; this.name = name; this.timeoutMs = timeoutMs; this.__onTimeoutExpired = null; this.__onTimerStarted = null; } get onTimeoutExpired() { return this.__onTimeoutExpired || function() {}; } get onTimerStarted() { return this.__onTimerStarted || function() {}; } /** * @param {function} val */ set onTimeoutExpired(val) { this.__onTimeoutExpired = val; } /** * @param {function} val */ set onTimerStarted(val) { this.__onTimerStarted = val; } getWrapper() { this.createTimeout(); return (err) => { clearTimeout(this.timeoutId); this.callbackFn(err); }; } createTimeout() { this.timeoutId = setTimeout(() => { const err = new TimeoutError(`done() callback timeout of ${this.timeoutMs}ms was reached while executing "${this.name}".` + ' Make sure to call the done() callback when the operation finishes.'); this.onTimeoutExpired(err, this.name, this.timeoutMs); }, this.timeoutMs); this.onTimerStarted(this.timeoutId); } } module.exports = TimedCallback; ================================================ FILE: lib/utils/version.js ================================================ const packageConfig = require(__dirname + '/../../package.json'); const fullVersion = packageConfig.version; module.exports = { full: fullVersion, major: fullVersion.split('.')[0], minor: fullVersion.split('.')[1], patch: fullVersion.split('.').slice(2).join('.') }; ================================================ FILE: package.json ================================================ { "name": "nightwatch", "description": "Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.", "version": "3.15.0", "author": "Andrei Rusu", "homepage": "https://nightwatchjs.org", "main": "./lib/index.js", "types": "./types/index.d.ts", "license": "MIT", "bugs": { "url": "https://github.com/nightwatchjs/nightwatch/issues" }, "repository": { "type": "git", "url": "git+https://github.com/nightwatchjs/nightwatch.git" }, "dependencies": { "@nightwatch/chai": "5.0.3", "@nightwatch/html-reporter-template": "^0.3.0", "@nightwatch/nightwatch-inspector": "^1.0.1", "@types/chai": "^4.3.5", "@types/selenium-webdriver": "^4.1.14", "ansi-to-html": "0.7.2", "aria-query": "5.1.3", "assertion-error": "1.1.0", "boxen": "5.1.2", "chai-nightwatch": "^0.5.3", "chalk": "^4.1.2", "ci-info": "3.3.0", "cli-table3": "^0.6.3", "devtools-protocol": "^0.0.1140464", "didyoumean": "^1.2.2", "dotenv": "16.3.1", "ejs": "^3.1.10", "envinfo": "7.11.0", "glob": "7.2.3", "jsdom": "^24.1.0", "lodash": "^4.17.21", "minimatch": "3.1.2", "minimist": "1.2.6", "mocha": "10.8.2", "nightwatch-axe-verbose": "^2.3.0", "open": "8.4.2", "ora": "5.4.1", "piscina": "^4.3.1", "selenium-webdriver": "4.27.0", "semver": "7.5.4", "stacktrace-parser": "0.1.10", "strip-ansi": "6.0.1", "untildify": "4.0.0", "uuid": "8.3.2" }, "devDependencies": { "@cucumber/cucumber": "^8.2.1", "@swc/core": "^1.3.67", "@types/node": "^18.17.3", "@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/parser": "^6.3.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "eslint": "^8.46.0", "husky": "^8.0.0", "is-ci": "^3.0.1", "js-yaml": "^3.13.1", "lint-staged": "^13.2.2", "mocha-junit-reporter": "^2.0.2", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.2.1", "mochawesome-report-generator": "^6.2.0", "mockery": "~2.1.0", "nock": "^13.2.9", "nyc": "^15.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rimraf": "^3.0.2", "serve": "^14.2.0", "ts-node": "^10.9.1", "tsd": "^0.31.2", "wait-on": "^7.2.0" }, "peerDependencies": { "@cucumber/cucumber": "*" }, "peerDependenciesMeta": { "chromedriver": { "optional": true }, "geckodriver": { "optional": true }, "@cucumber/cucumber": { "optional": true } }, "bin": { "nightwatch": "./bin/nightwatch" }, "man": "", "scripts": { "eslint": "eslint index.js lib bin api examples test --quiet", "mocha": "mocha", "mocha-coverage": "nyc --reporter=html mocha test/src/ --recursive", "test": "mocha test/src/ --recursive --timeout 20000", "test:types": "tsd --files types/tests", "component-tests": "mocha test/component-tests/src", "test-cucumber": "mocha test/cucumber-integration-tests --parallel", "coverage": "npx nyc report --reporter=text-lcov > ./coverage/mocha_coverage.lcov", "prepare": "husky install" }, "eslintConfig": { "extends": "eslint:recommended", "env": { "browser": false, "node": true }, "rules": { "eqeqeq": "off", "curly": [ "error", "all" ], "quotes": [ "error", "single" ] } }, "engines": { "node": ">= 18.20.5" }, "keywords": [ "nightwatch", "nightwatchjs", "selenium", "testing", "webdriver", "browserstack", "end-to-end", "automated-testing", "e2e", "component", "integration", "test", "browser", "mobile", "runner", "appium", "cucumber", "mocha", "automation" ], "files": [ "bin", "examples", "lib", "api", "types", "README.md", "CODE_OF_CONDUCT.md", "LICENSE.md", "index.js" ], "lint-staged": { "**/*.js": [ "npx eslint --fix" ] } } ================================================ FILE: test/.eslintrc ================================================ { "extends": [ "eslint:recommended" ], "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": false } }, "env": { "browser": false, "mocha": true, "node": true, "es6": true }, "rules": { "eqeqeq": ["error", "smart"], "no-extra-boolean-cast": 0, "quotes": ["error", "single"], "curly": ["error", "all"], "no-console": [ "warn", { "allow": [ "error" ] } ], "no-debugger": 1, "semi": [ "warn", "always", { "omitLastInOneLineBlock": true } ], "no-trailing-spaces": 0, "no-else-return": 0, "no-extra-bind": 0, "no-empty": 1, "no-implicit-coercion": 0, "no-useless-call": 0, "no-return-assign": 0, "eol-last": 0, "no-unused-vars": 0, "no-extra-semi": 0, "no-underscore-dangle": 0, "no-lone-blocks": 0, "array-bracket-spacing": 1, "brace-style": [1, "1tbs", {"allowSingleLine": true}], "comma-spacing": 1, "comma-style": 1, "key-spacing": 1, "one-var": ["error", "never"], "semi-style": ["warn", "last"], "space-in-parens": ["warn", "never"], "keyword-spacing": [1, {"before": true, "after": true}], "padding-line-between-statements": [ "warn", { "blankLine": "always", "prev": "*", "next": "return" } ], "indent": [ "error", 2, { "SwitchCase": 1 } ] }, "globals": { "Promise": true, "Proxy": true, "Reflect": true } } ================================================ FILE: test/apidemos/actions-api/actionsApi.js ================================================ const assert = require('assert'); describe('Actions API demo tests', function() { before((browser) => browser.url('http://localhost')); after((browser) => browser.end()); test('browser.perform() actions', async (browser) => { const result = await browser.perform(function() { const actions = this.actions({async: true}); return actions //eslint-disable-next-line .keyDown(Keys.SHIFT) //eslint-disable-next-line .keyUp(Keys.SHIFT); }); assert.strictEqual(result, undefined); }); }); ================================================ FILE: test/apidemos/actions-api/asyncActionsApi.js ================================================ describe('Async perform Actions API demo tests', function() { test('browser.perform( async fn )', async browser => { const result = await browser.perform(async function() { const webelement = await element('#weblogin').findElement(); const actions = this.actions({async: true}); return actions.click(webelement); }); }); }); ================================================ FILE: test/apidemos/angular-test/angularTodoListWithClassicApis.js ================================================ describe('angularjs homepage todo list - with classic apis', function() { it('should add a todo using global element()', function(browser) { // adding a new task to the list browser .navigateTo('https://angularjs.org') .sendKeys('[ng-model="todoList.todoText"]', 'what is nightwatch?') .click('[value="add"]') // verifying if there are 3 tasks in the list .expect.elements('[ng-repeat="todo in todoList.todos"]').count.toEqual(3) // verifying if the third task if the one we have just added .expect.element({ selector: '[ng-repeat="todo in todoList.todos"]', index: 2 }).text.toEqual('what is nightwatch?') // find our task in the list and mark it as done .findElement({ selector: '[ng-repeat="todo in todoList.todos"] input', index: 2 }, function(inputResult) { const inputElement = inputResult.value; browser.click(inputElement); }) // verify if there are 2 tasks which are marked as done in the list .expect.elements('*[module=todoApp] li .done-true').count.to.equal(2); }); }); ================================================ FILE: test/apidemos/angular-test/angularTodoListWithElementGlobal.js ================================================ describe('angularjs homepage todo list - with element global', function() { // using the new element() global utility in Nightwatch 2 to init elements // before tests and use them later const todoElement = element('[ng-model="todoList.todoText"]'); const addButtonEl = element('[value="add"]'); it('should add a todo using global element()', function() { /////////////////////////////////////////////////// // browser can now also be accessed as a global | /////////////////////////////////////////////////// // adding a new task to the list browser .navigateTo('http://localhost') .sendKeys(todoElement, 'what is nightwatch?') .click(addButtonEl); /////////////////////////////////////////////////// // global expect is equivalent to browser.expect | /////////////////////////////////////////////////// // verifying if there are 3 tasks in the list expect.elements('[ng-repeat="todo in todoList.todos"]').count.to.equal(3); // verifying if the third task if the one we have just added const lastElementTask = element({ selector: '[ng-repeat="todo in todoList.todos"]', index: 2 }); expect(lastElementTask).text.to.equal('what is nightwatch?'); // find our task in the list and mark it as done lastElementTask.findElement('input', function(inputResult) { if (inputResult.error) { throw inputResult.error; } const inputElement = element(inputResult.value); browser.click(inputElement); }); // verify if there are 2 tasks which are marked as done in the list expect.elements('*[module=todoApp] li .done-true').count.to.equal(2); }); }); ================================================ FILE: test/apidemos/angular-test/angularTodoListWithElementGlobalAndError.js ================================================ describe('angularjs homepage todo list - with element global and stale element error', function() { // using the new element() global utility in Nightwatch 2 to init elements // before tests and use them later const todoElement = element('[ng-model="todoList.todoText"]'); it('should send keys to element', function() { browser.sendKeys(todoElement, 'what is nightwatch?'); }); it('should send keys to element again with stale element error', async function(browser) { await browser.sendKeys(todoElement, 'what is nightwatch?'); }); }); ================================================ FILE: test/apidemos/angular-test/angularTodoListWithElementGlobalAsync.js ================================================ describe('angularjs homepage todo list - with element global async', function() { // using the new element() global utility in Nightwatch 2 to init elements // before tests and use them later const todoElement = element('[ng-model="todoList.todoText"]'); const addButtonEl = element('[value="add"]'); it('should add a todo using global element()', async function() { await browser .navigateTo('https://angularjs.org') .sendKeys(todoElement, 'what is nightwatch?') .click(addButtonEl); await expect.elements('[ng-repeat="todo in todoList.todos"]').count.to.equal(3); const lastElementTask = element({ selector: '[ng-repeat="todo in todoList.todos"]', index: 2 }); await expect(lastElementTask).text.to.equal('what is nightwatch?'); // find our task in the list and mark it as done const inputElement = await lastElementTask.findElement('input'); await browser.click(inputElement); // verify if there are 2 tasks which are marked as done in the list expect.elements('*[module=todoApp] li .done-true').count.to.equal(2); }); }); ================================================ FILE: test/apidemos/appium/appiumTest.js ================================================ const assert = require('assert'); describe('appium api demo', function () { after((app) => app.end()); const availableAppiumCommands = [ 'startActivity', 'getCurrentActivity', 'getCurrentPackage', 'getOrientation', 'setOrientation', 'getGeolocation', 'setGeolocation', 'pressKeyCode', 'longPressKeyCode', 'hideKeyboard', 'isKeyboardShown', 'getContexts', 'getContext', 'setContext', 'resetApp' ]; it('test appium available API commands', async function () { // app variable is available globally assert.strictEqual(app !== undefined, true); availableAppiumCommands.forEach((command) => { assert.strictEqual(typeof app.appium[command], 'function'); }); }); it('Search for Nightwatch', async function () { app // available globally .waitForElementPresent({selector: 'Search Wikipedia', locateStrategy: 'accessibility id'}) .click('accessibility id', 'Search Wikipedia') .element('class name', 'android.widget.ImageButton') .sendKeys('id', 'com.app:id/search', 'Nightwatch'); }); }); ================================================ FILE: test/apidemos/cdp/registerAuth.js ================================================ describe('cdp tests', function() { it('register basic auth', function() { browser.registerBasicAuth('admin', 'admin') .navigateTo('http://localhost'); }); }); ================================================ FILE: test/apidemos/cdp/registerAuth2.js ================================================ describe('cdp tests', function() { it('register basic auth', function() { browser.registerBasicAuth('admin', 'admin') .navigateTo('http://localhost'); }); }); ================================================ FILE: test/apidemos/chrome/chromeTest.js ================================================ const assert = require('assert'); describe('chrome api demo', function () { after((browser) => browser.end()); const availableChromeCommands = [ 'launchApp', 'getNetworkConditions', 'setNetworkConditions', 'sendDevToolsCommand', 'sendAndGetDevToolsCommand', 'setPermission', 'setDownloadPath', 'getCastSinks', 'setCastSinkToUse', 'startCastTabMirroring', 'getCastIssueMessage', 'stopCasting' ]; it('test chrome available API commands', async function () { availableChromeCommands.forEach((command) => { assert.strictEqual(typeof browser.chrome[command], 'function'); }); }); it('test sample chrome CDP command', async function(browser) { browser.driver.sendAndGetDevToolsCommand = function(command, args) { return Promise.resolve({ args, command }); } const dom = await browser.chrome.sendAndGetDevToolsCommand('DOMSnapshot.captureSnapshot', { computedStyles: [] }); assert.deepStrictEqual(dom, { command: 'DOMSnapshot.captureSnapshot', args: { computedStyles: [] } }); }) }); ================================================ FILE: test/apidemos/cookies/cookieTests.js ================================================ const assert = require('assert'); describe('Cookie api demo tests', function() { before(async (browser) => { await browser.url('http://localhost'); return new Promise(resolve => { setTimeout(function () { browser.globals.calls++; resolve(); }, 200); }); }); after(async (browser) => { await browser.end(); browser.globals.calls++; }); test('browser.getCookie()', async (browser) => { await browser.assert.strictEqual(browser.globals.calls, 1); await browser.assert.urlContains('//localhost'); const test_cookie = await browser.getCookie('test_cookie'); assert.deepStrictEqual(test_cookie, { name: 'test_cookie', value: '123456', path: '/', domain: 'example.org', secure: false }); const other_cookie = await browser.getCookie('other_cookie'); assert.strictEqual(other_cookie, null); }); test('browser.getCookies()', async (browser) => { const cookies = await browser.getCookies(); assert.deepStrictEqual(cookies, [ { name: 'test_cookie', value: '123456', path: '/', domain: 'example.org', secure: false } ]); }); test('browser.cookies.get()', async (browser) => { await browser.assert.strictEqual(browser.globals.calls, 1); await browser.assert.urlContains('//localhost'); const test_cookie = await browser.cookies.get('test_cookie'); assert.deepStrictEqual(test_cookie, { name: 'test_cookie', value: '123456', path: '/', domain: 'example.org', secure: false }); const other_cookie = await browser.cookies.get('other_cookie'); assert.strictEqual(other_cookie, null); }); test('browser.cookies.getAll()', async (browser) => { const cookies = await browser.cookies.getAll(); assert.deepStrictEqual(cookies, [ { name: 'test_cookie', value: '123456', path: '/', domain: 'example.org', secure: false } ]); }); }); ================================================ FILE: test/apidemos/cookies/cookieTestsWithError.js ================================================ const assert = require('assert'); describe('Cookie api demo tests', function() { before((browser) => browser.url('http://localhost')); after((browser) => browser.end()); test('browser.getCookies() with network errors', async (browser) => { const cookies = await browser.getCookies(res => { assert.ok(res.error instanceof Error); assert.strictEqual(res.error.code, 'ECONNRESET'); assert.strictEqual(res.error.message, 'ECONNRESET socket hang up'); assert.strictEqual(res.status, -1); assert.strictEqual(res.value, null); }); assert.strictEqual(cookies, null); }); test('browser.cookies.getAll() with network errors', async (browser) => { const cookies = await browser.cookies.getAll(res => { assert.ok(res.error instanceof Error); assert.strictEqual(res.error.code, 'ECONNRESET'); assert.strictEqual(res.error.message, 'ECONNRESET socket hang up'); assert.strictEqual(res.status, -1); assert.strictEqual(res.value, null); }); assert.strictEqual(cookies, null); }); }); ================================================ FILE: test/apidemos/custom-commands/testNamespacedAliases.js ================================================ const assert = require('assert'); describe('custom command using namespaced aliases', function () { it('test aliases are available on requested namespace', async function() { assert.strictEqual(typeof browser.customPauseWithNamespacedAlias, 'function'); assert.strictEqual(typeof browser.newPause, 'function'); assert.strictEqual(typeof browser.sampleNamespace.amazingPause, 'function'); assert.strictEqual(typeof browser.fantasticNamespace.subNamespace.fantasticPause, 'function'); }); it('test custom command with a failure', async function() { browser.sampleNamespace.amazingPause('200'); }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingAsyncCustomAssert.js ================================================ describe('custom command using assert', function () { it('element visible using custom command', async function() { await browser.customVisible('#weblogin'); }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingAutoInvokeCommand.js ================================================ describe('Test Using ES6 Async Custom Commands', function() { before(browser => { browser .url('http://localhost'); }); it('sampleTest', browser => { browser.customCommandInvoke(); browser.end(); }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingCommandReturnFn.js ================================================ const assert = require('assert'); describe('Test using custom commands with returnFn', function() { before(browser => { browser .url('http://localhost'); }); it('sampleTest', browser => { const result = browser.customCommandReturnFn(); assert.deepStrictEqual(result, {status: 0}); browser.end(); }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingCustomExecute.js ================================================ const assert = require('assert'); describe('custom execute', function() { it('demo test', async function(browser) { await browser.pause(100) await browser.customExecuteAsync({prop: true}, function(endTime) { }) await browser.pause(200) }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingCustomGetEmail.js ================================================ const assert = require('assert'); describe('custom execute getEmail', function() { it('demo test - 1', async function(browser) { const result = await browser.getEmail(1); assert.ok(result instanceof Object); assert.deepStrictEqual(result, { status: -1, error: 'Email not found' }); }); it('demo test - 2', async function(browser) { const result = await browser.getEmail(2); assert.ok(result instanceof Object); assert.strictEqual(result.status, -1); assert.strictEqual(result.error.message, 'Error while running .getEmail(): Email not found'); }); it('demo test - 3', async function(browser) { const result = await browser.getEmail(3); assert.strictEqual(result.abortOnFailure, true); assert.strictEqual(result.error.message, 'Error while running .getEmail(): Email not found'); }); }); ================================================ FILE: test/apidemos/custom-commands/testUsingES6AsyncCustomCommands.js ================================================ describe('Test Using ES6 Async Custom Commands', function() { before(browser => { browser .url('http://localhost') .waitForElementPresent('#weblogin') .customFindElementsES6('#weblogin', function(elements) { this.assert.ok(Array.isArray(elements)); }); }); it('sampleTest', browser => { browser.end(); }); }); ================================================ FILE: test/apidemos/custom-commands-parallel/testUsingES6AsyncCustomCommands.js ================================================ describe('Test Using Sync Custom Command returning NightwatchAPI', function() { before(browser => { browser.url('http://localhost'); }); it('sampleTest', browser => { browser .otherCommand() .end(); }); }); ================================================ FILE: test/apidemos/elements/elementGlobalTest.js ================================================ const assert = require('assert'); const {WebElement} = require('selenium-webdriver'); describe('get text using element-global', function () { const signupSection = element(by.css('#signupSection')); after(browser => browser.end()); const availableElementCommands = [ 'getId', 'findElement', 'findElements', 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getProperty', 'getText', 'getAriaRole', 'getAccessibleName', 'getRect', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'takeScreenshot', 'getWebElement' ]; test('element globals command', async function() { const weblogin = element('#weblogin'); const tagName = await browser.waitForElementPresent(weblogin, 100).getTagName(weblogin); assert.strictEqual(tagName, 'div'); browser.assert.visible(weblogin); await expect(weblogin).to.be.visible; await expect.element('#weblogin').text.contains('sample'); const webElement = await weblogin.getWebElement(); assert.ok(webElement instanceof WebElement); availableElementCommands.forEach(command => { assert.strictEqual(typeof weblogin[command], 'function'); }); const result = await weblogin.getText(); assert.strictEqual(result, 'sample text'); const signupSectionId = await signupSection.getId(); assert.strictEqual(signupSectionId, '0'); }); }); ================================================ FILE: test/apidemos/elements/findElementsCustomCommand.js ================================================ const assert = require('assert'); describe('find elements using es6 custom command', function () { after(browser => browser.end()); test('find elements', function() { browser .waitForElementPresent('#weblogin', 100) .es6async.customFindElementsES6('#weblogin', function(elements) { assert.strictEqual(elements.length, 1); assert.strictEqual(elements[0].getId(), '0'); }) .es6async.customFindElements('#weblogin', function(elements) { assert.strictEqual(elements.length, 1); assert.strictEqual(elements[0].getId(), '0'); }); }); }); ================================================ FILE: test/apidemos/elements/findElementsPageCommands.js ================================================ const assert = require('assert'); describe('demo tests with find elements in page commands', function () { after(browser => browser.end()); const pageObject = this.page.workingPageObjPlain(); it('find elements', function() { pageObject .navigate() .waitForElementPresent('@loginAsString') .customFindElements('@loginAsString', function(elements) { assert.strictEqual(elements.length, 1); assert.strictEqual(elements[0].getId(), '0'); }); }); it('find elements with async/await', async function() { const elements = await pageObject.navigate() .waitForElementPresent('@loginAsString') .customFindElementsES6('@loginAsString'); assert.strictEqual(elements.length, 1); assert.strictEqual(elements[0].getId(), '0'); }); }); ================================================ FILE: test/apidemos/elements/testGlobalLocateStrategy.js ================================================ const assert = require('assert'); const {WebElement} = require('selenium-webdriver'); describe('element-global demo for locateStrategy', function () { after(browser => browser.end()); test('to determine whether or not globally set locate-strategy is ignored', async function() { const weblogin = element('//weblogin'); const webElement = await weblogin.getWebElement(); assert.ok(webElement instanceof WebElement); assert.strictEqual(weblogin.locateStrategy, 'use_xpath'); assert.strictEqual(browser.options.use_xpath, true); }); }); ================================================ FILE: test/apidemos/ensure/ensureTestError.js ================================================ module.exports = { after: function(browser) { browser.end(); }, 'Ensure badElement test': async function (browser) { browser.url('http://localhost'); const result = await browser.ensure.elementIsSelected('#badElement'); } }; ================================================ FILE: test/apidemos/ensure/ensureTestNotSelected.js ================================================ module.exports = { after(browser) { browser.end(); }, before() { browser.url('http://localhost'); }, 'ensure .not.elementIsSelected': function (browser) { browser.ensure.not.elementIsSelected('#weblogin'); } }; ================================================ FILE: test/apidemos/ensure/ensureTestSelected.js ================================================ describe('ensureTestSelected', function() { after(browser => { browser.end(); }); it('ensure elementIsSelected', function (browser) { browser .url('http://localhost') .ensure.elementIsSelected('#weblogin'); }); it('ensure elementsLocated', async function (browser) { await browser .url('http://localhost') .ensure.elementsLocated({ selector: '#weblogin', timeout: 100 }); }); }); ================================================ FILE: test/apidemos/expect-global/deepEqual.js ================================================ describe('expect() tests ', function () { it('deepEual', function() { const expectedMsg = [{a: 1, b: 4}]; const receivedMsg = [{b: 5}]; expect(expectedMsg).eql(receivedMsg); }); }); ================================================ FILE: test/apidemos/expect-global/expect.js ================================================ describe('expect() tests ', function () { const signupSection = element(by.css('#signupSection')); after(browser => browser.end()); it('weblogin has class container ', async function() { const weblogin = element('#weblogin'); expect(signupSection.isSelected()).to.be.true; expect(weblogin.property('className')).to.be.an('array').and.contains('container'); }); }); ================================================ FILE: test/apidemos/firefox/firefoxTest.js ================================================ const assert = require('assert'); describe('firefox api basic test', function () { after((browser) => browser.end()); const availableFirefoxCommands = ['getContext', 'setContext', 'installAddon', 'uninstallAddon']; it('test firefox available API commands', async function () { availableFirefoxCommands.forEach((command) => { assert.strictEqual(typeof browser.firefox[command], 'function'); }); }); }); ================================================ FILE: test/apidemos/namespaced-api/namespacedApiTest.js ================================================ const common = require('../../common.js'); const {browser, appium, assert, expect, firefox, document} = common.require('index.js'); describe('namespaced api test', function() { it('browser.navigateTo', function () { const result = browser.navigateTo('http://localhost'); assert .strictEqual(typeof result.navigateTo, 'function') .strictEqual(typeof result.debug, 'function') .strictEqual(typeof result.appium.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'string'); }); it('browser.appium.setOrientation', function () { const result = browser.appium.setOrientation('LANDSCAPE'); assert .strictEqual(typeof result.navigateTo, 'function') .strictEqual(typeof result.debug, 'function') .strictEqual(typeof result.appium.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'string'); }); it('appium.setOrientation', function () { const result = appium.setOrientation('LANDSCAPE'); assert .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'undefined'); }); it('document.customExecute (namespaced alias loaded on namespaced api)', function () { const result = document.customExecute('acme'); assert .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.injectScript, 'function') .strictEqual(typeof result.sessionId, 'undefined'); }); it('assert.titleEquals', function () { const result = assert.titleEquals('Localhost'); result .throws(() => { result.navigateTo(); }, new Error('Unknown api method "navigateTo".')) .throws(() => { result.debug(); }, new Error('Unknown api method "debug".')) .throws(() => { result.sessionId(); }, new Error('Unknown api method "sessionId".')) .titleEquals('Localhost'); }); it('assert.strictEqual', function () { const result = assert.strictEqual(2, 2); result .throws(() => { result.navigateTo(); }, new Error('Unknown api method "navigateTo".')) .throws(() => { result.debug(); }, new Error('Unknown api method "debug".')) .throws(() => { result.sessionId(); }, new Error('Unknown api method "sessionId".')) .titleEquals('Localhost'); }); it('expect assertions', function () { expect(2).to.equal(2); expect.title().to.equal('Localhost'); }); it('firefox.getContext', function () { const result = firefox.getContext(); assert .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.hideKeyboard, 'undefined') .strictEqual(typeof result.sessionId, 'undefined') .strictEqual(typeof result.installAddon, 'function'); }); it('browser.navigateTo async', async function () { const result = browser.navigateTo('http://localhost'); assert .strictEqual(typeof result.navigateTo, 'function') .strictEqual(typeof result.debug, 'function') .strictEqual(typeof result.appium.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'string'); assert.strictEqual(await result, null); }); it('browser.appium.setOrientation async', async function () { const result = browser.appium.setOrientation('LANDSCAPE'); assert .strictEqual(typeof result.navigateTo, 'function') .strictEqual(typeof result.debug, 'function') .strictEqual(typeof result.appium.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'string'); assert.strictEqual(await result, null); }); it('appium.setOrientation async', async function () { const result = appium.setOrientation('LANDSCAPE'); assert .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.hideKeyboard, 'function') .strictEqual(typeof result.sessionId, 'undefined'); assert.strictEqual(await result, null); }); it('assert.titleEquals async', async function () { const result = assert.titleEquals('Localhost'); result .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.hideKeyboard, 'undefined') .strictEqual(typeof result.sessionId, 'undefined') .titleEquals('Localhost') .strictEqual(await result, 'Localhost'); }); it('assert.strictEqual async', async function () { const result = assert.strictEqual(2, 2); result .strictEqual(typeof result.navigateTo, 'undefined') .strictEqual(typeof result.debug, 'undefined') .strictEqual(typeof result.hideKeyboard, 'undefined') .strictEqual(typeof result.sessionId, 'undefined') .titleEquals('Localhost') .deepStrictEqual(await result, {returned: 1, value: null}); }); }); ================================================ FILE: test/apidemos/navigation/navigateTest.js ================================================ const assert = require('assert'); describe('navigate test', function() { after(browser => { assert.strictEqual(browser.globals.calls, 5); browser.customQuit(result => { browser.globals.calls++; assert.strictEqual(result.client.sessionId, null); }, function() { browser.globals.calls++; }); }); it('navigateTo', function (browser) { browser.navigateTo('http://localhost').perform(function() { this.globals.calls++; }); }); it('navigateTo async', async function (browser) { const result = await browser.navigateTo('http://localhost', function() { browser.globals.calls++; return {status: 'success'}; }); assert.deepStrictEqual(result, {status: 'success'}); const url = await browser.getCurrentUrl(); assert.strictEqual(url, 'http://localhost'); const urlWithCallback = await browser.getCurrentUrl(function(result) { browser.globals.calls++; return result.value; }); assert.strictEqual(urlWithCallback, 'http://localhost'); }); }); ================================================ FILE: test/apidemos/page-objects/commandsReturnTypeTest.js ================================================ const assert = require('assert'); describe('test return type of various commands on page-objects', function () { after(browser => browser.end()); const pageObject = this.page.simplePageObj(); it('return correct type on page objects in non-async mode', function() { const pageClick = pageObject.click('@loginCss'); // pageClick does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageClick.submit, 'undefined'); assert.strictEqual(typeof pageClick.isChrome, 'undefined'); // pageClick has page specific methods/properties assert.strictEqual(typeof pageClick.api, 'object'); assert.strictEqual(typeof pageClick.testCommand, 'function'); const pageAssert = pageObject.assert.visible('@loginCss'); // pageAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageAssert.submit, 'undefined'); assert.strictEqual(typeof pageAssert.isChrome, 'undefined'); // pageAssert has page specific methods/properties assert.strictEqual(typeof pageAssert.api, 'object'); assert.strictEqual(typeof pageAssert.testCommand, 'function'); const pageNodeAssert = pageObject.assert.equal(1, 1); // pageNodeAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageNodeAssert.submit, 'undefined'); assert.strictEqual(typeof pageNodeAssert.isChrome, 'undefined'); // pageNodeAssert has page specific methods/properties assert.strictEqual(typeof pageNodeAssert.api, 'object'); assert.strictEqual(typeof pageNodeAssert.testCommand, 'function'); const pageCustomCommand = pageObject.testCommand(); // pageCustomCommand does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageCustomCommand.submit, 'undefined'); assert.strictEqual(typeof pageCustomCommand.isChrome, 'undefined'); // pageCustomCommand has page specific methods/properties assert.strictEqual(typeof pageCustomCommand.api, 'object'); assert.strictEqual(typeof pageCustomCommand.testCommand, 'function'); const pageChaiAssert = pageObject.expect.element('@loginCss').to.be.visible; // pageChaiAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageChaiAssert.submit, 'undefined'); assert.strictEqual(typeof pageChaiAssert.isChrome, 'undefined'); // pageChaiAssert *does not* have page specific methods/properties assert.strictEqual(typeof pageChaiAssert.api, 'undefined'); assert.strictEqual(typeof pageChaiAssert.testCommand, 'undefined'); // pageChaiAssert only have page Expect property/methods assert.strictEqual(typeof pageChaiAssert.a, 'function'); assert.strictEqual(typeof pageChaiAssert.present, 'object'); }); it('return correct type on page objects in async mode', async function() { const pageClick = pageObject.click('@loginCss'); assert.strictEqual(pageClick instanceof Promise, true); // pageClick does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageClick.submit, 'undefined'); assert.strictEqual(typeof pageClick.isChrome, 'undefined'); // pageClick has page specific methods/properties assert.strictEqual(typeof pageClick.api, 'object'); assert.strictEqual(typeof pageClick.testCommand, 'function'); const pageAssert = pageObject.assert.visible('@loginCss'); assert.strictEqual(pageAssert instanceof Promise, true); // pageAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageAssert.submit, 'undefined'); assert.strictEqual(typeof pageAssert.isChrome, 'undefined'); // pageAssert has page specific methods/properties assert.strictEqual(typeof pageAssert.api, 'object'); assert.strictEqual(typeof pageAssert.testCommand, 'function'); const pageNodeAssert = pageObject.assert.equal(1, 1); assert.strictEqual(pageNodeAssert instanceof Promise, true); // pageNodeAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageNodeAssert.submit, 'undefined'); assert.strictEqual(typeof pageNodeAssert.isChrome, 'undefined'); // pageNodeAssert has page specific methods/properties assert.strictEqual(typeof pageNodeAssert.api, 'object'); assert.strictEqual(typeof pageNodeAssert.testCommand, 'function'); const pageCustomCommand = pageObject.testCommand(); assert.strictEqual(pageCustomCommand instanceof Promise, false); // pageCustomCommand does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageCustomCommand.submit, 'undefined'); assert.strictEqual(typeof pageCustomCommand.isChrome, 'undefined'); // pageCustomCommand has page specific methods/properties assert.strictEqual(typeof pageCustomCommand.api, 'object'); assert.strictEqual(typeof pageCustomCommand.testCommand, 'function'); const pageChaiAssert = pageObject.expect.element('@loginCss').to.be.visible; assert.strictEqual(pageChaiAssert instanceof Promise, true); // pageChaiAssert does not have methods specific to NightwatchAPI assert.strictEqual(typeof pageChaiAssert.submit, 'undefined'); assert.strictEqual(typeof pageChaiAssert.isChrome, 'undefined'); // pageChaiAssert *does not* have page specific methods/properties assert.strictEqual(typeof pageChaiAssert.api, 'undefined'); assert.strictEqual(typeof pageChaiAssert.testCommand, 'undefined'); // pageChaiAssert only have page Expect property/methods assert.strictEqual(typeof pageChaiAssert.a, 'function'); assert.strictEqual(typeof pageChaiAssert.present, 'object'); }); it('return correct type on sections in non-async mode', function() { const signUpSection = pageObject.section.signUp; const sectionClick = signUpSection.click('@help'); // sectionClick does not have methods specific to NightwatchAPI assert.strictEqual(typeof sectionClick.submit, 'undefined'); assert.strictEqual(typeof sectionClick.isChrome, 'undefined'); // sectionClick does not page specific methods/properties assert.strictEqual(typeof sectionClick.testCommand, 'undefined'); // sectionClick have section specific property/methods assert.strictEqual(typeof sectionClick.sectionElements, 'function'); assert.strictEqual(typeof sectionClick.section, 'object'); }); it('return correct type on sections in async mode', async function() { const signUpSection = pageObject.section.signUp; const sectionClick = signUpSection.click('@help'); assert.strictEqual(sectionClick instanceof Promise, true); // sectionClick does not have methods specific to NightwatchAPI assert.strictEqual(typeof sectionClick.submit, 'undefined'); assert.strictEqual(typeof sectionClick.isChrome, 'undefined'); // sectionClick does not page specific methods/properties assert.strictEqual(typeof sectionClick.testCommand, 'undefined'); // sectionClick have section specific property/methods assert.strictEqual(typeof sectionClick.sectionElements, 'function'); assert.strictEqual(typeof sectionClick.section, 'object'); }); }); ================================================ FILE: test/apidemos/relative-locators/sample-with-relative-locators.js ================================================ /* eslint-disable no-undef */ describe('sample with relative locators', function () { it('locate password input', function (browser) { const passwordElement = locateWith(By.tagName('input')).below(By.css('input[type=email]')); browser .waitForElementVisible(passwordElement) .expect.element(passwordElement).to.be.an('input'); browser.setValue(passwordElement, 'password'); }); }); ================================================ FILE: test/apidemos/web-elements/assertionTest.js ================================================ describe('element assertion test', function() { it('element is visible', function({element}) { element.find('#weblogin').assert.visible(); element.find('#weblogin').assert.enabled(); }); it('element is not visible - failure', function({element}) { element.find('#weblogin').assert.not.visible(); }); it('element is present', function({element}){ element.find('#weblogin').assert.present(); }); }); ================================================ FILE: test/apidemos/web-elements/elementApiWithPageObjects.js ================================================ const assert = require('assert'); const {WebElement} = require('selenium-webdriver'); describe('demo tests with new element api in page objects', function () { after(browser => browser.end()); const pageObject = this.page.simplePageObj(); it('element api on page objects', async function() { const elementResult = await pageObject.element('@loginAsString'); assert.strictEqual(elementResult instanceof WebElement, true); assert.strictEqual(await elementResult.getId(), '5cc459b8-36a8-3042-8b4a-258883ea642b'); const elementFindResult = await pageObject.element.find('@loginIndexed'); assert.strictEqual(elementFindResult instanceof WebElement, true); assert.strictEqual(await elementFindResult.getId(), '3783b042-7001-0740-a2c0-afdaac732e9f'); const elementFindAllResult = await pageObject.element.findAll('@loginXpath'); assert.strictEqual(elementFindAllResult.length, 2); assert.strictEqual(elementFindAllResult[0] instanceof WebElement, true); assert.strictEqual(await elementFindAllResult[1].getId(), '3783b042-7001-0740-a2c0-afdaac732e9f'); const findByTextResult = await pageObject.element.findByText('Web Login'); assert.strictEqual(findByTextResult instanceof WebElement, true); assert.strictEqual(await findByTextResult.getId(), '5cc459b8-36a8-3042-8b4a-258883ea642b'); }); it('element api on sections', async function() { const signupSection = pageObject.section.signUp; const elementResult = await signupSection.element('@help'); assert.strictEqual(elementResult instanceof WebElement, true); assert.strictEqual(await elementResult.getId(), '1'); const elementFindResult = await signupSection.element.find('@help'); assert.strictEqual(elementFindResult instanceof WebElement, true); assert.strictEqual(await elementFindResult.getId(), '1'); const findByTextResult = await signupSection.element.findByText('Help'); assert.strictEqual(findByTextResult instanceof WebElement, true); assert.strictEqual(await findByTextResult.getId(), '2'); const getStartedSection = signupSection.section.getStarted; const elementFindResult2 = await getStartedSection.element.find('#getStartedStart'); assert.strictEqual(elementFindResult2 instanceof WebElement, true); assert.strictEqual(await elementFindResult2.getId(), '4'); const elementFindAllResult = await getStartedSection.element.findAll('@start'); assert.strictEqual(elementFindAllResult.length, 3); assert.strictEqual(elementFindAllResult[0] instanceof WebElement, true); assert.strictEqual(await elementFindAllResult[2].getId(), '6'); }); }); ================================================ FILE: test/apidemos/web-elements/waitUntilElementNotPresent.js ================================================ describe('demo of failure of waitUntil element commands', function() { it('waitUntil element is visible - element not present', function({element}) { element.find('#badElement').waitUntil('visible'); }); }); ================================================ FILE: test/apidemos/web-elements/waitUntilFailureTest.js ================================================ describe('demo of failure of waitUntil element commands', function() { it('waitUntil element is visible and but not selected', function({element}) { element.find('#weblogin').waitUntil('visible').waitUntil('not.selected'); }); }); ================================================ FILE: test/apidemos/web-elements/waitUntilTest.js ================================================ describe('demo tests using waitUntil element APIs', function() { it('wait until element is visible', function({element}) { element('#weblogin').waitUntil('visible'); }); it('wait until element is selected', function({element, state}) { element('#weblogin').waitUntil('present').waitUntil('selected'); }); it('wait until element is enabled', function({element}) { element('#weblogin').waitUntil('enabled'); }); it('wait until with custom message', function({element}) { element('#weblogin').waitUntil('enabled', {message: 'elemento %s no era presente en %d ms'}); }); }); ================================================ FILE: test/asynchookstests/afterEach-timeout/sampleAsyncHooks.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, afterEach: function(client, done) { } }; ================================================ FILE: test/asynchookstests/async-provide-error/after.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, after: function(client, done) { done(new Error('Provided error after')); } }; ================================================ FILE: test/asynchookstests/async-provide-error/afterAsync.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, after: function(client, done) { setTimeout(function() { done(new Error('Provided error afterAsync')); }, 10); } }; ================================================ FILE: test/asynchookstests/async-provide-error/afterEach.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, afterEach: function(client, done) { done(new Error('Provided error afterEach')); } }; ================================================ FILE: test/asynchookstests/async-provide-error/afterEachAsync.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, afterEach: function(client, done) { setTimeout(function() { done(new Error('Provided error afterEachAsync')); }, 10); } }; ================================================ FILE: test/asynchookstests/async-provide-error/afterEachWithClient.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, afterEach: function(client, done) { client.end(function() { done(new Error('Provided error afterEachWithClient')); }); } }; ================================================ FILE: test/asynchookstests/async-provide-error/afterWithClient.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, after: function(client, done) { client.end(function() { done(new Error('Provided error afterWithClient')); }); } }; ================================================ FILE: test/asynchookstests/async-provide-error/before.js ================================================ module.exports = { demoTest: function (client) { client .url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, before: function(client, done) { done(new Error('Provided error before')); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeAsync.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, before: function(client, done) { setTimeout(function() { done(new Error('Provided error beforeAsync')); }, 10); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeEach.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, beforeEach: function(client, done) { done(new Error('Provided error beforeEach')); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeEachAsync.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, beforeEach: function(client, done) { setTimeout(function() { done(new Error('Provided error beforeEachAsync')); }, 10); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeEachAsyncWithClient.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, beforeEach: function(client, done) { client.perform(function() { setTimeout(function() { done(new Error('Provided error beforeEachAsyncWithClient')); }, 10); }); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeEachAsyncWithClientMultiple.js ================================================ var c = 0; module.exports = { demoTest1: function (client) { client.url('http://localhost'); }, demoTest2: function (client) { client .assert.elementPresent('#weblogin') .end(); }, beforeEach: function(client, done) { c++; if (c === 1) { return done(); } client.perform(function() { setTimeout(function() { done(new Error('Provided error beforeEachAsyncWithClientMultiple')); }, 10); }); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeEachWithClient.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, beforeEach: function(client, done) { client.perform(function() { done(new Error('Provided error beforeEachWithClient')); }); } }; ================================================ FILE: test/asynchookstests/async-provide-error/beforeWithClient.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, before: function(client, done) { client.perform(function() { done(new Error('Provided error beforeWithClient')); }); } }; ================================================ FILE: test/asynchookstests/before-timeout/sampleAsyncHooks.js ================================================ module.exports = { demoTest: function (client) { client.url('http://localhost') .assert.elementPresent('#weblogin') .end(); }, before: function(client, done) { } }; ================================================ FILE: test/asynchookstests/sampleWithAssertionFailedInAfter.js ================================================ module.exports = { before(client) { client.url('http://localhost').perform(function() { client.globals.calls++; }); }, demoTestAsyncOne: function (client) { client.url('http://localhost'); }, after(client) { client.assert.equal(0, 1); client.end(); } }; ================================================ FILE: test/asynchookstests/sampleWithAssertionFailedInBefore.js ================================================ describe('sampleWithAssertionFailedInBefore', function() { before(function(c) { c.url('http://localhost'); c.assert.equal(0, 1); }); test('demo test async', c => { c.url('http://localhost'); }); }); ================================================ FILE: test/asynchookstests/sampleWithErrorInTestcaseAndAfter.js ================================================ describe('sampleWithFailureInTestcaseAndAfter', function() { before(function(c) { c.url('http://localhost'); }); test('demo test async', c => { c.url('http://localhost'); throw new Error('error in testcase'); }); after(function(c) { c.assert.strictEqual(0, 1); }); }); ================================================ FILE: test/asynchookstests/sampleWithFailureInBeforeAndAfter.js ================================================ describe('sampleWithAssertionFailedInBefore', function() { before(function(c) { c.url('http://localhost'); c.assert.equal(0, 1); }); test('demo test async', c => { c.url('http://localhost'); }); after(function(c) { c.assert.strictEqual(0, 1); }); }); ================================================ FILE: test/asynchookstests/sampleWithFailureInTestcaseAndAfter.js ================================================ describe('sampleWithFailureInTestcaseAndAfter', function() { before(function(c) { c.url('http://localhost'); }); test('demo test async', c => { c.url('http://localhost'); c.assert.equal(0, 1); }); after(function(c) { c.assert.strictEqual(0, 1); }); }); ================================================ FILE: test/asynchookstests/unittest-async-timeout.js ================================================ module.exports = { demoTest: function (done) { } }; ================================================ FILE: test/asynchookstests/unittest-error.js ================================================ module.exports = { '@unitTest': true, client: {}, demoSync() { } }; ================================================ FILE: test/asynchookstests/unittest-failure/unittest-failure.js ================================================ var assert = require('assert'); module.exports = { demoTest: function (done) { assert.strictEqual(1, 0); } }; ================================================ FILE: test/common.js ================================================ const BASE_PATH = process.env.NIGHTWATCH_COV ? 'lib-cov' : 'lib'; const path = require('path'); module.exports = { require(relativeFilePath) { try { return require(path.join('../', BASE_PATH, relativeFilePath)); } catch (err) { console.error('Error', err); throw err; } }, requireApi(relativeFilePath) { return require(path.join('../api', relativeFilePath)); }, resolve(relativeFilePath) { return path.join('../', BASE_PATH, relativeFilePath); }, requireMock(relativeFilePath, ...args) { const mockedModule = require(path.join(__dirname, './lib/mocks', relativeFilePath)); return mockedModule(...args); }, settings(settings) { return Object.assign({ selenium: { port: 10195, start_process: false }, selenium_host: 'localhost', persist_globals: true, output_folder: false, output: false, silent: false }, settings); } }; // process.on('unhandledRejection', err => { // console.error('TEST unhandledRejection:') // console.error(err.stack); // }); ================================================ FILE: test/component-tests/samples/nightwatch.conf-noplugins.js ================================================ module.exports = { src_folders: [], page_objects_path: [], custom_commands_path: [], custom_assertions_path: '', globals: {}, webdriver: {}, test_settings: { default: { launch_url: 'http://localhost:3001', persist_globals: true, desiredCapabilities: { browserName: 'chrome', 'goog:chromeOptions': { w3c: true, args: [] } }, webdriver: { start_process: true, server_path: require('chromedriver').path, cli_args: [] } } } }; ================================================ FILE: test/component-tests/samples/react/nightwatch.conf.js ================================================ module.exports = { src_folders: ['test/src'], page_objects_path: [], custom_commands_path: [], custom_assertions_path: '', globals: {}, plugins: ['@nightwatch/react'], webdriver: {}, test_settings: { default: { disable_error_log: false, launch_url: 'http://localhost:3001', persist_globals: true, desiredCapabilities: { browserName: 'chrome', 'goog:chromeOptions': { w3c: true, args: [ ] } }, webdriver: { start_process: true, server_path: require('chromedriver').path, cli_args: [ ] } } } }; ================================================ FILE: test/component-tests/samples/react/src/Form.jsx ================================================ import React from 'react'; class Form extends React.Component { constructor(props) { super(props); this.state = {name: ''}; } handleSubmit = (e) => { e.preventDefault(); if (!this.state.name.trim()) { return; } this.setState({name: ''}); }; handleChange = (e) => { this.setState({name: e.target.value}); }; render() { return (

); } } export default Form; ================================================ FILE: test/component-tests/samples/react/tests/Form.spec--_with-$hooks.jsx ================================================ // DO NOT rename the file import { fireEvent, within } from '@testing-library/dom'; import Form from '../src/Form.jsx'; export default { title: 'Form', component: Form, } export const FormStory = () =>
export const AnotherForm = Object.assign(() => , { async beforeMount() { await new Promise((resolve, reject) => setTimeout(function() { resolve() }, 50)); }, async afterMount() { await new Promise((resolve, reject) => setTimeout(function() { resolve() //reject(new Error('something failed in after mount')); }, 100)); }, async play({canvasElement, args}) { const async_value = await new Promise((resolve) => setTimeout(function() { resolve('test_value_async'); }, 100)); const root = within(canvasElement); const input = root.getByTestId('new-todo-input'); fireEvent.change(input, { target: { value: 'I entered the value' } }); return { component_element: window['@component_element'], fromPlay: input, async_value } }, test: async (browser, {component, result}) => { await browser.assert.deepEqual(Object.keys(result), ['async_value', 'component_element', 'fromPlay']); await browser.assert.strictEqual(result.async_value, 'test_value_async'); await browser.assert.ok('driver_' in result.fromPlay); await expect(component).to.be.visible; await expect(component.find('input')).to.have.property('value').equal('I entered the value'); } }); ================================================ FILE: test/component-tests/samples/react/tests/formTest--notFound.js ================================================ describe('Render React Component test', function() { let formComponent; it('checks the react component', async function(browser) { formComponent = await browser.mountComponent('/test/components/Form.jsx', {}); await browser.expect.element(formComponent).to.be.visible; }); }); ================================================ FILE: test/component-tests/samples/react/tests/formTest.js ================================================ describe('Render React Component test', function() { let formComponent; it('checks the react component', async function(browser) { formComponent = await browser.mountComponent('/test/component-tests/samples/react/src/Form.jsx', {}); await browser.expect.element(formComponent).to.be.visible; }); }); ================================================ FILE: test/component-tests/samples/vue/nightwatch.conf.js ================================================ module.exports = { src_folders: [], page_objects_path: [], custom_commands_path: [], custom_assertions_path: '', globals: {}, plugins: ['@nightwatch/vue'], webdriver: {}, test_settings: { default: { disable_error_log: false, launch_url: 'http://localhost:3001', persist_globals: true, desiredCapabilities: { browserName: 'chrome', 'goog:chromeOptions': { w3c: true, args: [ ] } }, webdriver: { start_process: true, server_path: require('chromedriver').path, cli_args: [ ] } } } }; ================================================ FILE: test/component-tests/samples/vue/src/Form.vue ================================================