Creating a JHipster Playwright Blueprint - Part 2

When creating a JHipster blueprint you usually start with an application and modify it to your needs. Afterwards you can start to modify existing templates or creating new ones if needed. As JHipster offers a lot of different options you might need to modify multiple applications in order to get the templates right.

In this post we will integrate Playwright into an existing JHipster application. You can find the example code on github.

Generate JHipster Application

After installing JHipster you can create an application with default options and Gradle as build system by using the jhipster cli:

jhipster jdl default-gradle

By default no end-2-end testing framework is configured. As the Playwright blueprint is supposed to replace Cypress as end-2-end testing framework it is a good idea to add it. To add Cypress to you application you need to modify .yo-rc.json and run the generator again.

{
  "generator-jhipster": {
    ...
    "skipClient": false,
    "testFrameworks": ["cypress"],
    "websocket": false,
    ...
  }
}

You can start the application via ./gradlew and run Cypress via npm run e2e.

Adding Playwright

Playwright supports different test runners. We choose Jest as JHipster already uses Jest for running unit and component tests.

npm install -D jest-playwright-preset playwright ts-jest eslint-plugin-jest-playwright

The configuration is stored in jest-playwright.config.js:

jest-playwright.config.js
module.exports = {
  preset: 'jest-playwright-preset',
  testMatch: ['<rootDir>/src/test/javascript/playwright/**/@(*.)@(spec.ts)'], 1
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  testEnvironmentOptions: {
    'jest-playwright': {
      browsers: ['firefox', 'chromium'], 2
      launchOptions: {
        headless: false, 3
        devtools: false,
      },
    },
  },
};
  1. Playwright specs are stored in src/test/javascript/playwrigth
  2. Playwright browser options
  3. Run not in headless mode

To have no typescript errors add following to types to compilerOptions in tsconfig.json:

"types": ["jest", "jest-playwright-preset", "expect-playwright"]

To start playwright add an additional command to package.json:

"jest:e2e": "jest --config jest-playwright.config.js"

First Playwright Test

The existing Cypress tests are located under src/test/javascript/cypress. Respectively all Playwright files will be located under src/test/javascript/playwright. As a starting point we look at the existing login-page.spec and try to create the same test with Playwright. To structure the test the Page Object Model Pattern is used. All page objects will be stored under src/test/javasript/playwright/models. The specification files will be located e.g. under src/test/playwright/account.

login.ts
export class LoginPage {
  page: any;

  constructor(page: any) {
    this.page = page;
  }
  async navigate() {
    await this.page.goto('http://localhost:8080/login');
  }

  async loginErrorVisible() { 1
    await page.waitForSelector('[data-cy="loginError"]');
  }

  async login(username: string, password: string, awaitNavigation: boolean = true) { 2
    await this.page.fill('[data-cy="username"]', username); 3
    await this.page.fill('[data-cy="password"]', password);
    await this.page.click('[data-cy="submit"]');
    if (awaitNavigation) {
      await this.page.waitForNavigation();
    }
  }
}
  1. Function to check if login failed
  2. Function to login
  3. Reuse data-cy attributes to select elements
login-page.spec.ts
import { LoginPage } from '../models/login';

let loginPage: LoginPage;

beforeAll(async () => { 1
  loginPage = new LoginPage(page);
  await loginPage.navigate();
});

test('requires username', async () => {
  await loginPage.login('', 'a-password', false);
  await loginPage.loginErrorVisible();
});

test('requires password', async () => {
  await loginPage.login('a-login', '', false);
  await loginPage.loginErrorVisible();
});

test('errors when password is incorrect', async () => {
  await loginPage.login('admin', 'bad-password', false);
  await loginPage.loginErrorVisible();
});

test('go to login page when successfully logs in', async () => {
  await loginPage.login('admin', 'admin'); 2
  await page.waitForSelector('[data-cy="adminMenu"]');
});
  1. Open login page before all tests
  2. Login and wait for admin menu to be visible

Mocking Responses

To avoid really e.g. really changing the password within a test some requests are mocked and replaced with a fixed responses. This also helps to test failure handling.

password-page.spec.ts
test('should be able to update password', async () => {
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
  await page.route('**/api/account/change-password', route => 1
    route.fulfill({
      status: 200,
    })
  );

  await changePasswordPage.updatePassword('correct-current-password', 'jhipster', 'jhipster'); 2
  await page.waitForSelector('.alert-success');

  await page.unroute('**/api/account/change-password');  3
});
  1. Intercept requests to /api/account/change-password and return a sucessfull response if intercepted
  2. Execute password change. See change-password.ts page object for details.
  3. Remote the route interception otherwise it will stay enabled

With this approach and techniques each Cypress test can be replaced with a corresponding Playwright test. In the next part we will have a look at how to write Playwright tests for generated entities.

The source code for this post is available on GitHub.