Search code examples
protractorgmailheadless

Protractor login to gmail fails headless, and works when not headless


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.


Solution

  • 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.