Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] global beforeEach/beforeAll hooks #9468

Closed
Tomsk666 opened this issue Oct 13, 2021 · 39 comments · Fixed by #32348
Closed

[Feature] global beforeEach/beforeAll hooks #9468

Tomsk666 opened this issue Oct 13, 2021 · 39 comments · Fixed by #32348
Assignees
Labels

Comments

@Tomsk666
Copy link

Instead of having the same test.beforeEach hook defined in every file, how do I only define the beforeEach hook in a single file (say hooks.js) at a global level, so then each file can use that hook?
In other words define my hooks in a single file once, but hooks are then used by all of my tests in all my spec files?
Thanks

@yury-s

This comment was marked as outdated.

@yury-s yury-s closed this as completed Oct 13, 2021
@Tomsk666
Copy link
Author

Tomsk666 commented Oct 14, 2021

Hi @yury-s I'm using JavaScript not ts, and seem to keep getting an error:

Using config at C:\Users\tom\OneDrive\Documents\projects\vs-code\Playwright-test-training\playwright.config.js
Error: tests\hooks_ex.spec.js: JavaScript files must end with .mjs to use import.
at errorWithFile (C:\Users\tom\OneDrive\Documents\projects\vs-code\Playwright-test-training\node_modules@playwright\test\lib\test\util.js:191:10)
at Loader._requireOrImport (C:\Users\tom\OneDrive\Documents\projects\vs-code\Playwright-test-training\node_modules@playwright\test\lib\test\loader.js:219:145)
at Loader.loadTestFile (C:\Users\tom\OneDrive\Documents\projects\vs-code\Playwright-test-training\node_modules@playwright\test\lib\test\loader.js:130:18)
at Runner._run (C:\Users\tom\OneDrive\Documents\projects\vs-code\Playwright-test-training\node_modules@playwright\test\lib\test\runner.js:219:59)

My hooks.js (now renamed to hooks.mjs but still ge the same error) is:

import { test } from '@playwright/test'

test.beforeEach(async ({ page }) => {
  await page.goto('/webdriver2/sdocs/auth.php');
  await expect(page).toHaveTitle('Automated Tools Test Site');
});

export default test

and main test file hooks_ex.spec.js contains:

const { expect } = require('@playwright/test');
import { test } from './hooks.mjs'

