Vue Storefront is now Alokai! Learn More
Writing tests

Adding Playwright Tests to Your Module

To ensure the quality and reliability of your module, you can include integration tests using the Playwright framework. This guide will show you how to incorporate Playwright tests into your module, following the recommended structure and best practices.

Overview of Playwright Tests

Playwright tests are typically designed to verify how a specific page, view, or component on the storefront behaves, especially after a module has been installed or modified. In such tests, various actions or interactions occur within this new part of the view. A page object is used to isolate what appears on the page and the underlying business logic. The page object serves as a layer of abstraction, helping to separate test interactions from the implementation details of the page elements.

This abstraction allows developers to modify the page object when UI changes occur without changing the test logic itself, as long as the overall functionality remains the same. This ensures that tests focus on verifying business functionalities rather than UI implementation details.

Additionally, while actions happen on the page, they may also trigger server requests to the middleware. In integration tests, we mock these requests to ensure we're only validating the frontend part of the application, rather than the full middleware functionality. This approach simplifies testing and allows us to focus on the specific module's impact on the storefront.

Directory Structure

Your module should include a dedicated playwright directory, similar to the existing next, nuxt, and middleware directories. All Playwright-related files and tests should reside within this directory.

Within the playwright directory, it’s recommended (though not required) to organize your files into the following subdirectories:

module/
└── playwright/
    ├── mocks/
    ├── pageObjects/
    ├── tests/
    ├── config.ts
    ├── test.ts
    └── tsconfig.json
  • tests: Contains your test files. Test files should end with the .test.ts extension.
  • pageObjects: Contains your page object models. While not mandatory, it’s a good practice to name these files with the .page.ts extension.
  • mocks: Contains mock factories and endpoints for the middleware. This helps in creating mocked responses for your tests.
  • config.ts: This file contains the configuration for Playwright and defines where the tests are located and how the project is named. It is essential to ensure Playwright knows how to execute your tests. Details about creating and configuring this file are provided below.
  • tsconfig.json: This TypeScript configuration file is critical for defining module aliases, such as @core, which helps Playwright locate the correct files in the Playwright application.
  • test.ts: This file is used to set up the base fixtures for your tests. It extends the core fixtures provided by the core package and adds any specific ones required for your module. This base fixture setup is fundamental, as it forms the foundation for all other tests, ensuring consistency and access to necessary context and utilities.

Configuration File (config.ts)

Inside the playwright directory, you must create a config.ts file. This file should export a configuration object using the defineConfig function from the core package. This configuration tells Playwright where to find your tests and how to name your project.

Example config.ts

import { defineConfig } from '@core';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const dirname = fileURLToPath(new URL('.', import.meta.url));

export default defineConfig({
  name: 'your-module',
  testDir: resolve(dirname, 'tests'),
});

Explanation

  • defineConfig: A function from the core package that helps define Playwright configuration.
  • name: Specifies the name of your test project. This is how it will appear in Playwright reports.
  • testDir: Defines the directory where your tests are located. It resolves the path to the tests folder within your playwright directory.

This configuration ensures that Playwright knows where to find your tests and under what name to run them.

TypeScript (tsconfig.json)

Your module should also include a tsconfig.json file within the Playwright directory. This file contains the TypeScript configuration specific to the Playwright environment within your module.

The most crucial part of this configuration is the use of aliases, particularly the @core alias. This alias ensures that Playwright can resolve imports correctly by pointing to the core files in the Playwright application. These alias paths are dynamically adjusted by the Storefront CLI during module installation to ensure compatibility.

Example tsconfig.json

{
  "compilerOptions": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "module": "preserve",
    "noEmit": true,
    "lib": ["es2022", "dom", "dom.iterable"],
    "paths": {
      "@core": ["../../setup/core/index.ts"],
      "@core/*": ["../../setup/core/*.ts"]
    }
  }
}

Explanation

@core alias: The @core alias allows Playwright to resolve the appropriate core files required for your module. During the installation of the module, the Storefront CLI will automatically adjust these paths to ensure that they point to the correct files in the Playwright application environment.

Base Test Fixture

Create a test.ts file that sets up your test fixtures. This file extends the base fixtures provided by the core package and adds any module-specific fixtures you may need.

