sdevk4
sdevk4

Reputation: 11

WebDriverError when running Protractor tests against Safari

First of all, thank you so much in advance for all the help. I have been trying to googling possible solutions, but couldn't find any lead.

I am trying to run some UI tests with the SerenityJS framework, which is a layer on top of Protractor.

I have my protractor.config.js looks like the following:

const cwd = process.cwd();
const modules = `${cwd}/node_modules`;
const glob = require(`${modules}/glob`);
const protractor = require.resolve(`${modules}/protractor`);
const protractor_node_modules = protractor.substring(0, protractor.lastIndexOf('node_modules') + 'node_modules'.length);
const seleniumJar = glob.sync(`${cwd}/${protractor_node_modules}/protractor/**/selenium-server-standalone-*.jar`).pop();
const appiumCapabilities = require('./appium-capabilities');

const dashboardTestRootDir = 'dashboard';

const usePhotographer = process.env.PHOTOGRAPHER;

let configObject = {};

configObject = {
    seleniumServerJar: seleniumJar,

    // See https://github.com/angular/protractor/blob/master/docs/timeouts.md
    allScriptsTimeout: 11 * 1000,

    disableChecks: true,

    // See https://github.com/protractor-cucumber-framework/protractor-cucumber-framework#uncaught-exceptions
    ignoreUncaughtExceptions: true,

    framework: 'custom',
    frameworkPath: require.resolve(`${modules}/serenity-js`),

    serenity: {
        stageCueTimeout: 30 * 1000,
    },

    specs: [`${cwd}/features/**/*.feature`],

    cucumberOpts: {
        require: [
            // loads step definitions:
            `${cwd}/features/**/*.ts`, // TypeScript
            `${cwd}/features/**/*.js` // JavaScript
        ],
        format: 'pretty',
        compiler: 'ts:ts-node/register'
    },
};


if (cwd.includes(dashboardTestRootDir)) {

    configObject.multiCapabilities = appiumCapabilities['multiBrowsers'];

    // This is needed to run sequentially in multiCapability, i.e. one browser at a time
    configObject.maxSessions = 1;

    configObject.onPrepare = function() {
        // obtain browser name
        browser.getBrowserName = function() {
            return browser.getCapabilities().then(function(caps) {
                browser.browserName = caps.get('browserName');
                browser.manage().window().maximize();
            }
        )}
        // resolve the promised so the browser name is obtained.
        browser.getBrowserName();
    }
}

exports.config = configObject;

Where I have the browser specific configuration as follow:

// browser: chrome and firefox
const chrome = {
    'browserName': 'chrome',
    'chromeOptions': {
        'args': [
            'disable-infobars'
            // 'incognito',
            // 'disable-extensions',
            // 'show-fps-counter=true'
        ]
    }
};

const firefox = { // https://github.com/mozilla/geckodriver#firefox-capabilities
    'browserName': 'firefox',
    'marionette': true,
    'moz:firefoxOptions': {
        'args': [ // https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options
            // '--safe-mode',
            // '--private',
        ]
    }
};

const safari = { // https://developer.apple.com/documentation/webkit/about_webdriver_for_safari
    'browserName': 'safari',
    'safari.options' : {
        technologyPreview: false, // set to true if Safari Technology Preview to be used
        cleanSession: true,
    }
}

// Comment/Uncomment to select the browser used in the test run
const multiBrowsersDirectConnect = [
    chrome,
    // firefox,
]

// Safari 12 and later or Safari Technology Preview is needed to run the tests
const multiBrowsers = [
    // safari need to run alone, as it does not support directConnect
    safari,
]

module.exports = { firefox,
                   safari,
                   multiBrowsers,
                   multiBrowsersDirectConnect, }

I have some step definition in Gherkins as following:

Feature: Login to the dashboard

    As a staff member
    I want to be able to access the dashboard
    So that I can use the dashboard to manage the community

    @sanity @smoke @ruthere
    Scenario: Login to Dashboard with Valid Known Email and Valid Password

        Given a dashboard user named Richard Belding
        When he enters his credentials as DASHBOARD_EMAIL and DASHBOARD_PASSWORD
        Then Richard should see the dashboard welcome page

    @sanity
    Scenario: Login to Dashboard with Valid Unknown Email and Valid Password

        # Valid unknown Email and valid password meaning with valid E-mail & password
        # format, but the user does not exist
        Given a dashboard user named Richard Belding
        When he enters a valid unknown credential as DASHBOARD_EMAIL_UNKNOWN and DASHBOARD_PASSWORD
        Then Richard should be unauthorized to use the dashboard

