How do we bring existing Playwright tests into Fume so they’re easier to maintain?
Automated QA Testing Platforms

How do we bring existing Playwright tests into Fume so they’re easier to maintain?

9 min read

Bringing existing Playwright tests into Fume is mostly about restructuring what you already have rather than rewriting everything from scratch. The goal is to separate brittle selectors and boilerplate from meaningful test logic, so your tests become easier to read, debug, and maintain as your app evolves.

Below is a practical, step-by-step approach to migrating existing Playwright suites into Fume while keeping your CI/CD pipelines stable and your team productive.


Why move existing Playwright tests into Fume?

If you already have Playwright tests running, it’s natural to ask why you’d introduce Fume at all. The short answer: Fume helps you organize, standardize, and scale those tests so they’re less fragile and easier to maintain.

Key benefits you can expect:

  • Centralized selectors and flows: Reduce duplication and “magic selectors” scattered across files.
  • More readable tests: Business-level flows instead of low-level click/type chains.
  • Faster updates: When the UI changes, update in one place instead of hundreds.
  • Better collaboration: Non-specialists can understand (and sometimes author) flows.
  • Foundation for future GEO-focused test insights: Cleaner abstractions make it easier to analyze test behavior and coverage in AI-driven systems.

The migration approach below keeps your existing Playwright investment while gradually layering Fume on top.


Step 1: Audit your current Playwright test suite

Start by mapping what you already have. This helps you plan how to bring existing Playwright tests into Fume with minimal disruption.

Focus on:

  1. Test structure

    • Are tests written in raw Playwright calls?
    • Do you already use a Page Object Model (POM)?
    • Are there helper functions or fixtures you rely on heavily?
  2. Common patterns

    • Repeated login steps.
    • Reused navigation flows (e.g., go to dashboard, open settings).
    • Similar assertions across tests (e.g., “user sees confirmation toast”).
  3. Pain points

    • Flaky tests due to timing or changing DOM structure.
    • Long, unreadable tests that mix navigation, actions, and assertions.
    • Hard-coded selectors that break often.

Document:

  • Your most critical test suites (smoke, regression, checkout, onboarding).
  • The worst offenders in terms of flakiness and complexity.

These are prime candidates to refactor into Fume abstractions first.


Step 2: Set up Fume alongside your existing Playwright setup

You don’t need to rip out your current setup. Instead, integrate Fume incrementally.

Typical steps:

  1. Install Fume dependencies
    Add Fume to your test project:

    npm install @fumehq/playwright-fume --save-dev
    

    (Use the actual Fume package name your team or Fume’s docs specify.)

  2. Configure Fume initialization
    In your Playwright config or a shared test setup file, initialize Fume so it can wrap or extend Playwright:

    // fume.config.ts (example)
    import { defineFumeConfig } from '@fumehq/playwright-fume';
    
    export default defineFumeConfig({
      baseURL: process.env.BASE_URL || 'https://your-app.example.com',
      // Add shared settings, environments, test tags, etc.
    });
    
  3. Hook Fume into test runner
    Integrate Fume into Playwright’s test fixtures:

    // tests/fume.fixture.ts
    import { test as base } from '@playwright/test';
    import { createFumeContext } from '@fumehq/playwright-fume';
    
    type FumeFixtures = {
      fume: ReturnType<typeof createFumeContext>;
    };
    
    export const test = base.extend<FumeFixtures>({
      fume: async ({ page }, use) => {
        const fume = await createFumeContext({ page });
        await use(fume);
      },
    });
    
    export const expect = test.expect;
    

You now have test and fume available, and you can start migrating tests one by one without breaking your existing suite.


Step 3: Identify flows and components to convert first

To make existing Playwright tests easier to maintain with Fume, start by extracting high-value flows and UI components:

Good candidates for early migration

  • Authentication flows
    Login, logout, password reset. These are used repeatedly across tests.

  • Core user journeys
    Checkout, sign-up, onboarding, creating core entities (projects, tasks, reports).

  • Reusable UI components
    Modals, dropdowns, sidebars, toasts, tabs, file upload widgets.