Example Fixture

import { baseFixtureFactory } from '@core';
import { YourPage } from './pageObjects/your.page';
import { routerFactory } from './mocks/server';

const base = baseFixtureFactory(routerFactory);

type TestFixtures = {
  yourPage: YourPage;
};

export const test = base.extend<TestFixtures>({
  yourPage: async ({ dataFactory, db, framework, frontendUrl, page, utils }, use) => {
    const yourPage = new YourPage({ dataFactory, db, framework, frontendUrl, page, utils });
    await use(await yourPage.prepare());
  },
});

Explanation

  • baseFixtureFactory: Imports the base fixtures from the core package, allowing you to build upon them.
  • routerFactory: A function that sets up your mocked endpoints (we will discuss this later).
  • TestFixtures: Defines the custom fixtures that will be available in your tests.
  • yourPage: A fixture that initializes the "YourPage" page object.

By extending the base fixtures, you ensure that your tests have access to all necessary context and utilities.

Installation process

During the installation process, the CLI will handle the Playwright directory of your module by copying it into the Playwright application, specifically into the sf-modules directory under a folder named after your module. For example: playwright/sf-modules/your-module-name. This ensures that the Playwright tests and related files are properly integrated into the testing environment of the storefront application.

In addition to the standard installation process, you can create a custom installation script (install.js) that defines specific behaviors for the Playwright integration. The install.js script can be used to modify configurations, add new commands to package.json, or any other custom setup required for your Playwright tests.

Example Custom Installation Script (install.js)

You can use tools provided by the module-kit package to define a custom installation schema for Playwright. This allows you to add custom logic such as modifying the package.json file to include new scripts for running Playwright tests.

import {
  defineSchema,
  defineSchemas,
} from "@vsf-enterprise/module-kit";

// ... other schemas for nuxt, next and middleware

const playwrightSchema = defineSchema((context) => {
  const { paths } = context.playwright;

  return {
    name: "your-module-playwright",
    modifyFile: [
      {
        type: "json",
        inputPath: paths.packageJson,
        visitor: (packageJson) => {
          packageJson.scripts['test:integration'] = 'PW_IS_B2B=true playwright test';
          packageJson.scripts['test:integration:dev'] = 'PW_IS_B2B=true playwright test --ui';
        },
      },
    ],
  };
});

export default defineSchemas({
  playwright: playwrightSchema,
  middleware: middlewareSchema,
  nextjs: nextSchema,
  nuxt: nuxtSchema,
});

Explanation

  • defineSchema: This function from the module-kit package helps define custom behaviors for the installation process of your module.
  • modifyFile: Allows you to modify specific files during installation. In the above example, it modifies package.json to add new scripts related to running Playwright integration tests.
  • visitor function: Updates the scripts section of package.json to add commands for running integration tests with specific environment variables (PW_IS_B2B=true).

This kind of customization ensures that the environment is properly set up for your Playwright tests, and allows you to automate configuration changes that would otherwise need to be done manually.

For more details on the installation process and custom installation scripts, refer to the dedicated installation section that explains how the CLI interacts with different parts of the module, including Playwright, during the installation process.

Writing Tests

Place your test files inside the tests/ directory. Test files should have the .test.ts extension.

Example Test File

import { test } from '../test';

test.describe('YourPage', () => {
  test('should render form', async ({ yourPage }) => {
    await yourPage.goto('/register');
    await yourPage.hasProperLayout();
  });

  test('should register user', async ({ yourPage }) => {
    await yourPage.goto('/register');

    await yourPage.fillRegisterForm();
    await yourPage.submitForm();

    await yourPage.hasModalOpened();

    await yourPage.clickModalButton();

    await yourPage.hasURLChangedTo('/category');
  });
});

This example test suite verifies the functionality of the B2B registration page.

Explanation

Page Objects

Page objects encapsulate the interactions with specific pages or components in your application. They promote reusability and cleaner test code. Read more about them in Playwright docs.

Page objects encapsulate the interactions with specific pages or components in your application. They promote reusability and cleaner test code, providing a structured way to interact with page elements and maintain separation between the UI and the test logic itself.

We strongly recommend extending the BasePage class from the core application (@core). The BasePage provides structures and utilities that are essential for developing Playwright tests in the Storefront environment.

