# Organizing Playwright Scripts

> How to organize Playwright scripts in Loadster—start simple, then add helper functions, page objects (POM), and fixtures as your scripts grow.

Source: https://loadster.com/manual/playwright-scripts/organizing-scripts/

A Loadster Playwright script is a **single, self-contained file**. You write it in one code editor, and Loadster runs
that one file with the Playwright Test runner. There's no file tree, and a script can't `import` other files you've
written—only the Playwright libraries and the [`@loadster/bot` module](https://loadster.com/manual/playwright-scripts/bot/) are available.

For most load tests and monitors, that's all you need. A script usually represents one focused user journey, and the
simplest thing—a single linear `test()`—is often the best thing. Reach for more structure only when a script grows large
enough to actually benefit from it.

The rest of this page walks through ways to organize a script, from simplest to most structured. Start at the top and
only go as far down as your script actually needs:

* Start with a plain, linear test.
* Extract **helper functions** when you catch yourself repeating steps.
* Adopt the **Page Object Model (POM)** when you want selectors and actions grouped by page.
* Use **fixtures** when you want Playwright to hand a ready-made object to each test.

## Start with a simple Playwright script

Most Playwright scripts don't need any special structure at all. Just write the steps of your user journey directly in a
single `test()` block:

```javascript
import {test, expect} from '@playwright/test';

test('complete checkout', async ({page}) => {
    await page.goto('https://example.com/login');
    await page.getByLabel('Email').fill('demo@example.com');
    await page.getByLabel('Password').fill('secret');
    await page.getByRole('button', {name: 'Sign in'}).click();

    await page.getByRole('link', {name: 'Cart'}).click();
    await page.getByRole('button', {name: 'Checkout'}).click();

    await expect(page).toHaveURL(/order-confirmation/);
});
```

This is easy to read, easy to debug, and a good fit for load testing and monitoring. If your script is short and the
steps only appear once, you can happily stop here.

## Extract Playwright helper functions

When you catch yourself repeating the same sequence of steps—logging in is the classic example—pull it into a plain
function and call it from your test. This is the lightest-weight way to stay organized, and it's usually enough:

```javascript
import {test, expect} from '@playwright/test';
import bot from '@loadster/bot';

async function login(page) {
    const [username, password] = bot.getVariable('credentials');

    await page.goto('https://example.com/login');
    await page.fill('#username', username);
    await page.fill('#password', password);
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL(/dashboard/);
}

test('view account after login', async ({page}) => {
    await login(page);

    await page.click('a.account');
    await expect(page.locator('h1')).toContainText('Account');
});
```

## Page Object Model (POM) in a Playwright script

The Page Object Model (POM) is a popular way to organize larger test suites. It wraps each page (or component) of your
application in a class, so that selectors and actions live in one place and your tests read at a high level. In a typical
Playwright project each page object is its own file that the test imports:

```
tests/
  checkout.spec.ts
pages/
  LoginPage.ts      <-- imported by the test
  CartPage.ts       <-- imported by the test
```

Loadster scripts don't support that multi-file layout, but the *pattern* works perfectly well when you define the page
object classes in the same file as your test. Just declare the classes above your `test()` blocks:

```typescript
import {test, expect, type Page} from '@playwright/test';

class LoginPage {
    constructor(private page: Page) {}

    async goto() {
        await this.page.goto('https://example.com/login');
    }

    async login(username: string, password: string) {
        await this.page.getByLabel('Email').fill(username);
        await this.page.getByLabel('Password').fill(password);
        await this.page.getByRole('button', {name: 'Sign in'}).click();
    }
}

class CartPage {
    constructor(private page: Page) {}

    async checkout() {
        await this.page.getByRole('link', {name: 'Cart'}).click();
        await this.page.getByRole('button', {name: 'Checkout'}).click();
    }
}

test('complete checkout', async ({page}) => {
    const login = new LoginPage(page);
    const cart = new CartPage(page);

    await login.goto();
    await login.login('demo@example.com', 'secret');
    await cart.checkout();

    await expect(page).toHaveURL(/order-confirmation/);
});
```

You get the same benefits you'd get from a multi-file POM—selectors defined once, tests that read as high-level
steps—without needing multiple files. It's worth the extra structure when a script covers a lot of pages, or when you're
porting an existing POM suite; for a short script, plain helper functions are usually plenty.

## Playwright Test fixtures

Playwright Test [fixtures](https://playwright.dev/docs/test-fixtures) also work, and they're a clean way to hand a
ready-to-use page object (or a logged-in session) to each test. Define the fixture with `test.extend()` at the top of
your script:

```typescript
import {test as base, expect, type Page} from '@playwright/test';

class DashboardPage {
    constructor(private page: Page) {}

    async open() {
        await this.page.goto('https://example.com/dashboard');
    }
}

const test = base.extend<{dashboard: DashboardPage}>({
    dashboard: async ({page}, use) => {
        await use(new DashboardPage(page));
    },
});

test('dashboard loads', async ({dashboard, page}) => {
    await dashboard.open();
    await expect(page.locator('h1')).toBeVisible();
});
```

## Adapting an existing Playwright test suite

If you already maintain a Playwright suite that's split across page objects, fixtures, and helper files, you can bring an
individual test into Loadster by **flattening** it—inlining the page objects and helpers it depends on into a single
script, as shown above. The logic doesn't change; you're just collapsing the imports into one file.

A few tips:

* **Bring in one journey at a time.** You don't need your whole suite—pick the specific user journey you want to load
  test or monitor and flatten just that test plus the page objects it uses.
* **Let an AI assistant do the mechanical work.** Flattening a POM test into a single file is exactly the kind of
  transformation LLMs handle well. Paste your test and its page object files in and ask for a single self-contained
  Playwright Test script.
* **Swap external config for datasets.** Where your suite reads credentials or test data from external files or
  environment variables, use a [dataset](https://loadster.com/manual/dynamic-datasets/) and the
  [`@loadster/bot` module](https://loadster.com/manual/playwright-scripts/bot/) instead so every bot gets its own values.

## Reusing logic across Playwright scripts

Because each Playwright script is self-contained, there's no shared library of page objects that multiple Playwright
scripts import from—if two scripts both log in, each carries its own copy of the login logic. (The
[Include Script](https://loadster.com/faqs/including-scripts-for-reuse/) feature, which composes one script from another, applies to Protocol
Bot and Browser Bot scripts, not Playwright scripts.)

When you want a load test to exercise several different journeys, the Loadster-native approach is to keep each journey as
its own focused script and combine them in a [load test scenario](https://loadster.com/manual/load-test-scenarios/)—add multiple bot groups,
each running a different Playwright script, and set the mix of bots across them. That keeps each script small and focused
while letting a single test cover many behaviors at once.

## Related Playwright documentation

* [Playwright Scripting](https://loadster.com/manual/playwright-scripts/) — the main guide to writing and playing Playwright scripts
* [@loadster/bot Reference](https://loadster.com/manual/playwright-scripts/bot/) — dataset variables and bot metadata inside your scripts
* [Dynamic Datasets](https://loadster.com/manual/dynamic-datasets/) — supply unique data to each bot

