Browser tests

Browser tests

Browser tests are executed inside real browsers like Chrome, Firefox, or Safari. We make use of Playwright (a browser automation tool) for interacting with webpages programmatically.

Playwright is both a testing framework and a library that exposes JavaScript APIs to interact with the browser. We do not use the Playwright testing framework because we are already using Japa, and using multiple testing frameworks inside a single project will only lead to confusion and config bloat.

Instead, we will use the Browser Client plugin from Japa, which integrates nicely with Playwright and offers a great testing experience.

Setup

The first step is to install the following packages from the npm packages registry.

npm i -D playwright @japa/browser-client

Registering browser suite

Let's start by creating a new test suite for browser tests inside the adonisrc.ts file. The test files for the browser suite will be stored inside the tests/browser directory.

{
tests: {
suites: [
{
files: [
'tests/browser/**/*.spec(.ts|.js)'
],
name: 'browser',
timeout: 300000
}
]
}
}

Configuring the plugin

Before you can start writing tests, you must register the browserClient plugin within the tests/bootstrap.ts file.

import { browserClient } from '@japa/browser-client'
export const plugins: Config['plugins'] = [
assert(),
apiClient(),
browserClient({
runInSuites: ['browser']
}),
pluginAdonisJS(app)
]

Basic example

Let's create an example test that will open the home page of your AdonisJS application and verify the contents of the page. The visit helper opens a new page and visits a URL. The return value is the page object.

See also: List of assertions methods

node ace make:test pages/home --suite=browser
# DONE: create tests/browser/pages/home.spec.ts
tests/browser/pages/home.spec.ts
import { test } from '@japa/runner'
test.group('Home page', () => {
test('see welcome message', async ({ visit }) => {
const page = await visit('/')
await page.assertTextContains('body', 'It works!')
})
})

Finally, let's run the above test using the test command. You may use the --watch flag to create a file watcher and re-run tests on every file change.

node ace test browser

Reading/writing cookies

When testing inside a real browser, the cookies are persisted throughout the lifecycle of a browser context.

Japa creates a fresh browser context for each test. Therefore, the cookies from one test will not leak onto other tests. However, multiple page visits inside a single test will share the cookies because they use the same browserContext.

test.group('Home page', () => {
test('see welcome message', async ({ visit, browserContext }) => {
await browserContext.setCookie('username', 'virk')
// The "username" cookie will be sent during the request
const homePage = await visit('/')
// The "username" cookie will also be sent during this request
const aboutPage = await visit('/about')
})
})

Similarly, the cookies set by the server can be accessed using the browserContext.getCookie method.

import router from '@adonisjs/core/services/router'
router.get('/', async ({ response }) => {
response.cookie('cartTotal', '100')
return 'It works!'
})
test.group('Home page', () => {
test('see welcome message', async ({ visit, browserContext }) => {
const page = await visit('/')
console.log(await browserContext.getCookie('cartTotal'))
})
})

You may use the following methods to read/write encrypted and plain cookies.

// Write
await browserContext.setEncryptedCookie('username', 'virk')
await browserContext.setPlainCookie('username', 'virk')
// Read
await browserContext.getEncryptedCookie('cartTotal')
await browserContext.getPlainCookie('cartTotal')

Populating session store

If you are using the @adonisjs/session package to read/write session data in your application, you may also want to use the sessionBrowserClient plugin to populate the session store when writing tests.

Setup

The first step is registering the plugin inside the tests/bootstrap.ts file.

import { sessionBrowserClient } from '@adonisjs/session/plugins/browser_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
sessionBrowserClient(app)
]

And then, update the .env.test file (create one if it is missing) and set the SESSON_DRIVER to memory.

.env.test
SESSION_DRIVER=memory

Writing session data

You may use the browserContext.setSession method to define the session data for the current browser context.

All page visits using the same browser context will have access to the same session data. However, the session data will be removed when the browser or the context is closed.

test('checkout with cart items', async ({ browserContext, visit }) => {
await browserContext.setSession({
cartItems: [
{
id: 1,
name: 'South Indian Filter Press Coffee'
},
{
id: 2,
name: 'Cold Brew Bags',
}
]
})
const page = await visit('/checkout')
})

Like the setSession method, you may use the browser.setFlashMessages method to define flash messages.

/**
* Define flash messages
*/
await browserContext.setFlashMessages({
success: 'Post created successfully',
})
const page = await visit('/posts/1')
/**
* Assert the post page shows the flash message
* inside ".alert-success" div.
*/
await page.assertExists(page.locator(
'div.alert-success',
{ hasText: 'Post created successfully' }
))

Reading session data

You may read the data inside a session store using the browserContext.getSession and browser.getFlashMessages methods. These methods will return all the data for the session ID associated with a specific browser context instance.

const session = await browserContext.getSession()
const flashMessages = await browserContext.getFlashMessages()

Authenticating users

If you are using the @adonisjs/auth package to authenticate users in your application, you may use the authBrowserClient Japa plugin to authenticate users when making HTTP requests to your application.

The first step is registering the plugin inside the tests/bootstrap.ts file.

tests/bootstrap.ts
import { authBrowserClient } from '@adonisjs/auth/plugins/browser_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
authBrowserClient(app)
]

If you are using session-based authentication, then make sure to switch the session driver to an in-memory store.

.env.test
SESSION_DRIVER=memory

That's all. Now, you may login users using the loginAs method. The method accepts the user object as the only argument and marks the user as logged in the current browser context.

All page visits using the same browser context will have access to the logged-in user. However, the user session will be destroyed when the browser or the context is closed.

import User from '#models/user'
test('get payments list', async ({ browserContext, visit }) => {
const user = await User.create(payload)
await browserContext.loginAs(user)
const page = await visit('/dashboard')
})

The loginAs method uses the default guard configured inside the config/auth.ts file for authentication. However, you may specify a custom guard using the withGuard method. For example:

const user = await User.create(payload)
await browserContext
.withGuard('admin')
.loginAs(user)

The route helper

You may use the route helper from the TestContext to create a URL for a route. Using the route helper ensures that whenever you update your routes, you do not have to come back and fix all the URLs inside your tests.

The route helper accepts the same set of arguments accepted by the global template method route.

test('see list of users', ({ visit, route }) => {
const page = await visit(
route('users.list')
)
})