feat: Initialize POM structure with fixtures on Playwright (#2015)

* feat: video recorder on failures

* feat: fixture for sample page class

* feat: video recorder on failures

* feat: fixture for sample page class

* feat: video recorder on failures

* feat: fixture for sample page class

* feat: Apply Screenshot on failures

* feat: set timeout by default

* chore: clean up import

* feat: video recorder on failures

* feat: fixture for sample page class

* feat: add wait for app update

* chore: correct timeout

* chore: correct timeout

* chore: test timeout

* chore: test timeout

* chore: test timeout

* chore: browser context config

* chore: temporally disable the video recorder to bypass issue
This commit is contained in:
Van Pham 2024-02-15 20:18:02 +07:00 committed by GitHub
parent 05eebfa430
commit 82b361a5be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 235 additions and 122 deletions

View File

@ -1,5 +1,6 @@
name: Jan Electron Linter & Test
on:
workflow_dispatch:
push:
branches:
- main

View File

@ -3,14 +3,12 @@ import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
testDir: './tests/e2e',
retries: 0,
globalTimeout: 300000,
globalTimeout: 350000,
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
reporter: [['html', { outputFolder: './playwright-report' }]],
}
export default config

View File

@ -0,0 +1,4 @@
export const Constants = {
VIDEO_DIR: './playwright-video',
TIMEOUT: '300000',
}

View File

@ -0,0 +1,119 @@
import {
_electron as electron,
BrowserContext,
ElectronApplication,
expect,
Page,
test as base,
} from '@playwright/test'
import {
ElectronAppInfo,
findLatestBuild,
parseElectronApp,
stubDialog,
} from 'electron-playwright-helpers'
import { Constants } from './constants'
import { HubPage } from '../pages/hubPage'
import { CommonActions } from '../pages/commonActions'
export let electronApp: ElectronApplication
export let page: Page
export let appInfo: ElectronAppInfo
export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT)
export async function setupElectron() {
process.env.CI = 'e2e'
const latestBuild = findLatestBuild('dist')
expect(latestBuild).toBeTruthy()
// parse the packaged Electron app and find paths and other info
appInfo = parseElectronApp(latestBuild)
expect(appInfo).toBeTruthy()
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
// recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings
})
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
page = await electronApp.firstWindow({
timeout: TIMEOUT,
})
}
export async function teardownElectron() {
await page.close()
await electronApp.close()
}
/**
* this fixture is needed to record and attach videos / screenshot on failed tests when
* tests are run in serial mode (i.e. browser is not closed between tests)
*/
export const test = base.extend<
{
commonActions: CommonActions
hubPage: HubPage
attachVideoPage: Page
attachScreenshotsToReport: void
},
{ createVideoContext: BrowserContext }
>({
commonActions: async ({ request }, use, testInfo) => {
await use(new CommonActions(page, testInfo))
},
hubPage: async ({ commonActions }, use) => {
await use(new HubPage(page, commonActions))
},
createVideoContext: [
async ({ playwright }, use) => {
const context = electronApp.context()
await use(context)
},
{ scope: 'worker' },
],
attachVideoPage: [
async ({ createVideoContext }, use, testInfo) => {
await use(page)
if (testInfo.status !== testInfo.expectedStatus) {
const path = await createVideoContext.pages()[0].video()?.path()
await createVideoContext.close()
await testInfo.attach('video', {
path: path,
})
}
},
{ scope: 'test', auto: true },
],
attachScreenshotsToReport: [
async ({ commonActions }, use, testInfo) => {
await use()
// After the test, we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
await commonActions.takeScreenshot('')
}
},
{ auto: true },
],
})
test.setTimeout(TIMEOUT)
test.beforeAll(async () => {
await setupElectron()
await page.waitForSelector('img[alt="Jan - Logo"]', {
state: 'visible',
timeout: TIMEOUT,
})
})
test.afterAll(async () => {
// temporally disabling this due to the config for parallel testing WIP
// teardownElectron()
})

View File

