PlaywrightでNext.js+supabaseアプリの会員機能をテストする。(会員ランクに応じた登録数制限テスト)
Next.js+supabaseをつかって開発した、米国株チャートアプリの会員機能のe2eテストをPlaywrightで実施しました。
テスト環境前提 (GitHubActions,Vercelの設定については別記事に記載)
・開発フレームワーク:Next.js
・データベース:supabaseの開発用DB環境(本番環境とは別)
・ホスティング:Vercel
・VercelのPreviewの環境変数に、supabasesの開発用DBの情報を設定
・GithubActionsと連携して、テスト自動化
テストシナリオ
・会員のログイン
・ブックマーク登録、株式メモ、株式マーカーをそれぞれ登録数上限の9件まで登録。
・登録数が上限に達した場合、画面にワーニングテキストが表示されるので、それを確認。
・ブックマーク登録、株式メモ、株式マーカーをそれぞれ1件ずつ削除。
・登録数上限のワーニングテキストが消えているかを確認。
・データベースクリーンアップの為、残りの8件も削除。
・会員ページですべての登録データが削除されたかを確認。
技術トピックは以下の3つになります。
・環境変数の適用:global-setup
・Loop処理について
・PageObject
1)環境変数の適用:global-setup
テストファイルからは、直接、Next.jsの環境変数 .env.localにアクセスができないようです。調査した結果、ルート直下に、global-setup.js を配置し、以下の記述を行った上で、テストファイルから、環境変数を読み込む流れとなります。
module.exports = async config => {
process.env.BASE_URL = 'https://xxxxxx.xxxx' // テスト環境URL
}
テストファイル内で、以下のように環境変数を読み込みます。
const { BASE_URL } = process.env
※今回のコードでは、この設定がなくても対応できましたが、今後の応用の為に設定しました。
2)ループ処理
テストシナリオで、「データを9個入力する」「データを8個削除する」という部分があります。テストコード、以下4つの内、a)とc)がループ処理対応となります。
a) データ9件入力(1会員あたり入力可能上限値)
b) 入力ステイタス確認
入力不可のステイタス確認->1件削除->入力可ステイタス確認
c) 残り8件のデータ削除
d) 登録データが削除されたか再確認。
テストコード内でのループ処理には、若干癖があって、forEachが使えない(使いにくい)等の問題がありまして。
a) のループ処理では、for of 構文を使いました。
for (const data of stockList) {}
c) のループ処理では、インデックスを使いたいのでbasicなforLoop文を使いました。
for (let i = 0; i < stockList.length; i++) {}
for of 構文でも、インデックスが使えるはずですが、私の環境ではエラーがでたので、一旦使用を見送りました。
テストコード本体
import { test } from '@playwright/test'
import { LoginPage } from './page-objects/LoginPage'
import { MemberPage } from './page-objects/MemberPage'
import { StockIdPage } from './page-objects/StockIdPage'
const stockList = [
{
Ticker: 'A',
Name: 'Agilent Technologies Inc. Common Stock',
},
// 省略、同様のデータがあと8件
]
test.describe('Ck Membership', async () => {
let loginPage: LoginPage
let memberPage: MemberPage
let stockIdPage: StockIdPage
const { BASE_URL } = process.env
// データ入力
test('Input Data', async ({ page }) => {
loginPage = new LoginPage(page)
memberPage = new MemberPage(page)
stockIdPage = new StockIdPage(page)
await page.goto(`${BASE_URL}`)
await page.goto('auth/signin')
await loginPage.login()
// 会員ページに自動遷移
await memberPage.assertLoginEmail()
for (const data of stockList) {
await page.goto(`/stocks/${data.Ticker}`)
console.log(data.Ticker)
await stockIdPage.inputComment()
await stockIdPage.clickBookMark()
await stockIdPage.inputMarker()
}
})
// 入力不可ステイタス確認、1個削除後、入力可ステイタス確認
test('Check 入力不可ステイタス', async ({ page }) => {
console.log('Check 入力不可ステイタス Start')
loginPage = new LoginPage(page)
memberPage = new MemberPage(page)
stockIdPage = new StockIdPage(page)
await page.goto(`${BASE_URL}`)
await page.goto('auth/signin')
await loginPage.login()
// 会員ページに自動遷移
await memberPage.assertLoginEmail()
await memberPage.assertCanNotAddBookMark()
// 個別ページで入力不可確認 /stocks/A stockList 0番目
await page.goto(`/stocks/${stockList[0].Ticker}`)
await stockIdPage.checkCanNotInput()
// 個別ページで 1個データ削除
await stockIdPage.clickBookMark()
await stockIdPage.inputMembersOnly.click()
page.on('dialog', async (dialog) => {
await dialog.accept()
})
await stockIdPage.deleteMarker()
await stockIdPage.deleteComment()
await stockIdPage.checkCanInput()
// 会員ページで 入力可確認 bookmark,marker,comment
await memberPage.assertCanDataInput()
console.log('Check 入力不可ステイタス End')
})
// 後処理 データ削除 残り8個
test(`Clean up Data`, async ({ page }) => {
loginPage = new LoginPage(page)
memberPage = new MemberPage(page)
stockIdPage = new StockIdPage(page)
await page.goto(`${BASE_URL}`)
await page.goto('auth/signin')
await loginPage.login()
// 会員ページに自動遷移
await memberPage.assertLoginEmail()
page.on('dialog', async (dialog) => {
await dialog.accept()
})
for (let i = 0; i < stockList.length; i++) {
if (i == 0) {
continue
} else {
await page.goto(`/stocks/${stockList[i].Ticker}`)
console.log(stockList[i].Ticker)
await stockIdPage.checkTickerTitle(stockList[i].Ticker)
await stockIdPage.inputMembersOnly.click()
await stockIdPage.deleteMarkerWithConfirm()
await stockIdPage.clickBookMark()
await stockIdPage.deleteCommentWithConfirm()
}
}
})
// 会員ページで、データ登録が0であることを確認する。
test('Logout', async ({ page }) => {
loginPage = new LoginPage(page)
memberPage = new MemberPage(page)
stockIdPage = new StockIdPage(page)
await page.goto(`${BASE_URL}`)
await page.goto('auth/signin')
await loginPage.login()
await memberPage.assertLoginEmail()
await memberPage.assertNoDataRegistered()
})
})
テストコードの見通しをよくする為にPlaywrightのPageObjectをつかってリファクタリングしました。
PageObjectは全部で3ファイルです。
- LoginPage.ts
- Member.ts
- StockId.ts
全部乗せてしまうと長くなるので、LoginPage.tsとMember.tsの
コードだけ以下に載せています。
import { Locator, Page } from '@playwright/test'
export class LoginPage {
// Define Selectors
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
// Init Selectors
constructor(page: Page) {
this.page = page
this.emailInput = page.locator('input[type="email"]')
this.passwordInput = page.locator('input[type="password"]')
this.submitButton = page.locator('data-testid=login-submit')
}
// Define login page methods
async login() {
await this.emailInput.type('exampleEmail@example.com')
await this.passwordInput.type('examplePassword')
await this.submitButton.click()
}
}
import { expect, Locator, Page } from '@playwright/test'
export class MemberPage {
// Define Selectors
readonly page: Page
readonly body: Locator
readonly canBookMarkInput: Locator
readonly canMarkerInput: Locator
readonly canCommentInput: Locator
readonly tableRowData: Locator
// Init Selectors
constructor(page: Page) {
this.page = page
this.body = page.locator('body')
this.canBookMarkInput = page.locator('data-testid=canBookMarkInput')
this.canMarkerInput = page.locator('data-testid=canMarkerInput')
this.canCommentInput = page.locator('data-testid=canCommentInput')
this.tableRowData = page.locator('td')
}
// Assert Functions
async assertCanBookMarkInput() {
await expect(this.canBookMarkInput).toContainText('Registration is available.')
}
async assertCanMarkerInput() {
await expect(this.canMarkerInput).toContainText('Registration is available.')
}
async assertCanCommentInput() {
await expect(this.canCommentInput).toContainText('Registration is available.')
}
async assertCanDataInput() {
await this.page.goto(`/member`)
await expect(this.canBookMarkInput).toContainText('Registration is available.')
await expect(this.canMarkerInput).toContainText('Registration is available.')
await expect(this.canCommentInput).toContainText('Registration is available.')
}
async assertCanNotAddBookMark() {
await expect(this.canBookMarkInput).toContainText('Registration limit has been reached')
}
async assertLoginEmail() {
await expect(this.page.locator('data-testid=memberEmail')).toContainText('exampleEmail@example.com')
}
async assertNoDataRegistered() {
await expect(this.tableRowData).not.toBeVisible()
}
}
まだまた、改善の余地はありますが、PageObjectをつかったおかげで、だいぶすっきりしてきました。
Discussion