diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml index 6d5aaf150..40085391f 100644 --- a/.github/workflows/jan-electron-linter-and-test.yml +++ b/.github/workflows/jan-electron-linter-and-test.yml @@ -1,5 +1,6 @@ name: Jan Electron Linter & Test on: + workflow_dispatch: push: branches: - main diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts index 8047b7513..d3dff40c6 100644 --- a/electron/playwright.config.ts +++ b/electron/playwright.config.ts @@ -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 diff --git a/electron/tests/config/constants.ts b/electron/tests/config/constants.ts new file mode 100644 index 000000000..7039ad58c --- /dev/null +++ b/electron/tests/config/constants.ts @@ -0,0 +1,4 @@ +export const Constants = { + VIDEO_DIR: './playwright-video', + TIMEOUT: '300000', +} diff --git a/electron/tests/config/fixtures.ts b/electron/tests/config/fixtures.ts new file mode 100644 index 000000000..680b09785 --- /dev/null +++ b/electron/tests/config/fixtures.ts @@ -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() +}) diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts index 68632058e..d968e7641 100644 --- a/electron/tests/e2e/hub.e2e.spec.ts +++ b/electron/tests/e2e/hub.e2e.spec.ts @@ -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() +}) diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts index 2da59953c..66924ce78 100644 --- a/electron/tests/e2e/navigation.e2e.spec.ts +++ b/electron/tests/e2e/navigation.e2e.spec.ts @@ -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 diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts index 54215d9b1..06b4d1acc 100644 --- a/electron/tests/e2e/settings.e2e.spec.ts +++ b/electron/tests/e2e/settings.e2e.spec.ts @@ -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 }) }) diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts index 5f1a6fca1..4e16a3c23 100644 --- a/electron/tests/pages/basePage.ts +++ b/electron/tests/pages/basePage.ts @@ -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) diff --git a/electron/tests/pages/commonActions.ts b/electron/tests/pages/commonActions.ts new file mode 100644 index 000000000..08ea15f92 --- /dev/null +++ b/electron/tests/pages/commonActions.ts @@ -0,0 +1,34 @@ +import { Page, TestInfo } from '@playwright/test' +import { page } from '../config/fixtures' + +export class CommonActions { + private testData = new Map() + + 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) + } +} diff --git a/electron/tests/pages/hubPage.ts b/electron/tests/pages/hubPage.ts new file mode 100644 index 000000000..0299ab15d --- /dev/null +++ b/electron/tests/pages/hubPage.ts @@ -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) + } +} diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index 206a9013d..fdc4b4cdc 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -95,6 +95,7 @@ const TopBar = () => {