test(electron): add cloud test (#4184)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
Alex Yang
2023-09-17 13:26:06 -07:00
committed by GitHub
parent 40e094dcdd
commit cdad7edf15
28 changed files with 366 additions and 83 deletions
@@ -0,0 +1,34 @@
import { test } from '@affine-test/kit/electron';
import {
createRandomUser,
enableCloudWorkspace,
loginUser,
} from '@affine-test/kit/utils/cloud';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
let user: {
name: string;
email: string;
password: string;
};
test.beforeEach(async () => {
user = await createRandomUser();
});
test.beforeEach(async ({ page }) => {
await loginUser(page, user.email);
});
test('new page', async ({ page }) => {
await page.reload();
await waitForEditorLoad(page);
await createLocalWorkspace(
{
name: 'test',
},
page
);
await enableCloudWorkspace(page);
});
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@affine-test/affine-desktop-cloud",
"private": true,
"scripts": {
"e2e": "DEBUG=pw:browser yarn playwright test"
},
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine-test/kit": "workspace:*",
"@playwright/test": "^1.37.1",
"@types/fs-extra": "^11.0.1",
"fs-extra": "^11.1.1"
},
"version": "0.9.0-canary.8"
}
@@ -0,0 +1,61 @@
import type { PlaywrightTestConfig } from '@playwright/test';
// import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './e2e',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,
use: {
viewport: { width: 1440, height: 800 },
},
reporter: process.env.CI ? 'github' : 'list',
webServer: [
// Intentionally not building the web, reminds you to run it by yourself.
{
command: 'yarn -T run start:web-static',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
env: {
COVERAGE: process.env.COVERAGE || 'false',
},
},
{
command: 'yarn workspace @affine/server start',
port: 3010,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
env: {
DATABASE_URL:
process.env.DATABASE_URL ??
'postgresql://affine:affine@localhost:5432/affine',
NODE_ENV: 'development',
AFFINE_ENV: process.env.AFFINE_ENV ?? 'dev',
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
},
},
],
};
if (process.env.CI) {
config.retries = 3;
config.workers = '50%';
}
export default config;
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"outDir": "lib"
},
"include": ["e2e"],
"references": [
{
"path": "../kit"
},
{
"path": "../fixtures"
}
]
}
+190
View File
@@ -0,0 +1,190 @@
import { platform } from 'node:os';
import { test } from '@affine-test/kit/electron';
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test';
test('new page', async ({ page, workspace }) => {
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
const flavour = (await workspace.current()).flavour;
expect(flavour).toBe('local');
});
// macOS only
if (platform() === 'darwin') {
test('app sidebar router forward/back', async ({ page }) => {
await page.getByTestId('help-island').click();
await page.getByTestId('easy-guide').click();
await page.getByTestId('onboarding-modal-next-button').click();
await page.getByTestId('onboarding-modal-close-button').click();
{
// create pages
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-doc-page-block-title');
await page.type('.affine-doc-page-block-title', 'test1', {
delay: 100,
});
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-doc-page-block-title');
await page.type('.affine-doc-page-block-title', 'test2', {
delay: 100,
});
await page.waitForTimeout(500);
await page.getByTestId('new-page-button').click({
delay: 100,
});
await page.waitForSelector('v-line');
await page.focus('.affine-doc-page-block-title');
await page.type('.affine-doc-page-block-title', 'test3', {
delay: 100,
});
}
{
const title = (await page
.locator('.affine-doc-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test3');
}
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
await page.waitForTimeout(1000);
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
await page.waitForTimeout(1000);
{
const title = (await page
.locator('.affine-doc-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test1');
}
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
await page.waitForTimeout(1000);
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
await page.waitForTimeout(1000);
{
const title = (await page
.locator('.affine-doc-page-block-title')
.textContent()) as string;
expect(title.trim()).toBe('test3');
}
});
}
test('clientBorder value should disable by default on window', async ({
page,
}) => {
await clickSideBarSettingButton(page);
await page.waitForTimeout(1000);
const settingItem = page.locator(
'[data-testid="client-border-style-trigger"]'
);
expect(await settingItem.locator('input').inputValue()).toEqual(
process.platform === 'win32' ? 'off' : 'on'
);
});
test('app theme', async ({ page, electronApp }) => {
const root = page.locator('html');
{
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);
expect(themeMode).toBe('light');
const theme = await electronApp.evaluate(({ nativeTheme }) => {
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
});
expect(theme).toBe('light');
}
{
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('appearance-panel-trigger').click();
await page.waitForTimeout(50);
await page.getByTestId('dark-theme-trigger').click();
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);
expect(themeMode).toBe('dark');
const theme = await electronApp.evaluate(({ nativeTheme }) => {
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
});
expect(theme).toBe('dark');
}
});
test('affine onboarding button', async ({ page }) => {
await page.getByTestId('help-island').click();
await page.getByTestId('easy-guide').click();
const onboardingModal = page.locator('[data-testid=onboarding-modal]');
expect(await onboardingModal.isVisible()).toEqual(true);
const switchVideo = page.locator(
'[data-testid=onboarding-modal-switch-video]'
);
expect(await switchVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-next-button').click();
const editingVideo = page.locator(
'[data-testid=onboarding-modal-editing-video]'
);
expect(await editingVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-close-button').click();
expect(await onboardingModal.isVisible()).toEqual(false);
});
test('windows only check', async ({ page }) => {
const windowOnlyUI = page.locator('[data-platform-target=win32]');
if (process.platform === 'win32') {
await expect(windowOnlyUI).toBeVisible();
} else {
await expect(windowOnlyUI).not.toBeVisible();
}
});
test('delete workspace', async ({ page }) => {
await page.getByTestId('current-workspace').click();
await page.getByTestId('new-workspace').click();
await page.getByTestId('create-workspace-input').type('Delete Me', {
delay: 100,
});
await page.getByTestId('create-workspace-create-button').click({
delay: 100,
});
await page.getByTestId('create-workspace-continue-button').click({
delay: 100,
});
await page.waitForTimeout(1000);
await clickSideBarSettingButton(page);
await page.getByTestId('current-workspace-label').click();
expect(await page.getByTestId('workspace-name-input').inputValue()).toBe(
'Delete Me'
);
const contentElement = await page.getByTestId('setting-modal-content');
const boundingBox = await contentElement.boundingBox();
if (!boundingBox) {
throw new Error('boundingBox is null');
}
await page.mouse.move(
boundingBox.x + boundingBox.width / 2,
boundingBox.y + boundingBox.height / 2
);
await page.mouse.wheel(0, 500);
await page.getByTestId('delete-workspace-button').click();
await page.getByTestId('delete-workspace-input').type('Delete Me');
await page.getByTestId('delete-workspace-confirm-button').click();
await page.waitForTimeout(1000);
expect(await page.getByTestId('workspace-name').textContent()).toBe(
'Demo Workspace'
);
});
+117
View File
@@ -0,0 +1,117 @@
import path from 'node:path';
import { test } from '@affine-test/kit/electron';
import { expect } from '@playwright/test';
import fs from 'fs-extra';
test('check workspace has a DB file', async ({ appInfo, workspace }) => {
const w = await workspace.current();
const dbPath = path.join(
appInfo.sessionData,
'workspaces',
w.id,
'storage.db'
);
// check if db file exists
expect(await fs.exists(dbPath)).toBe(true);
});
test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
// goto workspace setting
await page.getByTestId('workspace-list-item').click();
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp-dir');
// move db file to tmp folder
await page.evaluate(tmpPath => {
window.apis?.dialog.setFakeDialogResult({
filePath: tmpPath,
});
}, tmpPath);
await page.getByTestId('move-folder').click();
// check if db file exists
await page.waitForSelector('text="Move folder success"');
expect(await fs.exists(tmpPath)).toBe(true);
// check if db file exists under tmpPath (a file ends with .affine)
const files = await fs.readdir(tmpPath);
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
});
//TODO:fix test
test.fixme('export then add', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
await page.focus('.affine-doc-page-block-title');
await page.fill('.affine-doc-page-block-title', 'test1');
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
const originalId = w.id;
const newWorkspaceName = 'new-test-name';
// goto workspace setting
await page.getByTestId('workspace-list-item').click();
const input = page.getByTestId('workspace-name-input');
await expect(input).toBeVisible();
// change workspace name
await input.fill(newWorkspaceName);
await page.getByTestId('save-workspace-name').click();
await page.waitForSelector('text="Update workspace name success"');
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp.db');
// export db file to tmp folder
await page.evaluate(tmpPath => {
window.apis?.dialog.setFakeDialogResult({
filePath: tmpPath,
});
}, tmpPath);
await page.getByTestId('export-affine-backup').click();
await page.waitForSelector('text="Export success"');
await page.waitForTimeout(1000);
expect(await fs.exists(tmpPath)).toBe(true);
await page.getByTestId('modal-close-button').click();
// add workspace
// we are reusing the same db file so that we don't need to maintain one
// in the codebase
await page.getByTestId('current-workspace').click();
await page.getByTestId('add-or-new-workspace').click();
await page.evaluate(tmpPath => {
window.apis?.dialog.setFakeDialogResult({
filePath: tmpPath,
});
}, tmpPath);
// load the db file
await page.getByTestId('add-workspace').click();
// should show "Added Successfully" dialog
await page.waitForSelector('text="Added Successfully"');
await page.getByTestId('create-workspace-continue-button').click();
// sleep for a while to wait for the workspace to be added :D
await page.waitForTimeout(2000);
const newWorkspace = await workspace.current();
expect(newWorkspace.id).not.toBe(originalId);
// check its name is correct
await expect(page.getByTestId('workspace-name')).toHaveText(newWorkspaceName);
// find button which has the title "test1"
const test1PageButton = await page.waitForSelector(`text="test1"`);
await test1PageButton.click();
const title = page.locator('[data-block-is-title] >> text="test1"');
await expect(title).toBeVisible();
});
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@affine-test/affine-desktop",
"private": true,
"scripts": {
"e2e": "DEBUG=pw:browser yarn playwright test"
},
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine-test/kit": "workspace:*",
"@playwright/test": "^1.37.1",
"@types/fs-extra": "^11.0.1",
"fs-extra": "^11.1.1"
},
"version": "0.9.0-canary.8"
}
+27
View File
@@ -0,0 +1,27 @@
import type { PlaywrightTestConfig } from '@playwright/test';
// import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './e2e',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,
use: {
viewport: { width: 1440, height: 800 },
},
};
if (process.env.CI) {
config.retries = 3;
config.workers = '50%';
}
export default config;
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"outDir": "lib"
},
"include": ["e2e"],
"references": [
{
"path": "../kit"
},
{
"path": "../fixtures"
}
]
}
+130
View File
@@ -0,0 +1,130 @@
import crypto from 'node:crypto';
import { join, resolve } from 'node:path';
import type { Page } from '@playwright/test';
import fs from 'fs-extra';
import type { ElectronApplication } from 'playwright';
import { _electron as electron } from 'playwright';
import {
enableCoverage,
istanbulTempDir,
test as base,
testResultDir,
} from './playwright';
import { removeWithRetry } from './utils/utils';
const projectRoot = resolve(__dirname, '..', '..');
const electronRoot = resolve(projectRoot, 'apps', 'electron');
function generateUUID() {
return crypto.randomUUID();
}
type RoutePath = 'setting';
export const test = base.extend<{
electronApp: ElectronApplication;
appInfo: {
appPath: string;
appData: string;
sessionData: string;
};
router: {
goto: (path: RoutePath) => Promise<void>;
};
}>({
page: async ({ electronApp }, use) => {
const page = await electronApp.firstWindow();
await page.getByTestId('onboarding-modal-close-button').click({
delay: 100,
});
// wait for blocksuite to be loaded
await page.waitForSelector('v-line');
if (enableCoverage) {
await fs.promises.mkdir(istanbulTempDir, { recursive: true });
await page.exposeFunction(
'collectIstanbulCoverage',
(coverageJSON?: string) => {
if (coverageJSON)
fs.writeFileSync(
join(
istanbulTempDir,
`playwright_coverage_${generateUUID()}.json`
),
coverageJSON
);
}
);
}
await use(page as Page);
if (enableCoverage) {
await page.evaluate(() =>
// @ts-expect-error
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
);
}
await page.close();
},
// eslint-disable-next-line no-empty-pattern
electronApp: async ({}, use) => {
// a random id to avoid conflicts between tests
const id = generateUUID();
const ext = process.platform === 'win32' ? '.cmd' : '';
const dist = resolve(electronRoot, 'dist');
const clonedDist = resolve(electronRoot, 'e2e-dist-' + id);
await fs.copy(dist, clonedDist);
const packageJson = await fs.readJSON(
resolve(electronRoot, 'package.json')
);
// overwrite the app name
packageJson.name = 'affine-test-' + id;
// overwrite the path to the main script
packageJson.main = './main.js';
// write to the cloned dist
await fs.writeJSON(resolve(clonedDist, 'package.json'), packageJson);
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value) {
env[key] = value;
}
}
if (process.env.DEV_SERVER_URL) {
env.DEV_SERVER_URL = process.env.DEV_SERVER_URL;
}
const electronApp = await electron.launch({
args: [clonedDist],
env,
executablePath: resolve(
electronRoot,
'node_modules',
'.bin',
`electron${ext}`
),
cwd: clonedDist,
recordVideo: {
dir: testResultDir,
},
colorScheme: 'light',
});
await use(electronApp);
try {
await removeWithRetry(clonedDist);
} catch (error) {
console.log(error);
}
},
appInfo: async ({ electronApp }, use) => {
const appInfo = await electronApp.evaluate(async ({ app }) => {
return {
appPath: app.getAppPath(),
appData: app.getPath('appData'),
sessionData: app.getPath('sessionData'),
};
});
await use(appInfo);
},
});
+1
View File
@@ -4,6 +4,7 @@
"type": "module",
"version": "0.9.0-canary.12",
"exports": {
"./electron": "./electron.ts",
"./playwright": "./playwright.ts",
"./utils/*": "./utils/*.ts"
},
+26
View File
@@ -1,4 +1,7 @@
import { setTimeout } from 'node:timers/promises';
import type { Page } from '@playwright/test';
import fs from 'fs-extra';
export async function waitForLogMessage(
page: Page,
@@ -12,3 +15,26 @@ export async function waitForLogMessage(
});
});
}
export async function removeWithRetry(
filePath: string,
maxRetries = 5,
delay = 500
) {
for (let i = 0; i < maxRetries; i++) {
try {
await fs.remove(filePath);
console.log(`File ${filePath} successfully deleted.`);
return true;
} catch (err: any) {
if (err.code === 'EBUSY' || err.code === 'EPERM') {
console.log(`File ${filePath} is busy or locked, retrying...`);
await setTimeout(delay);
} else {
console.error(`Failed to delete file ${filePath}:`, err);
}
}
}
// Add a return statement here to ensure that a value is always returned
return false;
}
+9 -1
View File
@@ -19,12 +19,20 @@ export async function createLocalWorkspace(
// open create workspace modal
await page.getByTestId('new-workspace').click();
const isDesktop: boolean = await page.evaluate(() => {
return !!window.appInfo?.electron;
}, []);
// input workspace name
await page.getByPlaceholder('Set a Workspace name').click();
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
// click create button
return page.getByRole('button', { name: 'Create' }).click({
await page.getByRole('button', { name: 'Create' }).click({
delay: 500,
});
if (isDesktop) {
await page.getByTestId('create-workspace-continue-button').click();
}
}