2

I have a routine that logs into gmail using Protractor, that is called from the middle of my script (which is why some things that appear unnecessary are there), but I have isolated it as much as I can. When I run it not headless it passes. When I run it headless, it fails. I looked at the related posts, and they did not seem to be Protractor specific, and they did seem to parallel my code here.

Here is the code:

    const EC = ExpectedConditions;

beforeAll(function(){ 


});

beforeEach(function() {
  //because I am using gmail after sending an email from an angular app with a link to get back into one
  browser.waitForAngularEnabled(true);
  browser.ignoreSynchronization = false;

});

afterEach(function() {
  browser.waitForAngularEnabled(true);
  browser.ignoreSynchronization = false;
});


var gmailLogin = function(){
      browser.waitForAngularEnabled(false);//gmail screens not angular
      browser.ignoreSynchronization = true;
      browser.sleep(2000);//because ignore sync takes time to settle in
      browser.driver.manage().timeouts().implicitlyWait(10000);//set in config, but seems to work only if here
      browser.get("https://mail.google.com/mail");
      browser.wait(EC.titleContains("Gmail"), 10000, "wait for gmail page");
      $('[data-g-label="Sign in"]').click().then(
        //this sometimes appears and sometimes is skipped, so ignore result
        function(retval){},function(err){}
      )
      var variousInput = element(by.id('identifierId'));

      browser.wait(EC.presenceOf(variousInput), 10000, "wait for identier ID prompt").then(
      function(retVal){
      var variousInput2 = browser.driver.findElement(by.id('identifierId'));
      variousInput2.sendKeys("myemail here");
      variousInput2=browser.driver.findElement(by.id("identifierNext"));
      variousInput2.click(); 
      variousInput2 =  browser.driver.findElement(by.name('password'));
      variousInput2.sendKeys('my password here');
      variousInput2=browser.driver.findElement(by.id("passwordNext"));
      variousInput2.click();

      },

      function(err){}//assume not found because cookie still around, proceed to next step
      )
      browser.wait(EC.titleContains("Inbox"), 10000, "wait for inbox");
}


describe('runs gmail test for so', function() {
    it('tests gmail', function() {
        gmailLogin();
      expect(browser.getTitle()).toContain('Inbox');
    }, 2 * 60 * 1000); //should always come up within 2 minutes


}); //end of describe

And here is the headed configuration file:

    exports.config = {
  directConnect: true,
  allScriptsTimeout: 120000,
  getPageTimeout: 60000,

  // Capabilities to be passed to the webdriver instance.
  capabilities: {
    'browserName': 'chrome',

    chromeOptions: {
       //args: ["--headless","--disable-gpu","--no-sandbox"]
  },

  // Framework to use. Jasmine is recommended.
  framework: 'jasmine',

  // Spec patterns are relative to the current working directory when
  // protractor is called.
  specs: [
    './so.ts'
  ],

  // Options to be passed to Jasmine.
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 180000
  },
  beforeLaunch: function() {

  },
  onPrepare() {
    browser.manage().window().setSize(1600, 1000);
    browser.driver.manage().timeouts().implicitlyWait(15000);
    }
}
}

and here is the headless (you can see I threw the kitchen sink at the options).

exports.config = {
  directConnect: true,
  allScriptsTimeout: 60000,
  getPageTimeout: 30000,

  // Capabilities to be passed to the webdriver instance.
  capabilities: {
    'browserName': 'chrome',

    chromeOptions: {
       args: ["--headless","--disable-gpu","--window-size=1600,1000","--disable-infobars","--disable-extensions","--auth-server-whitelist","--remote-debugging-port=9222"]
  },

  // Framework to use. Jasmine is recommended.
  framework: 'jasmine',

  // Spec patterns are relative to the current working directory when
  // protractor is called.
  specs: [
    './so.ts'
  ],

  // Options to be passed to Jasmine.
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 180000
  },
  beforeLaunch: function() {

  },
  onPrepare() {
    // screen size set in chrome options
      browser.driver.manage().timeouts().implicitlyWait(15000);

    }
}
}

If there is some kind of underlying, undocumented wisdom about which locators work or do not work headless, I would love to know.

Thanks, jk

SLIGHT UPDATE: I cleaned up the code to use only explicit waits and straight Protractor (which is how I had it originally before reading pieces on the web that were based in other languages). Here is the revised version, which still passes not headless and fails headless (I also removed the implicit wait setting in OnPrepare() and all but the first three chrome options when doing headless).

