From 97d8660a547940d8f23f0f9f5dc544a940d7606d Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Wed, 18 Oct 2023 22:14:30 -0500 Subject: [PATCH] refactor(electron): fix vitest and add behavior test (#4655) --- .github/workflows/build-desktop.yml | 4 +- .github/workflows/build.yml | 7 + nx.json | 13 +- packages/common/env/package.json | 2 - packages/common/env/src/global.ts | 16 -- packages/frontend/core/src/atoms/settings.ts | 2 +- packages/frontend/electron/scripts/common.ts | 15 +- packages/frontend/electron/scripts/dev.ts | 48 ++--- .../src/helper/db/__tests__/.gitignore | 1 - .../src/helper/workspace/__tests__/.gitignore | 1 - .../electron/src/main/__tests__/.gitignore | 1 - .../src/main/__tests__/integration.spec.ts | 170 ------------------ packages/frontend/electron/src/main/config.ts | 11 +- packages/frontend/electron/src/main/logger.ts | 1 - .../electron/src/preload/bootstrap.ts | 1 - .../__tests__ => test/db}/ensure-db.spec.ts | 24 ++- .../__tests__ => test/db}/migration.spec.ts | 8 +- .../db/__tests__ => test/db}/old-db.affine | Bin .../db}/workspace-db-adapter.spec.ts | 29 ++- .../workspace}/handlers.spec.ts | 32 +++- packages/frontend/electron/tests/utils.ts | 0 packages/frontend/electron/tsconfig.json | 6 +- packages/frontend/electron/tsconfig.test.json | 21 +++ .../frontend/electron/tsconfig.tests.json | 12 -- packages/frontend/electron/types/env.d.ts | 19 -- packages/frontend/electron/vitest.config.ts | 11 +- packages/frontend/workspace/src/atom.ts | 2 +- scripts/setup/i18n.ts | 18 -- tests/affine-desktop/e2e/basic.spec.ts | 2 - tests/affine-desktop/e2e/behavior.spec.ts | 38 ++++ tests/kit/utils/ipc.ts | 55 ++++++ tools/@types/env/__all.d.ts | 31 ++-- tsconfig.json | 2 +- vitest.config.ts | 1 - vitest.workspace.ts | 1 + 35 files changed, 256 insertions(+), 349 deletions(-) delete mode 100644 packages/frontend/electron/src/helper/db/__tests__/.gitignore delete mode 100644 packages/frontend/electron/src/helper/workspace/__tests__/.gitignore delete mode 100644 packages/frontend/electron/src/main/__tests__/.gitignore delete mode 100644 packages/frontend/electron/src/main/__tests__/integration.spec.ts rename packages/frontend/electron/{src/helper/db/__tests__ => test/db}/ensure-db.spec.ts (85%) rename packages/frontend/electron/{src/helper/db/__tests__ => test/db}/migration.spec.ts (92%) rename packages/frontend/electron/{src/helper/db/__tests__ => test/db}/old-db.affine (100%) rename packages/frontend/electron/{src/helper/db/__tests__ => test/db}/workspace-db-adapter.spec.ts (82%) rename packages/frontend/electron/{src/helper/workspace/__tests__ => test/workspace}/handlers.spec.ts (83%) delete mode 100644 packages/frontend/electron/tests/utils.ts create mode 100644 packages/frontend/electron/tsconfig.test.json delete mode 100644 packages/frontend/electron/tsconfig.tests.json delete mode 100644 packages/frontend/electron/types/env.d.ts delete mode 100644 scripts/setup/i18n.ts create mode 100644 tests/affine-desktop/e2e/behavior.spec.ts create mode 100644 tests/kit/utils/ipc.ts create mode 100644 vitest.workspace.ts diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 52f658f1ad..d9d58d7ee9 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -128,10 +128,12 @@ jobs: target: ${{ matrix.spec.target }} package: '@affine/native' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + - name: Run unit tests if: ${{ matrix.spec.test }} shell: bash - run: yarn workspace @affine/electron vitest + run: yarn vitest + working-directory: packages/frontend/electron - name: Download core artifact uses: actions/download-artifact@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46246e27f4..e3a3ac28e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,6 +184,13 @@ jobs: with: electron-install: 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: Unit Test run: yarn nx test:coverage @affine/monorepo diff --git a/nx.json b/nx.json index 22a1ae8c24..7835c08f79 100644 --- a/nx.json +++ b/nx.json @@ -35,8 +35,8 @@ "{projectRoot}/storybook-static" ], "inputs": [ - "{workspaceRoot}/infra/**/*", - "{workspaceRoot}/sdk/**/*", + "{workspaceRoot}/packages/frontend/infra/**/*", + "{workspaceRoot}/packages/frontend/sdk/**/*", { "runtime": "node -v" }, @@ -80,9 +80,6 @@ "test": { "outputs": ["{workspaceRoot}/.nyc_output"], "inputs": [ - { - "env": "NATIVE_TEST" - }, { "env": "ENABLE_PRELOADING" }, @@ -94,9 +91,6 @@ "test:ui": { "outputs": ["{workspaceRoot}/.nyc_output"], "inputs": [ - { - "env": "NATIVE_TEST" - }, { "env": "ENABLE_PRELOADING" }, @@ -108,9 +102,6 @@ "test:coverage": { "outputs": ["{workspaceRoot}/.nyc_output"], "inputs": [ - { - "env": "NATIVE_TEST" - }, { "env": "ENABLE_PRELOADING" } diff --git a/packages/common/env/package.json b/packages/common/env/package.json index 4c3e0afd46..5a92ca94a2 100644 --- a/packages/common/env/package.json +++ b/packages/common/env/package.json @@ -2,8 +2,6 @@ "name": "@affine/env", "private": true, "type": "module", - "main": "./src/index.ts", - "module": "./src/index.ts", "devDependencies": { "@blocksuite/global": "0.0.0-20231018100009-361737d3-nightly", "@blocksuite/store": "0.0.0-20231018100009-361737d3-nightly", diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts index 9257cb2862..225539e67e 100644 --- a/packages/common/env/src/global.ts +++ b/packages/common/env/src/global.ts @@ -42,22 +42,6 @@ export type BlockSuiteFeatureFlags = z.infer; export type RuntimeConfig = z.infer; -export const platformSchema = z.enum([ - 'aix', - 'android', - 'darwin', - 'freebsd', - 'haiku', - 'linux', - 'openbsd', - 'sunos', - 'win32', - 'cygwin', - 'netbsd', -]); - -export type Platform = z.infer; - type BrowserBase = { /** * @example https://app.affine.pro diff --git a/packages/frontend/core/src/atoms/settings.ts b/packages/frontend/core/src/atoms/settings.ts index eac2abafcd..22f02d0e78 100644 --- a/packages/frontend/core/src/atoms/settings.ts +++ b/packages/frontend/core/src/atoms/settings.ts @@ -49,7 +49,7 @@ export const fontStyleOptions = [ }[]; const appSettingBaseAtom = atomWithStorage('affine-settings', { - clientBorder: environment.isDesktop && globalThis.platform !== 'win32', + clientBorder: environment.isDesktop && !environment.isWindows, fullWidthLayout: false, windowFrameStyle: 'frameless', fontStyle: 'Sans', diff --git a/packages/frontend/electron/scripts/common.ts b/packages/frontend/electron/scripts/common.ts index 828917f224..bcb6d31ec2 100644 --- a/packages/frontend/electron/scripts/common.ts +++ b/packages/frontend/electron/scripts/common.ts @@ -9,22 +9,13 @@ export const rootDir = resolve(electronDir, '..', '..', '..'); export const NODE_MAJOR_VERSION = 18; -// hard-coded for now: -// fixme(xp): report error if app is not running on DEV_SERVER_URL -const DEV_SERVER_URL = process.env.DEV_SERVER_URL; - export const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development'); export const config = (): BuildOptions => { - const define = Object.fromEntries([ - ['process.env.NODE_ENV', `"${mode}"`], - ['process.env.USE_WORKER', '"true"'], - ]); + const define: Record = {}; - if (DEV_SERVER_URL) { - define['process.env.DEV_SERVER_URL'] = `"${DEV_SERVER_URL}"`; - } + define['REPLACE_ME_BUILD_ENV'] = `"${process.env.BUILD_TYPE ?? 'stable'}"`; return { entryPoints: [ @@ -45,11 +36,11 @@ export const config = (): BuildOptions => { 'semver', 'tinykeys', ], - define: define, format: 'cjs', loader: { '.node': 'copy', }, + define, assetNames: '[name]', treeShaking: true, sourcemap: 'linked', diff --git a/packages/frontend/electron/scripts/dev.ts b/packages/frontend/electron/scripts/dev.ts index 28488ff3bd..6b84ab7e7e 100644 --- a/packages/frontend/electron/scripts/dev.ts +++ b/packages/frontend/electron/scripts/dev.ts @@ -1,11 +1,10 @@ -/* eslint-disable no-async-promise-executor */ import { spawn } from 'node:child_process'; import type { ChildProcessWithoutNullStreams } from 'child_process'; -import electronPath from 'electron'; +import type { BuildContext } from 'esbuild'; import * as esbuild from 'esbuild'; -import { config } from './common'; +import { config, electronDir } from './common'; // this means we don't spawn electron windows, mainly for testing const watchMode = process.argv.includes('--watch'); @@ -30,7 +29,10 @@ function spawnOrReloadElectron() { spawnProcess = null; } - spawnProcess = spawn(String(electronPath), ['.']); + spawnProcess = spawn('electron', ['.'], { + cwd: electronDir, + env: process.env, + }); spawnProcess.stdout.on('data', d => { const str = d.toString().trim(); @@ -38,6 +40,7 @@ function spawnOrReloadElectron() { console.log(str); } }); + spawnProcess.stderr.on('data', d => { const data = d.toString().trim(); if (!data) return; @@ -47,16 +50,20 @@ function spawnOrReloadElectron() { }); // Stops the watch script when the application has quit - spawnProcess.on('exit', process.exit); + spawnProcess.on('exit', code => { + if (code && code !== 0) { + console.log(`Electron exited with code ${code}`); + } + process.exit(code ?? 0); + }); } const common = config(); async function watchLayers() { - return new Promise(async resolve => { - let initialBuild = false; - - const buildContext = await esbuild.context({ + let initialBuild = false; + return new Promise(resolve => { + const buildContextPromise = esbuild.context({ ...common, plugins: [ ...(common.plugins ?? []), @@ -68,7 +75,7 @@ async function watchLayers() { console.log(`[layers] has changed, [re]launching electron...`); spawnOrReloadElectron(); } else { - resolve(); + buildContextPromise.then(resolve); initialBuild = true; } }); @@ -76,19 +83,18 @@ async function watchLayers() { }, ], }); - await buildContext.watch(); + buildContextPromise.then(async buildContext => { + await buildContext.watch(); + }); }); } -async function main() { - await watchLayers(); +await watchLayers(); - if (watchMode) { - console.log(`Watching for changes...`); - } else { - spawnOrReloadElectron(); - console.log(`Electron is started, watching for changes...`); - } +if (watchMode) { + console.log(`Watching for changes...`); +} else { + console.log('Starting electron...'); + spawnOrReloadElectron(); + console.log(`Electron is started, watching for changes...`); } - -main(); diff --git a/packages/frontend/electron/src/helper/db/__tests__/.gitignore b/packages/frontend/electron/src/helper/db/__tests__/.gitignore deleted file mode 100644 index a9a5aecf42..0000000000 --- a/packages/frontend/electron/src/helper/db/__tests__/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp diff --git a/packages/frontend/electron/src/helper/workspace/__tests__/.gitignore b/packages/frontend/electron/src/helper/workspace/__tests__/.gitignore deleted file mode 100644 index a9a5aecf42..0000000000 --- a/packages/frontend/electron/src/helper/workspace/__tests__/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp diff --git a/packages/frontend/electron/src/main/__tests__/.gitignore b/packages/frontend/electron/src/main/__tests__/.gitignore deleted file mode 100644 index a9a5aecf42..0000000000 --- a/packages/frontend/electron/src/main/__tests__/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp diff --git a/packages/frontend/electron/src/main/__tests__/integration.spec.ts b/packages/frontend/electron/src/main/__tests__/integration.spec.ts deleted file mode 100644 index b4c90a370e..0000000000 --- a/packages/frontend/electron/src/main/__tests__/integration.spec.ts +++ /dev/null @@ -1,170 +0,0 @@ -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 type { MainIPCHandlerMap } from '../exposed'; - -const registeredHandlers = new Map< - string, - ((...args: any[]) => Promise)[] ->(); - -type WithoutFirstParameter = T extends (_: any, ...args: infer P) => infer R - ? (...args: P) => R - : T; - -// common mock dispatcher for ipcMain.handle AND app.on -// alternatively, we can use single parameter for T & F, eg, dispatch('workspace:list'), -// however this is too hard to be typed correctly -async function dispatch< - T extends keyof MainIPCHandlerMap, - F extends keyof MainIPCHandlerMap[T], ->( - namespace: T, - functionName: F, - ...args: Parameters> -): // @ts-expect-error -ReturnType { - // @ts-expect-error - const handlers = registeredHandlers.get(namespace + ':' + functionName); - assert(handlers); - - // we only care about the first handler here - return await handlers[0](null, ...args); -} - -const SESSION_DATA_PATH = path.join(__dirname, './tmp', 'affine-test'); -const DOCUMENTS_PATH = path.join(__dirname, './tmp', 'affine-test-documents'); - -const browserWindow = { - isDestroyed: () => { - return false; - }, - setWindowButtonVisibility: (_v: boolean) => { - // will be stubbed later - }, - webContents: { - send: (_type: string, ..._args: any[]) => { - // will be stubbed later - }, - }, -}; - -const ipcMain = { - handle: (key: string, callback: (...args: any[]) => Promise) => { - const handlers = registeredHandlers.get(key) || []; - handlers.push(callback); - registeredHandlers.set(key, handlers); - }, - setMaxListeners: (_n: number) => { - // noop - }, -}; - -const nativeTheme = { - themeSource: 'light', -}; - -const electronModule = { - app: { - getPath: (name: string) => { - if (name === 'sessionData') { - return SESSION_DATA_PATH; - } else if (name === 'documents') { - return DOCUMENTS_PATH; - } - throw new Error('not implemented'); - }, - name: 'affine-test', - on: (name: string, callback: (...args: any[]) => any) => { - const handlers = registeredHandlers.get(name) || []; - handlers.push(callback); - registeredHandlers.set(name, handlers); - }, - addListener: (...args: any[]) => { - // @ts-expect-error - electronModule.app.on(...args); - }, - removeListener: () => {}, - }, - BrowserWindow: { - getAllWindows: () => { - return [browserWindow]; - }, - }, - utilityProcess: {}, - nativeTheme: nativeTheme, - ipcMain, - shell: {} as Partial, - dialog: {} as Partial, -}; - -// dynamically import handlers so that we can inject local variables to mocks -vi.doMock('electron', () => { - return electronModule; -}); - -beforeEach(async () => { - const { registerHandlers } = await import('../handlers'); - registerHandlers(); - - // should also register events - const { registerEvents } = await import('../events'); - registerEvents(); - await fs.mkdirp(SESSION_DATA_PATH); - - registeredHandlers.get('ready')?.forEach(fn => fn()); -}); - -afterEach(async () => { - // reset registered handlers - registeredHandlers.get('before-quit')?.forEach(fn => fn()); - await removeWithRetry(SESSION_DATA_PATH); -}); - -describe('UI handlers', () => { - test('theme-change', async () => { - await dispatch('ui', 'handleThemeChange', 'dark'); - expect(nativeTheme.themeSource).toBe('dark'); - await dispatch('ui', 'handleThemeChange', 'light'); - expect(nativeTheme.themeSource).toBe('light'); - }); - - test('sidebar-visibility-change (macOS)', async () => { - vi.stubGlobal('process', { platform: 'darwin' }); - const setWindowButtonVisibility = vi.fn(); - browserWindow.setWindowButtonVisibility = setWindowButtonVisibility; - await dispatch('ui', 'handleSidebarVisibilityChange', true); - expect(setWindowButtonVisibility).toBeCalledWith(true); - await dispatch('ui', 'handleSidebarVisibilityChange', false); - expect(setWindowButtonVisibility).toBeCalledWith(false); - vi.unstubAllGlobals(); - }); - - test('sidebar-visibility-change (non-macOS)', async () => { - vi.stubGlobal('process', { platform: 'linux' }); - const setWindowButtonVisibility = vi.fn(); - browserWindow.setWindowButtonVisibility = setWindowButtonVisibility; - await dispatch('ui', 'handleSidebarVisibilityChange', true); - expect(setWindowButtonVisibility).not.toBeCalled(); - vi.unstubAllGlobals(); - }); -}); - -describe('applicationMenu', () => { - // test some basic IPC events - test('applicationMenu event', async () => { - const { applicationMenuSubjects } = await import('../application-menu'); - const sendStub = vi.fn(); - browserWindow.webContents.send = sendStub; - applicationMenuSubjects.newPageAction.next(); - expect(sendStub).toHaveBeenCalledWith( - 'applicationMenu:onNewPageAction', - undefined - ); - browserWindow.webContents.send = () => {}; - }); -}); diff --git a/packages/frontend/electron/src/main/config.ts b/packages/frontend/electron/src/main/config.ts index 95d0ea9163..14ef8a7c9f 100644 --- a/packages/frontend/electron/src/main/config.ts +++ b/packages/frontend/electron/src/main/config.ts @@ -7,11 +7,12 @@ export const ReleaseTypeSchema = z.enum([ 'internal', ]); -export const envBuildType = ( - process.env.BUILD_TYPE_OVERRIDE || - process.env.BUILD_TYPE || - 'canary' -) +declare global { + // THIS variable should be replaced during the build process + const REPLACE_ME_BUILD_ENV: string; +} + +export const envBuildType = (process.env.BUILD_TYPE || REPLACE_ME_BUILD_ENV) .trim() .toLowerCase(); diff --git a/packages/frontend/electron/src/main/logger.ts b/packages/frontend/electron/src/main/logger.ts index f7089a9b3d..9f6faef8df 100644 --- a/packages/frontend/electron/src/main/logger.ts +++ b/packages/frontend/electron/src/main/logger.ts @@ -2,7 +2,6 @@ import { shell } from 'electron'; import log from 'electron-log'; export const logger = log.scope('main'); -export const pluginLogger = log.scope('plugin'); log.initialize(); export function getLogFilePath() { diff --git a/packages/frontend/electron/src/preload/bootstrap.ts b/packages/frontend/electron/src/preload/bootstrap.ts index 83a5b3e0fc..444d0deb40 100644 --- a/packages/frontend/electron/src/preload/bootstrap.ts +++ b/packages/frontend/electron/src/preload/bootstrap.ts @@ -9,7 +9,6 @@ import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('appInfo', appInfo); contextBridge.exposeInMainWorld('apis', apis); contextBridge.exposeInMainWorld('events', events); - contextBridge.exposeInMainWorld('platform', process.platform); // Credit to microsoft/vscode const globals = { diff --git a/packages/frontend/electron/src/helper/db/__tests__/ensure-db.spec.ts b/packages/frontend/electron/test/db/ensure-db.spec.ts similarity index 85% rename from packages/frontend/electron/src/helper/db/__tests__/ensure-db.spec.ts rename to packages/frontend/electron/test/db/ensure-db.spec.ts index 3734a22bcf..a485418cc4 100644 --- a/packages/frontend/electron/src/helper/db/__tests__/ensure-db.spec.ts +++ b/packages/frontend/electron/test/db/ensure-db.spec.ts @@ -8,7 +8,7 @@ import { afterEach, beforeEach, expect, test, vi } from 'vitest'; const tmpDir = path.join(__dirname, 'tmp'); const appDataPath = path.join(tmpDir, 'app-data'); -vi.doMock('../../main-rpc', () => ({ +vi.doMock('@affine/electron/helper/main-rpc', () => ({ mainRPC: { getPath: async () => appDataPath, }, @@ -22,7 +22,7 @@ function existProcess() { process.emit('beforeExit', 0); } -vi.doMock('../secondary-db', () => { +vi.doMock('@affine/electron/helper/db/secondary-db', () => { return { SecondaryWorkspaceSQLiteDB: class { constructor(...args: any[]) { @@ -49,7 +49,9 @@ afterEach(async () => { }); test('can get a valid WorkspaceSQLiteDB', async () => { - const { ensureSQLiteDB } = await import('../ensure-db'); + const { ensureSQLiteDB } = await import( + '@affine/electron/helper/db/ensure-db' + ); const workspaceId = v4(); const db0 = await ensureSQLiteDB(workspaceId); expect(db0).toBeDefined(); @@ -64,7 +66,9 @@ test('can get a valid WorkspaceSQLiteDB', async () => { }); test('db should be destroyed when app quits', async () => { - const { ensureSQLiteDB } = await import('../ensure-db'); + const { ensureSQLiteDB } = await import( + '@affine/electron/helper/db/ensure-db' + ); const workspaceId = v4(); const db0 = await ensureSQLiteDB(workspaceId); const db1 = await ensureSQLiteDB(v4()); @@ -82,7 +86,9 @@ test('db should be destroyed when app quits', async () => { }); test('db should be removed in db$Map after destroyed', async () => { - const { ensureSQLiteDB, db$Map } = await import('../ensure-db'); + const { ensureSQLiteDB, db$Map } = await import( + '@affine/electron/helper/db/ensure-db' + ); const workspaceId = v4(); const db = await ensureSQLiteDB(workspaceId); await db.destroy(); @@ -92,8 +98,12 @@ test('db should be removed in db$Map after destroyed', async () => { // we have removed secondary db feature test.skip('if db has a secondary db path, we should also poll that', async () => { - const { ensureSQLiteDB } = await import('../ensure-db'); - const { storeWorkspaceMeta } = await import('../../workspace'); + const { ensureSQLiteDB } = await import( + '@affine/electron/helper/db/ensure-db' + ); + const { storeWorkspaceMeta } = await import( + '@affine/electron/helper/workspace' + ); const workspaceId = v4(); await storeWorkspaceMeta(workspaceId, { secondaryDBPath: path.join(tmpDir, 'secondary.db'), diff --git a/packages/frontend/electron/src/helper/db/__tests__/migration.spec.ts b/packages/frontend/electron/test/db/migration.spec.ts similarity index 92% rename from packages/frontend/electron/src/helper/db/__tests__/migration.spec.ts rename to packages/frontend/electron/test/db/migration.spec.ts index 0eaaef8402..809e05e3b8 100644 --- a/packages/frontend/electron/src/helper/db/__tests__/migration.spec.ts +++ b/packages/frontend/electron/test/db/migration.spec.ts @@ -1,18 +1,20 @@ import path from 'node:path'; +import { + copyToTemp, + migrateToSubdocAndReplaceDatabase, +} from '@affine/electron/helper/db/migration'; 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 { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration'; - const tmpDir = path.join(__dirname, 'tmp'); const testDBFilePath = path.resolve(__dirname, 'old-db.affine'); const appDataPath = path.join(tmpDir, 'app-data'); -vi.mock('../../main-rpc', () => ({ +vi.mock('@affine/electron/helper/main-rpc', () => ({ mainRPC: { getPath: async () => appDataPath, }, diff --git a/packages/frontend/electron/src/helper/db/__tests__/old-db.affine b/packages/frontend/electron/test/db/old-db.affine similarity index 100% rename from packages/frontend/electron/src/helper/db/__tests__/old-db.affine rename to packages/frontend/electron/test/db/old-db.affine diff --git a/packages/frontend/electron/src/helper/db/__tests__/workspace-db-adapter.spec.ts b/packages/frontend/electron/test/db/workspace-db-adapter.spec.ts similarity index 82% rename from packages/frontend/electron/src/helper/db/__tests__/workspace-db-adapter.spec.ts rename to packages/frontend/electron/test/db/workspace-db-adapter.spec.ts index 8afa93bd19..f910d3db7e 100644 --- a/packages/frontend/electron/src/helper/db/__tests__/workspace-db-adapter.spec.ts +++ b/packages/frontend/electron/test/db/workspace-db-adapter.spec.ts @@ -1,17 +1,16 @@ import path from 'node:path'; +import { dbSubjects } from '@affine/electron/helper/db/subjects'; 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 { dbSubjects } from '../subjects'; - const tmpDir = path.join(__dirname, 'tmp'); const appDataPath = path.join(tmpDir, 'app-data'); -vi.doMock('../../main-rpc', () => ({ +vi.doMock('@affine/electron/helper/main-rpc', () => ({ mainRPC: { getPath: async () => appDataPath, }, @@ -47,7 +46,9 @@ function getTestSubDocUpdates() { } test('can create new db file if not exists', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const db = await openWorkspaceDatabase(workspaceId); const dbPath = path.join( @@ -60,7 +61,9 @@ test('can create new db file if not exists', async () => { }); test('on applyUpdate (from self), will not trigger update', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const onUpdate = vi.fn(); @@ -72,7 +75,9 @@ test('on applyUpdate (from self), will not trigger update', async () => { }); test('on applyUpdate (from renderer), will trigger update', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const onUpdate = vi.fn(); const onExternalUpdate = vi.fn(); @@ -87,7 +92,9 @@ test('on applyUpdate (from renderer), will trigger update', async () => { }); test('on applyUpdate (from renderer, subdoc), will trigger update', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const onUpdate = vi.fn(); const insertUpdates = vi.fn(); @@ -112,7 +119,9 @@ test('on applyUpdate (from renderer, subdoc), will trigger update', async () => }); test('on applyUpdate (from external), will trigger update & send external update event', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const onUpdate = vi.fn(); const onExternalUpdate = vi.fn(); @@ -128,7 +137,9 @@ test('on applyUpdate (from external), will trigger update & send external update }); test('on destroy, check if resources have been released', async () => { - const { openWorkspaceDatabase } = await import('../workspace-db-adapter'); + const { openWorkspaceDatabase } = await import( + '@affine/electron/helper/db/workspace-db-adapter' + ); const workspaceId = v4(); const db = await openWorkspaceDatabase(workspaceId); const updateSub = { diff --git a/packages/frontend/electron/src/helper/workspace/__tests__/handlers.spec.ts b/packages/frontend/electron/test/workspace/handlers.spec.ts similarity index 83% rename from packages/frontend/electron/src/helper/workspace/__tests__/handlers.spec.ts rename to packages/frontend/electron/test/workspace/handlers.spec.ts index 9031763c30..68e0194253 100644 --- a/packages/frontend/electron/src/helper/workspace/__tests__/handlers.spec.ts +++ b/packages/frontend/electron/test/workspace/handlers.spec.ts @@ -8,13 +8,13 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; const tmpDir = path.join(__dirname, 'tmp'); const appDataPath = path.join(tmpDir, 'app-data'); -vi.doMock('../../db/ensure-db', () => ({ +vi.doMock('@affine/electron/helper/db/ensure-db', () => ({ ensureSQLiteDB: async () => ({ destroy: () => {}, }), })); -vi.doMock('../../main-rpc', () => ({ +vi.doMock('@affine/electron/helper/main-rpc', () => ({ mainRPC: { getPath: async () => appDataPath, }, @@ -26,7 +26,9 @@ afterEach(async () => { describe('list workspaces', () => { test('listWorkspaces (valid)', async () => { - const { listWorkspaces } = await import('../handlers'); + const { listWorkspaces } = await import( + '@affine/electron/helper/workspace/handlers' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); const meta = { @@ -39,7 +41,9 @@ describe('list workspaces', () => { }); test('listWorkspaces (without meta json file)', async () => { - const { listWorkspaces } = await import('../handlers'); + const { listWorkspaces } = await import( + '@affine/electron/helper/workspace/handlers' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); await fs.ensureDir(workspacePath); @@ -56,7 +60,9 @@ describe('list workspaces', () => { describe('delete workspace', () => { test('deleteWorkspace', async () => { - const { deleteWorkspace } = await import('../handlers'); + const { deleteWorkspace } = await import( + '@affine/electron/helper/workspace/handlers' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); await fs.ensureDir(workspacePath); @@ -73,7 +79,9 @@ describe('delete workspace', () => { describe('getWorkspaceMeta', () => { test('can get meta', async () => { - const { getWorkspaceMeta } = await import('../meta'); + const { getWorkspaceMeta } = await import( + '@affine/electron/helper/workspace/meta' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); const meta = { @@ -85,7 +93,9 @@ describe('getWorkspaceMeta', () => { }); test('can create meta if not exists', async () => { - const { getWorkspaceMeta } = await import('../meta'); + const { getWorkspaceMeta } = await import( + '@affine/electron/helper/workspace/meta' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); await fs.ensureDir(workspacePath); @@ -99,7 +109,9 @@ describe('getWorkspaceMeta', () => { }); test('can migrate meta if db file is a link', async () => { - const { getWorkspaceMeta } = await import('../meta'); + const { getWorkspaceMeta } = await import( + '@affine/electron/helper/workspace/meta' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); await fs.ensureDir(workspacePath); @@ -121,7 +133,9 @@ describe('getWorkspaceMeta', () => { }); test('storeWorkspaceMeta', async () => { - const { storeWorkspaceMeta } = await import('../handlers'); + const { storeWorkspaceMeta } = await import( + '@affine/electron/helper/workspace/handlers' + ); const workspaceId = v4(); const workspacePath = path.join(appDataPath, 'workspaces', workspaceId); await fs.ensureDir(workspacePath); diff --git a/packages/frontend/electron/tests/utils.ts b/packages/frontend/electron/tests/utils.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/frontend/electron/tsconfig.json b/packages/frontend/electron/tsconfig.json index 20d89ac249..7d62f4f91d 100644 --- a/packages/frontend/electron/tsconfig.json +++ b/packages/frontend/electron/tsconfig.json @@ -28,12 +28,12 @@ { "path": "../../common/env" }, - - // Tests { "path": "./tsconfig.node.json" }, - { "path": "../../../tests/kit" } + { + "path": "../../../tests/kit" + } ], "ts-node": { "esm": true, diff --git a/packages/frontend/electron/tsconfig.test.json b/packages/frontend/electron/tsconfig.test.json new file mode 100644 index 0000000000..bf1defc464 --- /dev/null +++ b/packages/frontend/electron/tsconfig.test.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "composite": true, + "target": "ESNext", + "module": "ESNext", + "resolveJsonModule": true, + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "noEmit": false, + "outDir": "./lib/tests", + "types": ["node"], + "allowJs": true + }, + "references": [ + { + "path": "./tsconfig.json" + } + ], + "include": ["./test"] +} diff --git a/packages/frontend/electron/tsconfig.tests.json b/packages/frontend/electron/tsconfig.tests.json deleted file mode 100644 index c2bf5037c2..0000000000 --- a/packages/frontend/electron/tsconfig.tests.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "composite": true - }, - "include": ["**/__tests__/**/*", "./tests"], - "references": [ - { - "path": "./tsconfig.json" - } - ] -} diff --git a/packages/frontend/electron/types/env.d.ts b/packages/frontend/electron/types/env.d.ts deleted file mode 100644 index 658c0eb443..0000000000 --- a/packages/frontend/electron/types/env.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Describes all existing environment variables and their types. - * Required for Code completion and type checking - * - * Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code - * - * @see https://github.com/vitejs/vite/blob/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface - * @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc - */ -interface ImportMetaEnv { - /** - * The value of the variable is set in scripts/watch.js and depend on layers/main/vite.config.js - */ - readonly DEV_SERVER_URL: undefined | string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} diff --git a/packages/frontend/electron/vitest.config.ts b/packages/frontend/electron/vitest.config.ts index 32df7fd361..a1a3861464 100644 --- a/packages/frontend/electron/vitest.config.ts +++ b/packages/frontend/electron/vitest.config.ts @@ -4,24 +4,17 @@ import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vitest/config'; const rootDir = fileURLToPath(new URL('../../..', import.meta.url)); -const pluginOutputDir = resolve( - rootDir, - './packages/frontend/electron/dist/plugins' -); export default defineConfig({ resolve: { alias: { // prevent tests using two different sources of yjs yjs: resolve(rootDir, 'node_modules/yjs'), + '@affine/electron': resolve(rootDir, 'packages/frontend/electron/src'), }, }, - define: { - 'process.env.PLUGIN_DIR': JSON.stringify(pluginOutputDir), - }, test: { - include: ['./src/**/*.spec.ts'], - exclude: ['**/node_modules', '**/dist', '**/build', '**/out'], + include: ['./test/**/*.spec.ts'], testTimeout: 5000, singleThread: true, threads: false, diff --git a/packages/frontend/workspace/src/atom.ts b/packages/frontend/workspace/src/atom.ts index 67d7d93469..31188095d7 100644 --- a/packages/frontend/workspace/src/atom.ts +++ b/packages/frontend/workspace/src/atom.ts @@ -112,7 +112,7 @@ const fetchMetadata: FetchMetadata = async (get, { signal }) => { // migration step, only data in `METADATA_STORAGE_KEY` will be migrated if ( maybeMetadata.some(meta => !('version' in meta)) && - !globalThis.$migrationDone + !window.$migrationDone ) { await new Promise((resolve, reject) => { signal.addEventListener('abort', () => reject(), { once: true }); diff --git a/scripts/setup/i18n.ts b/scripts/setup/i18n.ts deleted file mode 100644 index 7a35fd73c0..0000000000 --- a/scripts/setup/i18n.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { runCli } from '@magic-works/i18n-codegen'; -import { beforeAll } from 'vitest'; - -beforeAll(async () => { - runCli( - { - watch: false, - cwd: join(fileURLToPath(import.meta.url), '../../../.i18n-codegen.json'), - }, - error => { - console.error(error); - process.exit(1); - } - ); -}); diff --git a/tests/affine-desktop/e2e/basic.spec.ts b/tests/affine-desktop/e2e/basic.spec.ts index b20f3beb5f..048907f36a 100644 --- a/tests/affine-desktop/e2e/basic.spec.ts +++ b/tests/affine-desktop/e2e/basic.spec.ts @@ -1,5 +1,3 @@ -// import { platform } from 'node:os'; - import { test } from '@affine-test/kit/electron'; import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard'; import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic'; diff --git a/tests/affine-desktop/e2e/behavior.spec.ts b/tests/affine-desktop/e2e/behavior.spec.ts new file mode 100644 index 0000000000..a060608cbf --- /dev/null +++ b/tests/affine-desktop/e2e/behavior.spec.ts @@ -0,0 +1,38 @@ +import os from 'node:os'; + +import { test } from '@affine-test/kit/electron'; +import { shouldCallIpcRendererHandler } from '@affine-test/kit/utils/ipc'; + +test.describe('behavior test', () => { + if (os.platform() === 'darwin') { + test('system button should hidden correctly', async ({ + page, + electronApp, + }) => { + { + const promise = shouldCallIpcRendererHandler( + electronApp, + 'ui:handleSidebarVisibilityChange' + ); + await page + .locator( + '[data-testid=app-sidebar-arrow-button-collapse][data-show=true]' + ) + .click(); + await promise; + } + { + const promise = shouldCallIpcRendererHandler( + electronApp, + 'ui:handleSidebarVisibilityChange' + ); + await page + .locator( + '[data-testid=app-sidebar-arrow-button-expand][data-show=true]' + ) + .click(); + await promise; + } + }); + } +}); diff --git a/tests/kit/utils/ipc.ts b/tests/kit/utils/ipc.ts new file mode 100644 index 0000000000..1c03168c8e --- /dev/null +++ b/tests/kit/utils/ipc.ts @@ -0,0 +1,55 @@ +// Credit: https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/ipc_helpers.ts +import type { Page } from '@playwright/test'; +import type { ElectronApplication } from 'playwright'; + +export function ipcRendererInvoke(page: Page, channel: string, ...args: any[]) { + return page.evaluate( + ({ channel, args }) => { + return window.affine.ipcRenderer.invoke(channel, ...args); + }, + { channel, args } + ); +} + +export function ipcRendererSend(page: Page, channel: string, ...args: any[]) { + return page.evaluate( + ({ channel, args }) => { + window.affine.ipcRenderer.send(channel, ...args); + }, + { channel, args } + ); +} + +type IpcMainWithHandlers = Electron.IpcMain & { + _invokeHandlers: Map< + string, + (e: Electron.IpcMainInvokeEvent, ...args: unknown[]) => Promise + >; +}; + +export function shouldCallIpcRendererHandler( + electronApp: ElectronApplication, + channel: string +) { + return electronApp.evaluate( + async ({ ipcMain }, { channel }) => { + const ipcMainWH = ipcMain as IpcMainWithHandlers; + // this is all a bit of a hack, so let's test as we go + if (!ipcMainWH._invokeHandlers) { + throw new Error(`Cannot access ipcMain._invokeHandlers`); + } + const handler = ipcMainWH._invokeHandlers.get(channel); + if (!handler) { + throw new Error(`No ipcMain handler registered for '${channel}'`); + } + return new Promise(resolve => { + ipcMainWH._invokeHandlers.set(channel, async (e, ...args) => { + ipcMainWH._invokeHandlers.set(channel, handler); + resolve(); + return handler(e, ...args); + }); + }); + }, + { channel } + ); +} diff --git a/tools/@types/env/__all.d.ts b/tools/@types/env/__all.d.ts index 0f6774fe09..c91966a97e 100644 --- a/tools/@types/env/__all.d.ts +++ b/tools/@types/env/__all.d.ts @@ -1,4 +1,4 @@ -import type { Environment, Platform, RuntimeConfig } from '@affine/env/global'; +import type { Environment, RuntimeConfig } from '@affine/env/global'; import type { DBHandlerManager, DebugHandlerManager, @@ -26,6 +26,25 @@ declare global { workspace: UnwrapManagerHandlerToClientSide; }; events: EventMap; + affine: { + ipcRenderer: { + send(channel: string, ...args: any[]): void; + invoke(channel: string, ...args: any[]): Promise; + on( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ): this; + once( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ): this; + removeListener( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ): this; + }; + }; + $migrationDone: boolean | undefined; } interface WindowEventMap { @@ -37,21 +56,11 @@ declare global { env: Record; }; // eslint-disable-next-line no-var - var $migrationDone: boolean; - // eslint-disable-next-line no-var - var platform: Platform | undefined; - // eslint-disable-next-line no-var var environment: Environment; // eslint-disable-next-line no-var var runtimeConfig: RuntimeConfig; // eslint-disable-next-line no-var var $AFFINE_SETUP: boolean | undefined; - // eslint-disable-next-line no-var - var editorVersion: string | undefined; - // eslint-disable-next-line no-var - var prefixUrl: string; - // eslint-disable-next-line no-var - var websocketPrefixUrl: string; } declare module '@blocksuite/store' { diff --git a/tsconfig.json b/tsconfig.json index fca4351244..f9f213b038 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -103,7 +103,7 @@ "path": "./packages/frontend/core" }, { - "path": "./packages/frontend/electron" + "path": "./packages/frontend/electron/tsconfig.test.json" }, { "path": "./packages/frontend/graphql" diff --git a/vitest.config.ts b/vitest.config.ts index 2751951c37..927a23e2bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,7 +19,6 @@ export default defineConfig({ test: { setupFiles: [ resolve(rootDir, './scripts/setup/lit.ts'), - resolve(rootDir, './scripts/setup/i18n.ts'), resolve(rootDir, './scripts/setup/lottie-web.ts'), resolve(rootDir, './scripts/setup/global.ts'), ], diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000000..e7c2f7e2fc --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1 @@ +export default ['.', './packages/frontend/electron'];