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

View File

@@ -101,6 +101,7 @@ jobs:
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
@@ -125,21 +126,15 @@ jobs:
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Upload desktop dist
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: ./apps/electron/dist
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
run: yarn workspace @affine-test/affine-desktop e2e
env:
COVERAGE: true
@@ -149,7 +144,7 @@ jobs:
SKIP_BUNDLE: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Bundle output check
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
yarn ts-node-esm ./scripts/macos-arm64-output-check.mts

View File

@@ -184,7 +184,7 @@ jobs:
path: ./apps/server
- name: Run playwright tests
run: yarn workspace @affine-test/affine-cloud e2e --forbid-only
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-cloud e2e --forbid-only
env:
COVERAGE: true
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
@@ -208,3 +208,103 @@ jobs:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore
server-desktop-e2e-test:
name: Server Desktop E2E Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
hard-link-nm: false
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
target: x86_64-unknown-linux-gnu
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build:dev
- name: Run playwright tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn workspace @affine-test/affine-desktop-cloud e2e
env:
COVERAGE: true
DEV_SERVER_URL: http://localhost:8080
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
ENABLE_LOCAL_EMAIL: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: server-e2etest
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-server
path: ./tests/affine-cloud/test-results
if-no-files-found: ignore

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, '..'),
});
}

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",

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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<

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;
}

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"

View File

@@ -3,7 +3,7 @@
"compilerOptions": {
"composite": true
},
"include": ["**/__tests__/**/*", "./tests", "./e2e"],
"include": ["**/__tests__/**/*", "./tests"],
"references": [
{
"path": "./tsconfig.json"

View File

@@ -14,6 +14,8 @@
"tests/kit",
"tests/affine-legacy/*",
"tests/affine-local",
"tests/affine-desktop",
"tests/affine-desktop-cloud",
"tests/affine-plugin",
"tests/affine-prototype",
"tests/affine-cloud"

View File

@@ -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);
});

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"
}

View File

@@ -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;

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"outDir": "lib"
},
"include": ["e2e"],
"references": [
{
"path": "../kit"
},
{
"path": "../fixtures"
}
]
}

View File

@@ -1,10 +1,9 @@
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';
import { test } from './fixture';
test('new page', async ({ page, workspace }) => {
await page.getByTestId('new-page-button').click({
delay: 100,

View File

@@ -1,10 +1,9 @@
import path from 'node:path';
import { test } from '@affine-test/kit/electron';
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(

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"
}

View File

@@ -12,7 +12,6 @@ import type { PlaywrightTestConfig } from '@playwright/test';
*/
const config: PlaywrightTestConfig = {
testDir: './e2e',
testIgnore: '**/lib/**',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,
use: {

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"outDir": "lib"
},
"include": ["e2e"],
"references": [
{
"path": "../kit"
},
{
"path": "../fixtures"
}
]
}

View File

@@ -1,19 +1,21 @@
/* eslint-disable no-empty-pattern */
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 '@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';
} from './playwright';
import { removeWithRetry } from './utils/utils';
import { removeWithRetry } from '../tests/utils';
const projectRoot = resolve(__dirname, '..', '..');
const electronRoot = resolve(projectRoot, 'apps', 'electron');
function generateUUID() {
return crypto.randomUUID();
@@ -37,14 +39,7 @@ export const test = base.extend<{
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
// wait for blocksuite to be loaded
await page.waitForSelector('v-line');
if (enableCoverage) {
await fs.promises.mkdir(istanbulTempDir, { recursive: true });
@@ -71,15 +66,16 @@ export const test = base.extend<{
}
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(__dirname, '..', 'dist');
const clonedDist = resolve(__dirname, '../e2e-dist-' + id);
const dist = resolve(electronRoot, 'dist');
const clonedDist = resolve(electronRoot, 'e2e-dist-' + id);
await fs.copy(dist, clonedDist);
const packageJson = await fs.readJSON(
resolve(__dirname, '..', 'package.json')
resolve(electronRoot, 'package.json')
);
// overwrite the app name
packageJson.name = 'affine-test-' + id;
@@ -88,15 +84,27 @@ export const test = base.extend<{
// 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(
__dirname,
'..',
electronRoot,
'node_modules',
'.bin',
`electron${ext}`
),
cwd: clonedDist,
recordVideo: {
dir: testResultDir,
},

View File

@@ -4,6 +4,7 @@
"type": "module",
"version": "0.9.0-canary.12",
"exports": {
"./electron": "./electron.ts",
"./playwright": "./playwright.ts",
"./utils/*": "./utils/*.ts"
},

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;
}

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();
}
}

View File

@@ -181,6 +181,9 @@
{
"path": "./tests/affine-cloud"
},
{
"path": "./tests/affine-desktop"
},
{
"path": "./tests/affine-legacy/0.8.0-canary.7"
},

View File

@@ -80,6 +80,30 @@ __metadata:
languageName: unknown
linkType: soft
"@affine-test/affine-desktop-cloud@workspace:tests/affine-desktop-cloud":
version: 0.0.0-use.local
resolution: "@affine-test/affine-desktop-cloud@workspace:tests/affine-desktop-cloud"
dependencies:
"@affine-test/fixtures": "workspace:*"
"@affine-test/kit": "workspace:*"
"@playwright/test": ^1.37.1
"@types/fs-extra": ^11.0.1
fs-extra: ^11.1.1
languageName: unknown
linkType: soft
"@affine-test/affine-desktop@workspace:tests/affine-desktop":
version: 0.0.0-use.local
resolution: "@affine-test/affine-desktop@workspace:tests/affine-desktop"
dependencies:
"@affine-test/fixtures": "workspace:*"
"@affine-test/kit": "workspace:*"
"@playwright/test": ^1.37.1
"@types/fs-extra": ^11.0.1
fs-extra: ^11.1.1
languageName: unknown
linkType: soft
"@affine-test/affine-local@workspace:tests/affine-local":
version: 0.0.0-use.local
resolution: "@affine-test/affine-local@workspace:tests/affine-local"
@@ -388,7 +412,6 @@ __metadata:
"@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
async-call-rpc: ^6.3.1
cross-env: 7.0.3