top of page

Welcome
to NumpyNinja Blogs

NumpyNinja: Blogs. Demystifying Tech,

One Blog at a Time.
Millions of views. 

Mastering Playwright Config: My DSAlgo Portal Test Suite

Feb 24
5 min read

When I first opened playwright.config.js for my DSAlgo Portal project, I thought it was just another settings file. Copy some boilerplate, adjust a few paths, done.

I was so wrong it hurts to think about it.

The config file turned out to be the entire blueprint of my test execution strategy. It’s where I solved parallelization, eliminated repeated logins, and orchestrated 130+ tests to run in perfect sequence. Once I understood it, my test suite went from a chaotic mess to a well-oiled machine.

Let me show you what I built.


The Three Layers That Changed Everything

I learned to think about my config in three layers, like a layer cake (but way more useful).

Layer 1: The Execution Foundation

This is where I set the ground rules:

export default defineConfig({ workers: process.env.CI ? 2 : 4, retries: process.env.CI ? 0 : 0, 
use:{ 
baseURL: process.env.BASE_URL ||   'https://dsportalapp.herokuapp.com', 
screenshot: 'only-on-failure', 
video: 'retain-on-failure', }, });

Workers = parallelization. Locally, I run 4 workers because my laptop can handle it. In CI (GitHub Actions), I drop to 2 because cloud environments are flakier. This one line gave me fast local runs and stable CI.


BaseURL changed my life. Instead of writing the full URL 100+ times:

await page.goto('https://dsportalapp.herokuapp.com/login'); // ■ Everywhere

I write:

await page.goto('/login'); // ■ Just the path

Want to test staging? Just change BASE_URL in .env. Same code, different environment. Screenshots and videos only on failure = saving gigabytes of disk space and making failure debugging instant. I don’t need 100 videos of passing tests.

Layer 2: Global Setup and Teardown

This is where I hit gold:

export default defineConfig({ 
globalSetup: './global-setup.js', 
globalTeardown: './global-teardown.js', });

My global-setup.js logs in ONCE before any test runs:

await page.goto(process.env.BASE_URL + '/login'); await page.getByLabel('Username:').fill(process.env.TEST_USERNAME);await page.getByLabel('Password:').fill(process.env.TEST_PASSWORD); await page.getByRole('button', { name: 'Login' }).click(); await context.storageState({ path:'.auth/user.json' });

It saves the logged-in session to .auth/user.json.


Then, every authenticated test reuses it:

