Skip to content

Commit 7db05d7

Browse files
committed
test: migrate to playwright tests
1 parent 82dd7a4 commit 7db05d7

22 files changed

+613
-95
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
"serve": "vite preview --port 4173",
1111
"type-check": "vue-tsc --noEmit",
1212
"lint": "eslint --fix .",
13-
"test": "npm run test:unit && npm run test:e2e:ci",
13+
"test": "npm run test:unit && npm run test:playwright",
1414
"test:cypress:local": "cypress open --e2e -c baseUrl=http://localhost:5173",
1515
"test:cyprsss:prod": "cypress run --e2e -c baseUrl=https://vue3-realworld-example-app-mutoe.vercel.app",
1616
"test:e2e": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress open --e2e -c baseUrl=http://localhost:4173\"",
1717
"test:e2e:ci": "npm run build && concurrently -rk -s first \"npm run serve\" \"cypress run --e2e -c baseUrl=http://localhost:4173\"",
1818
"test:playwright:ci": "playwright test",
1919
"test:playwright:local": "playwright test --ui",
20-
"test:playwright:local:debug": "playwright test --ui --debug",
20+
"test:playwright:local:debug": "playwright test --ui --headed --debug",
2121
"test:unit": "vitest run",
2222
"generate:api": "curl -sL https://raw.githubusercontent.com/gothinkster/realworld/main/api/openapi.yml -o ./src/services/openapi.yml && sta -p ./src/services/openapi.yml -o ./src/services -n api.ts"
2323
},

playwright.config.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,37 @@ import { defineConfig, devices } from '@playwright/test'
99

1010
const baseURL = 'http://localhost:5173'
1111

