-
-
Notifications
You must be signed in to change notification settings - Fork 5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Chore: Desktop: Set up integration testing with Playwright (#9043)
- Loading branch information
1 parent
5733017
commit 2d06fd9
Showing
15 changed files
with
363 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Integration tests | ||
|
||
The integration tests in this directory can be run with `yarn playwright test`. | ||
|
||
- Tests use a `test-profile` directory that should be re-created before every test. | ||
- Only one Electron application should be instantiated per test file. | ||
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom). | ||
|
||
# References | ||
|
||
The following sources are helpful for designing and implementing Electron integration tests | ||
with Playwright: | ||
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89) | ||
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts). | ||
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication) | ||
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example) | ||
- [Playwright best practices](https://playwright.dev/docs/best-practices) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { test, expect } from './util/test'; | ||
import MainScreen from './models/MainScreen'; | ||
import activateMainMenuItem from './util/activateMainMenuItem'; | ||
import SettingsScreen from './models/SettingsScreen'; | ||
|
||
|
||
test.describe('main', () => { | ||
test('app should launch', async ({ mainWindow }) => { | ||
// A window should open with the correct title | ||
expect(await mainWindow.title()).toMatch(/^Joplin/); | ||
|
||
const mainPage = new MainScreen(mainWindow); | ||
await mainPage.waitFor(); | ||
}); | ||
|
||
test('should be able to create and edit a new note', async ({ mainWindow }) => { | ||
const mainScreen = new MainScreen(mainWindow); | ||
await mainScreen.newNoteButton.click(); | ||
|
||
const editor = mainScreen.noteEditor; | ||
await editor.waitFor(); | ||
|
||
// Wait for the title input to have the correct placeholder | ||
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor(); | ||
|
||
// Fill the title | ||
await editor.noteTitleInput.click(); | ||
await editor.noteTitleInput.fill('Test note'); | ||
|
||
// Note list should contain the new note | ||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible(); | ||
|
||
// Focus the editor | ||
await editor.codeMirrorEditor.click(); | ||
|
||
// Type some text | ||
await mainWindow.keyboard.type('# Test note!'); | ||
await mainWindow.keyboard.press('Enter'); | ||
await mainWindow.keyboard.press('Enter'); | ||
await mainWindow.keyboard.type('New note content!'); | ||
|
||
// Should render | ||
const viewerFrame = editor.getNoteViewerIframe(); | ||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!'); | ||
}); | ||
|
||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => { | ||
const mainScreen = new MainScreen(mainWindow); | ||
await mainScreen.waitFor(); | ||
|
||
// Sort order buttons should be visible by default | ||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible(); | ||
|
||
// Open settings (check both labels so that this works on MacOS) | ||
expect( | ||
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'), | ||
).toBe(true); | ||
|
||
// Should be on the settings screen | ||
const settingsScreen = new SettingsScreen(mainWindow); | ||
await settingsScreen.waitFor(); | ||
|
||
// Open the appearance tab | ||
await settingsScreen.appearanceTabButton.click(); | ||
|
||
// Find the sort order visible checkbox | ||
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/); | ||
|
||
await expect(sortOrderVisibleCheckbox).toBeChecked(); | ||
await sortOrderVisibleCheckbox.click(); | ||
await expect(sortOrderVisibleCheckbox).not.toBeChecked(); | ||
|
||
// Save settings & close | ||
await settingsScreen.okayButton.click(); | ||
await mainScreen.waitFor(); | ||
|
||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible(); | ||
}); | ||
}); |
20 changes: 20 additions & 0 deletions
20
packages/app-desktop/integration-tests/models/MainScreen.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { Page, Locator } from '@playwright/test'; | ||
import NoteEditorScreen from './NoteEditorScreen'; | ||
|
||
export default class MainScreen { | ||
public readonly newNoteButton: Locator; | ||
public readonly noteListContainer: Locator; | ||
public readonly noteEditor: NoteEditorScreen; | ||
|
||
public constructor(page: Page) { | ||
this.newNoteButton = page.locator('.new-note-button'); | ||
this.noteListContainer = page.locator('.rli-noteList'); | ||
this.noteEditor = new NoteEditorScreen(page); | ||
} | ||
|
||
public async waitFor() { | ||
await this.newNoteButton.waitFor(); | ||
await this.noteEditor.waitFor(); | ||
await this.noteListContainer.waitFor(); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
packages/app-desktop/integration-tests/models/NoteEditorScreen.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
|
||
import { Locator, Page } from '@playwright/test'; | ||
|
||
export default class NoteEditorPage { | ||
public readonly codeMirrorEditor: Locator; | ||
public readonly noteTitleInput: Locator; | ||
private readonly containerLocator: Locator; | ||
|
||
public constructor(private readonly page: Page) { | ||
this.containerLocator = page.locator('.rli-editor'); | ||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor'); | ||
this.noteTitleInput = this.containerLocator.locator('.title-input'); | ||
} | ||
|
||
public getNoteViewerIframe() { | ||
// The note viewer can change content when the note re-renders. As such, | ||
// a new locator needs to be created after re-renders (and this can't be a | ||
// static property). | ||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ }); | ||
} | ||
|
||
public async waitFor() { | ||
await this.codeMirrorEditor.waitFor(); | ||
await this.noteTitleInput.waitFor(); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
packages/app-desktop/integration-tests/models/SettingsScreen.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
import { Page, Locator } from '@playwright/test'; | ||
|
||
export default class SettingsScreen { | ||
public readonly okayButton: Locator; | ||
public readonly appearanceTabButton: Locator; | ||
|
||
public constructor(page: Page) { | ||
this.okayButton = page.locator('button', { hasText: 'OK' }); | ||
this.appearanceTabButton = page.getByText('Appearance'); | ||
} | ||
|
||
public async waitFor() { | ||
await this.okayButton.waitFor(); | ||
await this.appearanceTabButton.waitFor(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#!/bin/sh | ||
|
||
echo "Running desktop integration tests..." | ||
|
||
export CI=true | ||
|
||
if test "$RUNNER_OS" = "Linux" ; then | ||
# The Ubuntu Github CI doesn't have a display server. | ||
# Start a virtual one with xvfb-run. | ||
xvfb-run -- yarn run playwright test | ||
else | ||
yarn run playwright test | ||
fi |
36 changes: 36 additions & 0 deletions
36
packages/app-desktop/integration-tests/util/activateMainMenuItem.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
|
||
import type { ElectronApplication } from '@playwright/test'; | ||
import type { MenuItem } from 'electron'; | ||
|
||
|
||
// Roughly based on | ||
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts | ||
|
||
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]). | ||
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => { | ||
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => { | ||
const activateItemInSubmenu = (submenu: MenuItem[]) => { | ||
for (const item of submenu) { | ||
if (item.label === menuItemLabel && item.visible) { | ||
// Found! | ||
item.click(); | ||
return true; | ||
} else if (item.submenu) { | ||
const foundItem = activateItemInSubmenu(item.submenu.items); | ||
|
||
if (foundItem) { | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
// No item found | ||
return false; | ||
}; | ||
|
||
const appMenu = Menu.getApplicationMenu(); | ||
return activateItemInSubmenu(appMenu.items); | ||
}, menuItemLabel); | ||
}; | ||
|
||
export default activateMainMenuItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { resolve, join, dirname } from 'path'; | ||
import { remove, mkdirp } from 'fs-extra'; | ||
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test'; | ||
import uuid from '@joplin/lib/uuid'; | ||
|
||
|
||
|
||
type JoplinFixtures = { | ||
electronApp: ElectronApplication; | ||
mainWindow: Page; | ||
}; | ||
|
||
// A custom fixture that loads an electron app. See | ||
// https://playwright.dev/docs/test-fixtures | ||
|
||
export const test = base.extend<JoplinFixtures>({ | ||
// Playwright fails if we don't use the object destructuring | ||
// pattern in the first argument. | ||
// | ||
// See https://github.com/microsoft/playwright/issues/8798 | ||
// | ||
// eslint-disable-next-line no-empty-pattern | ||
electronApp: async ({ }, use) => { | ||
const profilePath = resolve(join(dirname(__dirname), 'test-profile')); | ||
const profileSubdir = join(profilePath, uuid.createNano()); | ||
await mkdirp(profileSubdir); | ||
|
||
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir]; | ||
const electronApp = await electron.launch({ args: startupArgs }); | ||
|
||
await use(electronApp); | ||
|
||
await electronApp.firstWindow(); | ||
await electronApp.close(); | ||
await remove(profileSubdir); | ||
}, | ||
|
||
mainWindow: async ({ electronApp }, use) => { | ||
const window = await electronApp.firstWindow(); | ||
await use(window); | ||
}, | ||
}); | ||
|
||
|
||
export { expect } from '@playwright/test'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { defineConfig } from '@playwright/test'; | ||
|
||
// See https://playwright.dev/docs/test-configuration. | ||
export default defineConfig({ | ||
testDir: './integration-tests', | ||
|
||
// Only match .ts files (no compiled .js files) | ||
testMatch: '*.spec.ts', | ||
|
||
// Allow running tests in parallel (note: each Joplin instance | ||
// is given its own profile directory). | ||
fullyParallel: true, | ||
|
||
// Fail the build on CI if you accidentally left test.only in the source code. | ||
forbidOnly: !!process.env.CI, | ||
|
||
// Retry on CI only | ||
retries: process.env.CI ? 2 : 0, | ||
|
||
// Opt out of parallel tests on CI | ||
workers: process.env.CI ? 1 : undefined, | ||
|
||
// Reporter to use. See https://playwright.dev/docs/test-reporters | ||
reporter: process.env.CI ? 'line' : 'html', | ||
|
||
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. | ||
use: { | ||
// Base URL to use in actions like `await page.goto('/')`. | ||
// baseURL: 'http://127.0.0.1:3000', | ||
|
||
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer | ||
trace: 'on-first-retry', | ||
}, | ||
}); |
Oops, something went wrong.