mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-30 21:29:37 +08:00
feat(core): improve mobile perf (#15317)
#### PR Dependency Tree * **PR #15317** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Virtualized mobile navigation with shell navigation and interactive swipe menus; coordinated mobile back handling with interactive phases/state restoration. * Added shared auth request proxy and message-port based token handling across mobile and worker flows. * **Bug Fixes** * Hydrated remote worker error stacks for calls and observable errors. * Improved SQLite FTS/indexer and nbstore optional text handling; refined docs-search ref parsing and notification loading/retry. * **Refactor / UX** * Modal focus-preservation and pointer behavior updates; improved mobile menu controls and back gesture plugins. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const importDocs = vi.fn();
|
||||
const docsServiceToken = Symbol('DocsService');
|
||||
const organizeServiceToken = Symbol('OrganizeService');
|
||||
|
||||
vi.mock('../../blocksuite/block-suite-editor', () => ({}));
|
||||
vi.mock('@blocksuite/affine/widgets/linked-doc', () => ({
|
||||
ZipTransformer: {
|
||||
importDocs,
|
||||
},
|
||||
}));
|
||||
vi.mock('@affine/templates/onboarding.zip', () => ({
|
||||
default: '/onboarding.zip',
|
||||
}));
|
||||
vi.mock('../../modules/doc', () => ({
|
||||
DocsService: docsServiceToken,
|
||||
}));
|
||||
vi.mock('../../modules/organize', () => ({
|
||||
OrganizeService: organizeServiceToken,
|
||||
}));
|
||||
vi.mock('../../modules/workspace', () => ({
|
||||
getAFFiNEWorkspaceSchema: () => 'schema',
|
||||
}));
|
||||
|
||||
const originalBuildConfig = globalThis.BUILD_CONFIG;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
importDocs.mockReset();
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(new Blob()))
|
||||
);
|
||||
vi.stubGlobal('BUILD_CONFIG', {
|
||||
...originalBuildConfig,
|
||||
isMobileEdition: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function createWorkspacesService({
|
||||
existing = [],
|
||||
createWorkspace,
|
||||
}: {
|
||||
existing?: Array<{ id: string; flavour: string }>;
|
||||
createWorkspace?: (
|
||||
flavour: string
|
||||
) => Promise<{ id: string; flavour: string }>;
|
||||
} = {}) {
|
||||
const workspaces = [...existing];
|
||||
const createMock = vi.fn(
|
||||
createWorkspace ??
|
||||
(async (flavour: string) => {
|
||||
const meta = { id: `workspace-${workspaces.length + 1}`, flavour };
|
||||
workspaces.push(meta);
|
||||
return meta;
|
||||
})
|
||||
);
|
||||
const waitForDocReady = vi.fn(async () => {});
|
||||
const create = vi.fn(
|
||||
async (
|
||||
flavour: string,
|
||||
setup: (docCollection: {
|
||||
meta: { initialize: () => void };
|
||||
doc: { getMap: () => { set: (key: string, value: string) => void } };
|
||||
}) => Promise<void>
|
||||
) => {
|
||||
const meta = await createMock(flavour);
|
||||
await setup({
|
||||
meta: { initialize: vi.fn() },
|
||||
doc: {
|
||||
getMap: () => ({
|
||||
set: vi.fn(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
return meta;
|
||||
}
|
||||
);
|
||||
const service = {
|
||||
list: {
|
||||
['workspaces$']: {
|
||||
get value() {
|
||||
return workspaces;
|
||||
},
|
||||
},
|
||||
},
|
||||
create,
|
||||
open: ({ metadata }: { metadata: { id: string } }) => ({
|
||||
workspace: {
|
||||
id: metadata.id,
|
||||
engine: {
|
||||
doc: {
|
||||
waitForDocReady,
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
get: (token: symbol) => {
|
||||
if (token === docsServiceToken) {
|
||||
return {
|
||||
list: {
|
||||
['docs$']: {
|
||||
value: [
|
||||
{
|
||||
id: 'getting-started',
|
||||
['title$']: { value: 'Getting Started' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error('Unexpected service token');
|
||||
},
|
||||
},
|
||||
},
|
||||
dispose: vi.fn(),
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
service,
|
||||
createMock,
|
||||
workspaces,
|
||||
};
|
||||
}
|
||||
|
||||
describe('createFirstAppData', () => {
|
||||
test('does not create on desktop when the first-open marker exists', async () => {
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
const { createFirstAppData } = await import('../first-app-data');
|
||||
const { service, createMock } = createWorkspacesService();
|
||||
|
||||
expect(createFirstAppData(service as never)).toBeUndefined();
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creates on mobile when the first-open marker is stale and no workspace exists', async () => {
|
||||
vi.stubGlobal('BUILD_CONFIG', {
|
||||
...originalBuildConfig,
|
||||
isMobileEdition: true,
|
||||
});
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
const { createFirstAppData } = await import('../first-app-data');
|
||||
const { service, createMock } = createWorkspacesService();
|
||||
|
||||
await expect(createFirstAppData(service as never)).resolves.toMatchObject({
|
||||
meta: { id: 'workspace-1', flavour: 'local' },
|
||||
defaultPageId: 'getting-started',
|
||||
});
|
||||
expect(createMock).toHaveBeenCalledOnce();
|
||||
expect(localStorage.getItem('is-first-open')).toBe('false');
|
||||
});
|
||||
|
||||
test('does not create when any workspace already exists', async () => {
|
||||
vi.stubGlobal('BUILD_CONFIG', {
|
||||
...originalBuildConfig,
|
||||
isMobileEdition: true,
|
||||
});
|
||||
const { createFirstAppData } = await import('../first-app-data');
|
||||
const { service, createMock } = createWorkspacesService({
|
||||
existing: [{ id: 'existing-workspace', flavour: 'local' }],
|
||||
});
|
||||
|
||||
expect(createFirstAppData(service as never)).toBeUndefined();
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('coalesces concurrent creation attempts', async () => {
|
||||
vi.stubGlobal('BUILD_CONFIG', {
|
||||
...originalBuildConfig,
|
||||
isMobileEdition: true,
|
||||
});
|
||||
const { createFirstAppData } = await import('../first-app-data');
|
||||
let resolveCreate:
|
||||
| ((meta: { id: string; flavour: string }) => void)
|
||||
| undefined;
|
||||
const { service, createMock, workspaces } = createWorkspacesService({
|
||||
createWorkspace: flavour =>
|
||||
new Promise(resolve => {
|
||||
resolveCreate = meta => {
|
||||
workspaces.push(meta);
|
||||
resolve(meta);
|
||||
};
|
||||
expect(flavour).toBe('local');
|
||||
}),
|
||||
});
|
||||
|
||||
const first = createFirstAppData(service as never);
|
||||
const second = createFirstAppData(service as never);
|
||||
resolveCreate?.({ id: 'workspace-1', flavour: 'local' });
|
||||
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([
|
||||
{
|
||||
meta: { id: 'workspace-1', flavour: 'local' },
|
||||
defaultPageId: 'getting-started',
|
||||
},
|
||||
{
|
||||
meta: { id: 'workspace-1', flavour: 'local' },
|
||||
defaultPageId: 'getting-started',
|
||||
},
|
||||
]);
|
||||
expect(createMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('does not persist the first-open marker when creation fails', async () => {
|
||||
vi.stubGlobal('BUILD_CONFIG', {
|
||||
...originalBuildConfig,
|
||||
isMobileEdition: true,
|
||||
});
|
||||
const { createFirstAppData } = await import('../first-app-data');
|
||||
const error = new Error('create failed');
|
||||
const { service, createMock } = createWorkspacesService({
|
||||
createWorkspace: async () => {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
await expect(createFirstAppData(service as never)).rejects.toThrow(error);
|
||||
await expect(createFirstAppData(service as never)).rejects.toThrow(error);
|
||||
expect(createMock).toHaveBeenCalledTimes(2);
|
||||
expect(localStorage.getItem('is-first-open')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -67,16 +67,33 @@ export async function buildShowcaseWorkspace(
|
||||
|
||||
const logger = new DebugLogger('createFirstAppData');
|
||||
|
||||
export async function createFirstAppData(workspacesService: WorkspacesService) {
|
||||
if (localStorage.getItem('is-first-open') !== null) {
|
||||
let firstAppDataPromise:
|
||||
| Promise<Awaited<ReturnType<typeof buildShowcaseWorkspace>>>
|
||||
| undefined;
|
||||
|
||||
export function createFirstAppData(workspacesService: WorkspacesService) {
|
||||
if (workspacesService.list.workspaces$.value.length > 0) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
||||
|
||||
if (
|
||||
!BUILD_CONFIG.isMobileEdition &&
|
||||
localStorage.getItem('is-first-open') !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstAppDataPromise ??= buildShowcaseWorkspace(
|
||||
workspacesService,
|
||||
'local',
|
||||
DEFAULT_WORKSPACE_NAME
|
||||
);
|
||||
logger.info('create first workspace', defaultDocId);
|
||||
return { meta, defaultPageId: defaultDocId };
|
||||
).finally(() => {
|
||||
firstAppDataPromise = undefined;
|
||||
});
|
||||
|
||||
return firstAppDataPromise.then(({ meta, defaultDocId }) => {
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
logger.info('create first workspace', defaultDocId);
|
||||
return { meta, defaultPageId: defaultDocId };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user