{ name: 'phase5-array', 
use: { storageState: '.auth/user.json', // ← Boom, already logged in }, }

No more logging in 100 times.

Time saved: 5 seconds × 100 tests = 8+ minutes. Every. Single. Run.

My global-teardown.js cleans up afterward: logs out, deletes the auth file, clears cookies. Leaves no traces.

Layer 3: Projects (Where the Magic Happens)

This is the layer that transformed everything. Projects let me split my 130+ tests into logical execution phases with dependencies:

projects: [ { name: 'phase1-noauth', 
testMatch: /.*01_homepage\.feature/, 
grep:/@noauth/, }, 
{ name: 'phase2-registration', 
testMatch: /.*02_registration\.feature/,
dependencies: ['phase1-noauth'], // ← Waits for phase1 to finish }, ]

That dependencies line creates a waterfall. Phase2 won’t start until phase1 completes.


Here’s my entire execution flow:

phase1-noauth → phase2-registration → phase3-signin → phase4-authenticated-homepage ↓[phase5: 7 modules run in PARALLEL] ↓ security-tests

Phase5 is where it gets powerful: 7 data structure modules (Array, Stack, Queue, LinkedList, Tree,Graph, DataStructures) all start together after phase4 and run in parallel with my 4 workers.


Before: Sequential testing took 10 minutes.


After: Parallel phase5 with dependencies = under 5 minutes locally.


Execution time: cut in half.



The Pattern I Wish I’d Known Day One

Use grep and grepInvert to slice one feature file into multiple projects:

// Phase 3: Run signin functionality tests
 { name: 'phase3-signin', 
testMatch: /.*03_signin\.feature/, 
grep: /@signin/, 
grepInvert: /@security/, // ← Skip @security tagged scenarios 
} 
// Security tests: Run ONLY @security scenarios 
{ name:'security-tests', 
testMatch: /.*03_signin\.feature/, 
grep: /@security/, // ← Only @security tagged scenarios }

Same feature file. Two different execution contexts. Different timing. Different purposes.

I don’t need separate files for signin tests vs security tests. I just tag scenarios differently and let grep handle the rest.

Mind = blown.


My Current Mistakes (Yes, I’m Still Learning)

Mistake 1: Commented Out Dependency

{ name: 'phase3-signin', //dependencies: ['phase2-registration'], // ← Commented out! }

I had to comment this out because phase2 has failing tests (invalid input validation scenarios). With the dependency enabled, when phase2 fails, Playwright skips phase3, phase4, and ALL of phase5.


My temporary workaround: Run phase3 independently. It works because globalSetup creates the login session anyway.


What I should do: Add ignoreTestFailures: true to phase2 so known failures don’t block the entire pipeline. I just haven’t applied it to this version yet.


Mistake 2: Not Using Environment Variables Initially


I learned this the hard way. My first config had:

// ■ Bad - hardcoded use: { baseURL: 'https://dsportalapp.herokuapp.com', }

Switching to staging? Change the code. Testing locally? Change the code again. Merge conflict hell.

Now:



// ■ Good - flexible use: { baseURL: process.env.BASE_URL ||
'https://dsportalapp.herokuapp.com', }

One .env file change. No code changes. Works everywhere.


The Config Pattern That Saved Me

Here’s my complete structure:

export default defineConfig({ 
// Execution controls 
workers: process.env.CI ? 2 : 4, 
//Lifecycle hooks 
globalSetup: ‘./global-setup.js’, 
globalTeardown: ‘./global-teardown.js’, 
// Base settings 
use: { baseURL: process.env.BASE_URL, 
screenshot: ‘only-on-failure’, 
video: ‘retain-on-failure’, }, 
// Projects with dependencies 
projects: [ { name: ‘phase1-noauth’ }, 
{ name: ‘phase2-registration’, 
dependencies: [‘phase1-noauth’], }, 
{ name: ‘phase3-signin’, 
// dependencies: [‘phase2-registration’], ← fixing this }, 
{ name: ‘phase4-authenticated-homepage’,
dependencies: [‘phase3-signin’], 
use: { storageState: ‘.auth/user.json’ }, }, // … 7phase5 modules in parallel { name: ‘security-tests’, // ← needs dependencies on all
phase5 }, ], });

This structure:

• Runs setup once

• Tests unauthenticated features first

• Tests authenticated features with saved session

• Parallelizes independent modules

• Different behavior in CI vs local


The Numbers That Matter

Before understanding the config:

• Tests ran in random order

• Login repeated 100+ times

• Total execution: ~10 minutes

• CI failures: frequent and confusing


After architecting with projects:

• Tests run in logical sequence

• Login happens once globally

• Total execution: ~5 minutes locally

• CI failures: much clearer (when config is complete)


Execution time: 60% faster.


What I’m Fixing Next


In my other project versions, I’ve already applied:

1. ignoreTestFailures: true on phase2, phase3, phase4, and all phase5 modules

2. Dependencies on security-tests to make it truly run last

3. Changed CI workers from 2 back to 1 for even more stabilityThis version (V1) still has the issues because I’m learning incrementally. But that’s the point — your config evolves with your understanding.


Key Takeaways

1. Use globalSetup for expensive one-time operations (saves 8+ minutes per run)

2. Use projects with dependencies to control execution order (no more random chaos)

3. Use storageState to skip repeated logins (massive time saver)

4. Use environment variables so the same code works in dev, staging, and production

5. Different worker counts for CI vs local keeps you sane


The config file isn’t just settings. It’s your test suite’s architecture. Master it, and everything else falls

into place.


*Testing the DSAlgo Portal taught me: spend time on your config file. It’s the foundation. Get it right,

and your tests become faster, more reliable, and infinitely more maintainable.*. 



 
 

+1 (302) 200-8320

NumPy_Ninja_Logo (1).png

Numpy Ninja Inc. 8 The Grn Ste A Dover, DE 19901

© Copyright 2025 by Numpy Ninja Inc.

  • Twitter
  • LinkedIn
bottom of page