12+
const isCI = process.env.CI
13+
1214
/**
1315
* See https://playwright.dev/docs/test-configuration.
1416
*/
1517
export default defineConfig({
1618
testDir: './playwright',
1719
/* Run tests in files in parallel */
18-
fullyParallel: true,
20+
fullyParallel: false,
1921
/* Fail the build on CI if you accidentally left test.only in the source code. */
20-
forbidOnly: !!process.env.CI,
22+
forbidOnly: !!isCI,
2123
/* Retry on CI only */
22-
retries: process.env.CI ? 1 : 0,
24+
retries: isCI ? 1 : 0,
2325
/* Opt out of parallel tests on CI. */
24-
workers: process.env.CI ? 1 : undefined,
26+
workers: isCI ? 1 : undefined,
2527
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
26-
reporter: 'html',
28+
reporter: [
29+
['line'],
30+
['html', { open: 'never' }],
31+
],
2732
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2833
use: {
2934
/* Base URL to use in actions like `await page.goto('/')`. */
3035
baseURL,
3136

37+
navigationTimeout: 4000,
38+
3239
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
33-
trace: 'on-first-retry',
40+
screenshot: 'only-on-failure',
41+
trace: isCI ? 'on-first-retry' : 'retain-on-failure',
42+
video: isCI ? 'on-first-retry' : 'retain-on-failure',
3443
},
3544

3645
/* Configure projects for major browsers */
@@ -40,12 +49,12 @@ export default defineConfig({
4049
use: { ...devices['Desktop Chrome'] },
4150
},
4251

43-
{
52+
isCI && {
4453
name: 'firefox',
4554
use: { ...devices['Desktop Firefox'] },
4655
},
4756

48-
{
57+
isCI && {
4958
name: 'webkit',
5059
use: { ...devices['Desktop Safari'] },
5160
},
@@ -69,13 +78,13 @@ export default defineConfig({
6978
// name: 'Google Chrome',
7079
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
7180
// },
72-
],
81+
].filter(Boolean),
7382

7483
/* Run your local dev server before starting the tests */
7584
webServer: {
7685
command: 'npm run dev',
7786
url: baseURL,
78-
reuseExistingServer: !process.env.CI,
87+
reuseExistingServer: !isCI,
7988
ignoreHTTPSErrors: true,
8089
},
8190
})

playwright/article/article.spec.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

playwright/extends.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { test as base } from '@playwright/test'
2-
import { ConductPageObject } from './conduct.page-object.ts'
2+
import { ConduitPageObject } from 'page-objects/conduit.page-object'
33

44
export const test = base.extend<{
5-
conduct: ConductPageObject
5+
conduit: ConduitPageObject
66
}>({
7-
conduct: async ({ page }, use) => {
8-
const buyscoutPageObject = new ConductPageObject(page)
7+
conduit: async ({ page }, use) => {
8+
const buyscoutPageObject = new ConduitPageObject(page)
99
await use(buyscoutPageObject)
1010
},
1111
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Page } from '@playwright/test'
2+
import { ConduitPageObject } from './conduit.page-object'
3+
4+
export class ArticleDetailPageObject extends ConduitPageObject {
5+
constructor(public page: Page) {
6+
super(page)
7+
}
8+
9+
positionMap = {
10+
'banner': 0,
11+
'article footer': 1,
12+
} as const
13+
14+
private async clickOperationButton(position: keyof typeof this.positionMap = 'banner', buttonName: string) {
15+
await this.page.getByRole('button', { name: buttonName }).nth(this.positionMap[position]).click()
16+
}
17+
18+
async clickEditArticle(position: keyof typeof this.positionMap = 'banner') {
19+
return this.clickOperationButton(position, 'Edit Article')
20+
}
21+
22+
async clickDeleteArticle(position: keyof typeof this.positionMap = 'banner') {
23+
await this.page.getByRole('button', { name: 'Delete article' }).nth(this.positionMap[position]).dispatchEvent('click')
24+
}
25+
26+
async clickFollowUser(position: keyof typeof this.positionMap = 'banner') {
27+
await this.page.getByRole('button', { name: 'Follow' }).nth(this.positionMap[position]).dispatchEvent('click')
28+
}
29+
30+
async clickFavoriteArticle(position: keyof typeof this.positionMap = 'banner') {
31+
await this.page.getByRole('button', { name: 'Favorite article' }).nth(this.positionMap[position]).dispatchEvent('click')
32+
}
33+
}

playwright/conduct.page-object.ts renamed to playwright/page-objects/conduit.page-object.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,45 @@ import fs from 'node:fs/promises'
22
import path from 'node:path'
33
import url from 'node:url'
44
import type { Page, Response } from '@playwright/test'
5-
import type { User } from '../src/services/api.ts'
6-
import { Route } from './constant'
7-
import { expect } from './extends.ts'
8-
import { boxedStep } from './utils/test-decorators.ts'
5+
import type { User } from 'src/services/api.ts'
6+
import { Route } from '../constant'
7+
import { expect } from '../extends.ts'
8+
import { boxedStep } from '../utils/test-decorators.ts'
99

1010
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
11-
const fixtureDir = path.join(__dirname, '../cypress/fixtures')
11+
const fixtureDir = path.join(__dirname, '../../cypress/fixtures')
1212

13-
export class ConductPageObject {
13+
export class ConduitPageObject {
1414
constructor(
1515
public readonly page: Page,
1616
) {}
1717

1818
async intercept(method: 'POST' | 'GET' | 'PATCH' | 'DELETE' | 'PUT', url: string | RegExp, options: {
1919
fixture?: string
20+
postFixture?: (fixture: any) => void | unknown
2021
statusCode?: number
2122
body?: unknown
22-
} = {}): Promise<(timeout?: number) => Promise<Response>> {
23+
timeout?: number
24+
} = {}): Promise<() => Promise<Response>> {
2325
await this.page.route(url, async route => {
2426
if (route.request().method() !== method)
2527
return route.continue()
2628

29+
if (options.postFixture && options.fixture) {
30+
const body = await this.getFixture(options.fixture)
31+
const returnValue = await options.postFixture(body)
32+
options.body = returnValue === undefined ? body : returnValue
33+
options.fixture = undefined
34+
}
35+
2736
return await route.fulfill({
2837
status: options.statusCode || undefined,
2938
json: options.body ?? undefined,
3039
path: options.fixture ? path.join(fixtureDir, options.fixture) : undefined,
3140
})
3241
})
3342

34-
return (timeout: number = 1000) => this.page.waitForResponse(response => {
43+
return () => this.page.waitForResponse(response => {
3544
const request = response.request()
3645
if (request.method() !== method)
3746
return false
@@ -40,7 +49,7 @@ export class ConductPageObject {
4049
return request.url().includes(url)
4150

4251
return url.test(request.url())
43-
}, { timeout })
52+
}, { timeout: options.timeout ?? 4000 })
4453
}
4554

4655
async getFixture<T = unknown>(fixture: string): Promise<T> {
@@ -49,21 +58,29 @@ export class ConductPageObject {
4958
}
5059

5160
async goto(route: Route) {
52-
await this.page.goto(route)
61+
await this.page.goto(route, { waitUntil: 'domcontentloaded' })
5362
}
5463

5564
@boxedStep
5665
async login(username = 'plumrx') {
5766
const userFixture = await this.getFixture<{ user: User }>('user.json')
5867
userFixture.user.username = username
59-
await this.intercept('POST', /users\/login$/, { statusCode: 200, body: userFixture })
6068

6169
await this.goto(Route.Login)
6270

6371
await this.page.getByPlaceholder('Email').fill('foo@example.com')
6472
await this.page.getByPlaceholder('Password').fill('12345678')
65-
await this.page.getByRole('button', { name: 'Sign in' }).click()
73+
74+
const waitForLogin = await this.intercept('POST', /users\/login$/, { statusCode: 200, body: userFixture })
75+
await Promise.all([
76+
waitForLogin(),
77+
this.page.getByRole('button', { name: 'Sign in' }).click(),
78+
])
6679

6780
await expect(this.page).toHaveURL(Route.Home)
6881
}
82+
83+
async toContainText(text: string) {
84+
await expect(this.page.locator('body')).toContainText(text)
85+
}
6986
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Page } from '@playwright/test'
2+
import { ConduitPageObject } from './conduit.page-object.ts'
3+
4+
export class EditArticlePageObject extends ConduitPageObject {
5+
constructor(public page: Page) {
6+
super(page)
7+
}
8+
9+
async fillTitle(title: string) {
10+
await this.page.getByPlaceholder('Article Title').fill(title)
11+
}
12+
13+
async fillDescription(description: string) {
14+
await this.page.getByPlaceholder("What's this article about?").fill(description)
15+
}
16+
17+
async fillContent(content: string) {
18+
await this.page.getByPlaceholder('Write your article (in markdown)').fill(content)
19+
}
20+
21+
async fillTags(tags: string | string[]) {
22+
if (!Array.isArray(tags))
23+
tags = [tags]
24+
for (const tag of tags) {
25+
await this.page.getByPlaceholder('Enter tags').fill(tag)
26+
await this.page.getByPlaceholder('Enter tags').press('Enter')
27+
}
28+
}
29+
30+
async fillForm({ title, description, content, tags }: { title?: string, description?: string, content?: string, tags?: string | string[] }) {
31+
if (title !== undefined)
32+
await this.fillTitle(title)
33+
if (description !== undefined)
34+
await this.fillDescription(description)
35+
if (content !== undefined)
36+
await this.fillContent(content)
37+
if (tags !== undefined)
38+
await this.fillTags(tags)
39+
}
40+
41+
async clickPublishArticle() {
42+
await this.page.getByRole('button', { name: 'Publish Article' }).dispatchEvent('click')
43+
}
44+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Page } from '@playwright/test'
2+
import { ConduitPageObject } from './conduit.page-object.ts'
3+
4+
export class LoginPageObject extends ConduitPageObject {
5+
constructor(public page: Page) {
6+
super(page)
7+
}
8+
9+
async fillEmail(email: string = 'foo@example.com') {
10+
await this.page.getByPlaceholder('Email').fill(email)
11+
}
12+
13+
async fillPassword(password = '12345678') {
14+
await this.page.getByPlaceholder('Password').fill(password)
15+
}
16+
17+
async fillForm(form: { email?: string, password?: string }) {
18+
if (form.email !== undefined)
19+
await this.fillEmail(form.email)
20+
if (form.password !== undefined)
21+
await this.fillPassword(form.password)
22+
}
23+
24+
async clickSignIn() {
25+
await this.page.getByRole('button', { name: 'Sign in' }).dispatchEvent('click')
26+
}
27+
}

0 commit comments

Comments
 (0)