Search code examples
angulartypescriptautomated-testsplaywrightplaywright-test

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


*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;

Solution

  • 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
        });
    });