Playright login to application using global-setup.ts cached session timeouts waiting for selector with DEBUG=0, doesn't timeout with DEBUG=1

Viewed 270

*Updates 22.6.2022 / Reproduced the problem with another site that uses OAuth, when the globalSetup should do stuff on the OAuth domain, it fails 21.6.2022 / Trace.zip shows that the url to OAuth login is correct, but the screenshot shows an empty white page

What I'm trying to achieve:

I'm trying to use cached authentication with my Playwright TypeScript test project. Following their docs availaable here - https://playwright.dev/docs/test-auth.

This way before each test is run, the login in global-setup.ts is done once and then cached for future tests.

The issue:

Test fails with page.click: Timeout 30000ms exceeded. When I use $env:PWDEBUG=1 there is no issue. If I use 0, the issue appears.

Quick workflow:

  1. Main login page in domain abc.com

  2. Click "login by email"

  3. it goes to a different domain with the email login form (< [placeholder="Email Address"] exists here)

What I noticed: I took screenshots and logs, and it seems that with DEBUG=0, it never goes to the page where the 'missing' element should be. In the workflow #1, a click is performed to a button "login with email", but then the email login form page isn't visible for some reason. I tried adding more timeout, more waiting for loads etc. but nothing works. Note: the login page after clicking goes to a different domain for the login if it matters. But it doesn't matter when DEBUG is 1..

Also everything works well if I just use beforeEach, but then again it's not what Playwright instructs in their docs.

npx playwright test

Running 3 tests using 1 worker

page.click: Timeout 30000ms exceeded.
=========================== logs ===========================
waiting for selector "[placeholder="Email Address"]"
============================================================

   at ..\global-setup.ts:19

  18 |   // Click [placeholder="Email Address"]
> 19 |   await page.click('[placeholder="Email Address"]');
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://website.myapp.fi/app/');

  // Have tried this without waitForNavigation with no difference in output
  await Promise.all([
    page.waitForNavigation(),
    page.locator('div[role="button"]:has-text("email")').click(),
  ]);

  // Click [placeholder="Email Address"]
  await page.click('[placeholder="Email Address"]');
  await page.locator('[placeholder="Email Address"]').fill('email..');

  // Click [placeholder="Password"]
  await page.click('[placeholder="Password"]');
  await page.locator('[placeholder="Password"]').fill('password..');

  // Click button:has-text("Sign in")
  await page.click('button:has-text("Sign in")');

  // Select company
  await page.click('.b-number-cell');
  await page.waitForLoadState('networkidle');
  // Save signed-in state to 'storageState.json'.
  await page.context().storageState({ path: 'storageState.json' });
  await browser.close();
}

export default globalSetup;

EDIT: added playwright.config.ts

import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

const config: PlaywrightTestConfig = {
  globalSetup: require.resolve('./global-setup'),
  testDir: './tests',
  timeout: 30 * 1000,
  expect: {
    timeout: 5000
  },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    actionTimeout: 0,
    trace: 'on-first-retry',
    // Tell all tests to load signed-in state from 'storageState.json'.
    storageState: 'storageState.json'
  },
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
      },
    },
  ],
};

export default config;
2 Answers

From the details provided, it looks like you have provided the timeout of 30 seconds. It is obvious that with debug mode test don't need to honor this. Can you please try increasing this as first step

timeout: 60 * 1000,

Also as error is specific to particular action -

waiting for selector "[placeholder="Email Address"]"

you should also set timeout for that specific action -

await page.click('[placeholder="Email Address"]', { timeout: 60000 });

I solved the issue by using one separate login test to start the test suite instead of using globalSetup.

The login saves cookies, localStorage and sessionStorage:

test.describe('Login suite', () => {
    test("that login works", async ({ page, context }) => {

        // ...
        // The login routine comes here first, then cache needed values after they are complete
        
        // Save cookies and localstorage to a file, which we can use later in the tests to be logged in automatically
        await context.storageState({ path: 'state.json' });

        // Get session storage and store as env variable
        const sessionStorage = await page.evaluate(() => JSON.stringify(sessionStorage));
        process.env.SESSION_STORAGE = sessionStorage;

        // Log sessionStorage and localStorage after it has been stored for debugging purposes
        const localStorage = await page.evaluate(() =>
            JSON.stringify(window.localStorage)
        );
    });
});

Each test then use cookies and localStorage with "test.use({ storageState: 'state.json' });" and sessionStorage is populated from env variable. context.storageState does not save sessionStorage, so that is why we have to use a different approach to it.

test.describe('Test suite', () => {
    test.use({ storageState: 'state.json' });
    test("test", async ({ page, context }) => {
    
        // Set session storage in a new context
        const sessionStorage = process.env.SESSION_STORAGE;

        await context.addInitScript(storage => {
            console.log(window.location.hostname);
            if (window.location.hostname === 'mysite.eu') {
                const entries = JSON.parse(storage);
                for (const [key, value] of Object.entries(entries)) {
                    window.sessionStorage.setItem(key, value);
                }
            }
        }, sessionStorage);
        
        // ...
        // The test routine comes here
    });
});
Related