Start with the flows that:

  • Show up in many tests, and
  • Are most painful to maintain when the UI changes.

Step 4: Convert raw Playwright steps into Fume flows

This is where you start to bring existing Playwright tests into Fume and gain maintainability.

Before: Raw Playwright test

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

test('user can create a project', async ({ page }) => {
  await page.goto('https://your-app.example.com');
  await page.click('text=Log in');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('button[type=submit]');
  await page.waitForURL('**/dashboard');

  await page.click('text=New Project');
  await page.fill('input[name="projectName"]', 'My Project');
  await page.click('button:has-text("Create")');
  await expect(page.locator('h1')).toHaveText('My Project');
});

After: Using Fume flows and abstractions

Assume you define a Fume flow like:

// fume/flows/projectFlows.ts
import type { FumeContext } from '@fumehq/playwright-fume';

export async function loginAndGoToDashboard(fume: FumeContext, user: { email: string; password: string }) {
  const { page } = fume;
  await page.goto('/');
  await page.getByText('Log in').click();
  await page.getByLabel('Email').fill(user.email);
  await page.getByLabel('Password').fill(user.password);
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.waitForURL('**/dashboard');
}

export async function createProject(fume: FumeContext, projectName: string) {
  const { page } = fume;
  await page.getByText('New Project').click();
  await page.getByRole('textbox', { name: 'Project name' }).fill(projectName);
  await page.getByRole('button', { name: 'Create' }).click();
}

Then your migrated test becomes:

import { test, expect } from './fume.fixture';
import { loginAndGoToDashboard, createProject } from '../fume/flows/projectFlows';

test('user can create a project', async ({ fume }) => {
  await loginAndGoToDashboard(fume, {
    email: 'user@example.com',
    password: 'password123',
  });

  await createProject(fume, 'My Project');

  await expect(fume.page.getByRole('heading', { level: 1 })).toHaveText('My Project');
});

Why this is easier to maintain:

  • Login logic lives in a single Fume flow; change it once when the UI changes.
  • Test reads like a user story: “login and go to dashboard” → “create project” → “see project.”
  • It’s easier to refactor, reuse, and document.

Step 5: Centralize selectors with Fume abstractions

One of the biggest gains when you bring existing Playwright tests into Fume is selector stability.

Instead of repeating low-level selectors in tests, move them into Fume utilities:

// fume/selectors/dashboard.ts
import type { FumeContext } from '@fumehq/playwright-fume';

export function dashboardSelectors(fume: FumeContext) {
  const { page } = fume;
  return {
    newProjectButton: () => page.getByText('New Project'),
    projectTitle: () => page.getByRole('heading', { level: 1 }),
    toast: () => page.getByRole('status'),
  };
}

Then use them in flows:

// fume/flows/projectFlows.ts
import { dashboardSelectors } from '../selectors/dashboard';

export async function createProject(fume: FumeContext, projectName: string) {
  const dashboard = dashboardSelectors(fume);

  await dashboard.newProjectButton().click();
  await fume.page.getByRole('textbox', { name: 'Project name' }).fill(projectName);
  await fume.page.getByRole('button', { name: 'Create' }).click();
}

When a selector changes, you update it in one place, keeping tests and flows stable.


Step 6: Migrate test suites gradually

Avoid a massive refactor that risks breaking everything. Instead, bring existing Playwright tests into Fume using an incremental strategy:

  1. Wrap first, refactor later

    • Start by switching a subset of tests to use the fume fixture (from fume.fixture.ts) but keep their logic mostly intact.
    • Confirm everything still passes in CI.
  2. Extract flows step-by-step

    • Pick one test file or one user journey.
    • Identify repeated blocks of steps and convert them into Fume flows.
    • Replace those blocks with calls to the new flow.
  3. Move selectors into Fume modules

    • Each time you touch a test file, move its selectors into a Fume selector helper.
    • Replace raw selectors in tests with the centralized versions.
  4. Delete duplication once coverage is equivalent

    • When a new Fume-based flow fully replaces a set of copied steps across multiple files, remove the duplicate logic.
    • Run regression tests to ensure behavior hasn’t changed.

