mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
fix(electron): create doc shortcut should follow default type in settings (#14678)
This commit is contained in:
@@ -46,7 +46,10 @@ export function setupEvents(frameworkProvider: FrameworkProvider) {
|
|||||||
const { workspace } = currentWorkspace;
|
const { workspace } = currentWorkspace;
|
||||||
const docsService = workspace.scope.get(DocsService);
|
const docsService = workspace.scope.get(DocsService);
|
||||||
|
|
||||||
const page = docsService.createDoc({ primaryMode: type });
|
const page =
|
||||||
|
type === 'default'
|
||||||
|
? docsService.createDoc()
|
||||||
|
: docsService.createDoc({ primaryMode: type });
|
||||||
workspace.scope.get(WorkbenchService).workbench.openDoc(page.id);
|
workspace.scope.get(WorkbenchService).workbench.openDoc(page.id);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function createApplicationMenu() {
|
|||||||
click: async () => {
|
click: async () => {
|
||||||
await initAndShowMainWindow();
|
await initAndShowMainWindow();
|
||||||
// fixme: if the window is just created, the new page action will not be triggered
|
// fixme: if the window is just created, the new page action will not be triggered
|
||||||
applicationMenuSubjects.newPageAction$.next('page');
|
applicationMenuSubjects.newPageAction$.next('default');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { MainEventRegister } from '../type';
|
import type { MainEventRegister } from '../type';
|
||||||
import { applicationMenuSubjects } from './subject';
|
import { applicationMenuSubjects, type NewPageAction } from './subject';
|
||||||
|
|
||||||
export * from './create';
|
export * from './create';
|
||||||
export * from './subject';
|
export * from './subject';
|
||||||
@@ -11,7 +11,7 @@ export const applicationMenuEvents = {
|
|||||||
/**
|
/**
|
||||||
* File -> New Doc
|
* File -> New Doc
|
||||||
*/
|
*/
|
||||||
onNewPageAction: (fn: (type: 'page' | 'edgeless') => void) => {
|
onNewPageAction: (fn: (type: NewPageAction) => void) => {
|
||||||
const sub = applicationMenuSubjects.newPageAction$.subscribe(fn);
|
const sub = applicationMenuSubjects.newPageAction$.subscribe(fn);
|
||||||
return () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
export type NewPageAction = 'page' | 'edgeless' | 'default';
|
||||||
|
|
||||||
export const applicationMenuSubjects = {
|
export const applicationMenuSubjects = {
|
||||||
newPageAction$: new Subject<'page' | 'edgeless'>(),
|
newPageAction$: new Subject<NewPageAction>(),
|
||||||
openJournal$: new Subject<void>(),
|
openJournal$: new Subject<void>(),
|
||||||
openInSettingModal$: new Subject<{
|
openInSettingModal$: new Subject<{
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { beforeAppQuit } from './cleanup';
|
|||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { powerEvents } from './power';
|
import { powerEvents } from './power';
|
||||||
import { recordingEvents } from './recording';
|
import { recordingEvents } from './recording';
|
||||||
|
import { checkSource } from './security-restrictions';
|
||||||
import { sharedStorageEvents } from './shared-storage';
|
import { sharedStorageEvents } from './shared-storage';
|
||||||
import { uiEvents } from './ui/events';
|
import { uiEvents } from './ui/events';
|
||||||
import { updaterEvents } from './updater/event';
|
import { updaterEvents } from './updater/event';
|
||||||
@@ -70,7 +71,7 @@ export function registerEvents() {
|
|||||||
action: 'subscribe' | 'unsubscribe',
|
action: 'subscribe' | 'unsubscribe',
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
if (typeof channel !== 'string') return;
|
if (!checkSource(event) || typeof channel !== 'string') return;
|
||||||
if (action === 'subscribe') {
|
if (action === 'subscribe') {
|
||||||
addSubscription(event.sender, channel);
|
addSubscription(event.sender, channel);
|
||||||
if (channel === 'power:power-source') {
|
if (channel === 'power:power-source') {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { configStorageHandlers } from './config-storage';
|
|||||||
import { findInPageHandlers } from './find-in-page';
|
import { findInPageHandlers } from './find-in-page';
|
||||||
import { getLogFilePath, logger, revealLogFile } from './logger';
|
import { getLogFilePath, logger, revealLogFile } from './logger';
|
||||||
import { recordingHandlers } from './recording';
|
import { recordingHandlers } from './recording';
|
||||||
|
import { checkSource } from './security-restrictions';
|
||||||
import { sharedStorageHandlers } from './shared-storage';
|
import { sharedStorageHandlers } from './shared-storage';
|
||||||
import { uiHandlers } from './ui/handlers';
|
import { uiHandlers } from './ui/handlers';
|
||||||
import { updaterHandlers } from './updater';
|
import { updaterHandlers } from './updater';
|
||||||
@@ -49,7 +50,7 @@ export const registerHandlers = () => {
|
|||||||
...args: any[]
|
...args: any[]
|
||||||
) => {
|
) => {
|
||||||
// args[0] is the `{namespace:key}`
|
// args[0] is the `{namespace:key}`
|
||||||
if (typeof args[0] !== 'string') {
|
if (!checkSource(e) || typeof args[0] !== 'string') {
|
||||||
logger.error('invalid ipc message', args);
|
logger.error('invalid ipc message', args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,6 +98,8 @@ export const registerHandlers = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on(AFFINE_API_CHANNEL_NAME, (e, ...args: any[]) => {
|
ipcMain.on(AFFINE_API_CHANNEL_NAME, (e, ...args: any[]) => {
|
||||||
|
if (!checkSource(e)) return;
|
||||||
|
|
||||||
handleIpcMessage(e, ...args)
|
handleIpcMessage(e, ...args)
|
||||||
.then(ret => {
|
.then(ret => {
|
||||||
e.returnValue = ret;
|
e.returnValue = ret;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import './security-restrictions';
|
|
||||||
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/electron/main';
|
import * as Sentry from '@sentry/electron/main';
|
||||||
@@ -15,6 +13,7 @@ import { registerHandlers } from './handlers';
|
|||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { registerProtocol } from './protocol';
|
import { registerProtocol } from './protocol';
|
||||||
import { setupRecordingFeature } from './recording/feature';
|
import { setupRecordingFeature } from './recording/feature';
|
||||||
|
import { registerSecurityRestrictions } from './security-restrictions';
|
||||||
import { setupTrayState } from './tray';
|
import { setupTrayState } from './tray';
|
||||||
import { registerUpdater } from './updater';
|
import { registerUpdater } from './updater';
|
||||||
import { launch } from './windows-manager/launcher';
|
import { launch } from './windows-manager/launcher';
|
||||||
@@ -105,6 +104,7 @@ app.on('activate', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setupDeepLink(app);
|
setupDeepLink(app);
|
||||||
|
registerSecurityRestrictions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create app window when background process will be ready
|
* Create app window when background process will be ready
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { pathToFileURL } from 'node:url';
|
|||||||
import { app, net, protocol, session } from 'electron';
|
import { app, net, protocol, session } from 'electron';
|
||||||
import cookieParser from 'set-cookie-parser';
|
import cookieParser from 'set-cookie-parser';
|
||||||
|
|
||||||
|
import { anotherHost, mainHost } from '../shared/internal-origin';
|
||||||
import { isWindows, resourcesPath } from '../shared/utils';
|
import { isWindows, resourcesPath } from '../shared/utils';
|
||||||
import { buildType, isDev } from './config';
|
import { buildType, isDev } from './config';
|
||||||
import { anotherHost, mainHost } from './constants';
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
const webStaticDir = join(resourcesPath, 'web-static');
|
const webStaticDir = join(resourcesPath, 'web-static');
|
||||||
|
|||||||
@@ -1,71 +1,71 @@
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
import { anotherHost, mainHost } from './constants';
|
import { isInternalUrl } from '../shared/internal-origin';
|
||||||
|
import { logger } from './logger';
|
||||||
import { openExternalSafely } from './security/open-external';
|
import { openExternalSafely } from './security/open-external';
|
||||||
import { validateRedirectProxyUrl } from './security/redirect-proxy';
|
import { validateRedirectProxyUrl } from './security/redirect-proxy';
|
||||||
|
|
||||||
app.on('web-contents-created', (_, contents) => {
|
export const checkSource = (
|
||||||
const isInternalUrl = (url: string) => {
|
e: Electron.IpcMainInvokeEvent | Electron.IpcMainEvent
|
||||||
try {
|
) => {
|
||||||
const parsed = new URL(url);
|
const url = e.senderFrame?.url || e.sender.getURL();
|
||||||
if (
|
const result = isInternalUrl(url);
|
||||||
parsed.protocol === 'assets:' &&
|
if (!result) logger.error('invalid source', url);
|
||||||
(parsed.hostname === mainHost || parsed.hostname === anotherHost)
|
return result;
|
||||||
) {
|
};
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Block navigation to origins not on the allowlist.
|
|
||||||
*
|
|
||||||
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
|
|
||||||
* from its current page, they can possibly force the app to open web sites on the Internet.
|
|
||||||
*
|
|
||||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
|
||||||
*/
|
|
||||||
contents.on('will-navigate', (event, url) => {
|
|
||||||
if (isInternalUrl(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Prevent navigation
|
|
||||||
event.preventDefault();
|
|
||||||
openExternalSafely(url).catch(error => {
|
|
||||||
console.error('[security] Failed to open external URL:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
export const registerSecurityRestrictions = () => {
|
||||||
* Hyperlinks to allowed sites open in the default browser.
|
app.on('web-contents-created', (_, contents) => {
|
||||||
*
|
/**
|
||||||
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
* Block navigation to origins not on the allowlist.
|
||||||
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
*
|
||||||
* You should deny any unexpected window creation.
|
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
|
||||||
*
|
* from its current page, they can possibly force the app to open web sites on the Internet.
|
||||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
*
|
||||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||||
*/
|
*/
|
||||||
contents.setWindowOpenHandler(({ url }) => {
|
contents.on('will-navigate', (event, url) => {
|
||||||
if (!isInternalUrl(url)) {
|
if (isInternalUrl(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent navigation
|
||||||
|
event.preventDefault();
|
||||||
openExternalSafely(url).catch(error => {
|
openExternalSafely(url).catch(error => {
|
||||||
console.error('[security] Failed to open external URL:', error);
|
console.error('[security] Failed to open external URL:', error);
|
||||||
});
|
});
|
||||||
} else if (url.includes('/redirect-proxy')) {
|
});
|
||||||
const result = validateRedirectProxyUrl(url);
|
|
||||||
if (!result.allow) {
|
|
||||||
console.warn(
|
|
||||||
`[security] Blocked redirect proxy: ${result.reason}`,
|
|
||||||
result.redirectTarget ?? url
|
|
||||||
);
|
|
||||||
return { action: 'deny' };
|
|
||||||
}
|
|
||||||
|
|
||||||
openExternalSafely(result.redirectTarget).catch(error => {
|
/**
|
||||||
console.error('[security] Failed to open external URL:', error);
|
* Hyperlinks to allowed sites open in the default browser.
|
||||||
});
|
*
|
||||||
}
|
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
||||||
// Prevent creating new window in application
|
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
||||||
return { action: 'deny' };
|
* You should deny any unexpected window creation.
|
||||||
|
*
|
||||||
|
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
||||||
|
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||||
|
*/
|
||||||
|
contents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (!isInternalUrl(url)) {
|
||||||
|
openExternalSafely(url).catch(error => {
|
||||||
|
console.error('[security] Failed to open external URL:', error);
|
||||||
|
});
|
||||||
|
} else if (url.includes('/redirect-proxy')) {
|
||||||
|
const result = validateRedirectProxyUrl(url);
|
||||||
|
if (!result.allow) {
|
||||||
|
console.warn(
|
||||||
|
`[security] Blocked redirect proxy: ${result.reason}`,
|
||||||
|
result.redirectTarget ?? url
|
||||||
|
);
|
||||||
|
return { action: 'deny' };
|
||||||
|
}
|
||||||
|
|
||||||
|
openExternalSafely(result.redirectTarget).catch(error => {
|
||||||
|
console.error('[security] Failed to open external URL:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Prevent creating new window in application
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
import { BrowserWindow, type Display, screen } from 'electron';
|
import { BrowserWindow, type Display, screen } from 'electron';
|
||||||
|
|
||||||
|
import { customThemeViewUrl } from '../../shared/internal-origin';
|
||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { customThemeViewUrl } from '../constants';
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { buildWebPreferences } from '../web-preferences';
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
|||||||
import electronWindowState from 'electron-window-state';
|
import electronWindowState from 'electron-window-state';
|
||||||
import { BehaviorSubject, map, shareReplay } from 'rxjs';
|
import { BehaviorSubject, map, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
|
import { mainWindowOrigin } from '../../shared/internal-origin';
|
||||||
import { isLinux, isMacOS, isWindows, resourcesPath } from '../../shared/utils';
|
import { isLinux, isMacOS, isWindows, resourcesPath } from '../../shared/utils';
|
||||||
import { beforeAppQuit } from '../cleanup';
|
import { beforeAppQuit } from '../cleanup';
|
||||||
import { buildType } from '../config';
|
import { buildType } from '../config';
|
||||||
import { mainWindowOrigin } from '../constants';
|
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
import { BrowserWindow, screen } from 'electron';
|
import { BrowserWindow, screen } from 'electron';
|
||||||
|
|
||||||
|
import { onboardingViewUrl } from '../../shared/internal-origin';
|
||||||
import { isDev } from '../config';
|
import { isDev } from '../config';
|
||||||
import { onboardingViewUrl } from '../constants';
|
|
||||||
// import { getExposedMeta } from './exposed';
|
// import { getExposedMeta } from './exposed';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { buildWebPreferences } from '../web-preferences';
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { popupViewUrl } from '../constants';
|
import { popupViewUrl } from '../../shared/internal-origin';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import type { MainEventRegister, NamespaceHandlers } from '../type';
|
import type { MainEventRegister, NamespaceHandlers } from '../type';
|
||||||
import { buildWebPreferences } from '../web-preferences';
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import {
|
|||||||
type Unsubscribable,
|
type Unsubscribable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { mainWindowOrigin, shellViewUrl } from '../../shared/internal-origin';
|
||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { beforeAppQuit, onTabClose } from '../cleanup';
|
import { beforeAppQuit, onTabClose } from '../cleanup';
|
||||||
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
|||||||
|
|
||||||
import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron';
|
import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron';
|
||||||
|
|
||||||
import { backgroundWorkerViewUrl } from '../constants';
|
import { backgroundWorkerViewUrl } from '../../shared/internal-origin';
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { buildWebPreferences } from '../web-preferences';
|
import { buildWebPreferences } from '../web-preferences';
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import '@sentry/electron/preload';
|
|||||||
|
|
||||||
import { contextBridge } from 'electron';
|
import { contextBridge } from 'electron';
|
||||||
|
|
||||||
|
import { isInternalUrl } from '../shared/internal-origin';
|
||||||
import { apis, appInfo, events } from './electron-api';
|
import { apis, appInfo, events } from './electron-api';
|
||||||
import { sharedStorage } from './shared-storage';
|
import { sharedStorage } from './shared-storage';
|
||||||
import { listenWorkerApis } from './worker';
|
import { listenWorkerApis } from './worker';
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('__appInfo', appInfo);
|
const locationLike = (globalThis as { location?: { href?: unknown } }).location;
|
||||||
contextBridge.exposeInMainWorld('__apis', apis);
|
|
||||||
contextBridge.exposeInMainWorld('__events', events);
|
|
||||||
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
|
|
||||||
|
|
||||||
listenWorkerApis();
|
const currentUrl =
|
||||||
|
typeof locationLike?.href === 'string' ? locationLike.href : null;
|
||||||
|
|
||||||
|
if (currentUrl && isInternalUrl(currentUrl)) {
|
||||||
|
contextBridge.exposeInMainWorld('__appInfo', appInfo);
|
||||||
|
contextBridge.exposeInMainWorld('__apis', apis);
|
||||||
|
contextBridge.exposeInMainWorld('__events', events);
|
||||||
|
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
|
||||||
|
|
||||||
|
listenWorkerApis();
|
||||||
|
}
|
||||||
|
|||||||
+10
@@ -1,5 +1,6 @@
|
|||||||
export const mainHost = '.';
|
export const mainHost = '.';
|
||||||
export const anotherHost = 'another-host';
|
export const anotherHost = 'another-host';
|
||||||
|
export const internalHosts = new Set([mainHost, anotherHost]);
|
||||||
|
|
||||||
export const mainWindowOrigin = `assets://${mainHost}`;
|
export const mainWindowOrigin = `assets://${mainHost}`;
|
||||||
export const anotherOrigin = `assets://${anotherHost}`;
|
export const anotherOrigin = `assets://${anotherHost}`;
|
||||||
@@ -13,3 +14,12 @@ export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor`;
|
|||||||
// Notes from electron official docs:
|
// Notes from electron official docs:
|
||||||
// "The zoom policy at the Chromium level is same-origin, meaning that the zoom level for a specific domain propagates across all instances of windows with the same domain. Differentiating the window URLs will make zoom work per-window."
|
// "The zoom policy at the Chromium level is same-origin, meaning that the zoom level for a specific domain propagates across all instances of windows with the same domain. Differentiating the window URLs will make zoom work per-window."
|
||||||
export const popupViewUrl = `${anotherOrigin}/popup.html`;
|
export const popupViewUrl = `${anotherOrigin}/popup.html`;
|
||||||
|
|
||||||
|
export const isInternalUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.protocol === 'assets:' && internalHosts.has(parsed.hostname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { test } from '@affine-test/kit/electron';
|
import { test } from '@affine-test/kit/electron';
|
||||||
|
import {
|
||||||
|
ensureInEdgelessMode,
|
||||||
|
ensureInPageMode,
|
||||||
|
} from '@affine-test/kit/utils/editor';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
|
waitForEditorLoad,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
||||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||||
@@ -14,12 +19,55 @@ const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setNewDocDefaultMode = async (
|
||||||
|
page: Page,
|
||||||
|
mode: 'page' | 'edgeless' | 'ask'
|
||||||
|
) => {
|
||||||
|
const modeTriggerByValue = {
|
||||||
|
page: 'page-mode-trigger',
|
||||||
|
edgeless: 'edgeless-mode-trigger',
|
||||||
|
ask: 'ask-every-time-trigger',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await clickSideBarSettingButton(page);
|
||||||
|
await page.getByTestId('editor-panel-trigger').click();
|
||||||
|
await page.getByTestId('new-doc-default-mode-trigger').click();
|
||||||
|
await page.getByTestId(modeTriggerByValue[mode]).click();
|
||||||
|
await page.getByTestId('modal-close-button').click();
|
||||||
|
};
|
||||||
|
|
||||||
test('new page', async ({ page, workspace }) => {
|
test('new page', async ({ page, workspace }) => {
|
||||||
await clickNewPageButton(page);
|
await clickNewPageButton(page);
|
||||||
const flavour = (await workspace.current()).meta.flavour;
|
const flavour = (await workspace.current()).meta.flavour;
|
||||||
expect(flavour).toBe('local');
|
expect(flavour).toBe('local');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('application menu respects default new doc mode', async ({
|
||||||
|
electronApp,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await ensureInPageMode(page);
|
||||||
|
|
||||||
|
await setNewDocDefaultMode(page, 'edgeless');
|
||||||
|
await electronApp.evaluate(({ BrowserWindow, Menu }) => {
|
||||||
|
const menuItem =
|
||||||
|
Menu.getApplicationMenu()?.getMenuItemById('affine:new-page');
|
||||||
|
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
|
||||||
|
if (!menuItem) {
|
||||||
|
throw new Error('Missing application menu item: affine:new-page');
|
||||||
|
}
|
||||||
|
if (!focusedWindow) {
|
||||||
|
throw new Error('Missing focused window for application menu dispatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItem.click(undefined, focusedWindow, focusedWindow.webContents);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureInEdgelessMode(page);
|
||||||
|
});
|
||||||
|
|
||||||
test('app sidebar router forward/back', async ({ page }) => {
|
test('app sidebar router forward/back', async ({ page }) => {
|
||||||
// create pages
|
// create pages
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|||||||
Reference in New Issue
Block a user