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
yarn add -D playwright @japa/browser-client
pnpm add -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
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
.
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.
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.
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')
)
})