Why Use BasePage?

The BasePage class includes several critical features that simplify the process of writing and maintaining tests:

  • Access to Essential Tools: In its constructor, BasePage receives a set of useful tools, such as DataFactory, DB, framework, frontend URL, utils, and others. These tools can be used directly in the methods written in your PageObject, allowing you to:
    • Interact with the page elements depending on the framework in use (Next.js or Nuxt) and encapsulate different implementations of the same action or assertion.
    • Access DataFactory to generate consistent test data, making it easier to create reusable test scenarios.
    • Utilize PageUtils for common operations, such as waiting for a specific action on the page to complete.
    • Use DB for accessing a mock database to manipulate or verify data.
    • Framework-Specific Behavior: The BasePage class provides the ability to implement different behavior for different frontend frameworks. This abstraction ensures that tests remain consistent regardless of the underlying framework, while the details of each framework are managed internally by the page object.
  • Abstract prepare Method: The BasePage class includes an abstract prepare method, which must be implemented by any subclass. The prepare method is responsible for setting up the necessary state before navigating to a page, such as setting up mock responses in the middleware. This guarantees that all PageObjects adhere to a consistent setup pattern, making tests more reliable and reducing duplicated setup code.

By using BasePage, developers can take advantage of these pre-built features, enabling them to focus on writing meaningful tests rather than managing setup or framework-specific differences.

Example Page Object (your.page.ts)

import { expect } from '@playwright/test';
import { BasePage } from '@core';

export class YourPage extends BasePage {
  private email = this.page.getByTestId('email-input');
  private firstName = this.page.getByTestId('first-name-input');
  private lastName = this.page.getByTestId('last-name-input');
  private message = this.page.getByTestId('textarea');
  private modal = this.page.getByTestId('modal');
  private submitButton = this.page.getByTestId('submit-button');

  private get modalButton() {
    return this.modal.getByTestId('button').first();
  }

  async clickModalButton() {
    await this.modalButton.click();
  }

  async fillRegisterForm() {
    await this.firstName.fill('John');
    await this.lastName.fill('Doe');
    await this.email.fill('john@doe.com');
    await this.message.fill('foo bar');
  }

  async hasModalOpened() {
    await expect(this.modal).toBeVisible();
    await expect(this.modalButton).toBeVisible();
  }

  async hasProperLayout() {
    await expect(this.page.locator('footer')).toBeVisible();
    await expect(this.email).toBeVisible();
    await expect(this.firstName).toBeVisible();
    await expect(this.lastName).toBeVisible();
    await expect(this.message).toBeVisible();
    await expect(this.submitButton).toBeVisible();
  }

  override async prepare(): Promise<this> {
    await this.dataFactory.unified.setDefaultSearchProductsResponse();
    await this.dataFactory.unified.setEmptyCart();
    await this.dataFactory.unified.setGuestCustomer();
    return this;
  }

  async submitForm() {
    await this.submitButton.click();
  }
}

This page object is created for the B2B registration page example purposes.

Explanation

  • Selectors: Uses getByTestId to locate elements, ensuring stable and maintainable selectors.
  • Methods: Encapsulates interactions like filling out forms, clicking buttons, and verifying UI elements.
  • prepare Method: Sets up the necessary data and state before tests run, such as mocking API responses.

Using page objects simplifies your tests by abstracting page interactions.

Mocking Endpoints

To test your module effectively, you might need to mock API endpoints. Place your mock server and endpoints inside the mocks directory.

Example Router (server.ts)

import type { MockFactoryContext } from './types';
import { createRouter, useBase, type Router } from 'h3';
import { pipe } from '@core';
import { registerCustomer } from './endpoints';

export function routerFactory(mainRouter: Router) {
  const serverFactory = [(ctx: MockFactoryContext) => registerCustomer(ctx)];

  const yourRouter = pipe(serverFactory, createRouter());
  mainRouter.use('/commerce/your-module-name/**', useBase('/commerce/your-module-name', yourRouter.handler));

  return mainRouter;
}

Explanation

  • routerFactory: Creates a router for your mocked endpoints.
  • serverFactory: An array of functions that define the behavior of your mock server.
  • mainRouter.use: Integrates your mock router into the main router, ensuring that requests to specific endpoints are intercepted.