var gmailLogin = function() {
browser.waitForAngularEnabled(false); //gmail screens not angular
browser.ignoreSynchronization = true;
browser.sleep(2000); //because ignore sync takes time to settle in
browser.get("https://mail.google.com/mail");
browser.wait(EC.titleContains("Gmail"), 10000, "wait for gmail page");
$('[data-g-label="Sign in"]').click().then(
    //this sometimes appears and sometimes is skipped, so ignore result
    function(retval) {},
    function(err) {}
);
var variousInput = element(by.id('identifierId'));

browser.wait(EC.presenceOf(variousInput), 10000, "wait for identifier ID prompt").then(
    function(retVal) {
        var variousInput2 = element(by.id('identifierId'));
        variousInput2.sendKeys("email address");
        variousInput2 = element(by.id("identifierNext"));
        variousInput2.click();
        variousInput2 = element(by.name('password'));
        browser.wait(EC.presenceOf(variousInput2), 10000, "wait for password prompt");
        browser.wait(EC.visibilityOf(variousInput2), 10000, "wait for password prompt");
        variousInput2.sendKeys('my password');
        variousInput2 = element(by.id("passwordNext"));
        variousInput2.click();

    },

    function(err) {} //assume not found because cookie still around, proceed to next step
    )
    browser.wait(EC.titleContains("Inbox"), 10000, "wait for inbox");
}

BIGGER UPDATE: this may be something funky about headless after all. I added the following lines right before waiting for the identifier ID element(by.tagName('html')).getText().then(function(text){console.log(text);}); in not headless mode, that produced

    Sign in
to continue to Gmail
Email or phone
Forgot email?
Not your computer? Use Guest mode to sign in privately.
Learn more
NEXT
Create account
‪English (United States)‬
HelpPrivacyTerms

in headless, it gave

One account. All of Google.
Sign in to continue to Gmail
Find my account
Create account
One Google Account for everything Google
About Google Privacy Terms Help

followed by a long list of languages from Afrikaans to ‪繁體中. So it seems almost as if in headless the browser has forgotten where it lives (at the very least the addition of One account all of Google and the languages says it is not apples to apples). It makes me wonder if then IdentifierId might also have a different name in such a case. One last update for now: To debug I added the following code when that first page loads:

var inputs=element.all(by.tagName('input'));
  inputs.each(function(element,index){
  element.getAttribute("Id").then(function(text){console.log('input '+index+' '+text);})
  })

not headless, we get:

input 0 identifierId
input 1 null
input 2 ca
input 3 ct
input 4 pstMsg
input 5 checkConnection
input 6 checkedDomains

but headless we get:

input 0 null
input 1 null
input 2 null
input 3 null
input 4 null
input 5 null
input 6 null
input 7 null
input 8 null
input 9 null
input 10 null
input 11 profile-information
input 12 session-state
input 13 null
input 14 _utf8
input 15 bgresponse
input 16 Email
input 17 Passwd-hidden
input 18 next

So Protractor is right that it cannot find by ID identifierID. But how come?

FINAL: So depending on headless or not, google was redirecting to two different urls with two different sets of Ids and names. I posted revised code that handles both in my answer.

Thanks for the guidance, all.

