From 0bafc35aad818f4af1b221999062c4bf28ab7dfc Mon Sep 17 00:00:00 2001 From: fundon Date: Mon, 9 Dec 2024 03:12:09 +0000 Subject: [PATCH] feat: linked doc supports aliases (#9009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [AF-1882](https://linear.app/affine-design/issue/AF-1882/实现-generatedocurlservice) Upstreams: https://github.com/toeverything/blocksuite/pull/8806 --- .../affine/reference-link/index.tsx | 44 ++- .../block-suite-editor/lit-adaper.tsx | 9 +- .../specs/custom/database-block.ts | 2 +- .../specs/custom/spec-patchers.tsx | 34 +- .../specs/custom/widgets/toolbar.ts | 2 +- .../components/hooks/affine/use-share-url.ts | 23 +- .../services/doc-display-meta.ts | 11 + tests/affine-local/e2e/links.spec.ts | 302 ++++++++++++++++++ 8 files changed, 394 insertions(+), 33 deletions(-) diff --git a/packages/frontend/core/src/components/affine/reference-link/index.tsx b/packages/frontend/core/src/components/affine/reference-link/index.tsx index c936e89dc..94f1a90df 100644 --- a/packages/frontend/core/src/components/affine/reference-link/index.tsx +++ b/packages/frontend/core/src/components/affine/reference-link/index.tsx @@ -30,6 +30,7 @@ import * as styles from './styles.css'; interface AffinePageReferenceProps { pageId: string; params?: URLSearchParams; + title?: string | null; // title alias className?: string; Icon?: ComponentType; onClick?: (e: MouseEvent) => void; @@ -38,20 +39,21 @@ interface AffinePageReferenceProps { function AffinePageReferenceInner({ pageId, params, + title, Icon: UserIcon, }: AffinePageReferenceProps) { const docDisplayMetaService = useService(DocDisplayMetaService); const docsService = useService(DocsService); const i18n = useI18n(); - let linkWithMode: DocMode | null = null; - let linkToNode = false; + let referenceWithMode: DocMode | null = null; + let referenceToNode = false; if (params) { const m = params.get('mode'); if (m && (m === 'page' || m === 'edgeless')) { - linkWithMode = m as DocMode; + referenceWithMode = m as DocMode; } - linkToNode = params.has('blockIds') || params.has('elementIds'); + referenceToNode = params.has('blockIds') || params.has('elementIds'); } const Icon = useLiveData( @@ -61,25 +63,33 @@ function AffinePageReferenceInner({ } return get( docDisplayMetaService.icon$(pageId, { - mode: linkWithMode ?? undefined, + mode: referenceWithMode ?? undefined, reference: true, - referenceToNode: linkToNode, + referenceToNode, + hasTitleAlias: Boolean(title), }) ); }) ); + const notFound = !useLiveData(docsService.list.doc$(pageId)); - let title = useLiveData( + const docTitle = useLiveData( docDisplayMetaService.title$(pageId, { reference: true }) ); - title = notFound ? i18n.t('com.affine.notFoundPage.title') : title; + if (notFound) { + title = i18n.t('com.affine.notFoundPage.title'); + } + + if (!title) { + title = i18n.t(docTitle); + } return ( - {i18n.t(title)} + {title} ); } @@ -87,6 +97,7 @@ function AffinePageReferenceInner({ export function AffinePageReference({ pageId, params, + title, className, Icon, onClick: userOnClick, @@ -152,7 +163,12 @@ export function AffinePageReference({ onClick={onClick} className={clsx(styles.pageReferenceLink, className)} > - + ); } @@ -161,6 +177,7 @@ export function AffineSharedPageReference({ pageId, docCollection, params, + title, Icon, onClick: userOnClick, }: AffinePageReferenceProps & { @@ -213,7 +230,12 @@ export function AffineSharedPageReference({ onClick={onClick} className={styles.pageReferenceLink} > - + ); } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index a2891cdf7..5e677123e 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -56,6 +56,7 @@ import { patchEmbedLinkedDocBlockConfig, patchForMobile, patchForSharedPage, + patchGenerateDocUrlExtension, patchNotificationService, patchParseDocUrlExtension, patchPeekViewService, @@ -112,6 +113,8 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => { const pageId = data.pageId; if (!pageId) return ; + // title alias + const title = data.title; const params = toURLSearchParams(data.params); if (workspaceService.workspace.openOptions.isSharedMode) { @@ -120,11 +123,14 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => { docCollection={workspaceService.workspace.docCollection} pageId={pageId} params={params} + title={title} /> ); } - return ; + return ( + + ); }; }, [workspaceService]); @@ -147,6 +153,7 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => { patched = patched.concat(patchPeekViewService(peekViewService)); patched = patched.concat(patchEdgelessClipboard()); patched = patched.concat(patchParseDocUrlExtension(framework)); + patched = patched.concat(patchGenerateDocUrlExtension(framework)); patched = patched.concat(patchQuickSearchService(framework)); patched = patched.concat(patchEmbedLinkedDocBlockConfig(framework)); if (shared) { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts index 0306861ca..416515968 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts @@ -52,7 +52,7 @@ function createCopyLinkToBlockMenuItem( const options: UseSharingUrl = { workspaceId, pageId, - shareMode: mode, + mode, blockIds: [model.id], }; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index d34022db3..489d4fd0e 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -7,6 +7,7 @@ import { toReactNode, type useConfirmModal, } from '@affine/component'; +import { WorkspaceServerService } from '@affine/core/modules/cloud'; import type { EditorService } from '@affine/core/modules/editor'; import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { resolveLinkToDoc } from '@affine/core/modules/navigation'; @@ -47,6 +48,7 @@ import { EdgelessRootBlockComponent, EmbedLinkedDocBlockComponent, EmbedLinkedDocBlockConfigExtension, + GenerateDocUrlExtension, MobileSpecsPatches, NotificationExtension, ParseDocUrlExtension, @@ -55,6 +57,7 @@ import { ReferenceNodeConfigExtension, } from '@blocksuite/affine/blocks'; import { type BlockSnapshot, Text } from '@blocksuite/affine/store'; +import type { ReferenceParams } from '@blocksuite/affine-model'; import { AIChatBlockSchema, type DocProps, @@ -68,6 +71,7 @@ import { customElement } from 'lit/decorators.js'; import { literal } from 'lit/static-html.js'; import { pick } from 'lodash-es'; +import { generateUrl } from '../../../../hooks/affine/use-share-url'; import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar'; export type ReferenceReactRenderer = ( @@ -110,13 +114,13 @@ export function patchReferenceRenderer( reactToLit: (element: ElementOrFactory) => TemplateResult, reactRenderer: ReferenceReactRenderer ): ExtensionType { - const litRenderer = (reference: AffineReference) => { + const customContent = (reference: AffineReference) => { const node = reactRenderer(reference); return reactToLit(node); }; return ReferenceNodeConfigExtension({ - customContent: litRenderer, + customContent, }); } @@ -463,18 +467,32 @@ export function patchParseDocUrlExtension(framework: FrameworkProvider) { const info = resolveLinkToDoc(url); if (!info || info.workspaceId !== workspaceService.workspace.id) return; - return { - docId: info.docId, - blockIds: info.blockIds, - elementIds: info.elementIds, - mode: info.mode, - }; + delete info.refreshKey; + + return info; }, }); return [ParseDocUrl]; } +export function patchGenerateDocUrlExtension(framework: FrameworkProvider) { + const workspaceService = framework.get(WorkspaceService); + const workspaceServerService = framework.get(WorkspaceServerService); + const GenerateDocUrl = GenerateDocUrlExtension({ + generateDocUrl(pageId: string, params?: ReferenceParams) { + return generateUrl({ + ...params, + pageId, + workspaceId: workspaceService.workspace.id, + baseUrl: workspaceServerService.server?.baseUrl ?? location.origin, + }); + }, + }); + + return [GenerateDocUrl]; +} + export function patchEdgelessClipboard() { class EdgelessClipboardWatcher extends BlockServiceWatcher { static override readonly flavour = 'affine:page'; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts index 610c98cb3..1afbe6b03 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts @@ -93,7 +93,7 @@ function createCopyLinkToBlockMenuItem( const mode = editor.mode$.value; const pageId = editor.doc.id; const workspaceId = editor.doc.workspace.id; - const options: UseSharingUrl = { workspaceId, pageId, shareMode: mode }; + const options: UseSharingUrl = { workspaceId, pageId, mode }; let type = ''; if (mode === 'page') { diff --git a/packages/frontend/core/src/components/hooks/affine/use-share-url.ts b/packages/frontend/core/src/components/hooks/affine/use-share-url.ts index 4e9024506..0e4abca87 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-share-url.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-share-url.ts @@ -13,7 +13,7 @@ import { useCallback } from 'react'; export type UseSharingUrl = { workspaceId: string; pageId: string; - shareMode?: DocMode; + mode?: DocMode; blockIds?: string[]; elementIds?: string[]; xywh?: string; // not needed currently @@ -30,7 +30,7 @@ export const generateUrl = ({ pageId, blockIds, elementIds, - shareMode: mode, + mode, xywh, // not needed currently }: UseSharingUrl & { baseUrl: string }) => { try { @@ -38,23 +38,24 @@ export const generateUrl = ({ const search = toURLSearchParams({ mode, blockIds, elementIds, xywh }); if (search?.size) url.search = search.toString(); return url.toString(); - } catch { - return null; + } catch (err) { + console.error(err); + return undefined; } }; const getShareLinkType = ({ - shareMode, + mode, blockIds, elementIds, }: { - shareMode?: DocMode; + mode?: DocMode; blockIds?: string[]; elementIds?: string[]; }) => { - if (shareMode === 'page') { + if (mode === 'page') { return 'doc'; - } else if (shareMode === 'edgeless') { + } else if (mode === 'edgeless') { return 'whiteboard'; } else if (blockIds && blockIds.length > 0) { return 'block'; @@ -131,17 +132,17 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => { const serverService = useService(ServerService); const onClickCopyLink = useCallback( - (shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => { + (mode?: DocMode, blockIds?: string[], elementIds?: string[]) => { const sharingUrl = generateUrl({ baseUrl: serverService.server.baseUrl, workspaceId, pageId, blockIds, elementIds, - shareMode, // if view is not provided, use the current view + mode, // if view is not provided, use the current view }); const type = getShareLinkType({ - shareMode, + mode, blockIds, elementIds, }); diff --git a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts index 5c46e8935..8cd617a1e 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts @@ -1,6 +1,7 @@ import { extractEmojiIcon } from '@affine/core/utils'; import { i18nTime } from '@affine/i18n'; import { + AliasIcon as LitAliasIcon, BlockLinkIcon as LitBlockLinkIcon, EdgelessIcon as LitEdgelessIcon, LinkedEdgelessIcon as LitLinkedEdgelessIcon, @@ -11,6 +12,7 @@ import { YesterdayIcon as LitYesterdayIcon, } from '@blocksuite/icons/lit'; import { + AliasIcon, BlockLinkIcon, EdgelessIcon, LinkedEdgelessIcon, @@ -42,6 +44,7 @@ interface DocDisplayIconOptions { mode?: 'edgeless' | 'page'; reference?: boolean; referenceToNode?: boolean; + hasTitleAlias?: boolean; /** * @default true */ @@ -57,6 +60,7 @@ interface DocDisplayTitleOptions { } const rcIcons = { + AliasIcon, BlockLinkIcon, EdgelessIcon, LinkedEdgelessIcon, @@ -67,6 +71,7 @@ const rcIcons = { YesterdayIcon, }; const litIcons = { + AliasIcon: LitAliasIcon, BlockLinkIcon: LitBlockLinkIcon, EdgelessIcon: LitEdgelessIcon, LinkedEdgelessIcon: LitLinkedEdgelessIcon, @@ -130,6 +135,12 @@ export class DocDisplayMetaService extends Service { const mode = doc ? get(doc.primaryMode$) : undefined; const finalMode = options?.mode ?? mode ?? 'page'; const referenceToNode = !!(options?.reference && options.referenceToNode); + const hasTitleAlias = !!(options?.reference && options?.hasTitleAlias); + + // increases block link priority with title alias + if (hasTitleAlias) { + return iconSet.AliasIcon; + } // increases block link priority if (referenceToNode) { diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index 449d7bcc5..9b91b3191 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -580,3 +580,305 @@ test('should show edgeless content when switching card view of linked mode doc i edgelessText.y + edgelessText.height ); }); + +// Aliases & Copy link +test.describe('Customize linked doc title and description', () => { + // Inline View + test('should set a custom title for inline link', async ({ page }) => { + await page.keyboard.press('Enter'); + await createLinkedPage(page, 'Test Page'); + + const link = page.locator('affine-reference'); + const title = link.locator('.affine-reference-title'); + await link.hover(); + + const toolbar = page.locator('reference-popup'); + + // Copies link + await toolbar.getByRole('button', { name: 'Copy link' }).click(); + const url0 = await link.locator('a').getAttribute('href'); + const url1 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + expect(url0).not.toBeNull(); + expect(new URL(url0!, coreUrl).pathname).toBe(new URL(url1).pathname); + + // Edits title + await toolbar.getByRole('button', { name: 'Edit' }).click(); + + // Title alias + await page.keyboard.type('Test Page Alias'); + await page.keyboard.press('Enter'); + + await expect(title).toHaveText('Test Page Alias'); + + // Original title + await link.hover(); + const docTitle = toolbar.getByRole('button', { name: 'Doc title' }); + await expect(docTitle).toHaveText('Test Page', { useInnerText: true }); + + // Edits title + await toolbar.getByRole('button', { name: 'Edit' }).click(); + + const aliasPopup = page.locator('reference-alias-popup'); + + // Input + await expect(aliasPopup.locator('input')).toHaveValue('Test Page Alias'); + + // Reset + await aliasPopup.getByRole('button', { name: 'Reset' }).click(); + + await expect(title).toHaveText('Test Page'); + + await link.hover(); + await expect(docTitle).toBeHidden(); + }); + + // Card View + test('should set a custom title and description for card link', async ({ + page, + }) => { + await page.keyboard.press('Enter'); + await createLinkedPage(page, 'Test Page'); + + const inlineLink = page.locator('affine-reference'); + const inlineToolbar = page.locator('reference-popup'); + + await inlineLink.hover(); + + // Copies link + await inlineToolbar.getByRole('button', { name: 'Copy link' }).click(); + const url0 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + + // Edits title + await inlineToolbar.getByRole('button', { name: 'Edit' }).click(); + + // Title alias + await page.keyboard.type('Test Page Alias'); + await page.keyboard.press('Enter'); + + await inlineLink.hover(); + + // Switches to card view + await inlineToolbar.getByRole('button', { name: 'Switch view' }).click(); + await inlineToolbar.getByRole('button', { name: 'Card view' }).click(); + + const cardLink = page.locator('affine-embed-linked-doc-block'); + const cardTitle = cardLink.locator( + '.affine-embed-linked-doc-content-title-text' + ); + const cardDescription = cardLink.locator( + '.affine-embed-linked-doc-content-note.alias' + ); + const cardToolbar = page.locator('affine-embed-card-toolbar'); + + await cardLink.click(); + + // Copies link + await cardToolbar.getByRole('button', { name: 'Copy link' }).click(); + const url1 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + + expect(url0).not.toBeNull(); + expect(url1).not.toBeNull(); + expect(url0).toBe(url1); + + const docTitle = cardToolbar.getByRole('button', { name: 'Doc title' }); + await expect(docTitle).toHaveText('Test Page', { useInnerText: true }); + await expect(cardTitle).toHaveText('Test Page Alias'); + + // Edits title & description + await cardToolbar.getByRole('button', { name: 'Edit' }).click(); + + const cardEditPopup = page.locator('embed-card-edit-modal'); + + // Title alias + await page.keyboard.type('Test Page Alias Again'); + await page.keyboard.press('Tab'); + // Description alias + await page.keyboard.type('This is a new description'); + + // Saves aliases + await cardEditPopup.getByRole('button', { name: 'Save' }).click(); + await expect(cardTitle).toHaveText('Test Page Alias Again'); + await expect(cardDescription).toHaveText('This is a new description'); + + await cardLink.click(); + + // Switches to inline view + { + await cardToolbar.getByRole('button', { name: 'Switch view' }).click(); + await cardToolbar.getByRole('button', { name: 'Inline view' }).click(); + + // Focuses inline editor + const bounds = (await inlineLink.boundingBox())!; + await page.mouse.click( + bounds.x + bounds.width + 30, + bounds.y + bounds.height / 2 + ); + + await inlineLink.hover(); + + const title = inlineLink.locator('.affine-reference-title'); + await expect(title).toHaveText('Test Page Alias Again'); + + // Switches to card view + await inlineToolbar.getByRole('button', { name: 'Switch view' }).click(); + await inlineToolbar.getByRole('button', { name: 'Card view' }).click(); + } + + await cardLink.click(); + + await cardToolbar.getByRole('button', { name: 'Edit' }).click(); + + // Resets + await cardEditPopup.getByRole('button', { name: 'Reset' }).click(); + + await expect(cardTitle).toHaveText('Test Page'); + await expect(cardDescription).toBeHidden(); + }); + + // Embed View + test('should automatically switch to card view and set a custom title and description', async ({ + page, + }) => { + await page.keyboard.press('Enter'); + await createLinkedPage(page, 'Test Page'); + + const inlineLink = page.locator('affine-reference'); + const inlineToolbar = page.locator('reference-popup'); + + await inlineLink.hover(); + + // Copies link + await inlineToolbar.getByRole('button', { name: 'Copy link' }).click(); + const url0 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + + // Switches to card view + await inlineToolbar.getByRole('button', { name: 'Switch view' }).click(); + await inlineToolbar.getByRole('button', { name: 'Card view' }).click(); + + const cardLink = page.locator('affine-embed-linked-doc-block'); + const cardTitle = cardLink.locator( + '.affine-embed-linked-doc-content-title-text' + ); + const cardDescription = cardLink.locator( + '.affine-embed-linked-doc-content-note.alias' + ); + const cardToolbar = page.locator('affine-embed-card-toolbar'); + + await cardLink.click(); + + // Copies link + await cardToolbar.getByRole('button', { name: 'Copy link' }).click(); + const url1 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + + // Switches to embed view + await cardToolbar.getByRole('button', { name: 'Switch view' }).click(); + await cardToolbar.getByRole('button', { name: 'Embed view' }).click(); + + const embedLink = page.locator('affine-embed-synced-doc-block'); + const embedTitle = embedLink.locator('.affine-embed-synced-doc-title'); + const embedToolbar = page.locator('affine-embed-card-toolbar'); + + await embedLink.click(); + + // Copies link + await embedToolbar.getByRole('button', { name: 'Copy link' }).click(); + const url2 = await ( + await page.evaluateHandle(() => navigator.clipboard.readText()) + ).jsonValue(); + + expect(url0).not.toBeNull(); + expect(url1).not.toBeNull(); + expect(url2).not.toBeNull(); + expect(url0).toBe(url1); + expect(url1).toBe(url2); + + // Edits title & description + await embedToolbar.getByRole('button', { name: 'Edit' }).click(); + + const embedEditPopup = page.locator('embed-card-edit-modal'); + + // Title alias + await page.keyboard.type('Test Page Alias Again'); + await page.keyboard.press('Tab'); + // Description alias + await page.keyboard.type('This is a new description'); + + // Cancels + await embedEditPopup.getByRole('button', { name: 'Cancel' }).click(); + await expect(embedEditPopup).toBeHidden(); + + await embedLink.click(); + + // Edits title & description + await embedToolbar.getByRole('button', { name: 'Edit' }).click(); + + // Title alias + await page.keyboard.type('Test Page Alias'); + await page.keyboard.press('Tab'); + // Description alias + await page.keyboard.type('This is a new description'); + + // Saves aliases + await embedEditPopup.getByRole('button', { name: 'Save' }).click(); + + // Automatically switch to card view + await expect(embedLink).toBeHidden(); + + await expect(cardTitle).toHaveText('Test Page Alias'); + await expect(cardDescription).toHaveText('This is a new description'); + + await cardLink.click(); + + const docTitle = cardToolbar.getByRole('button', { name: 'Doc title' }); + await expect(docTitle).toHaveText('Test Page', { useInnerText: true }); + await expect(cardTitle).toHaveText('Test Page Alias'); + + // Switches to embed view + await cardToolbar.getByRole('button', { name: 'Switch view' }).click(); + await cardToolbar.getByRole('button', { name: 'Embed view' }).click(); + + await expect(embedTitle).toHaveText('Test Page'); + + await embedLink.click(); + + // Switches to inline view + { + await embedToolbar.getByRole('button', { name: 'Switch view' }).click(); + await embedToolbar.getByRole('button', { name: 'Inline view' }).click(); + + // Focuses inline editor + const bounds = (await inlineLink.boundingBox())!; + await page.mouse.click( + bounds.x + bounds.width + 30, + bounds.y + bounds.height / 2 + ); + + await inlineLink.hover(); + + // Upstreams: https://github.com/toeverything/blocksuite/pull/8884 + // const title = inlineLink.locator('.affine-reference-title'); + // await expect(title).toHaveText('Test Page'); + + // Switches to embed view + await inlineToolbar.getByRole('button', { name: 'Switch view' }).click(); + await inlineToolbar.getByRole('button', { name: 'Embed view' }).click(); + } + + await embedLink.click(); + + await expect(embedTitle).toHaveText('Test Page'); + await expect( + embedToolbar.getByRole('button', { name: 'Doc title' }) + ).toBeHidden(); + }); +});