Where the step definition looks like this:

export = function loginSteps() {

    // Setting a large timeout for login, because from time to time, dashboard server
    // response is slow,and it takes a while for the login page to open, especially if
    // tests are run over wifi
    const LOGIN_MAX_TIMEOUT_MILLISECONDS: number = 15 * 1000;
    const LOGIN_MAX_TIMEOUT = { timeout: LOGIN_MAX_TIMEOUT_MILLISECONDS };

    this.Given(/^a dashboard user named (.*)$/, function(name: string) {
        return stage.theActorCalled(name)
                    .attemptsTo(
                        Start.asStaffMember(name),
                    );
    });

    this.When(/^s?he enters (?:his|her) credentials as (.*) and (.*)$/, LOGIN_MAX_TIMEOUT, function(
        emailEnvVariableName: string, passwordEnvVariableName: string) {
            const email = process.env[`${emailEnvVariableName}`];
            const password = process.env[`${passwordEnvVariableName}`];

            return stage.theActorInTheSpotlight()
                        .attemptsTo(
                            Login.withCredentials(email, password),
                            WaitLonger.until(MainMenu.contentOption.TARGET, Is.clickable()),
                            Click.on(MainMenu.contentOption.TARGET),
                            WaitLonger.until(welcomeToCommunityToast.TARGET, Is.absent()),
                        );
    });

    this.Then(/^(.*) should see the dashboard welcome page$/, function(name: string) {
        return expect(stage.theActorInTheSpotlight()
                           .toSee(Dashboard.GetStarted.QUESTION))
                           .eventually
                           .contain(Dashboard.GetStarted.LABEL);
    });

    this.When(/^s?he enters a valid unknown credential as (.*) and (.*)$/, function(
        emailEnvVariableName: string, passwordEnvVariableName: string) {

            const email = process.env[`${emailEnvVariableName}`];
            const password = process.env[`${passwordEnvVariableName}`];

            return stage.theActorInTheSpotlight()
                        .attemptsTo(
                            Login.withCredentials(email, password),
                            WaitLonger.until(unauthorizedToast.TARGET, Is.visible()),
                        );
    });

    this.Then(/^(.*) should be unauthorized to use the dashboard$/, function(name: string) {
        return expect(stage.theActorInTheSpotlight()
                           .toSee(unauthorizedToast.QUESTION))
                           .eventually
                           .include(unauthorizedToast.LABEL);
    });

};

And the login function looks like the following:

export class EmailAddress extends TinyTypeOf<string>() {}
export class Password extends TinyTypeOf<string>() {}

export class Credentials extends TinyType {
    static using(email: EmailAddress, password: Password) {
        return new Credentials(email, password);
    }

    private constructor(public readonly email: EmailAddress,
                        public readonly password: Password) {
        super();
    }
}

export class Login implements Task {
    readonly credentials: Credentials;

    static withCredentials(email: string = '', password: string = '') {
        const emailAddress: EmailAddress = new EmailAddress(email);
        const pw: Password = new Password(password);

        return new Login(emailAddress, pw);
    }


    private constructor(email: EmailAddress, password: Password) {
        this.credentials = Credentials.using(email, password);
    }

    @step('{0} logs in to the dashboard')
    // required by the Task interface and delegates the work to lower-level tasks
    performAs(actor: PerformsTasks): PromiseLike<void> {
        const staffEmail: string = this.credentials.email.value;
        const staffPassword: string = this.credentials.password.value;

        return actor.attemptsTo(
            Input.text(staffEmail)
                 .into(TextField.Input.labeled('email').TARGET),
            Input.text(staffPassword)
                 .into(TextField.Input.labeled('password').TARGET),
            // Wait will be adjusted according to different browser
            Wait.for(WaitDuration.BrowserBased.loginDuration()),
            WaitLonger.until(LoginDialog.LoginButton.TARGET, Is.clickable()),
            Click.on(LoginDialog.LoginButton.TARGET),
        );
    }
}

Now if I run the both test casess, the first test would always pass, and the second test would always fail at the step When he enters a valid unknown credential as DASHBOARD_EMAIL_UNKNOWN and DASHBOARD_PASSWORD. And an exception would be throw, the stack trace look like the following:

