diff --git a/apps/electron/tests/basic.spec.ts b/apps/electron/tests/basic.spec.ts index 9bf1e885ab..aeb8d72161 100644 --- a/apps/electron/tests/basic.spec.ts +++ b/apps/electron/tests/basic.spec.ts @@ -1,3 +1,5 @@ +import { platform } from 'node:os'; + import { expect } from '@playwright/test'; import { test } from './fixture'; @@ -11,6 +13,73 @@ test('new page', async ({ page, workspace }) => { expect(flavour).toBe('local'); }); +// macOS only +if (platform() === 'darwin') { + test('app sidebar router forward/back', async ({ page }) => { + await page.getByTestId('help-island').click(); + await page.getByTestId('easy-guide').click(); + await page.getByTestId('onboarding-modal-next-button').click(); + await page.getByTestId('onboarding-modal-close-button').click(); + { + // create pages + await page.waitForTimeout(500); + await page.getByTestId('new-page-button').click({ + delay: 100, + }); + await page.waitForSelector('v-line'); + await page.focus('.affine-default-page-block-title'); + await page.type('.affine-default-page-block-title', 'test1', { + delay: 100, + }); + await page.waitForTimeout(500); + await page.getByTestId('new-page-button').click({ + delay: 100, + }); + await page.waitForSelector('v-line'); + await page.focus('.affine-default-page-block-title'); + await page.type('.affine-default-page-block-title', 'test2', { + delay: 100, + }); + await page.waitForTimeout(500); + await page.getByTestId('new-page-button').click({ + delay: 100, + }); + await page.waitForSelector('v-line'); + await page.focus('.affine-default-page-block-title'); + await page.type('.affine-default-page-block-title', 'test3', { + delay: 100, + }); + } + { + const title = (await page + .locator('.affine-default-page-block-title') + .textContent()) as string; + expect(title.trim()).toBe('test3'); + } + + await page.click('[data-testid="app-sidebar-arrow-button-back"]'); + await page.waitForTimeout(1000); + await page.click('[data-testid="app-sidebar-arrow-button-back"]'); + await page.waitForTimeout(1000); + { + const title = (await page + .locator('.affine-default-page-block-title') + .textContent()) as string; + expect(title.trim()).toBe('test1'); + } + await page.click('[data-testid="app-sidebar-arrow-button-forward"]'); + await page.waitForTimeout(1000); + await page.click('[data-testid="app-sidebar-arrow-button-forward"]'); + await page.waitForTimeout(1000); + { + const title = (await page + .locator('.affine-default-page-block-title') + .textContent()) as string; + expect(title.trim()).toBe('test3'); + } + }); +} + test('app theme', async ({ page, electronApp }) => { const root = page.locator('html'); { diff --git a/apps/electron/tests/fixture.ts b/apps/electron/tests/fixture.ts index 6de57cf43c..b71e778a91 100644 --- a/apps/electron/tests/fixture.ts +++ b/apps/electron/tests/fixture.ts @@ -9,6 +9,7 @@ import { enableCoverage, istanbulTempDir, test as base, + testResultDir, } from '@affine-test/kit/playwright'; import fs from 'fs-extra'; import type { ElectronApplication, Page } from 'playwright'; @@ -90,6 +91,9 @@ export const test = base.extend<{ '.bin', `electron${ext}` ), + recordVideo: { + dir: testResultDir, + }, colorScheme: 'light', }); await use(electronApp); diff --git a/apps/web/src/atoms/history.ts b/apps/web/src/atoms/history.ts new file mode 100644 index 0000000000..1af3d1043e --- /dev/null +++ b/apps/web/src/atoms/history.ts @@ -0,0 +1,99 @@ +import { atom, useAtom, useSetAtom } from 'jotai'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect } from 'react'; + +export type History = { + stack: string[]; + current: number; + skip: boolean; +}; + +export const MAX_HISTORY = 50; + +export const historyBaseAtom = atom({ + stack: [], + current: 0, + skip: false, +}); + +// fixme(himself65): don't use hooks, use atom lifecycle instead +export function useTrackRouterHistoryEffect() { + const setBase = useSetAtom(historyBaseAtom); + const router = useRouter(); + useEffect(() => { + const callback = (url: string) => { + setBase(prev => { + console.log('push', url, prev.skip, prev.stack.length, prev.current); + if (prev.skip) { + return { + stack: [...prev.stack], + current: prev.current, + skip: false, + }; + } else { + if (prev.current < prev.stack.length - 1) { + const newStack = prev.stack.slice(0, prev.current); + newStack.push(url); + if (newStack.length > MAX_HISTORY) { + newStack.shift(); + } + return { + stack: newStack, + current: newStack.length - 1, + skip: false, + }; + } else { + const newStack = [...prev.stack, url]; + if (newStack.length > MAX_HISTORY) { + newStack.shift(); + } + return { + stack: newStack, + current: newStack.length - 1, + skip: false, + }; + } + } + }); + }; + + router.events.on('routeChangeComplete', callback); + return () => { + router.events.off('routeChangeComplete', callback); + }; + }, [router.events, setBase]); +} + +export function useHistoryAtom() { + const router = useRouter(); + const [base, setBase] = useAtom(historyBaseAtom); + return [ + base, + useCallback( + (forward: boolean) => { + setBase(prev => { + if (forward) { + const target = Math.min(prev.stack.length - 1, prev.current + 1); + const url = prev.stack[target]; + void router.push(url); + return { + ...prev, + current: target, + skip: true, + }; + } else { + const target = Math.max(0, prev.current - 1); + const url = prev.stack[target]; + void router.push(url); + return { + ...prev, + current: target, + skip: true, + }; + } + }); + }, + [router, setBase] + ), + ] as const; +} diff --git a/apps/web/src/components/root-app-sidebar/index.tsx b/apps/web/src/components/root-app-sidebar/index.tsx index 8bfd482d98..6ef03d917a 100644 --- a/apps/web/src/components/root-app-sidebar/index.tsx +++ b/apps/web/src/components/root-app-sidebar/index.tsx @@ -23,8 +23,9 @@ import type { Page } from '@blocksuite/store'; import { useAtom, useAtomValue } from 'jotai'; import type { ReactElement } from 'react'; import type React from 'react'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useHistoryAtom } from '../../atoms/history'; import type { AllWorkspace } from '../../shared'; import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list'; import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector'; @@ -115,10 +116,22 @@ export const RootAppSidebar = ({ }, [sidebarOpen, setSidebarOpen]); const clientUpdateAvailable = useAtomValue(updateAvailableAtom); + const [history, setHistory] = useHistoryAtom(); + const router = useMemo(() => { + return { + forward: () => { + setHistory(true); + }, + back: () => { + setHistory(false); + }, + history, + }; + }, [history, setHistory]); return ( <> - + = // todo(himself65): this is a hack, we should use a better way to set the language setUpLanguage(i18n); }, [i18n]); + useTrackRouterHistoryEffect(); const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); const meta = useMemo( diff --git a/packages/component/src/components/app-sidebar/index.tsx b/packages/component/src/components/app-sidebar/index.tsx index 6d0a49a0c2..d089cc55ed 100644 --- a/packages/component/src/components/app-sidebar/index.tsx +++ b/packages/component/src/components/app-sidebar/index.tsx @@ -21,9 +21,10 @@ import { updateAvailableAtom, } from './index.jotai'; import { ResizeIndicator } from './resize-indicator'; +import type { SidebarHeaderProps } from './sidebar-header'; import { SidebarHeader } from './sidebar-header'; -export type AppSidebarProps = PropsWithChildren; +export type AppSidebarProps = PropsWithChildren; function useEnableAnimation() { const [enable, setEnable] = useState(false); @@ -35,6 +36,11 @@ function useEnableAnimation() { return enable; } +export type History = { + stack: string[]; + current: number; +}; + export function AppSidebar(props: AppSidebarProps): ReactElement { const [open, setOpen] = useAtom(appSidebarOpenAtom); const appSidebarWidth = useAtomValue(appSidebarWidthAtom); @@ -91,7 +97,7 @@ export function AppSidebar(props: AppSidebarProps): ReactElement { data-enable-animation={enableAnimation && !isResizing} >