test.describe('Security Tests', () => {

  test('Login Sanity Test @smoke', async ({ page }) => {
    const locator = page.locator('body');
    await expect(locator).toContainText('not Logged in');
    await page.fill('id=username', 'edgewords');
    await page.fill('id=password', 'edgewords123');
    await page.click('text=Submit');
    const logOut = page.locator('text=Log Out');
    await expect(logOut).toBeVisible({ timeout: 10000 });

    await page.close();
  });

any chance you could help? many thanks

@yury-s yury-s reopened this Oct 14, 2021
@yury-s
Copy link
Member

yury-s commented Oct 14, 2021

As the error says you need to rename hooks_ex.spec.js to use .mjs extension.

@yury-s
Copy link
Member

yury-s commented Oct 14, 2021

You also should not mix require with import, this worked locally:

// hooks_ex.spec.mjs
import { expect } from '@playwright/test';
import test from './hooks.mjs'

test.describe('Security Tests', () => {

  test('Login Sanity Test @smoke', async ({ page }) => {
    const locator = page.locator('body');
    await expect(locator).toContainText('not Logged in');
    await page.fill('id=username', 'edgewords');
    await page.fill('id=password', 'edgewords123');
    await page.click('text=Submit');
    const logOut = page.locator('text=Log Out');
    await expect(logOut).toBeVisible({ timeout: 10000 });

    await page.close();
  });
  
});

@yury-s yury-s closed this as completed Oct 14, 2021
@Tomsk666
Copy link
Author

Brilliant, thanks @yury-s that's fixed it, all working.

@yury-s yury-s reopened this Oct 14, 2021
@yury-s
Copy link
Member

yury-s commented Oct 14, 2021

@Tomsk666 sorry for misleading you, this will not work as I thought, if you run with --workers=1 the hooks will only run in the test file that happens to be executed first, the rest will run without the hooks.

The best way to achieve what you need today will likely be something like this:

import { sharedBeforeEachFunction } from './hooks.mjs'

test.beforeEach(sharedBeforeEachFunction);

test('Login Sanity Test @smoke', async ({ page }) => {
});

Unfortunately this requires calling test.beforeEach in each test file.

@yury-s
Copy link
Member

yury-s commented Oct 14, 2021

Another way (more convoluted but also more idiomatic to playwright) to do this is to define your beforeAll/Each methods as fixtures that run automatically. The documentation covering this is quite scarce at the moment but if it works for you we can add some examples there.

// fixtures.js
const base = require('@playwright/test');

module.exports = base.test.extend({
  sharedBeforeAll: [ async ({}, use) => {
    console.log('Before All');
    await use();
    console.log('After All');
  }, { scope: 'worker', auto: true } ],  // starts automatically for every worker - we pass "auto" for that.

  sharedBeforeEach: [ async ({}, use) => {
    console.log('Before Each');
    await use();
    console.log('After Each');
  }, { scope: 'test', auto: true } ],  // starts automatically for every test - we pass "auto" for that.
});

The tests files will look like this:

// @ts-check
const test = require('./fixtures');

test('basic test 1', async ({ page }) => {
});

This is not completely equivalent to beforeAll() in every file since it will run once per worker process (see this doc to learn mode about playwright test runner model) but it may be even better for you depending on the use case.

@yury-s yury-s added the feature-test-runner Playwright test specific issues label Oct 14, 2021
@Tomsk666
Copy link
Author

Tomsk666 commented Oct 15, 2021

Thanks @yury-s I'll try some of these options out & see how I get on.
Maybe we could add it as a feature request, as many other frameworks like Mocha, Cucumber, Cypress have global hooks.
It'd be great just to put all the hooks in one file say called hooks.js, then in the Playwright config be able to have a parameter like globalHooks: './hooks.js' that would then mean all tests would use the global before/afterEach. Even better would be that if you then defined a local beforeEach in a test file, then that would have precendece over the global, so you had both options.
As you can tell I'm not a developer, so no idea how hard this would be to implement.
Anyway thanks for your help, really enjoying & impressed with Playwright.

@yury-s yury-s changed the title [Question]How to create a global beforeEach hook [Feature] global beforeEach/beforeAll hooks Oct 15, 2021
@yury-s
Copy link
Member

yury-s commented Oct 15, 2021

Maybe we could add it as a feature request,

Changed this issue to a feature request, let's see if there is enough upvotes to implement it.

@F3n67u
Copy link
Contributor

F3n67u commented Oct 16, 2021

I will support the global hook feature. global hook is easier to use than the fixture pattern. I am willing to contribute this feature.

@cunhabruno
Copy link

@yury-s that would be great to have this feature, I have tried using a beforeAll in a separate file as you recommended but if the test fails the retry will not call this beforeAll because the retry is only for the current spec file. The same problem happened adding in a Playwright fixture

@DamayantiRathore
Copy link

Waiting for any update on this feature request 💯

@saurabh4888
Copy link

@yury-s Any plan to implement this feature...it would be great to have global hooks for easier handling

@lifeart
Copy link

lifeart commented Sep 20, 2022

Up

@skozin-tns
Copy link

Is there a plan to implement this feature? It would be super helpful

@liranelisha
Copy link

this is really needed

@Krisell
Copy link

Krisell commented Jan 25, 2023

We had some trouble implementing a global beforeEach, using the initial suggestion here. It only ran before some early tests (but not once per worker, perhaps once per core regardless of worker-setting?).

However it looks like we have now found a solution based on the ideas here:

import { test as base } from '@playwright/test'

export const test = base.extend({
  page: async ({ baseURL, page }, use) => {
    // We have a few cases where we need our app to know it's running in Playwright. 
    // This is inspired by Cypress that auto-injects window.Cypress.
    await page.addInitScript(() => { 
      (window as any).Playwright = true 
    })

    // Here we can enable logging of all requests, which is useful to see sometimes.
    // We also block some unnecessary third-party requests to speed up the tests.
    await page.route('**/*', route => { 
      ...
    })

    use(page)
  },
});

And in our tests we import test from the file above instead of from '@playwright/test'. This is of course easy to forget, so a configurable way to do the same thing would be even better.

@uncvrd
Copy link

uncvrd commented Feb 27, 2023

Have to agree with @Krisell here. For some reason, my auto fixture for beforeEach only runs one time, instead on each test unless I run the suite in debug mode...not sure why that makes a difference but it does.

Here's my example:

const seedTest = base.extend<{ dbSeed: void }>({
  dbSeed: [async ({  }, use) => {

    execSync("npx prisma db seed");

    await use();

  }, { auto: true, scope: "test" }]
})

Where I have two tests here using the extended fixture:

seedTest.describe("Links", () => {

  seedTest.beforeEach(async ({ page }) => {

    await page.goto("/workspaces/links");

  })

  seedTest("creates a track link setting and sets as default", async ({ page }) => {
    
    // test code
    
  })

  seedTest("deletes a track link setting", async ({ page }) => {

   // test code

  })

})

Which results in my dbSeed command only running once unless I run the tests in debug mode, then it runs each time 🤔


Also just want to say that the above solution works for now but would love to use my own auto fixture instead of extending page!

@sbousamra
Copy link

Any updates on this? Would love this feature

@taramk
Copy link

taramk commented Jul 20, 2023

+1 for this

@JanMosigItemis
Copy link

+1

@iburahim786
Copy link

yury-s This is a highly recommended feature for all tests, Please add in the feature quickly. UPVOTES (1000)

@fmagaldea
Copy link

This feature would help a lot, hope it will be implemented soon.

@aMerkato
Copy link

aMerkato commented Dec 4, 2023

It would be great if you implement global beforeEach and afterEach.

@Studigrad
Copy link

+1

@SilverMaiden
Copy link

SilverMaiden commented Mar 12, 2024

Following this, I was able to get essentially the equivalent of a beforeEach and afterEach for each test case (even with just 1 worker), so thought I'd post it here for others. Hope it helps!

@samlader
Copy link

+1

3 similar comments
@LuisFloresSerna
Copy link

+1

@k4mr4n
Copy link

k4mr4n commented Jun 28, 2024

+1

@JohnnyChiang
Copy link

+1

@antebudimir
Copy link

Following this, I was able to get essentially the equivalent of a beforeEach and afterEach for each test case (even with just 1 worker), so thought I'd post it here for others. Hope it helps!

I've expanded on this to be able to intercept multiple api calls for different pages, e.g. this is my homepage-setup.ts:

import { test as base, expect } from '@playwright/test';
import { mockFilterCategories } from '@mocks/handlers';
import { API_FILTER_CATEGORIES, CONTENT_TYPE_JSON, STATUS_OK } from '@constants';

interface MockConfig {
  url: string | RegExp;
  response: {
    status: number;
    contentType: string;
    body: Record<string, unknown> | unknown[];
  };
}

const MOCKED_ROUTES: MockConfig[] = [
  {
    url: `**/${API_FILTER_CATEGORIES}`,
    response: {
      status: STATUS_OK,
      contentType: CONTENT_TYPE_JSON,
      body: mockFilterCategories,
    },
  },
];

const test = base.extend({
  page: async ({ page }, use) => {
    for (const mockConfig of MOCKED_ROUTES) {
      await page.route(mockConfig.url, (route) => {
        route.fulfill({
          status: mockConfig.response.status,
          contentType: mockConfig.response.contentType,
          body: JSON.stringify(mockConfig.response.body),
        });
      });
    }

    await use(page);

    for (const mockConfig of MOCKED_ROUTES) {
      await page.unroute(mockConfig.url);
    }
  },
});

export { expect, test };

@terrymun
Copy link

terrymun commented Aug 11, 2024

Thanks @Krisell, we ran into an issue where we want to disable JS-based animations on our app and your solution of introducing a global flag worked :)

With regards to this:

And in our tests we import test from the file above instead of from '@playwright/test'. This is of course easy to forget, so a configurable way to do the same thing would be even better.

...our solution is simply to setup an eslint no-restricted-import rule to ensure that users do not import test from @playwright/test by accident:

{
  files: ['**/*.e2e.ts'],
  rules: {
    'no-restricted-imports': [
      'error',
      {
        paths: [
            name: '@playwright/test',
            importNames: ['test'],
            message: 'Please import `test` from fixtures.ts instead',
          },
        ],
      },
    ],
  },
},