Example Endpoint (endpoints/registerCustomer.ts)

import type { MockFactoryContext } from '../types';
import { defineEventHandler } from 'h3';

export default function ({ router }: MockFactoryContext) {
  return router.post(
    `/registerCustomer`,
    defineEventHandler(async () => {
      return {};
    }),
  );
}

Explanation

  • defineEventHandler: Defines an event handler for the mocked endpoint.
  • Mock Response: Returns an empty object {} as the response to the /registerCustomer POST request.

By mocking endpoints, you can simulate backend interactions and test how your module handles various responses.

Using baseFixture

To streamline the development of integration tests, the baseFixture provided by the core package offers a set of predefined fixtures that can be leveraged for common tasks such as database setup, API mocking, and frontend interactions. This section describes the available fixtures within baseFixture and how to use them effectively. By leveraging baseFixture, you can create powerful, reusable test setups that reduce boilerplate and improve test consistency.

Available Fixtures

The baseFixture includes several key fixtures that help with preparing and managing test environments. These fixtures are available in every test, although some are configured at the worker level and shared across multiple tests:

dataFactory

This fixture provides access to the dataFactory, which is responsible for generating and managing test data, including mock product lists, cart data, and customer information. It allows you to configure data used by your tests to ensure consistent and repeatable results.

db

Provides access to a test database instance, enabling you to manipulate database records, reset states, and perform direct queries before, during, or after test execution. This fixture is configured automatically and is always available for use.

utils

The utils fixture offers utility functions to assist with various common tasks, such as interacting with page elements or performing computations. This fixture depends on the current framework (e.g., Next.js or Nuxt) and provides a unified interface for interacting with the application.

debug

This fixture allows you to check if debug mode is enabled or disabled during test execution. When debug is set to true, you can add additional logging and debugging information become available, which can assist in identifying issues during test runs.

framework

Specifies the framework being used for the frontend (e.g., Next.js or Nuxt). This allows you to write framework-specific logic within your tests and ensures compatibility with both frontend environments supported by Storefront.

frontendUrl

Provides the URL of the frontend application under test. This fixture is particularly useful for navigating to specific pages or making direct API requests during tests. The URL is dynamically determined based on the current configuration and framework.

isB2B

Indicates whether the current test is running in a B2B (Business-to-Business) environment. This fixture can help conditionally execute test logic that is specific to B2B scenarios.

middleware

Provides an instance of the middleware server for the application under test. This middleware instance can be used to intercept and mock API requests, allowing for controlled testing of different backend behaviors. The middleware is configured based on the routers provided to baseFixtureFactory.

Using routerFactory with baseFixtureFactory

The routerFactory parameter used in baseFixtureFactory allows you to define custom middleware routers that will be applied to the middleware server during test execution. These routers are useful for intercepting requests and providing mocked responses, making it easier to test various scenarios without needing access to a live backend.

How routerFactory Works?

The routerFactory is a function or an array of functions that takes an existing router and extends it by adding custom routes or handlers. It allows you to modify or enhance the middleware used in the test environment.

The routerFactory functions are combined with the core router, which already includes mocks for the Unified API, providing a comprehensive mocked environment.

Best Practices

  • Reusability: Define reusable fixtures for common pages or data, and use them across multiple test files to reduce redundancy.
  • Debugging: Use the debug fixture when investigating test failures to get more detailed output and insights.
  • Mocking Middleware: Utilize the middleware fixture to intercept network requests and simulate backend responses, allowing you to test edge cases and failure scenarios without needing a live backend.
  • Consistent Naming: Use consistent file extensions (.test.ts, .page.ts) to make it easy to identify test and page object files.
  • Modular Code: Keep your tests, page objects, and mocks modular to promote reusability and maintainability.
  • Mock Responsibly: Only mock what is necessary for your tests to run, to keep them as close to real-world scenarios as possible.

Summary

Adding Playwright tests to your module enhances its reliability and ensures it integrates well with the Storefront platform. By following the recommended directory structure and configuration, you can seamlessly include your tests and benefit from the existing testing infrastructure.

For more information on writing Playwright tests and using the core fixtures, refer to the Playwright documentation and the core package documentation.