Jeremy Kahan
  • 3,796
  • 1
  • 10
  • 23
  • Somewhat unrelated, but are you able to connect to the mail server instead of using the mail client? That would be easier IMO, similar to [this answer](https://stackoverflow.com/questions/29311154/fetching-values-from-email-in-protractor-test-case)? Then you won't have to worry about locators on the gmail side – Gunderson Jun 18 '18 at 23:48
  • Interesting idea @Gunderson. I will check. At some level, I should not be testing gmail anyway (though unclear whether testing sendgrid is any more relevant). – Jeremy Kahan Jun 19 '18 at 01:05
  • I agree with @Gunderson, use Gmail API instead of UI. Also, I see some bad practice in your method. It's too long - you need to decomposite into smaller pieces (methods). Remove implicitly waiter at all, this conception is bad (they're a lot of materials with description why) - use only explicit waiters. And the last one it's just from me, why you still use es5 instead of ES6? – Oleksii Jun 19 '18 at 03:42
  • Points well taken @Oleksii. I had it purely in protractor elements (no findElement), and I thought that might have been an issue, so I converted to driver calls, which gave me Failed: Cannot read property 'bind' of undefined for all my EC's/waits, hence the implicit wait to stop that. The rule of thumb used to be that if it fit on a screen, it was an okay method, but I concede screens have gotten bigger and styles may have changed. Honestly, I'm still trying to get my head around arrow notation. I can post slightly revised code with no implicit wait that still creates the same problems. – Jeremy Kahan Jun 19 '18 at 04:55
  • Could you show what error do you get? Also, you have implicitly waiter in headless config. – Oleksii Jun 19 '18 at 05:32
  • Failed: wait for inbox Wait timed out after 10002ms (plus whole stack trace). I did take the implicit wait out of my OnPrepare, but I did not update the post. The OnPrepare headless is now onPrepare() { // screen size set in chrome options } BTW, because I am ignoring errors, I am pretty sure (based on dev tools on port 9222 from local host) the failure is way back at the beginning of trying to send the identifier ID headless. – Jeremy Kahan Jun 19 '18 at 05:39
  • Actually, I am sure. I put a console.log('error on identifier id wait '+err); in that error function near the bottom and it kicked back: error on identifier id wait TimeoutError: wait for identifier ID prompt Wait timed out after 10009ms – Jeremy Kahan Jun 19 '18 at 05:48
  • Here is the why, sort of. Not headless is getting redirected to https://accounts.google.com/signin/v2/identifier?service=mail and headless is redirected to https://accounts.google.com/ServiceLogin?service=mail – Jeremy Kahan Jun 19 '18 at 15:38
  • @Gunderson thank you, that worked, although not without some real work. Still, it probably is the right approach. I had to set that gmail to accept less secure apps, and controlFlow.await would not work. – Jeremy Kahan Jul 02 '18 at 02:51

1 Answers1

2

So it turns out that Google will redirect the mail service request to two different versions of its interface depending on whether one goes in headless or not. I rewrote the code to handle either one. I also tried to simplify where I could, including no more implicit waits and adding more chaining (I also dipped my toes into ES6 as encouraged by Oleksii's comment).

    const EC = ExpectedConditions;

beforeAll(function(){ 
});

beforeEach(function() {
  //because I am using gmail after sending an email from an angular app with a link to get back into one
  browser.waitForAngularEnabled(true);
  browser.ignoreSynchronization = false;
});

afterEach(function() {
  browser.waitForAngularEnabled(true);
  browser.ignoreSynchronization = false;
});

var waitForIds = (id1,id2)=>{//waits for one of two ids, assumes they must exist or else it is an error
  var elm = element.all(by.xpath("//*[@id = '"+id1+"' or @id = '"+id2+"']")).first();
  browser.wait(EC.presenceOf(elm), 30000, "wait for "+id1+" or "+ id2);
  return elm;
}

var gmailLogin = () => {
browser.waitForAngularEnabled(false); //gmail screens not angular
browser.ignoreSynchronization = true;
browser.sleep(2000); //because ignore sync takes time to settle in
browser.get("https://accounts.google.com/ServiceLogin?service=mail");
browser.sleep(2000);


element(by.id('gbqfq')).isPresent().then((present) => {
    //if present, we are already on the inbox screen, because we found the search pane
    if (!present) { //still work to do to get there
        browser.wait(EC.titleContains("Gmail"), 10000, "wait for a gmail page");

        $('[data-g-label="Sign in"]').click().then(
            //this sometimes appears and sometimes is skipped, so ignore result
            (retval) => {}, (err) => {}
        );
        waitForIds('Email', 'identifierId').sendKeys("my email here");
        waitForIds("identifierNext", "next").click();
        waitForIds('Passwd', 'password').getAttribute('id').then((text) => {
            element(by.name(text)).sendKeys('my password here');
            waitForIds("signIn", "passwordNext").click();
        })
    }
})




browser.wait(EC.titleContains("Inbox"), 10000, "wait for inbox");
}


    describe('runs gmail test for so', function() {
        it('tests gmail', function() {
            gmailLogin();
            expect(browser.getTitle()).toContain('Inbox');
        }, 2 * 60 * 1000); //should always come up within 2 minutes 
    }); //end of describe

UPDATE: I am accepting this because it addresses my question of what happens and why and how to address is directly. I totally accept that despite answering the question, there are better ways to go about what I actually wanted to do (getting the href from an email) either by catching the email on its way out or by using the gmail api.

Jeremy Kahan
  • 3,796
  • 1
  • 10
  • 23