mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 12:41:41 +03:00
refactor(electron): fix vitest and add behavior test (#4655)
This commit is contained in:
parent
b14a6bc29e
commit
97d8660a54
4
.github/workflows/build-desktop.yml
vendored
4
.github/workflows/build-desktop.yml
vendored
@ -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
|
||||
|
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@ -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
|
||||
|
||||
|
13
nx.json
13
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"
|
||||
}
|
||||
|
2
packages/common/env/package.json
vendored
2
packages/common/env/package.json
vendored
@ -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",
|
||||
|
16
packages/common/env/src/global.ts
vendored
16
packages/common/env/src/global.ts
vendored
@ -42,22 +42,6 @@ export type BlockSuiteFeatureFlags = z.infer<typeof blockSuiteFeatureFlags>;
|
||||
|
||||
export type RuntimeConfig = z.infer<typeof runtimeFlagsSchema>;
|
||||
|
||||
export const platformSchema = z.enum([
|
||||
'aix',
|
||||
'android',
|
||||
'darwin',
|
||||
'freebsd',
|
||||
'haiku',
|
||||
'linux',
|
||||
'openbsd',
|
||||
'sunos',
|
||||
'win32',
|
||||
'cygwin',
|
||||
'netbsd',
|
||||
]);
|
||||
|
||||
export type Platform = z.infer<typeof platformSchema>;
|
||||
|
||||
type BrowserBase = {
|
||||
/**
|
||||
* @example https://app.affine.pro
|
||||
|
@ -49,7 +49,7 @@ export const fontStyleOptions = [
|
||||
}[];
|
||||
|
||||
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
||||
clientBorder: environment.isDesktop && globalThis.platform !== 'win32',
|
||||
clientBorder: environment.isDesktop && !environment.isWindows,
|
||||
fullWidthLayout: false,
|
||||
windowFrameStyle: 'frameless',
|
||||
fontStyle: 'Sans',
|
||||
|
@ -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<string, string> = {};
|
||||
|
||||
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',
|
||||
|
@ -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<void>(async resolve => {
|
||||
let initialBuild = false;
|
||||
|
||||
const buildContext = await esbuild.context({
|
||||
let initialBuild = false;
|
||||
return new Promise<BuildContext>(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();
|
||||
|
@ -1 +0,0 @@
|
||||
tmp
|
@ -1 +0,0 @@
|
||||
tmp
|
@ -1 +0,0 @@
|
||||
tmp
|
@ -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<any>)[]
|
||||
>();
|
||||
|
||||
type WithoutFirstParameter<T> = 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<WithoutFirstParameter<MainIPCHandlerMap[T][F]>>
|
||||
): // @ts-expect-error
|
||||
ReturnType<MainIPCHandlerMap[T][F]> {
|
||||
// @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<any>) => {
|
||||
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<Electron.Shell>,
|
||||
dialog: {} as Partial<Electron.Dialog>,
|
||||
};
|
||||
|
||||
// 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 = () => {};
|
||||
});
|
||||
});
|
@ -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();
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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 = {
|
||||
|
@ -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'),
|
@ -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,
|
||||
},
|
@ -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 = {
|
@ -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);
|
@ -28,12 +28,12 @@
|
||||
{
|
||||
"path": "../../common/env"
|
||||
},
|
||||
|
||||
// Tests
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{ "path": "../../../tests/kit" }
|
||||
{
|
||||
"path": "../../../tests/kit"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
|
21
packages/frontend/electron/tsconfig.test.json
Normal file
21
packages/frontend/electron/tsconfig.test.json
Normal file
@ -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"]
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"include": ["**/__tests__/**/*", "./tests"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
19
packages/frontend/electron/types/env.d.ts
vendored
19
packages/frontend/electron/types/env.d.ts
vendored
@ -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;
|
||||
}
|
@ -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,
|
||||
|
@ -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<void>((resolve, reject) => {
|
||||
signal.addEventListener('abort', () => reject(), { once: true });
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
@ -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';
|
||||
|
38
tests/affine-desktop/e2e/behavior.spec.ts
Normal file
38
tests/affine-desktop/e2e/behavior.spec.ts
Normal file
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
55
tests/kit/utils/ipc.ts
Normal file
55
tests/kit/utils/ipc.ts
Normal file
@ -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<unknown>
|
||||
>;
|
||||
};
|
||||
|
||||
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<void>(resolve => {
|
||||
ipcMainWH._invokeHandlers.set(channel, async (e, ...args) => {
|
||||
ipcMainWH._invokeHandlers.set(channel, handler);
|
||||
resolve();
|
||||
return handler(e, ...args);
|
||||
});
|
||||
});
|
||||
},
|
||||
{ channel }
|
||||
);
|
||||
}
|
31
tools/@types/env/__all.d.ts
vendored
31
tools/@types/env/__all.d.ts
vendored
@ -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<WorkspaceHandlerManager>;
|
||||
};
|
||||
events: EventMap;
|
||||
affine: {
|
||||
ipcRenderer: {
|
||||
send(channel: string, ...args: any[]): void;
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
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<string, string>;
|
||||
};
|
||||
// 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' {
|
||||
|
@ -103,7 +103,7 @@
|
||||
"path": "./packages/frontend/core"
|
||||
},
|
||||
{
|
||||
"path": "./packages/frontend/electron"
|
||||
"path": "./packages/frontend/electron/tsconfig.test.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/frontend/graphql"
|
||||
|
@ -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'),
|
||||
],
|
||||
|
1
vitest.workspace.ts
Normal file
1
vitest.workspace.ts
Normal file
@ -0,0 +1 @@
|
||||
export default ['.', './packages/frontend/electron'];
|
Loading…
Reference in New Issue
Block a user