From 0afa1f1f4a1688b453f519f8bfc4490488430486 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Tue, 13 Dec 2022 19:06:30 +0200 Subject: [PATCH] dx: introduce playwright for desktop e2e tests --- package-lock.json | 82 +++++++++++++++++++++++- package.json | 6 +- playwright.config.ts | 13 ++++ src/backend/loader.desktop.ts | 6 +- src/frontend/screens/central/view.ts | 20 +++--- test/e2e/{ => android}/README.md | 0 test/e2e/{ => android}/central.js | 0 test/e2e/{ => android}/compose.js | 0 test/e2e/{ => android}/connections.js | 0 test/e2e/{ => android}/drawer.js | 0 test/e2e/{ => android}/feed.js | 0 test/e2e/{ => android}/index.js | 1 + test/e2e/{ => android}/profile.js | 0 test/e2e/{ => android}/settings.js | 0 test/e2e/{ => android}/thread.js | 0 test/e2e/{ => android}/utils/recovery.js | 0 test/e2e/{ => android}/welcome.js | 0 test/e2e/desktop/01-welcome.test.ts | 54 ++++++++++++++++ test/e2e/desktop/02-central.test.ts | 49 ++++++++++++++ test/e2e/desktop/03-compose.test.ts | 43 +++++++++++++ test/e2e/desktop/utils.ts | 52 +++++++++++++++ tools/test-e2e-android | 2 +- tools/test-e2e-desktop | 38 +++++++++++ 23 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 playwright.config.ts rename test/e2e/{ => android}/README.md (100%) rename test/e2e/{ => android}/central.js (100%) rename test/e2e/{ => android}/compose.js (100%) rename test/e2e/{ => android}/connections.js (100%) rename test/e2e/{ => android}/drawer.js (100%) rename test/e2e/{ => android}/feed.js (100%) rename test/e2e/{ => android}/index.js (99%) rename test/e2e/{ => android}/profile.js (100%) rename test/e2e/{ => android}/settings.js (100%) rename test/e2e/{ => android}/thread.js (100%) rename test/e2e/{ => android}/utils/recovery.js (100%) rename test/e2e/{ => android}/welcome.js (100%) create mode 100644 test/e2e/desktop/01-welcome.test.ts create mode 100644 test/e2e/desktop/02-central.test.ts create mode 100644 test/e2e/desktop/03-compose.test.ts create mode 100644 test/e2e/desktop/utils.ts create mode 100755 tools/test-e2e-desktop diff --git a/package-lock.json b/package-lock.json index 77ad7a8f9..c35fdd195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2238,6 +2238,16 @@ "resolved": "https://registry.npmjs.org/@most/prelude/-/prelude-1.8.0.tgz", "integrity": "sha512-t1CcURpZzfmBA6fEWwqmCqeNzWAj1w2WqEmCk/2yXMe/p8Ut000wFmVKMy8A1Rl9VVxZEZ5nBHd/pU0dR4bv/w==" }, + "@playwright/test": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz", + "integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.1" + } + }, "@react-native-async-storage/async-storage": { "version": "1.15.17", "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.17.tgz", @@ -6836,6 +6846,14 @@ "optional": true, "requires": { "follow-redirects": "^1.14.8" + }, + "dependencies": { + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "optional": true + } } }, "babel-runtime": { @@ -7893,8 +7911,7 @@ "follow-redirects": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", - "optional": true + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" }, "forever-agent": { "version": "0.6.1", @@ -15085,6 +15102,61 @@ } } }, + "electron-playwright-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/electron-playwright-helpers/-/electron-playwright-helpers-1.2.2.tgz", + "integrity": "sha512-DQojB9noTJdovq988c32Z9RlU9Q+wf7avv2NesQ9QaxyiAkBhP/6tJVXZ9VYeeGAd9PEXwPfqzD58MwY5vCpOA==", + "dev": true, + "requires": { + "@electron/asar": "^3.2.2" + }, + "dependencies": { + "@electron/asar": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.2.tgz", + "integrity": "sha512-32fMU68x8a6zvxtC1IC/BhPDKTh8rQjdmwEplj3CDpnkcwBzZVN9v/8cK0LJqQ0FOQQVZW8BWZ1S6UU53TYR4w==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + } + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + } + } + }, "electron-publish": { "version": "22.13.1", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.13.1.tgz", @@ -22627,6 +22699,12 @@ } } }, + "playwright-core": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz", + "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==", + "dev": true + }, "plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", diff --git a/package.json b/package.json index c8d563bdc..ea07725b5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "update-top-backers": "./tools/update-top-backers.mjs && pretty-quick --pattern \"**/backers.ts\"", "test-e2e-android": "./tools/test-e2e-android", "test-smoke-android": "./tools/test-smoke-android", + "test-e2e-desktop": "./tools/test-e2e-desktop", "apply-prettier": "pretty-quick --staged --pattern \"**/*.*(ts|tsx|js|jsx)\"", "validate-license-year": "./tools/validate-license-year.sh", "precommit-hook": "npm run apply-prettier && npm run validate-license-year", @@ -167,6 +168,7 @@ "devDependencies": { "@babel/core": "^7.16.0", "@babel/runtime": "^7.16.3", + "@playwright/test": "1.28.1", "@react-native-community/cli": "~6.4.0", "@types/electron": "1.6.10", "@types/node": "~12.20.10", @@ -182,6 +184,7 @@ "cross-env": "^7.0.3", "css-loader": "5.2.7", "electron-builder": "22.13.1", + "electron-playwright-helpers": "1.2.2", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", "graceful-fs": "~4.2.9", @@ -199,6 +202,7 @@ "node-gyp-build": "4.3.0", "ora": "^5.4.1", "patch-package": "6.4.7", + "playwright-core": "1.28.1", "prettier": "~2.5.1", "pretty-quick": "~3.1.3", "propagate-replacement-fields": "1.3.0", @@ -220,4 +224,4 @@ "react-native": { "os": "react-native-os-staltz" } -} \ No newline at end of file +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..4dcd3d155 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2021-2022 The Manyverse Authors +// +// SPDX-License-Identifier: CC0-1.0 + +import {PlaywrightTestConfig} from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: './test/e2e/desktop', + maxFailures: 2, + workers: 1, // no parallelism +}; + +export default config; diff --git a/src/backend/loader.desktop.ts b/src/backend/loader.desktop.ts index 8a7442ef7..08b509f8c 100644 --- a/src/backend/loader.desktop.ts +++ b/src/backend/loader.desktop.ts @@ -40,13 +40,13 @@ process.env ??= {}; app.setName('manyverse'); // Set default directories -app.setPath('userData', path.join(app.getPath('appData'), 'manyverse')); +process.env.MV_USER_DATA ??= path.join(app.getPath('appData'), 'manyverse'); +app.setPath('userData', process.env.MV_USER_DATA); process.env.APP_DATA_DIR = app.getAppPath(); process.env.APP_TMP_DIR = app.getPath('temp'); process.env.OS = os.platform(); process.env.SHARED_SSB_DIR = path.resolve(os.homedir(), '.ssb'); -process.env.SSB_DIR = - process.env.SSB_DIR ?? path.resolve(app.getPath('userData'), 'ssb'); +process.env.SSB_DIR ??= path.resolve(app.getPath('userData'), 'ssb'); // Set global variables process.env.MANYVERSE_PLATFORM = 'desktop'; diff --git a/src/frontend/screens/central/view.ts b/src/frontend/screens/central/view.ts index 45419ee0e..83fe8c4dd 100644 --- a/src/frontend/screens/central/view.ts +++ b/src/frontend/screens/central/view.ts @@ -59,6 +59,10 @@ class CurrentTabPage extends PureComponent<{ } = this.props; const shown = styles.pageShown; const hidden = styles.pageHidden; + const isPublic = currentTab === 'public'; + const isPrivate = currentTab === 'private'; + const isActivity = currentTab === 'activity'; + const isConnections = currentTab === 'connections'; const fabSection = Platform.OS === 'web' @@ -73,20 +77,18 @@ class CurrentTabPage extends PureComponent<{ : h(FloatingAction, fab); return h(Fragment, [ - h(View, {style: [currentTab === 'public' ? shown : hidden]}, [ + h(View, {style: [isPublic ? shown : hidden]}, [ publicTab, - fabSection, + isPublic ? fabSection : null, ]), - h(View, {style: [currentTab === 'private' ? shown : hidden]}, [ + h(View, {style: [isPrivate ? shown : hidden]}, [ privateTab, - fabSection, + isPrivate ? fabSection : null, ]), - h(View, {style: [currentTab === 'activity' ? shown : hidden]}, [ - activityTab, - ]), - h(View, {style: [currentTab === 'connections' ? shown : hidden]}, [ + h(View, {style: [isActivity ? shown : hidden]}, [activityTab]), + h(View, {style: [isConnections ? shown : hidden]}, [ connectionsTab, - fabSection, + isConnections ? fabSection : null, ]), ]); } diff --git a/test/e2e/README.md b/test/e2e/android/README.md similarity index 100% rename from test/e2e/README.md rename to test/e2e/android/README.md diff --git a/test/e2e/central.js b/test/e2e/android/central.js similarity index 100% rename from test/e2e/central.js rename to test/e2e/android/central.js diff --git a/test/e2e/compose.js b/test/e2e/android/compose.js similarity index 100% rename from test/e2e/compose.js rename to test/e2e/android/compose.js diff --git a/test/e2e/connections.js b/test/e2e/android/connections.js similarity index 100% rename from test/e2e/connections.js rename to test/e2e/android/connections.js diff --git a/test/e2e/drawer.js b/test/e2e/android/drawer.js similarity index 100% rename from test/e2e/drawer.js rename to test/e2e/android/drawer.js diff --git a/test/e2e/feed.js b/test/e2e/android/feed.js similarity index 100% rename from test/e2e/feed.js rename to test/e2e/android/feed.js diff --git a/test/e2e/index.js b/test/e2e/android/index.js similarity index 99% rename from test/e2e/index.js rename to test/e2e/android/index.js index 5e5371da4..e5c9680de 100644 --- a/test/e2e/index.js +++ b/test/e2e/android/index.js @@ -22,6 +22,7 @@ const localCapabilities = { __dirname, '..', '..', + '..', 'android', 'app', 'build', diff --git a/test/e2e/profile.js b/test/e2e/android/profile.js similarity index 100% rename from test/e2e/profile.js rename to test/e2e/android/profile.js diff --git a/test/e2e/settings.js b/test/e2e/android/settings.js similarity index 100% rename from test/e2e/settings.js rename to test/e2e/android/settings.js diff --git a/test/e2e/thread.js b/test/e2e/android/thread.js similarity index 100% rename from test/e2e/thread.js rename to test/e2e/android/thread.js diff --git a/test/e2e/utils/recovery.js b/test/e2e/android/utils/recovery.js similarity index 100% rename from test/e2e/utils/recovery.js rename to test/e2e/android/utils/recovery.js diff --git a/test/e2e/welcome.js b/test/e2e/android/welcome.js similarity index 100% rename from test/e2e/welcome.js rename to test/e2e/android/welcome.js diff --git a/test/e2e/desktop/01-welcome.test.ts b/test/e2e/desktop/01-welcome.test.ts new file mode 100644 index 000000000..1d57cf9d9 --- /dev/null +++ b/test/e2e/desktop/01-welcome.test.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2022 The Manyverse Authors +// +// SPDX-License-Identifier: CC0-1.0 + +import {expect, test} from '@playwright/test'; +import {Page} from 'playwright-core'; +import setup from './utils'; + +const ctx = setup(); + +let page: Page; + +test.describe('welcome', () => { + test('overview', async () => { + page = await ctx.electronApp!.firstWindow(); + + expect(await page.title()).toBe('Manyverse'); + expect(await page.waitForSelector('"Welcome to Manyverse!"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=0').click(); + }); + + test('off the grid', async () => { + expect(await page.waitForSelector('"Off-the-grid"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=1').click(); + }); + + test('connections', async () => { + expect(await page.waitForSelector('"Many ways to connect"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=2').click(); + }); + + test('moderation', async () => { + expect(await page.waitForSelector('"Shared moderation"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=3').click(); + }); + + test('permanence', async () => { + expect(await page.waitForSelector('"Permanence"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=4').click(); + }); + + test('in construction', async () => { + expect(await page.waitForSelector('"In construction!"')).toBeTruthy(); + await page.locator('text="Continue" >> nth=5').click(); + }); + + test('account', async () => { + expect( + await page.waitForSelector('"Is this your first time?"'), + ).toBeTruthy(); + await page.locator('text="Create account"').click(); + await page.waitForTimeout(5000); // give it time to save to localstorage + }); +}); diff --git a/test/e2e/desktop/02-central.test.ts b/test/e2e/desktop/02-central.test.ts new file mode 100644 index 000000000..9d55dccfe --- /dev/null +++ b/test/e2e/desktop/02-central.test.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2022 The Manyverse Authors +// +// SPDX-License-Identifier: CC0-1.0 + +import {expect, test} from '@playwright/test'; +import {Page} from 'playwright-core'; +import setup from './utils'; + +const ctx = setup(); + +let page: Page; + +test.describe('central', () => { + test('public tab', async () => { + page = await ctx.electronApp!.firstWindow(); + + expect(await page.waitForSelector('"Public posts"')).toBeTruthy(); + expect( + await page.waitForSelector(':has-text("Where is everybody")'), + ).toBeTruthy(); + }); + + test('private tab', async () => { + await page.locator('text="Private"').click(); + expect(await page.waitForSelector('"Private chats"')).toBeTruthy(); + expect( + await page.waitForSelector(':has-text("Where is everybody")'), + ).toBeTruthy(); + }); + + test('activity tab', async () => { + await page.locator('text="Activity"').click(); + expect(await page.waitForSelector('"Activity"')).toBeTruthy(); + expect( + await page.waitForSelector(':has-text("Where is everybody")'), + ).toBeTruthy(); + }); + + test('connections tab', async () => { + await page.locator('text="Connections"').click(); + expect(await page.waitForSelector('"Connected peers"')).toBeTruthy(); + expect(await page.waitForSelector('"Not connected"')).toBeTruthy(); + }); + + test('back to public tab', async () => { + await page.locator('text="Public"').click(); + expect(await page.waitForSelector('"Public posts"')).toBeTruthy(); + }); +}); diff --git a/test/e2e/desktop/03-compose.test.ts b/test/e2e/desktop/03-compose.test.ts new file mode 100644 index 000000000..e5ca1fe51 --- /dev/null +++ b/test/e2e/desktop/03-compose.test.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2022 The Manyverse Authors +// +// SPDX-License-Identifier: CC0-1.0 + +import {expect, test} from '@playwright/test'; +import {Page} from 'playwright-core'; +import setup from './utils'; + +const ctx = setup(); + +let page: Page; + +test.describe('compose', () => { + test('opens', async () => { + page = await ctx.electronApp!.firstWindow(); + + const selector = '[aria-label="Floating Action Button"]'; + expect(await page.waitForSelector(selector)).toBeTruthy(); + await page.locator(selector).click(); + }); + + test('can write on the text field', async () => { + const fieldSelector = '[placeholder="Write a public message"]'; + expect(await page.waitForSelector(fieldSelector)).toBeTruthy(); + await page.locator(fieldSelector).fill('Hello mom'); + + const buttonSelector = '"Preview"'; + expect(await page.waitForSelector(buttonSelector)).toBeTruthy(); + await page.locator(buttonSelector).click(); + + const buttonSelector2 = '"Publish"'; + expect(await page.waitForSelector(buttonSelector2)).toBeTruthy(); + await page.locator(buttonSelector2).click(); + }); + + test('can see my post on the feed', async () => { + await page.waitForSelector('"Preview"', {state: 'hidden' as any}); + await page.waitForSelector('"Publish"', {state: 'hidden' as any}); + + const postSelector = '"Hello mom"'; + expect(await page.waitForSelector(postSelector)).toBeTruthy(); + }); +}); diff --git a/test/e2e/desktop/utils.ts b/test/e2e/desktop/utils.ts new file mode 100644 index 000000000..7dc6dde41 --- /dev/null +++ b/test/e2e/desktop/utils.ts @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 The Manyverse Authors +// +// SPDX-License-Identifier: CC0-1.0 + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {test} from '@playwright/test'; +import {findLatestBuild, parseElectronApp} from 'electron-playwright-helpers'; +import {ElectronApplication, _electron as electron} from 'playwright-core'; + +const sessionDir = path.join(os.tmpdir(), 'manyverse-e2e-test'); +if (!fs.existsSync(sessionDir)) { + fs.mkdirSync(sessionDir); +} + +export default function setup(subtest?: string) { + const ctx = {} as {electronApp?: ElectronApplication}; + + test.beforeAll(async () => { + const latestBuild = findLatestBuild('desktop/outputs'); + + // parse the directory and find paths and other info + const originalConsoleLog = console.log; + console.log = () => {}; + const appInfo = parseElectronApp(latestBuild); + console.log = originalConsoleLog; + if (!appInfo.arch) appInfo.arch = 'x64'; + if (!appInfo.executable.endsWith('/manyverse')) { + appInfo.executable += '/manyverse'; + } + + // Execute + ctx.electronApp = await electron.launch({ + args: [appInfo.main], + env: { + ...process.env, + MV_USER_DATA: path.join(sessionDir, subtest ?? 'default'), + }, + executablePath: appInfo.executable, + locale: 'en', + }); + }); + + test.afterAll(async () => { + // We should do `await electronApp.close()` but we have problems with + // terminating the worker_threads instance, so we use an overkill instead. + ctx.electronApp?.process().kill('SIGKILL'); + }); + + return ctx; +} diff --git a/tools/test-e2e-android b/tools/test-e2e-android index e1ad5a8ab..b2bed9671 100755 --- a/tools/test-e2e-android +++ b/tools/test-e2e-android @@ -36,5 +36,5 @@ if [ $ANSWER == "Y" ]; then popd $(npm bin)/appium --port 4995 > appium.log & echo $! > appium-server.pid sleep 5 - $(npm bin)/tape test/e2e/index.js | tap-spec + $(npm bin)/tape test/e2e/android/index.js | tap-spec fi diff --git a/tools/test-e2e-desktop b/tools/test-e2e-desktop new file mode 100755 index 000000000..5ff000829 --- /dev/null +++ b/tools/test-e2e-desktop @@ -0,0 +1,38 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2022 The Manyverse Authors +# +# SPDX-License-Identifier: CC0-1.0 + +function cleanUp { + if [ -f "appium-server.pid" ]; then + if ps -p $(cat appium-server.pid)> /dev/null; then + kill -KILL $(cat appium-server.pid) + fi + rm appium-server.pid + rm appium.log + fi +} + +set -eEu -o pipefail +shopt -s extdebug +IFS=$'\n\t' +trap 'onFailure $?' ERR +trap cleanUp EXIT + +function onFailure() { + echo "Unhandled script error $1 at ${BASH_SOURCE[0]}:${BASH_LINENO[0]}" >&2 + exit 1 +} + +FILE=desktop/outputs/linux-unpacked/manyverse +if [[ -f "$FILE" ]]; then + echo "Desktop build already exists" +else + echo "Desktop build does not exist, building now" + EB_PUBLISH=never npm run release-desktop-linux +fi + +rm -rf /tmp/manyverse-e2e-test + +$(npm bin)/playwright test