fix(editor): incorrect edgeless viewport in peek view (#11040)

### What Changes
- Fixed incorrect edgeless viewport display in peek view
  - Moved page block viewport fit animation logic from `EdgelessRootBlockComponent` to note config extension
  - Disabled page block viewport fit animation in peek view, using default `fitToScreen` instead
  - @doodlewind Fixed viewport resizing issues by adding a immediate update mechanism to ensure proper rendering during peek view operations.  The setViewportByBound is only called once during peek view initialization, so there are barely perf overhead.
- Updated related test cases
  - Refactored peek view test cases to make them clearer and more reliable
  - Added new test helper function `getViewportBound` for getting viewport boundary information
This commit is contained in:
L-Sun
2025-03-26 09:29:04 +00:00
parent a4b4029201
commit c23b8f604b
9 changed files with 246 additions and 145 deletions

View File

@@ -13,6 +13,10 @@ type NoteBlockContext = {
export type NoteConfig = { export type NoteConfig = {
edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult; edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult;
pageBlockTitle: (context: NoteBlockContext) => TemplateResult; pageBlockTitle: (context: NoteBlockContext) => TemplateResult;
/**
* @returns if the viewport fit animation executed
*/
pageBlockViewportFitAnimation?: (context: NoteBlockContext) => boolean;
}; };
export const NoteConfigExtension = export const NoteConfigExtension =

View File

@@ -1,3 +1,4 @@
import { NoteConfigExtension } from '@blocksuite/affine-block-note';
import type { import type {
SurfaceBlockComponent, SurfaceBlockComponent,
SurfaceBlockModel, SurfaceBlockModel,
@@ -11,13 +12,10 @@ import { isSingleMindMapNode } from '@blocksuite/affine-gfx-mindmap';
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape'; import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
import { import {
NoteBlockModel, NoteBlockModel,
NoteDisplayMode,
type RootBlockModel, type RootBlockModel,
type ShapeElementModel, type ShapeElementModel,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
import { import {
DocModeProvider,
EditorSettingProvider, EditorSettingProvider,
EditPropsStore, EditPropsStore,
FontLoaderService, FontLoaderService,
@@ -310,71 +308,33 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initViewport() { private _initViewport() {
const { std, gfx } = this; const { std, gfx } = this;
const pageBlockViewportFitAnimation = () => {
const primaryMode = std.get(DocModeProvider).getPrimaryMode(this.doc.id);
const note = this.model.children.find(
(child): child is NoteBlockModel =>
matchModels(child, [NoteBlockModel]) &&
child.props.displayMode !== NoteDisplayMode.EdgelessOnly
);
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse)
return false;
const leftPadding = parseInt(
window
.getComputedStyle(this)
.getPropertyValue('--affine-editor-side-padding')
.replace('px', '')
);
if (isNaN(leftPadding)) return false;
let editorWidth = parseInt(
window
.getComputedStyle(this)
.getPropertyValue('--affine-editor-width')
.replace('px', '')
);
if (isNaN(editorWidth)) return false;
const containerWidth = this.getBoundingClientRect().width;
const leftMargin =
containerWidth > editorWidth ? (containerWidth - editorWidth) / 2 : 0;
const pageTitleAnchor = gfx.viewport.toModelCoord(
leftPadding + leftMargin,
0
);
const noteBound = Bound.deserialize(note.xywh);
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
EDGELESS_BLOCK_CHILD_PADDING,
12,
]);
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
gfx.viewport.setCenter(center[0], center[1]);
gfx.viewport.smoothZoom(0.65, undefined, 15);
return true;
};
const run = () => { const run = () => {
const animationFn = std.getOptional(
NoteConfigExtension.identifier
)?.pageBlockViewportFitAnimation;
if (animationFn) {
const pageBlock = this.model.children.find(
(child): child is NoteBlockModel =>
matchModels(child, [NoteBlockModel]) && child.isPageBlock()
);
if (pageBlock && animationFn({ std: this.std, note: pageBlock })) {
return;
}
}
const storedViewport = std.get(EditPropsStore).getStorage('viewport'); const storedViewport = std.get(EditPropsStore).getStorage('viewport');
if (!storedViewport) { if (storedViewport) {
if (!pageBlockViewportFitAnimation()) { if ('xywh' in storedViewport) {
this.gfx.fitToScreen(); const bound = Bound.deserialize(storedViewport.xywh);
gfx.viewport.setViewportByBound(bound, storedViewport.padding);
} else {
const { zoom, centerX, centerY } = storedViewport;
gfx.viewport.setViewport(zoom, [centerX, centerY]);
} }
return; return;
} }
if ('xywh' in storedViewport) { this.gfx.fitToScreen();
const bound = Bound.deserialize(storedViewport.xywh);
gfx.viewport.setViewportByBound(bound, storedViewport.padding);
} else {
const { zoom, centerX, centerY } = storedViewport;
gfx.viewport.setViewport(zoom, [centerX, centerY]);
}
}; };
run(); run();

View File

@@ -9,3 +9,4 @@ export * from './gfx-tool';
export * from './utils/clipboard-utils.js'; export * from './utils/clipboard-utils.js';
export { sortEdgelessElements } from './utils/clone-utils.js'; export { sortEdgelessElements } from './utils/clone-utils.js';
export { isCanvasElement } from './utils/query.js'; export { isCanvasElement } from './utils/query.js';
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';

View File

@@ -129,28 +129,45 @@ export class Viewport {
.pipe(debounceTime(200)) .pipe(debounceTime(200))
.subscribe(({ width, height, left, top }) => { .subscribe(({ width, height, left, top }) => {
if (!this._shell || !this._initialTopLeft) return; if (!this._shell || !this._initialTopLeft) return;
this._completeResize(width, height, left, top);
const [initialTopLeftX, initialTopLeftY] = this._initialTopLeft;
const newCenterX = initialTopLeftX + width / (2 * this.zoom);
const newCenterY = initialTopLeftY + height / (2 * this.zoom);
this.setCenter(newCenterX, newCenterY);
this._width = width;
this._height = height;
this._left = left;
this._top = top;
this._isResizing = false;
this._initialTopLeft = null;
this.sizeUpdated.next({
left,
top,
width,
height,
});
}); });
} }
private _completeResize(
width: number,
height: number,
left: number,
top: number
) {
if (!this._initialTopLeft) return;
const [initialTopLeftX, initialTopLeftY] = this._initialTopLeft;
const newCenterX = initialTopLeftX + width / (2 * this.zoom);
const newCenterY = initialTopLeftY + height / (2 * this.zoom);
this.setCenter(newCenterX, newCenterY);
this._width = width;
this._height = height;
this._left = left;
this._top = top;
this._isResizing = false;
this._initialTopLeft = null;
this.sizeUpdated.next({
left,
top,
width,
height,
});
}
private _forceCompleteResize() {
if (this._isResizing && this._shell) {
const { width, height, left, top } = this.boundingClientRect;
this._completeResize(width, height, left, top);
}
}
get boundingClientRect() { get boundingClientRect() {
if (!this._shell) return new DOMRect(0, 0, 0, 0); if (!this._shell) return new DOMRect(0, 0, 0, 0);
if (!this._cachedBoundingClientRect) { if (!this._cachedBoundingClientRect) {
@@ -341,7 +358,17 @@ export class Viewport {
}); });
} }
setCenter(centerX: number, centerY: number) { /**
* Set the center of the viewport.
* @param centerX The new x coordinate of the center of the viewport.
* @param centerY The new y coordinate of the center of the viewport.
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
*/
setCenter(centerX: number, centerY: number, forceUpdate = false) {
if (forceUpdate && this._isResizing) {
this._forceCompleteResize();
}
this._center.x = centerX; this._center.x = centerX;
this._center.y = centerY; this._center.y = centerY;
this.panning$.value = true; this.panning$.value = true;
@@ -369,11 +396,24 @@ export class Viewport {
}); });
} }
/**
* Set the viewport to the new zoom and center.
* @param newZoom The new zoom value.
* @param newCenter The new center of the viewport.
* @param smooth Whether to animate the zooming and panning.
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
*/
setViewport( setViewport(
newZoom: number, newZoom: number,
newCenter = Vec.toVec(this.center), newCenter = Vec.toVec(this.center),
smooth = false smooth = false,
forceUpdate = smooth
) { ) {
// Force complete any pending resize operations if forceUpdate is true
if (forceUpdate && this._isResizing) {
this._forceCompleteResize();
}
const preZoom = this._zoom; const preZoom = this._zoom;
if (smooth) { if (smooth) {
const cofficient = preZoom / newZoom; const cofficient = preZoom / newZoom;
@@ -390,7 +430,7 @@ export class Viewport {
} else { } else {
this._center.x = newCenter[0]; this._center.x = newCenter[0];
this._center.y = newCenter[1]; this._center.y = newCenter[1];
this.setZoom(newZoom); this.setZoom(newZoom, undefined, false, forceUpdate);
} }
} }
@@ -401,11 +441,13 @@ export class Viewport {
* the value may be reduced if there is not enough space for the padding. * the value may be reduced if there is not enough space for the padding.
* Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding. * Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding.
* @param smooth whether to animate the zooming * @param smooth whether to animate the zooming
* @param forceUpdate whether to force complete any pending resize operations before setting the viewport
*/ */
setViewportByBound( setViewportByBound(
bound: Bound, bound: Bound,
padding: [number, number, number, number] = [0, 0, 0, 0], padding: [number, number, number, number] = [0, 0, 0, 0],
smooth = false smooth = false,
forceUpdate = smooth
) { ) {
let [pt, pr, pb, pl] = padding; let [pt, pr, pb, pl] = padding;
@@ -440,7 +482,7 @@ export class Viewport {
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2, bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
] as IVec; ] as IVec;
this.setViewport(zoom, center, smooth); this.setViewport(zoom, center, smooth, forceUpdate);
} }
/** This is the outer container of the viewport, which is the host of the viewport element */ /** This is the outer container of the viewport, which is the host of the viewport element */
@@ -460,7 +502,23 @@ export class Viewport {
this._resizeObserver.observe(el); this._resizeObserver.observe(el);
} }
setZoom(zoom: number, focusPoint?: IPoint, wheel = false) { /**
* Set the viewport to the new zoom.
* @param zoom The new zoom value.
* @param focusPoint The point to focus on after zooming, default is the center of the viewport.
* @param wheel Whether the zoom is caused by wheel event.
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
*/
setZoom(
zoom: number,
focusPoint?: IPoint,
wheel = false,
forceUpdate = false
) {
if (forceUpdate && this._isResizing) {
this._forceCompleteResize();
}
const prevZoom = this.zoom; const prevZoom = this.zoom;
focusPoint = (focusPoint ?? this._center) as IPoint; focusPoint = (focusPoint ?? this._center) as IPoint;
this._zoom = clamp(zoom, this.ZOOM_MIN, this.ZOOM_MAX); this._zoom = clamp(zoom, this.ZOOM_MIN, this.ZOOM_MAX);
@@ -474,7 +532,7 @@ export class Viewport {
if (wheel) { if (wheel) {
this.zooming$.value = true; this.zooming$.value = true;
} }
this.setCenter(newCenter[0], newCenter[1]); this.setCenter(newCenter[0], newCenter[1], forceUpdate);
this.viewportUpdated.next({ this.viewportUpdated.next({
zoom: this.zoom, zoom: this.zoom,
center: Vec.toVec(this.center) as IVec, center: Vec.toVec(this.center) as IVec,
@@ -497,7 +555,7 @@ export class Viewport {
const signY = delta.y > 0 ? 1 : -1; const signY = delta.y > 0 ? 1 : -1;
nextCenter.x = cutoff(nextCenter.x, x, signX); nextCenter.x = cutoff(nextCenter.x, x, signX);
nextCenter.y = cutoff(nextCenter.y, y, signY); nextCenter.y = cutoff(nextCenter.y, y, signY);
this.setCenter(nextCenter.x, nextCenter.y); this.setCenter(nextCenter.x, nextCenter.y, true);
if (nextCenter.x != x || nextCenter.y != y) innerSmoothTranslate(); if (nextCenter.x != x || nextCenter.y != y) innerSmoothTranslate();
}); });
@@ -515,7 +573,7 @@ export class Viewport {
const step = delta / numSteps; const step = delta / numSteps;
const nextZoom = cutoff(this.zoom + step, zoom, sign); const nextZoom = cutoff(this.zoom + step, zoom, sign);
this.setZoom(nextZoom, focusPoint); this.setZoom(nextZoom, focusPoint, undefined, true);
if (nextZoom != zoom) innerSmoothZoom(); if (nextZoom != zoom) innerSmoothZoom();
}); });

View File

@@ -19,6 +19,7 @@ import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { useInsidePeekView } from '@affine/core/modules/peek-view';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view'; import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
import { MemberSearchService } from '@affine/core/modules/permissions'; import { MemberSearchService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace'; import { WorkspaceService } from '@affine/core/modules/workspace';
@@ -147,6 +148,8 @@ const usePatchSpecs = (mode: DocMode) => {
const enableAI = useEnableAI(); const enableAI = useEnableAI();
const insidePeekView = useInsidePeekView();
const enableTurboRenderer = useLiveData( const enableTurboRenderer = useLiveData(
featureFlagService.flags.enable_turbo_renderer.$ featureFlagService.flags.enable_turbo_renderer.$
); );
@@ -165,7 +168,7 @@ const usePatchSpecs = (mode: DocMode) => {
builder.extend( builder.extend(
[ [
patchReferenceRenderer(reactToLit, referenceRenderer), patchReferenceRenderer(reactToLit, referenceRenderer),
patchForEdgelessNoteConfig(framework, reactToLit), patchForEdgelessNoteConfig(framework, reactToLit, insidePeekView),
patchNotificationService(confirmModal), patchNotificationService(confirmModal),
patchPeekViewService(peekViewService), patchPeekViewService(peekViewService),
patchOpenDocExtension(), patchOpenDocExtension(),
@@ -207,6 +210,7 @@ const usePatchSpecs = (mode: DocMode) => {
enableAI, enableAI,
reactToLit, reactToLit,
referenceRenderer, referenceRenderer,
insidePeekView,
confirmModal, confirmModal,
peekViewService, peekViewService,
docService, docService,

View File

@@ -1,6 +1,13 @@
import type { ElementOrFactory } from '@affine/component'; import type { ElementOrFactory } from '@affine/component';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note'; import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
import {
DocModeProvider,
EditPropsStore,
} from '@blocksuite/affine/shared/services';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
import { html, type TemplateResult } from 'lit'; import { html, type TemplateResult } from 'lit';
@@ -9,7 +16,8 @@ import { EdgelessNoteHeader } from './edgeless-note-header';
export function patchForEdgelessNoteConfig( export function patchForEdgelessNoteConfig(
framework: FrameworkProvider, framework: FrameworkProvider,
reactToLit: (element: ElementOrFactory) => TemplateResult reactToLit: (element: ElementOrFactory) => TemplateResult,
insidePeekView: boolean
) { ) {
return NoteConfigExtension({ return NoteConfigExtension({
edgelessNoteHeader: ({ note }) => edgelessNoteHeader: ({ note }) =>
@@ -23,5 +31,69 @@ export function patchForEdgelessNoteConfig(
return html`<doc-title .doc=${note.doc}></doc-title>`; return html`<doc-title .doc=${note.doc}></doc-title>`;
} }
}, },
pageBlockViewportFitAnimation: insidePeekView
? undefined
: ({ std, note }) => {
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
// if there is a stored viewport, don't run the animation
// in other word, this doc has been opened before
if (storedViewport) return false;
if (!std.store.root) return false;
const rootView = std.view.getBlock(std.store.root.id);
if (!rootView) return false;
const gfx = std.get(GfxControllerIdentifier);
const primaryMode = std
.get(DocModeProvider)
.getPrimaryMode(std.store.id);
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
return false;
}
const leftPadding = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-side-padding')
.replace('px', '')
);
if (isNaN(leftPadding)) {
return false;
}
let editorWidth = parseInt(
window
.getComputedStyle(rootView)
.getPropertyValue('--affine-editor-width')
.replace('px', '')
);
if (isNaN(editorWidth)) {
return false;
}
const containerWidth = rootView.getBoundingClientRect().width;
const leftMargin =
containerWidth > editorWidth
? (containerWidth - editorWidth) / 2
: 0;
const pageTitleAnchor = gfx.viewport.toModelCoord(
leftPadding + leftMargin,
0
);
const noteBound = Bound.deserialize(note.xywh);
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
EDGELESS_BLOCK_CHILD_PADDING,
12,
]);
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
gfx.viewport.setCenter(center[0], center[1], true);
gfx.viewport.smoothZoom(0.65, undefined, 15);
return true;
},
}); });
} }

View File

@@ -58,7 +58,8 @@ function fitViewport(
viewport.setViewportByBound( viewport.setViewportByBound(
Bound.deserialize(newViewport.xywh), Bound.deserialize(newViewport.xywh),
newViewport.padding, newViewport.padding,
false false,
true
); );
} else { } else {
gfx.fitToScreen({ gfx.fitToScreen({

View File

@@ -1,12 +1,14 @@
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import { import {
clickEdgelessModeButton, getSelectedXYWH,
clickPageModeButton, getViewportBound,
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { pressEnter, pressEscape } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage } from '@affine-test/kit/utils/load-page';
import { import {
clickNewPageButton, clickNewPageButton,
createLinkedPage, createLinkedPage,
type,
waitForEmptyEditor, waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
@@ -103,78 +105,66 @@ test('can open peek view via db+click link card', async ({ page }) => {
}); });
test('can open peek view for embedded frames', async ({ page }) => { test('can open peek view for embedded frames', async ({ page }) => {
await page.keyboard.press('Enter'); const frameInViewport = async () => {
const peekView = page.locator('[data-testid="peek-view-modal"]');
// wait for peek view ani
const frameTitle = peekView.locator('edgeless-editor affine-frame-title');
await frameTitle.waitFor({ state: 'visible' });
// create a frame await frameTitle.click();
await page.keyboard.insertText('```frame\nTest Frame\n```'); const frameXYWH = await getSelectedXYWH(page, 0, 1);
const viewportBound = await getViewportBound(page, 1);
return (
frameXYWH[0] >= viewportBound[0] &&
frameXYWH[1] >= viewportBound[1] &&
frameXYWH[0] + frameXYWH[2] <= viewportBound[0] + viewportBound[2] &&
frameXYWH[1] + frameXYWH[3] <= viewportBound[1] + viewportBound[3]
);
};
await clickEdgelessModeButton(page); await pressEnter(page);
// select the note // create a blank frame using slash command
await page await type(page, '/frame');
.locator('affine-edgeless-note:has-text("Test Frame")') await pressEnter(page);
.click({ force: true });
// enter F to create a frame
await page.keyboard.press('f');
// close affine-banner const surfaceRef = page.locator('affine-surface-ref');
await page.locator('[data-testid=local-demo-tips-close-button]').click(); const peekView = page.locator('[data-testid="peek-view-modal"]');
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
// insert the frame to page
await toolbar.getByLabel('Insert into Page').click();
// switch back to page mode
await clickPageModeButton(page);
// hover the frame to trigger surface-ref-toolbar
await page.locator('affine-surface-ref .affine-surface-ref').hover();
await expect(surfaceRef).toBeVisible();
await surfaceRef.hover();
await page await page
.locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]') .locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]')
.click(); .click();
await page await page
.locator( .locator(
'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]' 'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]'
) )
.click(); .click();
// verify peek view is opened await expect(peekView).toBeVisible();
await expect(page.getByTestId('peek-view-modal')).toBeVisible(); expect(await frameInViewport()).toBe(true);
await pressEscape(page);
// check if page is in edgeless mode await expect(peekView).toBeHidden();
await expect(
page.locator('edgeless-editor').locator('affine-frame')
).toBeInViewport();
// close peek view
await page.keyboard.press('Escape');
// check if can open peek view by shift+click // check if can open peek view by shift+click
await page await surfaceRef.click({ modifiers: ['Shift'] });
.locator('affine-surface-ref .affine-surface-ref')
.click({ modifiers: ['Shift'] });
// check if page is in edgeless mode await expect(peekView).toBeVisible();
await expect( expect(await frameInViewport()).toBe(true);
page.locator('edgeless-editor').locator('affine-frame') await pressEscape(page);
).toBeInViewport(); await expect(peekView).toBeHidden();
// close peek view
await page.keyboard.press('Escape');
// check if can open peek view by double click // check if can open peek view by double click
await page.locator('affine-surface-ref .affine-surface-ref').dblclick(); await surfaceRef.dblclick();
// check if page is in edgeless mode await expect(peekView).toBeVisible();
await expect( expect(await frameInViewport()).toBe(true);
page.locator('edgeless-editor').locator('affine-frame') await pressEscape(page);
).toBeInViewport(); await expect(peekView).toBeHidden();
// can close modal when navigate // can close modal when navigate
await openHomePage(page); await openHomePage(page);
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible(); await expect(peekView).toBeHidden();
}); });
test.skip('can open peek view for fav link', async ({ page }) => { test.skip('can open peek view for fav link', async ({ page }) => {

View File

@@ -109,6 +109,17 @@ export async function getViewportCenter(page: Page, editorIndex = 0) {
}); });
} }
export async function getViewportBound(page: Page, editorIndex = 0) {
const container = locateEditorContainer(page, editorIndex);
return container.evaluate(container => {
const root = container.querySelector('affine-edgeless-root');
if (!root) {
throw new Error('Edgeless root not found');
}
return root.gfx.viewport.viewportBounds.toXYWH();
});
}
export async function setViewportCenter( export async function setViewportCenter(
page: Page, page: Page,
center: IVec, center: IVec,