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
-191
View File
@@ -1,191 +0,0 @@
import { platform } from 'node:os';
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test';
import { test } from './fixture';
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'
);
});
-122
View File
@@ -1,122 +0,0 @@
/* eslint-disable no-empty-pattern */
import crypto from 'node:crypto';
import { join, resolve } from 'node:path';
import {
enableCoverage,
istanbulTempDir,
test as base,
testResultDir,
} from '@affine-test/kit/playwright';
import type { Page } from '@playwright/test';
import fs from 'fs-extra';
import type { ElectronApplication } from 'playwright';
import { _electron as electron } from 'playwright';
import { removeWithRetry } from '../tests/utils';
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,
});
if (!process.env.CI) {
await electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].webContents.openDevTools({
mode: 'detach',
});
});
}
// wat 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();
},
electronApp: async ({}, use) => {
// a random id to avoid conflicts between tests
const id = generateUUID();
const ext = process.platform === 'win32' ? '.cmd' : '';
const dist = resolve(__dirname, '..', 'dist');
const clonedDist = resolve(__dirname, '../e2e-dist-' + id);
await fs.copy(dist, clonedDist);
const packageJson = await fs.readJSON(
resolve(__dirname, '..', '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 electronApp = await electron.launch({
args: [clonedDist],
executablePath: resolve(
__dirname,
'..',
'node_modules',
'.bin',
`electron${ext}`
),
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);
},
});
-8
View File
@@ -1,8 +0,0 @@
import { execSync } from 'node:child_process';
import { join } from 'node:path';
export default async function () {
execSync('yarn ts-node-esm scripts/', {
cwd: join(__dirname, '..'),
});
}
-118
View File
@@ -1,118 +0,0 @@
import path from 'node:path';
import { expect } from '@playwright/test';
import fs from 'fs-extra';
import { test } from './fixture';
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();
});
+1 -2
View File
@@ -14,10 +14,10 @@
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
"build:dev": "NODE_ENV=development zx scripts/build-layers.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts",
"make-squirrel": "yarn ts-node-esm -T scripts/make-squirrel.mts"
},
"config": {
@@ -43,7 +43,6 @@
"@electron/remote": "2.0.11",
"@reforged/maker-appimage": "^3.3.1",
"@toeverything/infra": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/uuid": "^9.0.3",
"cross-env": "7.0.3",
"electron": "^26.1.0",
-28
View File
@@ -1,28 +0,0 @@
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',
testIgnore: '**/lib/**',
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;
@@ -1,11 +1,10 @@
import path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { removeWithRetry } from '@affine-test/kit/utils/utils';
import { v4 } from 'uuid';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../../tests/utils';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
@@ -1,10 +1,10 @@
import path from 'node:path';
import { SqliteConnection } from '@affine/native';
import { removeWithRetry } from '@affine-test/kit/utils/utils';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { applyUpdate, Doc as YDoc } from 'yjs';
import { removeWithRetry } from '../../../../tests/utils';
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
const tmpDir = path.join(__dirname, 'tmp');
@@ -1,11 +1,11 @@
import path from 'node:path';
import { removeWithRetry } from '@affine-test/kit/utils/utils';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, expect, test, vi } from 'vitest';
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { removeWithRetry } from '../../../../tests/utils';
import { dbSubjects } from '../subjects';
const tmpDir = path.join(__dirname, 'tmp');
@@ -1,11 +1,10 @@
import path from 'node:path';
import { removeWithRetry } from '@affine-test/kit/utils/utils';
import fs from 'fs-extra';
import { v4 } from 'uuid';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../../tests/utils';
const tmpDir = path.join(__dirname, 'tmp');
const appDataPath = path.join(tmpDir, 'app-data');
@@ -1,10 +1,10 @@
import assert from 'node:assert';
import path from 'node:path';
import { removeWithRetry } from '@affine-test/kit/utils/utils';
import fs from 'fs-extra';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { removeWithRetry } from '../../../tests/utils';
import type { MainIPCHandlerMap } from '../exposed';
const registeredHandlers = new Map<
-26
View File
@@ -1,26 +0,0 @@
import { setTimeout } from 'node:timers/promises';
import fs from 'fs-extra';
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;
}
+2 -2
View File
@@ -8,13 +8,13 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"types": ["node"],
"outDir": "dist",
"outDir": "lib",
"moduleResolution": "node",
"resolveJsonModule": true,
"noImplicitOverride": true
},
"include": ["./src"],
"exclude": ["node_modules", "out", "dist", "**/__tests__/**/*"],
"exclude": ["node_modules", "lib", "dist", "**/__tests__/**/*"],
"references": [
{
"path": "../../packages/infra"
+1 -1
View File
@@ -3,7 +3,7 @@
"compilerOptions": {
"composite": true
},
"include": ["**/__tests__/**/*", "./tests", "./e2e"],
"include": ["**/__tests__/**/*", "./tests"],
"references": [
{
"path": "./tsconfig.json"