mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-20 10:49:53 +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 }}
|
target: ${{ matrix.spec.target }}
|
||||||
package: '@affine/native'
|
package: '@affine/native'
|
||||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
if: ${{ matrix.spec.test }}
|
if: ${{ matrix.spec.test }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn workspace @affine/electron vitest
|
run: yarn vitest
|
||||||
|
working-directory: packages/frontend/electron
|
||||||
|
|
||||||
- name: Download core artifact
|
- name: Download core artifact
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
|
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@ -184,6 +184,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
electron-install: false
|
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
|
- name: Unit Test
|
||||||
run: yarn nx test:coverage @affine/monorepo
|
run: yarn nx test:coverage @affine/monorepo
|
||||||
|
|
||||||
|
13
nx.json
13
nx.json
@ -35,8 +35,8 @@
|
|||||||
"{projectRoot}/storybook-static"
|
"{projectRoot}/storybook-static"
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
"{workspaceRoot}/infra/**/*",
|
"{workspaceRoot}/packages/frontend/infra/**/*",
|
||||||
"{workspaceRoot}/sdk/**/*",
|
"{workspaceRoot}/packages/frontend/sdk/**/*",
|
||||||
{
|
{
|
||||||
"runtime": "node -v"
|
"runtime": "node -v"
|
||||||
},
|
},
|
||||||
@ -80,9 +80,6 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"outputs": ["{workspaceRoot}/.nyc_output"],
|
"outputs": ["{workspaceRoot}/.nyc_output"],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
|
||||||
"env": "NATIVE_TEST"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"env": "ENABLE_PRELOADING"
|
"env": "ENABLE_PRELOADING"
|
||||||
},
|
},
|
||||||
@ -94,9 +91,6 @@
|
|||||||
"test:ui": {
|
"test:ui": {
|
||||||
"outputs": ["{workspaceRoot}/.nyc_output"],
|
"outputs": ["{workspaceRoot}/.nyc_output"],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
|
||||||
"env": "NATIVE_TEST"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"env": "ENABLE_PRELOADING"
|
"env": "ENABLE_PRELOADING"
|
||||||
},
|
},
|
||||||
@ -108,9 +102,6 @@
|
|||||||
"test:coverage": {
|
"test:coverage": {
|
||||||
"outputs": ["{workspaceRoot}/.nyc_output"],
|
"outputs": ["{workspaceRoot}/.nyc_output"],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
|
||||||
"env": "NATIVE_TEST"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"env": "ENABLE_PRELOADING"
|
"env": "ENABLE_PRELOADING"
|
||||||
}
|
}
|
||||||
|
2
packages/common/env/package.json
vendored
2
packages/common/env/package.json
vendored
@ -2,8 +2,6 @@
|
|||||||
"name": "@affine/env",
|
"name": "@affine/env",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
|
||||||
"module": "./src/index.ts",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocksuite/global": "0.0.0-20231018100009-361737d3-nightly",
|
"@blocksuite/global": "0.0.0-20231018100009-361737d3-nightly",
|
||||||
"@blocksuite/store": "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 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 = {
|
type BrowserBase = {
|
||||||
/**
|
/**
|
||||||
* @example https://app.affine.pro
|
* @example https://app.affine.pro
|
||||||
|
@ -49,7 +49,7 @@ export const fontStyleOptions = [
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
||||||
clientBorder: environment.isDesktop && globalThis.platform !== 'win32',
|
clientBorder: environment.isDesktop && !environment.isWindows,
|
||||||
fullWidthLayout: false,
|
fullWidthLayout: false,
|
||||||
windowFrameStyle: 'frameless',
|
windowFrameStyle: 'frameless',
|
||||||
fontStyle: 'Sans',
|
fontStyle: 'Sans',
|
||||||
|
@ -9,22 +9,13 @@ export const rootDir = resolve(electronDir, '..', '..', '..');
|
|||||||
|
|
||||||
export const NODE_MAJOR_VERSION = 18;
|
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 =
|
export const mode = (process.env.NODE_ENV =
|
||||||
process.env.NODE_ENV || 'development');
|
process.env.NODE_ENV || 'development');
|
||||||
|
|
||||||
export const config = (): BuildOptions => {
|
export const config = (): BuildOptions => {
|
||||||
const define = Object.fromEntries([
|
const define: Record<string, string> = {};
|
||||||
['process.env.NODE_ENV', `"${mode}"`],
|
|
||||||
['process.env.USE_WORKER', '"true"'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (DEV_SERVER_URL) {
|
define['REPLACE_ME_BUILD_ENV'] = `"${process.env.BUILD_TYPE ?? 'stable'}"`;
|
||||||
define['process.env.DEV_SERVER_URL'] = `"${DEV_SERVER_URL}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
@ -45,11 +36,11 @@ export const config = (): BuildOptions => {
|
|||||||
'semver',
|
'semver',
|
||||||
'tinykeys',
|
'tinykeys',
|
||||||
],
|
],
|
||||||
define: define,
|
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
loader: {
|
loader: {
|
||||||
'.node': 'copy',
|
'.node': 'copy',
|
||||||
},
|
},
|
||||||
|
define,
|
||||||
assetNames: '[name]',
|
assetNames: '[name]',
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
sourcemap: 'linked',
|
sourcemap: 'linked',
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable no-async-promise-executor */
|
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
import electronPath from 'electron';
|
import type { BuildContext } from 'esbuild';
|
||||||
import * as esbuild 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
|
// this means we don't spawn electron windows, mainly for testing
|
||||||
const watchMode = process.argv.includes('--watch');
|
const watchMode = process.argv.includes('--watch');
|
||||||
@ -30,7 +29,10 @@ function spawnOrReloadElectron() {
|
|||||||
spawnProcess = null;
|
spawnProcess = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnProcess = spawn(String(electronPath), ['.']);
|
spawnProcess = spawn('electron', ['.'], {
|
||||||
|
cwd: electronDir,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
spawnProcess.stdout.on('data', d => {
|
spawnProcess.stdout.on('data', d => {
|
||||||
const str = d.toString().trim();
|
const str = d.toString().trim();
|
||||||
@ -38,6 +40,7 @@ function spawnOrReloadElectron() {
|
|||||||
console.log(str);
|
console.log(str);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
spawnProcess.stderr.on('data', d => {
|
spawnProcess.stderr.on('data', d => {
|
||||||
const data = d.toString().trim();
|
const data = d.toString().trim();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@ -47,16 +50,20 @@ function spawnOrReloadElectron() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Stops the watch script when the application has quit
|
// 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();
|
const common = config();
|
||||||
|
|
||||||
async function watchLayers() {
|
async function watchLayers() {
|
||||||
return new Promise<void>(async resolve => {
|
let initialBuild = false;
|
||||||
let initialBuild = false;
|
return new Promise<BuildContext>(resolve => {
|
||||||
|
const buildContextPromise = esbuild.context({
|
||||||
const buildContext = await esbuild.context({
|
|
||||||
...common,
|
...common,
|
||||||
plugins: [
|
plugins: [
|
||||||
...(common.plugins ?? []),
|
...(common.plugins ?? []),
|
||||||
@ -68,7 +75,7 @@ async function watchLayers() {
|
|||||||
console.log(`[layers] has changed, [re]launching electron...`);
|
console.log(`[layers] has changed, [re]launching electron...`);
|
||||||
spawnOrReloadElectron();
|
spawnOrReloadElectron();
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
buildContextPromise.then(resolve);
|
||||||
initialBuild = true;
|
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) {
|
if (watchMode) {
|
||||||
console.log(`Watching for changes...`);
|
console.log(`Watching for changes...`);
|
||||||
} else {
|
} else {
|
||||||
spawnOrReloadElectron();
|
console.log('Starting electron...');
|
||||||
console.log(`Electron is started, watching for changes...`);
|
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',
|
'internal',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const envBuildType = (
|
declare global {
|
||||||
process.env.BUILD_TYPE_OVERRIDE ||
|
// THIS variable should be replaced during the build process
|
||||||
process.env.BUILD_TYPE ||
|
const REPLACE_ME_BUILD_ENV: string;
|
||||||
'canary'
|
}
|
||||||
)
|
|
||||||
|
export const envBuildType = (process.env.BUILD_TYPE || REPLACE_ME_BUILD_ENV)
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { shell } from 'electron';
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
export const logger = log.scope('main');
|
export const logger = log.scope('main');
|
||||||
export const pluginLogger = log.scope('plugin');
|
|
||||||
log.initialize();
|
log.initialize();
|
||||||
|
|
||||||
export function getLogFilePath() {
|
export function getLogFilePath() {
|
||||||
|
@ -9,7 +9,6 @@ import { contextBridge, ipcRenderer } from 'electron';
|
|||||||
contextBridge.exposeInMainWorld('appInfo', appInfo);
|
contextBridge.exposeInMainWorld('appInfo', appInfo);
|
||||||
contextBridge.exposeInMainWorld('apis', apis);
|
contextBridge.exposeInMainWorld('apis', apis);
|
||||||
contextBridge.exposeInMainWorld('events', events);
|
contextBridge.exposeInMainWorld('events', events);
|
||||||
contextBridge.exposeInMainWorld('platform', process.platform);
|
|
||||||
|
|
||||||
// Credit to microsoft/vscode
|
// Credit to microsoft/vscode
|
||||||
const globals = {
|
const globals = {
|
||||||
|
@ -8,7 +8,7 @@ import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|||||||
const tmpDir = path.join(__dirname, 'tmp');
|
const tmpDir = path.join(__dirname, 'tmp');
|
||||||
const appDataPath = path.join(tmpDir, 'app-data');
|
const appDataPath = path.join(tmpDir, 'app-data');
|
||||||
|
|
||||||
vi.doMock('../../main-rpc', () => ({
|
vi.doMock('@affine/electron/helper/main-rpc', () => ({
|
||||||
mainRPC: {
|
mainRPC: {
|
||||||
getPath: async () => appDataPath,
|
getPath: async () => appDataPath,
|
||||||
},
|
},
|
||||||
@ -22,7 +22,7 @@ function existProcess() {
|
|||||||
process.emit('beforeExit', 0);
|
process.emit('beforeExit', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock('../secondary-db', () => {
|
vi.doMock('@affine/electron/helper/db/secondary-db', () => {
|
||||||
return {
|
return {
|
||||||
SecondaryWorkspaceSQLiteDB: class {
|
SecondaryWorkspaceSQLiteDB: class {
|
||||||
constructor(...args: any[]) {
|
constructor(...args: any[]) {
|
||||||
@ -49,7 +49,9 @@ afterEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can get a valid WorkspaceSQLiteDB', 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 workspaceId = v4();
|
||||||
const db0 = await ensureSQLiteDB(workspaceId);
|
const db0 = await ensureSQLiteDB(workspaceId);
|
||||||
expect(db0).toBeDefined();
|
expect(db0).toBeDefined();
|
||||||
@ -64,7 +66,9 @@ test('can get a valid WorkspaceSQLiteDB', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('db should be destroyed when app quits', 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 workspaceId = v4();
|
||||||
const db0 = await ensureSQLiteDB(workspaceId);
|
const db0 = await ensureSQLiteDB(workspaceId);
|
||||||
const db1 = await ensureSQLiteDB(v4());
|
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 () => {
|
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 workspaceId = v4();
|
||||||
const db = await ensureSQLiteDB(workspaceId);
|
const db = await ensureSQLiteDB(workspaceId);
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
@ -92,8 +98,12 @@ test('db should be removed in db$Map after destroyed', async () => {
|
|||||||
|
|
||||||
// we have removed secondary db feature
|
// we have removed secondary db feature
|
||||||
test.skip('if db has a secondary db path, we should also poll that', async () => {
|
test.skip('if db has a secondary db path, we should also poll that', async () => {
|
||||||
const { ensureSQLiteDB } = await import('../ensure-db');
|
const { ensureSQLiteDB } = await import(
|
||||||
const { storeWorkspaceMeta } = await import('../../workspace');
|
'@affine/electron/helper/db/ensure-db'
|
||||||
|
);
|
||||||
|
const { storeWorkspaceMeta } = await import(
|
||||||
|
'@affine/electron/helper/workspace'
|
||||||
|
);
|
||||||
const workspaceId = v4();
|
const workspaceId = v4();
|
||||||
await storeWorkspaceMeta(workspaceId, {
|
await storeWorkspaceMeta(workspaceId, {
|
||||||
secondaryDBPath: path.join(tmpDir, 'secondary.db'),
|
secondaryDBPath: path.join(tmpDir, 'secondary.db'),
|
@ -1,18 +1,20 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
copyToTemp,
|
||||||
|
migrateToSubdocAndReplaceDatabase,
|
||||||
|
} from '@affine/electron/helper/db/migration';
|
||||||
import { SqliteConnection } from '@affine/native';
|
import { SqliteConnection } from '@affine/native';
|
||||||
import { removeWithRetry } from '@affine-test/kit/utils/utils';
|
import { removeWithRetry } from '@affine-test/kit/utils/utils';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { applyUpdate, Doc as YDoc } from 'yjs';
|
import { applyUpdate, Doc as YDoc } from 'yjs';
|
||||||
|
|
||||||
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
|
|
||||||
|
|
||||||
const tmpDir = path.join(__dirname, 'tmp');
|
const tmpDir = path.join(__dirname, 'tmp');
|
||||||
const testDBFilePath = path.resolve(__dirname, 'old-db.affine');
|
const testDBFilePath = path.resolve(__dirname, 'old-db.affine');
|
||||||
|
|
||||||
const appDataPath = path.join(tmpDir, 'app-data');
|
const appDataPath = path.join(tmpDir, 'app-data');
|
||||||
|
|
||||||
vi.mock('../../main-rpc', () => ({
|
vi.mock('@affine/electron/helper/main-rpc', () => ({
|
||||||
mainRPC: {
|
mainRPC: {
|
||||||
getPath: async () => appDataPath,
|
getPath: async () => appDataPath,
|
||||||
},
|
},
|
@ -1,17 +1,16 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { dbSubjects } from '@affine/electron/helper/db/subjects';
|
||||||
import { removeWithRetry } from '@affine-test/kit/utils/utils';
|
import { removeWithRetry } from '@affine-test/kit/utils/utils';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { afterEach, expect, test, vi } from 'vitest';
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
import { Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { dbSubjects } from '../subjects';
|
|
||||||
|
|
||||||
const tmpDir = path.join(__dirname, 'tmp');
|
const tmpDir = path.join(__dirname, 'tmp');
|
||||||
const appDataPath = path.join(tmpDir, 'app-data');
|
const appDataPath = path.join(tmpDir, 'app-data');
|
||||||
|
|
||||||
vi.doMock('../../main-rpc', () => ({
|
vi.doMock('@affine/electron/helper/main-rpc', () => ({
|
||||||
mainRPC: {
|
mainRPC: {
|
||||||
getPath: async () => appDataPath,
|
getPath: async () => appDataPath,
|
||||||
},
|
},
|
||||||
@ -47,7 +46,9 @@ function getTestSubDocUpdates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('can create new db file if not exists', async () => {
|
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 workspaceId = v4();
|
||||||
const db = await openWorkspaceDatabase(workspaceId);
|
const db = await openWorkspaceDatabase(workspaceId);
|
||||||
const dbPath = path.join(
|
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 () => {
|
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 workspaceId = v4();
|
||||||
const onUpdate = vi.fn();
|
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 () => {
|
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 workspaceId = v4();
|
||||||
const onUpdate = vi.fn();
|
const onUpdate = vi.fn();
|
||||||
const onExternalUpdate = 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 () => {
|
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 workspaceId = v4();
|
||||||
const onUpdate = vi.fn();
|
const onUpdate = vi.fn();
|
||||||
const insertUpdates = 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 () => {
|
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 workspaceId = v4();
|
||||||
const onUpdate = vi.fn();
|
const onUpdate = vi.fn();
|
||||||
const onExternalUpdate = 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 () => {
|
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 workspaceId = v4();
|
||||||
const db = await openWorkspaceDatabase(workspaceId);
|
const db = await openWorkspaceDatabase(workspaceId);
|
||||||
const updateSub = {
|
const updateSub = {
|
@ -8,13 +8,13 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
|||||||
const tmpDir = path.join(__dirname, 'tmp');
|
const tmpDir = path.join(__dirname, 'tmp');
|
||||||
const appDataPath = path.join(tmpDir, 'app-data');
|
const appDataPath = path.join(tmpDir, 'app-data');
|
||||||
|
|
||||||
vi.doMock('../../db/ensure-db', () => ({
|
vi.doMock('@affine/electron/helper/db/ensure-db', () => ({
|
||||||
ensureSQLiteDB: async () => ({
|
ensureSQLiteDB: async () => ({
|
||||||
destroy: () => {},
|
destroy: () => {},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.doMock('../../main-rpc', () => ({
|
vi.doMock('@affine/electron/helper/main-rpc', () => ({
|
||||||
mainRPC: {
|
mainRPC: {
|
||||||
getPath: async () => appDataPath,
|
getPath: async () => appDataPath,
|
||||||
},
|
},
|
||||||
@ -26,7 +26,9 @@ afterEach(async () => {
|
|||||||
|
|
||||||
describe('list workspaces', () => {
|
describe('list workspaces', () => {
|
||||||
test('listWorkspaces (valid)', async () => {
|
test('listWorkspaces (valid)', async () => {
|
||||||
const { listWorkspaces } = await import('../handlers');
|
const { listWorkspaces } = await import(
|
||||||
|
'@affine/electron/helper/workspace/handlers'
|
||||||
|
);
|
||||||
const workspaceId = v4();
|
const workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
const meta = {
|
const meta = {
|
||||||
@ -39,7 +41,9 @@ describe('list workspaces', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('listWorkspaces (without meta json file)', async () => {
|
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 workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
||||||
@ -56,7 +60,9 @@ describe('list workspaces', () => {
|
|||||||
|
|
||||||
describe('delete workspace', () => {
|
describe('delete workspace', () => {
|
||||||
test('deleteWorkspace', async () => {
|
test('deleteWorkspace', async () => {
|
||||||
const { deleteWorkspace } = await import('../handlers');
|
const { deleteWorkspace } = await import(
|
||||||
|
'@affine/electron/helper/workspace/handlers'
|
||||||
|
);
|
||||||
const workspaceId = v4();
|
const workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
||||||
@ -73,7 +79,9 @@ describe('delete workspace', () => {
|
|||||||
|
|
||||||
describe('getWorkspaceMeta', () => {
|
describe('getWorkspaceMeta', () => {
|
||||||
test('can get meta', async () => {
|
test('can get meta', async () => {
|
||||||
const { getWorkspaceMeta } = await import('../meta');
|
const { getWorkspaceMeta } = await import(
|
||||||
|
'@affine/electron/helper/workspace/meta'
|
||||||
|
);
|
||||||
const workspaceId = v4();
|
const workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
const meta = {
|
const meta = {
|
||||||
@ -85,7 +93,9 @@ describe('getWorkspaceMeta', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can create meta if not exists', async () => {
|
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 workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
||||||
@ -99,7 +109,9 @@ describe('getWorkspaceMeta', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can migrate meta if db file is a link', async () => {
|
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 workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
||||||
@ -121,7 +133,9 @@ describe('getWorkspaceMeta', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('storeWorkspaceMeta', async () => {
|
test('storeWorkspaceMeta', async () => {
|
||||||
const { storeWorkspaceMeta } = await import('../handlers');
|
const { storeWorkspaceMeta } = await import(
|
||||||
|
'@affine/electron/helper/workspace/handlers'
|
||||||
|
);
|
||||||
const workspaceId = v4();
|
const workspaceId = v4();
|
||||||
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
const workspacePath = path.join(appDataPath, 'workspaces', workspaceId);
|
||||||
await fs.ensureDir(workspacePath);
|
await fs.ensureDir(workspacePath);
|
@ -28,12 +28,12 @@
|
|||||||
{
|
{
|
||||||
"path": "../../common/env"
|
"path": "../../common/env"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tests
|
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"path": "./tsconfig.node.json"
|
||||||
},
|
},
|
||||||
{ "path": "../../../tests/kit" }
|
{
|
||||||
|
"path": "../../../tests/kit"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"esm": true,
|
"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';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
const rootDir = fileURLToPath(new URL('../../..', import.meta.url));
|
const rootDir = fileURLToPath(new URL('../../..', import.meta.url));
|
||||||
const pluginOutputDir = resolve(
|
|
||||||
rootDir,
|
|
||||||
'./packages/frontend/electron/dist/plugins'
|
|
||||||
);
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// prevent tests using two different sources of yjs
|
// prevent tests using two different sources of yjs
|
||||||
yjs: resolve(rootDir, 'node_modules/yjs'),
|
yjs: resolve(rootDir, 'node_modules/yjs'),
|
||||||
|
'@affine/electron': resolve(rootDir, 'packages/frontend/electron/src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
define: {
|
|
||||||
'process.env.PLUGIN_DIR': JSON.stringify(pluginOutputDir),
|
|
||||||
},
|
|
||||||
test: {
|
test: {
|
||||||
include: ['./src/**/*.spec.ts'],
|
include: ['./test/**/*.spec.ts'],
|
||||||
exclude: ['**/node_modules', '**/dist', '**/build', '**/out'],
|
|
||||||
testTimeout: 5000,
|
testTimeout: 5000,
|
||||||
singleThread: true,
|
singleThread: true,
|
||||||
threads: false,
|
threads: false,
|
||||||
|
@ -112,7 +112,7 @@ const fetchMetadata: FetchMetadata = async (get, { signal }) => {
|
|||||||
// migration step, only data in `METADATA_STORAGE_KEY` will be migrated
|
// migration step, only data in `METADATA_STORAGE_KEY` will be migrated
|
||||||
if (
|
if (
|
||||||
maybeMetadata.some(meta => !('version' in meta)) &&
|
maybeMetadata.some(meta => !('version' in meta)) &&
|
||||||
!globalThis.$migrationDone
|
!window.$migrationDone
|
||||||
) {
|
) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
signal.addEventListener('abort', () => reject(), { once: true });
|
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 { test } from '@affine-test/kit/electron';
|
||||||
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
|
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
|
||||||
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic';
|
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 {
|
import type {
|
||||||
DBHandlerManager,
|
DBHandlerManager,
|
||||||
DebugHandlerManager,
|
DebugHandlerManager,
|
||||||
@ -26,6 +26,25 @@ declare global {
|
|||||||
workspace: UnwrapManagerHandlerToClientSide<WorkspaceHandlerManager>;
|
workspace: UnwrapManagerHandlerToClientSide<WorkspaceHandlerManager>;
|
||||||
};
|
};
|
||||||
events: EventMap;
|
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 {
|
interface WindowEventMap {
|
||||||
@ -37,21 +56,11 @@ declare global {
|
|||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line no-var
|
// 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;
|
var environment: Environment;
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var runtimeConfig: RuntimeConfig;
|
var runtimeConfig: RuntimeConfig;
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var $AFFINE_SETUP: boolean | undefined;
|
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' {
|
declare module '@blocksuite/store' {
|
||||||
|
@ -103,7 +103,7 @@
|
|||||||
"path": "./packages/frontend/core"
|
"path": "./packages/frontend/core"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./packages/frontend/electron"
|
"path": "./packages/frontend/electron/tsconfig.test.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./packages/frontend/graphql"
|
"path": "./packages/frontend/graphql"
|
||||||
|
@ -19,7 +19,6 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
resolve(rootDir, './scripts/setup/lit.ts'),
|
resolve(rootDir, './scripts/setup/lit.ts'),
|
||||||
resolve(rootDir, './scripts/setup/i18n.ts'),
|
|
||||||
resolve(rootDir, './scripts/setup/lottie-web.ts'),
|
resolve(rootDir, './scripts/setup/lottie-web.ts'),
|
||||||
resolve(rootDir, './scripts/setup/global.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