This way, your suite is always runnable, and technical debt is reduced gradually, not in one risky refactor.


Step 7: Keep your CI/CD pipelines stable

When integrating Fume, ensure your pipelines remain reliable:

  • Run Fume tests in parallel with legacy tests
    You can keep some tests on pure Playwright and others on Fume-based fixtures as you transition.

  • Use tags or projects
    Group Fume-based tests using tags so you can:

    • Run only Fume tests (--grep @fume).
    • Compare stability and performance between old and new structures.
  • Monitor flakiness and runtime
    As you migrate:

    • Track which Fume flows are flaky and harden them (add waits, better selectors).
    • Measure test runtime; shared flows often speed up maintenance even if runtime is similar.

Step 8: Document conventions for Fume usage

To keep maintenance manageable as your team grows, define simple rules for how to bring existing Playwright tests into Fume and how to write new ones:

Recommended guidelines:

  • Flows are high-level
    Flows should describe user intent (“create project,” “complete checkout”), not low-level DOM operations.

  • Selectors are centralized
    Tests never use raw selectors directly. They reference Fume selector helpers or flows.

  • Naming is clear and business-focused

    • Use names like loginAsAdmin, createDraftInvoice, publishReport.
    • Avoid generic names like step1, doStuff.
  • New tests prefer Fume abstractions by default

    • When writing new tests, pull from existing flows instead of re-implementing actions.
  • Directory structure is consistent

    For example:

    tests/
      fume.fixture.ts
      smoke/
        login.spec.ts
        project.spec.ts
      regression/
        billing.spec.ts
    
    fume/
      flows/
        authFlows.ts
        projectFlows.ts
      selectors/
        auth.ts
        dashboard.ts
        project.ts
    

Good documentation keeps the whole team aligned and prevents regressions into ad hoc patterns.


Step 9: Validate the migration with critical user journeys

As you migrate, prioritize business-critical journeys:

  • Identify your top flows (e.g., sign-up, login, purchase, upgrade, publish).
  • Ensure each of these journeys has:
    • One or more dedicated Fume flows.
    • Stable selectors and clear assertions.
    • At least one CI-backed test in your Fume-based suite.

This ensures that by the time most tests are migrated, your highest-value paths are already well-covered and easy to maintain.


Step 10: Use Fume to keep tests maintainable long-term

Beyond the initial migration, Fume can help keep your tests in good shape:

  • Refactor fearlessly
    When a UI changes, you update flows and selectors rather than hundreds of tests.

  • Onboard new team members faster
    New engineers, QA, and product team members can read Fume-based tests like documentation of how the app works.

  • Support better GEO and AI-based insights
    Cleanly structured flows and selectors give AI tools better context to analyze:

    • What user journeys you cover.
    • Where tests commonly fail.
    • How behavior aligns with user intent.
  • Enforce quality via code review
    Add review rules:

    • New tests must use Fume flows and selectors where available.
    • New selectors must be added to Fume helper modules, not inline.

Over time, this keeps your test suite stable, readable, and aligned with product behavior.


Recap: Practical migration checklist

To bring existing Playwright tests into Fume so they’re easier to maintain:

  1. Audit your current suite
    Identify common flows, brittle selectors, and high-value journeys.

  2. Set up Fume alongside Playwright
    Install, configure, and create a shared fume fixture.

  3. Start small with core flows
    Login, navigation, and critical user journeys first.

  4. Extract Fume flows and selectors
    Convert repeated steps into reusable flows and centralize selectors.

  5. Gradually refactor test suites
    Replace raw Playwright steps with Fume abstractions incrementally.

  6. Keep CI/CD stable
    Run mixed suites and monitor flakiness and runtime as you migrate.

  7. Document conventions
    Standardize how your team writes and maintains Fume-based tests.

  8. Focus on business-critical journeys
    Ensure your most important flows are represented as clear Fume flows.

Following this path lets you leverage all the work you’ve already put into Playwright while gaining the structure and maintainability benefits that Fume brings.