@ -1,34 +1,19 @@
import {
page,
test,
setupElectron,
teardownElectron,
TIMEOUT,
} from '../pages/basePage'
import { test, appInfo } from '../config/fixtures'
import { expect } from '@playwright/test'
test.beforeAll(async () => {
const appInfo = await setupElectron()
expect(appInfo.asar).toBe(true)
expect(appInfo.executable).toBeTruthy()
expect(appInfo.main).toBeTruthy()
expect(appInfo.name).toBe('jan')
expect(appInfo.packageJson).toBeTruthy()
expect(appInfo.packageJson.name).toBe('jan')
expect(appInfo.platform).toBeTruthy()
expect(appInfo.platform).toBe(process.platform)
expect(appInfo.resourcesDir).toBeTruthy()
})
test.afterAll(async () => {
await teardownElectron()
})
test('explores hub', async () => {
await page.getByTestId('Hub').first().click({
timeout: TIMEOUT,
})
await page.getByTestId('hub-container-test-id').isVisible({
timeout: TIMEOUT,
expect(appInfo).toMatchObject({
asar: true,
executable: expect.anything(),
main: expect.anything(),
name: 'jan',
packageJson: expect.objectContaining({ name: 'jan' }),
platform: process.platform,
resourcesDir: expect.anything(),
})
})
test('explores hub', async ({ hubPage }) => {
await hubPage.navigateByMenu()
await hubPage.verifyContainerVisible()
})

View File

@ -1,19 +1,5 @@
import { expect } from '@playwright/test'
import {
page,
setupElectron,
TIMEOUT,
test,
teardownElectron,
} from '../pages/basePage'
test.beforeAll(async () => {
await setupElectron()
})
test.afterAll(async () => {
await teardownElectron()
})
import { page, test, TIMEOUT } from '../config/fixtures'
test('renders left navigation panel', async () => {
const systemMonitorBtn = await page

View File

@ -1,23 +1,11 @@
import { expect } from '@playwright/test'
import {
setupElectron,
teardownElectron,
test,
page,
TIMEOUT,
} from '../pages/basePage'
test.beforeAll(async () => {
await setupElectron()
})
test.afterAll(async () => {
await teardownElectron()
})
import { test, page, TIMEOUT } from '../config/fixtures'
test('shows settings', async () => {
await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
await page.getByTestId('Settings').first().click({
timeout: TIMEOUT,
})
const settingDescription = page.getByTestId('testid-setting-description')
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
})

View File

@ -1,67 +1,49 @@
import {
expect,
test as base,
_electron as electron,
ElectronApplication,
Page,
} from '@playwright/test'
import {
findLatestBuild,
parseElectronApp,
stubDialog,
} from 'electron-playwright-helpers'
import { Page, expect } from '@playwright/test'
import { CommonActions } from './commonActions'
import { TIMEOUT } from '../config/fixtures'
export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
export class BasePage {
menuId: string
export let electronApp: ElectronApplication
export let page: Page
constructor(
protected readonly page: Page,
readonly action: CommonActions,
protected containerId: string
) {}
export async function setupElectron() {
process.env.CI = 'e2e'
public getValue(key: string) {
return this.action.getValue(key)
}
const latestBuild = findLatestBuild('dist')
expect(latestBuild).toBeTruthy()
public setValue(key: string, value: string) {
this.action.setValue(key, value)
}
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild)
expect(appInfo).toBeTruthy()
async takeScreenshot(name: string = '') {
await this.action.takeScreenshot(name)
}
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable, // path to the Electron executable
})
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
async navigateByMenu() {
await this.page.getByTestId(this.menuId).first().click()
}
page = await electronApp.firstWindow({
timeout: TIMEOUT,
})
// Return appInfo for future use
return appInfo
async verifyContainerVisible() {
const container = this.page.getByTestId(this.containerId)
expect(container.isVisible()).toBeTruthy()
}
async waitUpdateLoader() {
await this.isElementVisible('img[alt="Jan - Logo"]')
}
//wait and find a specific element with it's selector and return Visible
async isElementVisible(selector: any) {
let isVisible = true
await this.page
.waitForSelector(selector, { state: 'visible', timeout: TIMEOUT })
.catch(() => {
isVisible = false
})
return isVisible
}
}
export async function teardownElectron() {
await page.close()
await electronApp.close()
}
export const test = base.extend<{
attachScreenshotsToReport: void
}>({
attachScreenshotsToReport: [
async ({ request }, use, testInfo) => {
await use()
// After the test, we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
const screenshot = await page.screenshot()
await testInfo.attach('screenshot', {
body: screenshot,
contentType: 'image/png',
})
}
},
{ auto: true },
],
})
test.setTimeout(TIMEOUT)

View File

@ -0,0 +1,34 @@
import { Page, TestInfo } from '@playwright/test'
import { page } from '../config/fixtures'
export class CommonActions {
private testData = new Map<string, string>()
constructor(
public page: Page,
public testInfo: TestInfo
) {}
async takeScreenshot(name: string) {
const screenshot = await page.screenshot({
fullPage: true,
})
const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}`
await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), {
body: screenshot,
contentType: 'image/png',
})
}
async hooks() {
console.log('hook from the scenario page')
}
setValue(key: string, value: string) {
this.testData.set(key, value)
}
getValue(key: string) {
return this.testData.get(key)
}
}

View File

@ -0,0 +1,15 @@
import { Page } from '@playwright/test'
import { BasePage } from './basePage'
import { CommonActions } from './commonActions'
export class HubPage extends BasePage {
readonly menuId: string = 'Hub'
static readonly containerId: string = 'hub-container-test-id'
constructor(
public page: Page,
readonly action: CommonActions
) {
super(page, action, HubPage.containerId)
}
}

View File

@ -95,6 +95,7 @@ const TopBar = () => {
</div>
<div
className="unset-drag cursor-pointer pr-4"
data-testid="btn-create-thread"
onClick={onCreateConversationClick}
>
<PenSquareIcon size={20} className="text-muted-foreground" />