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:
parent
05eebfa430
commit
82b361a5be
@ -1,5 +1,6 @@
|
|||||||
name: Jan Electron Linter & Test
|
name: Jan Electron Linter & Test
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|||||||
@ -3,14 +3,12 @@ import { PlaywrightTestConfig } from '@playwright/test'
|
|||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e',
|
||||||
retries: 0,
|
retries: 0,
|
||||||
globalTimeout: 300000,
|
globalTimeout: 350000,
|
||||||
use: {
|
use: {
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
trace: 'retain-on-failure',
|
trace: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
reporter: [['html', { outputFolder: './playwright-report' }]],
|
reporter: [['html', { outputFolder: './playwright-report' }]],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|||||||
4
electron/tests/config/constants.ts
Normal file
4
electron/tests/config/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const Constants = {
|
||||||
|
VIDEO_DIR: './playwright-video',
|
||||||
|
TIMEOUT: '300000',
|
||||||
|
}
|
||||||
119
electron/tests/config/fixtures.ts
Normal file
119
electron/tests/config/fixtures.ts
Normal 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()
|
||||||
|
})
|
||||||
@ -1,34 +1,19 @@
|
|||||||
import {
|
import { test, appInfo } from '../config/fixtures'
|
||||||
page,
|
|
||||||
test,
|
|
||||||
setupElectron,
|
|
||||||
teardownElectron,
|
|
||||||
TIMEOUT,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
const appInfo = await setupElectron()
|
expect(appInfo).toMatchObject({
|
||||||
expect(appInfo.asar).toBe(true)
|
asar: true,
|
||||||
expect(appInfo.executable).toBeTruthy()
|
executable: expect.anything(),
|
||||||
expect(appInfo.main).toBeTruthy()
|
main: expect.anything(),
|
||||||
expect(appInfo.name).toBe('jan')
|
name: 'jan',
|
||||||
expect(appInfo.packageJson).toBeTruthy()
|
packageJson: expect.objectContaining({ name: 'jan' }),
|
||||||
expect(appInfo.packageJson.name).toBe('jan')
|
platform: process.platform,
|
||||||
expect(appInfo.platform).toBeTruthy()
|
resourcesDir: expect.anything(),
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('explores hub', async ({ hubPage }) => {
|
||||||
|
await hubPage.navigateByMenu()
|
||||||
|
await hubPage.verifyContainerVisible()
|
||||||
|
})
|
||||||
|
|||||||
@ -1,19 +1,5 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import {
|
import { page, test, TIMEOUT } from '../config/fixtures'
|
||||||
page,
|
|
||||||
setupElectron,
|
|
||||||
TIMEOUT,
|
|
||||||
test,
|
|
||||||
teardownElectron,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
await setupElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await teardownElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders left navigation panel', async () => {
|
test('renders left navigation panel', async () => {
|
||||||
const systemMonitorBtn = await page
|
const systemMonitorBtn = await page
|
||||||
|
|||||||
@ -1,23 +1,11 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import { test, page, TIMEOUT } from '../config/fixtures'
|
||||||
setupElectron,
|
|
||||||
teardownElectron,
|
|
||||||
test,
|
|
||||||
page,
|
|
||||||
TIMEOUT,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
await setupElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await teardownElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows settings', async () => {
|
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')
|
const settingDescription = page.getByTestId('testid-setting-description')
|
||||||
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
|
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,67 +1,49 @@
|
|||||||
import {
|
import { Page, expect } from '@playwright/test'
|
||||||
expect,
|
import { CommonActions } from './commonActions'
|
||||||
test as base,
|
import { TIMEOUT } from '../config/fixtures'
|
||||||
_electron as electron,
|
|
||||||
ElectronApplication,
|
|
||||||
Page,
|
|
||||||
} from '@playwright/test'
|
|
||||||
import {
|
|
||||||
findLatestBuild,
|
|
||||||
parseElectronApp,
|
|
||||||
stubDialog,
|
|
||||||
} from 'electron-playwright-helpers'
|
|
||||||
|
|
||||||
export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
|
export class BasePage {
|
||||||
|
menuId: string
|
||||||
|
|
||||||
export let electronApp: ElectronApplication
|
constructor(
|
||||||
export let page: Page
|
protected readonly page: Page,
|
||||||
|
readonly action: CommonActions,
|
||||||
|
protected containerId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
export async function setupElectron() {
|
public getValue(key: string) {
|
||||||
process.env.CI = 'e2e'
|
return this.action.getValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
const latestBuild = findLatestBuild('dist')
|
public setValue(key: string, value: string) {
|
||||||
expect(latestBuild).toBeTruthy()
|
this.action.setValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
async takeScreenshot(name: string = '') {
|
||||||
const appInfo = parseElectronApp(latestBuild)
|
await this.action.takeScreenshot(name)
|
||||||
expect(appInfo).toBeTruthy()
|
}
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
async navigateByMenu() {
|
||||||
args: [appInfo.main], // main file from package.json
|
await this.page.getByTestId(this.menuId).first().click()
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
}
|
||||||
})
|
|
||||||
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
|
||||||
|
|
||||||
page = await electronApp.firstWindow({
|
async verifyContainerVisible() {
|
||||||
timeout: TIMEOUT,
|
const container = this.page.getByTestId(this.containerId)
|
||||||
})
|
expect(container.isVisible()).toBeTruthy()
|
||||||
// Return appInfo for future use
|
}
|
||||||
return appInfo
|
|
||||||
|
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)
|
|
||||||
|
|||||||
34
electron/tests/pages/commonActions.ts
Normal file
34
electron/tests/pages/commonActions.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
electron/tests/pages/hubPage.ts
Normal file
15
electron/tests/pages/hubPage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -95,6 +95,7 @@ const TopBar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="unset-drag cursor-pointer pr-4"
|
className="unset-drag cursor-pointer pr-4"
|
||||||
|
data-testid="btn-create-thread"
|
||||||
onClick={onCreateConversationClick}
|
onClick={onCreateConversationClick}
|
||||||
>
|
>
|
||||||
<PenSquareIcon size={20} className="text-muted-foreground" />
|
<PenSquareIcon size={20} className="text-muted-foreground" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user