[protractor-ignore-rest]      WebDriverError:
[protractor-ignore-rest]      Build info: version: '3.14.0', revision: 'aacccce0', time: '2018-08-02T20:13:22.693Z'
[protractor-ignore-rest]      System info: host: 'Steves-MBP.k4connect.private', ip: 'fe80:0:0:0:8e8:8a47:e29:fa6c%en0', os.name: 'Mac OS X', os.arch: 'x86_64', os.version: '10.13.6', java.version: '1.8.0_172'
[protractor-ignore-rest]      Driver info: driver.version: unknown
[protractor-ignore-rest]          at Object.checkLegacyResponse (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/error.js:546:15)
[protractor-ignore-rest]          at parseHttpResponse (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/http.js:509:13)
[protractor-ignore-rest]          at doSend.then.response (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/http.js:441:30)
[protractor-ignore-rest]          at <anonymous>
[protractor-ignore-rest]          at process._tickCallback (internal/process/next_tick.js:188:7)
[protractor-ignore-rest]      From: Task: WebElement.click()
[protractor-ignore-rest]          at thenableWebDriverProxy.schedule (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/webdriver.js:807:17)
[protractor-ignore-rest]          at WebElement.schedule_ (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/webdriver.js:2010:25)
[protractor-ignore-rest]          at WebElement.click (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/webdriver.js:2092:17)
[protractor-ignore-rest]          at actionFn (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/protractor/built/element.js:89:44)
[protractor-ignore-rest]          at Array.map (<anonymous>)
[protractor-ignore-rest]          at actionResults.getWebElements.then (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/protractor/built/element.js:461:65)
[protractor-ignore-rest]          at ManagedPromise.invokeCallback_ (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/promise.js:1376:14)
[protractor-ignore-rest]          at TaskQueue.execute_ (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/promise.js:3084:14)
[protractor-ignore-rest]          at TaskQueue.executeNext_ (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/promise.js:3067:27)
[protractor-ignore-rest]          at asyncRun (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/selenium-webdriver/lib/promise.js:2927:27)Error
[protractor-ignore-rest]          at ElementArrayFinder.applyAction_ (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/protractor/built/element.js:459:27)
[protractor-ignore-rest]          at ElementArrayFinder.(anonymous function).args [as click] (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/protractor/built/element.js:91:29)
[protractor-ignore-rest]          at ElementFinder.(anonymous function).args [as click] (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/protractor/built/element.js:831:22)
[protractor-ignore-rest]          at Click.performAs (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/serenity-js/src/serenity-protractor/screenplay/interactions/click.ts:13:59)
[protractor-ignore-rest]          at /Users/sdev/k4/github/auto-ui-test/packages/community/node_modules/@serenity-js/core/src/screenplay/actor.ts:112:43
[protractor-ignore-rest]          at <anonymous>
[protractor-ignore-rest]          at process._tickCallback (internal/process/next_tick.js:188:7)
[protractor-ignore-rest]      From: Task: <anonymous>
[protractor-ignore-rest]          at World.stepWrapper (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/serenity-js/src/serenity-cucumber/webdriver_synchroniser.ts:72:18)
[protractor-ignore-rest]          at World.stepWrapper (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/serenity-js/src/serenity-cucumber/webdriver_synchroniser.ts:104:32)
[protractor-ignore-rest]          at World.arity2 (eval at module.exports (/Users/sdev/k4/github/auto-ui-test/packages/dashboard/node_modules/util-arity/arity.js:22:24), <anonymous>:3:45)
[protractor-ignore-rest]          at _combinedTickCallback (internal/process/next_tick.js:131:7)
[protractor-ignore-rest]          at process._tickCallback (internal/process/next_tick.js:180:9)

However, if I am running them individually, both will pass just by themselves.

Also does any one know what are possible safari.options we can have to config the safari browser,

I tried to look for them:

https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#safari-specific

How to enable private browsing for Safari in the Protractor configuration

But the documentation seems very limited.

All my test casses both works fine on Google Chrome, and Firefox. Safari seems to give me a lot of difficulties.

My specs are: OS: MacOS High Sierra (10.13.6)

WebDriver: 3.14.0

Safari Version: 12.0

npm version: 6.4.0

node version: v8.11.3

nvm version: 0.33.11

Thank you so much for all your help, let me know if you need any more information.

Cheers~

Upvotes: 0

Views: 1343

Answers (1)

sdevk4
sdevk4

Reputation: 11

I just find out that actually, this is due to the fact that the test went too fast where the DOM is not ready (certain elements only becomes clickable when some fields are filled out) when the click happened, therefore the action is unable to take place.

I have issued a wait before the clicking once the username and passwords are filled out, and once the DOM is fully loaded after the wait, I do not have the exceptions anymore.

Upvotes: 1

Related Questions