Skip to content

E2E Testing Optional

playwrighttestcontainersdocker

Supporting Multiple dummy accounts Best Practice

At startup, auth.setup.ts is executed first to handle the login process for all accounts used in parallel. The authentication data for each account is saved in the .auth directory via storageState. During actual tests, you can specify the account name using test.use, skipping the login process and speeding up execution.

What if you want to add a new user?

  • Add the user information to dummyUsers.ts
  • Add a setup to create a session for the new user in setup/auth.ts
  • Run useUser and registerUserToDB before executing the tests
ts
import type { User } from "next-auth";

export const user1: User = <const>{
  id: "id1",
  name: "user1",
  email: "user1@a.com",
  image:
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAMElEQVR42u3OMQEAAAQAMDrpp4Zuyojh2RIsa7bjUQoICAgICAgICAgICAgICHwHDhv0ROEuXMGUAAAAAElFTkSuQmCC",
  role: "user",
};

export const admin1: User = <const>{
  id: "id2",
  name: "admin1",
  email: "admin1@a.com",
  image:
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8qS5UDwAExgGj/3sspQAAAABJRU5ErkJggg==",
  role: "admin",
};
ts
import { test as setup } from "@playwright/test";
import { admin1, user1 } from "../dummyUsers";
import { createAuthState } from "../helpers/users";

setup("Create user1 auth", async ({ context }) => {
  await createAuthState(context, user1);
});

setup("Create admin1 auth", async ({ context }) => {
  await createAuthState(context, admin1);
});
ts
import { registerUserToDB, useUser } from "../helpers/users";

test.describe("user1", () => {
  useUser(test, user1);

  test.beforeEach(async ({ page }) => {
    await registerUserToDB(user1);
  });
});

Mocking and working around next-auth's JWT strategy

JWE is highly secure, making it very difficult to use dummy accounts in tests. To address this, the main code is configured to read a test-specific environment variable(NEXTAUTH_TEST_MODE). When this variable is present, an alternative encoding/decoding method is provided to bypass this limitation.

ts
import type { NextAuthConfig } from "next-auth";

export const configForTest = {
  jwt: {
    encode: async ({ token }) => {
      return btoa(JSON.stringify(token));
    },
    decode: async ({ token }) => {
      if (!token) {
        return {};
      }

      return JSON.parse(atob(token));
    },
  },
} satisfies Omit<NextAuthConfig, "providers">;

Introducing Page Object Models Best Practice

Page Object Models are that large test suites can be structured to optimize ease of authoring and maintenance. They are one such approach provided by Playwright to structure your test suite.

Each page is modeled and inherits from the Base class. One key feature is that all Locators used in tests are defined as members of the class upon instantiation, and these members are referenced thereafter. This approach allows for writing robust code that can handle changes to elements effectively.

ts
import type { Locator, Page } from "@playwright/test";
import type { User } from "next-auth";
import { expect } from "../fixtures";

export class Base {
  page: Page;
  signInLocator: Locator;
  signOutLocator: Locator;
  profileImageLocator: Locator;

  constructor(page: Page) {
    this.page = page;
    this.signInLocator = this.page.getByRole("button", { name: "Sign in" });
    this.signOutLocator = this.page.getByRole("button", { name: "Sign out" });
    this.profileImageLocator = this.page.getByRole("img", { name: "profile" });
  }

  async init() {
    await this.page.goto(process.env.NEXT_PUBLIC_SITE_URL, {
      waitUntil: "networkidle",
    });
  }

  async expectHeaderUI(state: "signIn" | "signOut", user: User) {
    if (state === "signIn") {
      await expect(this.signOutLocator).toBeVisible();
      expect(await this.profileImageLocator.getAttribute("src")).toBe(
        user.image,
      );
    }

    if (state === "signOut") {
      await expect(this.signInLocator).toBeVisible();
    }
  }
}
ts
import type { Locator, Page } from "@playwright/test";
import { expect } from "../fixtures";
import { Base } from "./Base";

export class MePage extends Base {
  nameInputLocator: Locator;
  submitButtonLocator: Locator;

  constructor(page: Page) {
    super(page);

    this.nameInputLocator = this.page.locator('input[name="name"]');
    this.submitButtonLocator = this.page.locator('button[type="submit"]');
  }

  async goTo() {
    await this.init();

    return await this.page.goto("/me");
  }

  async changeName(name: string) {
    await this.nameInputLocator.fill(name);
    await this.submitButtonLocator.click();
  }

  async expectUI() {
    await expect(this.nameInputLocator).toBeVisible();
    await expect(this.submitButtonLocator).toBeVisible();
  }
}