@Krisell
Copy link

Krisell commented Aug 12, 2024

...our solution is simply to setup an eslint no-restricted-import rule to ensure that users do not import test from @playwright/test by accident:

That is brilliant, thanks! It looks like the default import can also be used as the test function, so I added 'default' to the restricted import names as well.

@caseyjhol
Copy link

@yury-s I'm a little confused - how does acd2a4d solve the beforeAll problem if you only have 1 worker? It still only runs once per worker, even if you have multiple specs.

@silverwind
Copy link

silverwind commented Nov 28, 2024

These fixture-based solutions seem impractical because they require changing every test file to change the test import. I would appreciate a solution that works with the standard test export.

@terrymun
Copy link

terrymun commented Nov 28, 2024

@silverwind I might be missing something but I am not seeing an issue here. I don't think it's that difficult to replace the test command? You can have an eslint rule set up to throw an error whenever it's imported from the wrong source, and then do a project-wide search and replace.

We have this setup enabled on our relatively large monorepo and it took under an hour to migrate the code.

@silverwind
Copy link

silverwind commented Nov 28, 2024

Sure, it's not a hard refactor, but it's still code churn that could be avoided if only there were an better API. For example in Jest, one can have global beforeEach/afterEach defined in a setup file, which is ideal imho:

https://stackoverflow.com/a/50302253/808699

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.