feat(editor): add toolbar registry extension (#9572)

### What's Changed!

#### Added
Manage various types of toolbars uniformly in one place.

* `affine-toolbar-widget`
* `ToolbarRegistryExtension`

The toolbar currently supports and handles several scenarios:

1.  Select blocks: `BlockSelection`
2. Select text: `TextSelection` or `NativeSelection`
3. Hover a link: `affine-link` and `affine-reference`

#### Removed
Remove redundant toolbar implementations.

* `attachment` toolbar
* `bookmark` toolbar
* `embed` toolbar
* `formatting` toolbar
* `affine-link` toolbar
* `affine-reference` toolbar

### How to migrate?

Here is an example that can help us migrate some unrefactored toolbars:

Check out the more detailed types of [`ToolbarModuleConfig`](c178debf2d/blocksuite/affine/shared/src/services/toolbar-service/config.ts).

1.  Add toolbar configuration file to a block type, such as bookmark block: [`config.ts`](c178debf2d/blocksuite/affine/block-bookmark/src/configs/toolbar.ts)

```ts
export const builtinToolbarConfig = {
  actions: [
    {
      id: 'a.preview',
      content(ctx) {
        const model = ctx.getCurrentModelBy(BlockSelection, BookmarkBlockModel);
        if (!model) return null;

        const { url } = model;

        return html`<affine-link-preview .url=${url}></affine-link-preview>`;
      },
    },
    {
      id: 'b.conversions',
      actions: [
        {
          id: 'inline',
          label: 'Inline view',
          run(ctx) {
          },
        },
        {
          id: 'card',
          label: 'Card view',
          disabled: true,
        },
        {
          id: 'embed',
          label: 'Embed view',
          disabled(ctx) {
          },
          run(ctx) {
          },
        },
      ],
      content(ctx) {
      },
    } satisfies ToolbarActionGroup<ToolbarAction>,
    {
      id: 'c.style',
      actions: [
        {
          id: 'horizontal',
          label: 'Large horizontal style',
        },
        {
          id: 'list',
          label: 'Small horizontal style',
        },
      ],
      content(ctx) {
      },
    } satisfies ToolbarActionGroup<ToolbarAction>,
    {
      id: 'd.caption',
      tooltip: 'Caption',
      icon: CaptionIcon(),
      run(ctx) {
      },
    },
    {
      placement: ActionPlacement.More,
      id: 'a.clipboard',
      actions: [
        {
          id: 'copy',
          label: 'Copy',
          icon: CopyIcon(),
          run(ctx) {
          },
        },
        {
          id: 'duplicate',
          label: 'Duplicate',
          icon: DuplicateIcon(),
          run(ctx) {
          },
        },
      ],
    },
    {
      placement: ActionPlacement.More,
      id: 'b.refresh',
      label: 'Reload',
      icon: ResetIcon(),
      run(ctx) {
      },
    },
    {
      placement: ActionPlacement.More,
      id: 'c.delete',
      label: 'Delete',
      icon: DeleteIcon(),
      variant: 'destructive',
      run(ctx) {
      },
    },
  ],
} as const satisfies ToolbarModuleConfig;
```

2. Add configuration extension to a block spec: [bookmark's spec](c178debf2d/blocksuite/affine/block-bookmark/src/bookmark-spec.ts)

```ts
const flavour = BookmarkBlockSchema.model.flavour;

export const BookmarkBlockSpec: ExtensionType[] = [
  ...,
  ToolbarModuleExtension({
    id: BlockFlavourIdentifier(flavour),
    config: builtinToolbarConfig,
  }),
].flat();
```

3. If the bock type already has a toolbar configuration built in, we can customize it in the following ways:

Check out the [editor's config](c178debf2d/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts (L51C4-L54C8)) file.

```ts
// Defines a toolbar configuration for the bookmark block type
const customBookmarkToolbarConfig = {
  actions: [
    ...
  ]
} as const satisfies ToolbarModuleConfig;

// Adds it into the editor's config
 ToolbarModuleExtension({
    id: BlockFlavourIdentifier('custom:affine:bookmark'),
    config: customBookmarkToolbarConfig,
 }),
```

4. If we want to extend the global:

```ts
// Defines a toolbar configuration
const customWildcardToolbarConfig = {
  actions: [
    ...
  ]
} as const satisfies ToolbarModuleConfig;

// Adds it into the editor's config
 ToolbarModuleExtension({
    id: BlockFlavourIdentifier('custom:affine:*'),
    config: customWildcardToolbarConfig,
 }),
```

Currently, only most toolbars in page mode have been refactored. Next is edgeless mode.
This commit is contained in:
fundon
2025-03-06 06:46:03 +00:00
parent 06e4bd9aed
commit ec9bd1f383
147 changed files with 6389 additions and 5156 deletions

View File

@@ -269,7 +269,7 @@ test.describe('chat panel', () => {
await focusToEditor(page);
// insert below
await page.getByTestId('action-insert-below').click();
await page.waitForSelector('affine-format-bar-widget editor-toolbar');
await page.waitForSelector('affine-toolbar-widget editor-toolbar');
const editorContent = await getEditorContent(page);
expect(editorContent).toBe(content);
});

View File

@@ -157,14 +157,14 @@ test('should preview PDF in embed view', async ({ page }) => {
await insertAttachment(page, fixturesDir.join('lorem-ipsum.pdf').value);
const attachment = page.locator('affine-attachment');
await attachment.hover();
await attachment.click();
const attachmentToolbar = page.locator('.affine-attachment-toolbar');
await expect(attachmentToolbar).toBeVisible();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
// Switches to embed view
await attachmentToolbar.getByRole('button', { name: 'Switch view' }).click();
await attachmentToolbar.getByRole('button', { name: 'Embed view' }).click();
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await toolbar.getByRole('button', { name: 'Embed view' }).click();
await page.waitForTimeout(500);
@@ -207,7 +207,6 @@ test('should preview PDF in embed view', async ({ page }) => {
await doc1.click();
const inlineLink = page.locator('affine-reference').nth(0);
const inlineToolbar = page.locator('reference-popup');
const inlineTitle = inlineLink.locator('.affine-reference-title');
await expect(inlineTitle).toHaveText('PDF preview');
@@ -219,10 +218,10 @@ test('should preview PDF in embed view', async ({ page }) => {
await page.waitForTimeout(500);
await page.mouse.click(bouding!.x - 50, bouding!.y + bouding!.height / 2);
await inlineLink.hover({ timeout: 500 });
await inlineLink.hover();
// Edits title
await inlineToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('PDF embed preview');
@@ -264,10 +263,10 @@ test('should sync name in pdf embed view', async ({ page }) => {
await insertAttachment(page, fixturesDir.join('lorem-ipsum.pdf').value);
const attachment = page.locator('affine-attachment');
await attachment.hover();
await attachment.click();
const attachmentToolbar = page.locator('.affine-attachment-toolbar');
await expect(attachmentToolbar).toBeVisible();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
const attachmentTitle = attachment.locator(
'.affine-attachment-content-title-text'
@@ -275,7 +274,7 @@ test('should sync name in pdf embed view', async ({ page }) => {
await expect(attachmentTitle).toHaveText('lorem-ipsum.pdf');
// Renames
await attachmentToolbar.getByRole('button', { name: 'Rename' }).click();
await toolbar.getByRole('button', { name: 'Rename' }).click();
const input = page
.locator('.affine-attachment-rename-input-wrapper')
.locator('input');
@@ -283,11 +282,11 @@ test('should sync name in pdf embed view', async ({ page }) => {
await page.keyboard.press('Enter');
await expect(attachmentTitle).toHaveText('What is Lorem Ipsum.pdf');
await attachment.hover();
await attachment.click();
// Switches to embed view
await attachmentToolbar.getByRole('button', { name: 'Switch view' }).click();
await attachmentToolbar.getByRole('button', { name: 'Embed view' }).click();
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await toolbar.getByRole('button', { name: 'Embed view' }).click();
await page.waitForTimeout(500);
@@ -298,10 +297,10 @@ test('should sync name in pdf embed view', async ({ page }) => {
await page.waitForTimeout(500);
await expect(portalName).toHaveText('What is Lorem Ipsum.pdf');
await attachment.hover();
await attachment.click();
// Renames
await attachmentToolbar.getByRole('button', { name: 'Rename' }).click();
await toolbar.getByRole('button', { name: 'Rename' }).click();
await input.fill('lorem-ipsum');
await page.keyboard.press('Enter');
await expect(portalName).toHaveText('lorem-ipsum.pdf');

View File

@@ -47,9 +47,9 @@ test('should not show hidden note in embed view page mode', async ({
// switch to embed view
await inlineLink.hover();
const inlineToolbar = page.locator('reference-popup');
await inlineToolbar.getByLabel('Switch view').click();
await inlineToolbar.getByLabel('Embed view').click();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
// check the content
const embedLink = page.locator('affine-embed-synced-doc-block');

View File

@@ -187,16 +187,11 @@ test('outline viewer should be useable in doc peek preview', async ({
await page.locator('affine-reference').hover();
await expect(
page.locator('.affine-reference-popover-container')
).toBeVisible();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await page
.locator('editor-menu-button editor-icon-button[aria-label="Open doc"]')
.click();
await page
.locator('editor-menu-action:has-text("Open in center peek")')
.click();
await toolbar.getByLabel('Open doc').click();
await toolbar.getByLabel('Open in center peek').click();
const peekView = page.getByTestId('peek-view-modal');
await expect(peekView).toBeVisible();

View File

@@ -34,15 +34,15 @@ test('heading icon should be updated after change heading level', async ({
await selectAllByKeyboard(page);
const formatBar = locateFormatBar(page);
await formatBar.locator('.paragraph-button').hover();
await formatBar.getByTestId('affine:paragraph/h1').click();
await formatBar.getByLabel('Conversions').click();
await formatBar.getByLabel('Heading 1').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeVisible();
await selectAllByKeyboard(page);
await formatBar.locator('.paragraph-button').hover();
await formatBar.getByTestId('affine:paragraph/h2').click();
await formatBar.getByLabel('Conversions').click();
await formatBar.getByLabel('Heading 2').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeHidden();

View File

@@ -7,6 +7,27 @@ import {
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
function hexToRGB(hex: string) {
hex = hex.replace(/^#/, '');
const len = hex.length;
let arr: string[] = [];
if (len === 3 || len === 4) {
arr = hex.split('').map(s => s.repeat(2));
} else if (len === 6 || len === 8) {
arr = Array.from<number>({ length: len / 2 })
.fill(0)
.map((n, i) => n + i * 2)
.map(n => hex.substring(n, n + 2));
}
const values = arr
.map(s => parseInt(s, 16))
.map((n, i) => (i === 3 ? (n % 255) / 255 : n));
return `rgb${values.length === 4 ? 'a' : ''}(${values.join(', ')})`;
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
@@ -23,26 +44,25 @@ test.describe('Formatting', () => {
await page.keyboard.press('Shift+ArrowLeft');
const formatBar = locateFormatBar(page);
await formatBar.locator('.highlight-icon').hover();
const fgGreenButton = formatBar.locator(
'[data-testid="var(--affine-text-highlight-foreground-green)"]'
);
const highlightButton = formatBar.locator('affine-highlight-duotone-icon');
await highlightButton.click();
const fgGreenButton = formatBar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
const fgColor1 = await fgGreenButton
.locator('span')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('color'));
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
await expect(textSpan).toBeVisible();
const fgColor2 = await textSpan.evaluate(e =>
window.getComputedStyle(e).getPropertyValue('color')
);
expect(fgColor1).toBe(fgColor2);
await expect(textSpan).toBeVisible();
await expect(textSpan).toHaveCSS('color', hexToRGB(fgColor));
});
test('should change text background color', async ({ page }) => {
@@ -54,26 +74,26 @@ test.describe('Formatting', () => {
await page.keyboard.press('Shift+ArrowLeft');
const formatBar = locateFormatBar(page);
await formatBar.locator('.highlight-icon').hover();
const highlightButton = formatBar.locator('affine-highlight-duotone-icon');
const fgGreenButton = formatBar.locator(
'[data-testid="var(--affine-text-highlight-foreground-green)"]'
);
await highlightButton.click();
const fgGreenButton = formatBar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
const fgColor1 = await fgGreenButton
.locator('span')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('color'));
await page.waitForTimeout(200);
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan1 = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
const fgColor2 = await textSpan1.evaluate(e =>
window.getComputedStyle(e).getPropertyValue('color')
);
expect(fgColor1).toBe(fgColor2);
await expect(textSpan1).toHaveCSS('color', hexToRGB(fgColor));
await page.keyboard.press('ArrowRight');
@@ -81,10 +101,12 @@ test.describe('Formatting', () => {
await page.keyboard.press('Shift+ArrowLeft');
}
await formatBar.locator('.highlight-icon').hover();
await highlightButton.click();
const yellow = 'var(--affine-text-highlight-yellow)';
const bgYellowButton = formatBar.locator(`[data-testid="${yellow}"]`);
const bgYellowButton = formatBar.locator(
'[data-testid="background-yellow"]'
);
await bgYellowButton.click();
const textSpan2 = paragraph
@@ -95,10 +117,18 @@ test.describe('Formatting', () => {
await expect(textSpan2).toBeVisible();
const bgColor1 = await textSpan1.evaluate(e => e.style.backgroundColor);
const bgColor2 = await textSpan2.evaluate(e => e.style.backgroundColor);
expect(yellow).toBe(bgColor1);
expect(yellow).toBe(bgColor2);
const bgColor = await bgYellowButton
.locator('affine-text-duotone-icon')
.evaluate(e =>
window.getComputedStyle(e).getPropertyValue('--background')
);
await expect(textSpan1).toHaveCSS('background-color', hexToRGB(bgColor));
await expect(textSpan2).toHaveCSS('background-color', hexToRGB(bgColor));
});
});

View File

@@ -269,11 +269,11 @@ test('drag a page card block to another page', async ({ page }) => {
await pageReference.hover();
const inlineToolbar = page.locator('reference-popup');
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
// convert page reference to card block
await inlineToolbar.getByRole('button', { name: 'Switch view' }).click();
await inlineToolbar.getByRole('button', { name: 'Card view' }).click();
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await toolbar.getByRole('button', { name: 'Card view' }).click();
// hover the card block to show the drag handle
const box = await page.locator('affine-embed-linked-doc-block').boundingBox();

View File

@@ -26,6 +26,22 @@ test.beforeEach(async ({ page }) => {
await waitForEmptyEditor(page);
});
function toolbarButtons(page: Page) {
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const switchViewBtn = toolbar.getByLabel('Switch view');
const inlineViewBtn = toolbar.getByLabel('Inline view');
const cardViewBtn = toolbar.getByLabel('Card view');
const embedViewBtn = toolbar.getByLabel('Embed view');
return {
toolbar,
switchViewBtn,
inlineViewBtn,
cardViewBtn,
embedViewBtn,
};
}
async function enableEmojiDocIcon(page: Page) {
// Opens settings panel
await openEditorSetting(page);
@@ -64,23 +80,36 @@ test('not allowed to switch to embed view when linking to the same document', as
await writeTextToClipboard(page, url0.toString());
await pasteByKeyboard(page);
const { toolbar, switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
// Inline
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
await inlineLink.hover();
await inlineToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await notClickable(inlineToolbar.getByLabel('Inline view'));
await clickable(inlineToolbar.getByLabel('Card view'));
await notClickable(inlineToolbar.getByLabel('Embed view'));
await notClickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Switches to card view
await inlineToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
// Card
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardToolbar = page.locator('affine-embed-card-toolbar');
await expect(cardLink).toBeVisible();
await cardLink.click();
// In the test environment, a text selection update is triggered, which is very unstable.
await cardLink.click();
await expect(toolbar).toBeVisible();
await switchViewBtn.click();
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await notClickable(embedViewBtn);
await cardLink.dblclick();
@@ -90,13 +119,6 @@ test('not allowed to switch to embed view when linking to the same document', as
await page.keyboard.press('Escape');
await expect(peekViewModel).not.toBeVisible();
await page.click('body');
await cardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await notClickable(cardToolbar.getByLabel('Embed view'));
});
test('not allowed to switch to embed view when linking to block', async ({
@@ -105,23 +127,24 @@ test('not allowed to switch to embed view when linking to block', async ({
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const { toolbar, switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
// Inline
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
await inlineLink.hover();
await inlineToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await notClickable(inlineToolbar.getByLabel('Inline view'));
await clickable(inlineToolbar.getByLabel('Card view'));
await clickable(inlineToolbar.getByLabel('Embed view'));
await notClickable(inlineViewBtn);
await clickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to card view
await inlineToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
// Card
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardToolbar = page.locator('affine-embed-card-toolbar');
await cardLink.dblclick();
@@ -131,26 +154,26 @@ test('not allowed to switch to embed view when linking to block', async ({
await page.keyboard.press('Escape');
await expect(peekViewModel).not.toBeVisible();
await page.click('body');
await cardLink.click();
await cardToolbar.getByLabel('More').click();
await cardToolbar.getByLabel('Copy link to block').click();
await toolbar.getByLabel('More').click();
await toolbar.getByLabel('Copy link to block').click();
await page.keyboard.press('Enter');
await pasteByKeyboard(page);
await expect(inlineLink).toBeVisible();
const href0 = await inlineLink.locator('a').getAttribute('href');
await inlineLink.hover();
await inlineToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await notClickable(inlineToolbar.getByLabel('Inline view'));
await clickable(inlineToolbar.getByLabel('Card view'));
await notClickable(inlineToolbar.getByLabel('Embed view'));
await notClickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Switches to card view
await inlineToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
const otherCardLink = page.locator('affine-embed-linked-doc-block').nth(1);
await otherCardLink.dblclick();
@@ -162,14 +185,14 @@ test('not allowed to switch to embed view when linking to block', async ({
await page.click('body');
await otherCardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await notClickable(cardToolbar.getByLabel('Embed view'));
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await notClickable(embedViewBtn);
// Switches to inline view
await cardToolbar.getByLabel('Inline view').click();
await inlineViewBtn.click();
const href1 = await inlineLink.locator('a').getAttribute('href');
@@ -190,88 +213,79 @@ test('allow switching to embed view when linking to the other document without m
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const { switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
// Inline
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
await inlineLink.hover();
await inlineToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await notClickable(inlineToolbar.getByLabel('Inline view'));
await clickable(inlineToolbar.getByLabel('Card view'));
await clickable(inlineToolbar.getByLabel('Embed view'));
await notClickable(inlineViewBtn);
await clickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to card view
await inlineToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
// Card
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardToolbar = page.locator('affine-embed-card-toolbar');
await expect(cardLink).toBeVisible();
await cardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await cardLink.click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await clickable(cardToolbar.getByLabel('Embed view'));
await switchViewBtn.click();
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to embed view
await cardToolbar.getByLabel('Embed view').click();
await embedViewBtn.click();
// Embed
const embedLink = page.locator('affine-embed-synced-doc-block');
const embedToolbar = page.locator('affine-embed-card-toolbar');
await expect(embedLink).toBeVisible();
await embedLink.click();
await embedLink.click();
// refocus
await embedLink.click();
await expect(embedToolbar).toBeVisible();
await embedToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await clickable(embedToolbar.getByLabel('Inline view'));
await clickable(embedToolbar.getByLabel('Card view'));
await notClickable(embedToolbar.getByLabel('Embed view'));
await clickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Closes
await embedToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await expect(
page.locator('.affine-embed-synced-doc-container.page')
).toBeVisible();
// Opens in peek view
await embedLink.dblclick();
const peekViewModel = page.getByTestId('peek-view-modal');
await expect(peekViewModel).toBeVisible();
await expect(peekViewModel.locator('page-editor')).toBeVisible();
await page.keyboard.press('Escape');
await expect(peekViewModel).not.toBeVisible();
await page.waitForTimeout(300);
await embedLink.click();
await embedToolbar.getByLabel('Switch view').click();
await embedLink.click();
await clickable(embedToolbar.getByLabel('Inline view'));
await clickable(embedToolbar.getByLabel('Card view'));
await notClickable(embedToolbar.getByLabel('Embed view'));
await switchViewBtn.click();
await clickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Switches to card view
await embedToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
await cardLink.click();
// refocus
await cardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await clickable(cardToolbar.getByLabel('Embed view'));
await switchViewBtn.click();
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to inline view
await cardToolbar.getByLabel('Inline view').click();
await inlineViewBtn.click();
await expect(inlineLink).toBeVisible();
});
@@ -285,8 +299,10 @@ test('allow switching to embed view when linking to the other document with mode
const url = new URL(page.url());
url.searchParams.append('mode', 'edgeless');
const { switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
await inlineLink.click();
@@ -298,85 +314,69 @@ test('allow switching to embed view when linking to the other document with mode
// Inline
await inlineLink.hover();
await inlineToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await notClickable(inlineToolbar.getByLabel('Inline view'));
await clickable(inlineToolbar.getByLabel('Card view'));
await clickable(inlineToolbar.getByLabel('Embed view'));
await notClickable(inlineViewBtn);
await clickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to card view
await inlineToolbar.getByLabel('Card view').click();
await page.waitForTimeout(300);
await cardViewBtn.click();
// Card
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardToolbar = page.locator('affine-embed-card-toolbar');
await expect(cardLink).toBeVisible();
await cardLink.click();
// refocus
await cardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await clickable(cardToolbar.getByLabel('Embed view'));
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to embed view
await cardToolbar.getByLabel('Embed view').click();
await embedViewBtn.click();
// Embed
const embedLink = page.locator('affine-embed-synced-doc-block');
const embedToolbar = page.locator('affine-embed-card-toolbar');
await expect(embedLink).toBeVisible();
await embedLink.click();
// refocus
await embedLink.click();
await expect(embedToolbar).toBeVisible();
await embedToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await clickable(embedToolbar.getByLabel('Inline view'));
await clickable(embedToolbar.getByLabel('Card view'));
await notClickable(embedToolbar.getByLabel('Embed view'));
await clickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Closes
await embedToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await expect(
page.locator('.affine-embed-synced-doc-container.edgeless')
).toBeVisible();
// Opens in peek view
await embedLink.dblclick();
const peekViewModel = page.getByTestId('peek-view-modal');
await expect(peekViewModel).toBeVisible();
await expect(peekViewModel.locator('edgeless-editor')).toBeVisible();
await page.keyboard.press('Escape');
await expect(peekViewModel).not.toBeVisible();
await page.waitForTimeout(300);
// refocus
await embedLink.click();
await embedToolbar.getByLabel('Switch view').click();
await clickable(embedToolbar.getByLabel('Inline view'));
await clickable(embedToolbar.getByLabel('Card view'));
await notClickable(embedToolbar.getByLabel('Embed view'));
await switchViewBtn.click();
await clickable(inlineViewBtn);
await clickable(cardViewBtn);
await notClickable(embedViewBtn);
// Switches to card view
await embedToolbar.getByLabel('Card view').click();
await cardViewBtn.click();
await cardLink.click();
await cardToolbar.getByLabel('Switch view').click();
await switchViewBtn.click();
await clickable(cardToolbar.getByLabel('Inline view'));
await notClickable(cardToolbar.getByLabel('Card view'));
await clickable(cardToolbar.getByLabel('Embed view'));
await clickable(inlineViewBtn);
await notClickable(cardViewBtn);
await clickable(embedViewBtn);
// Switches to inline view
await cardToolbar.getByLabel('Inline view').click();
await inlineViewBtn.click();
await inlineLink.click();
@@ -655,14 +655,18 @@ test.describe('Customize linked doc title and description', () => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const link = page.locator('affine-reference');
const title = link.locator('.affine-reference-title');
await link.hover();
const toolbar = page.locator('reference-popup');
await link.hover();
await expect(toolbar).toBeVisible();
// Copies link
await toolbar.getByRole('button', { name: 'Copy link' }).click();
await expect(toolbar).toBeHidden();
const url0 = await link.locator('a').getAttribute('href');
const url1 = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
@@ -670,7 +674,10 @@ test.describe('Customize linked doc title and description', () => {
expect(url0).not.toBeNull();
expect(new URL(url0!, coreUrl).pathname).toBe(new URL(url1).pathname);
await page.waitForTimeout(200);
// Edits title
await link.hover();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
@@ -679,6 +686,8 @@ test.describe('Customize linked doc title and description', () => {
await expect(title).toHaveText('Test Page Alias');
await page.waitForTimeout(200);
// Original title
await link.hover();
const docTitle = toolbar.getByRole('button', { name: 'Doc title' });
@@ -687,7 +696,7 @@ test.describe('Customize linked doc title and description', () => {
// Edits title
await toolbar.getByRole('button', { name: 'Edit' }).click();
const aliasPopup = page.locator('reference-alias-popup');
const aliasPopup = page.locator('reference-popup');
// Input
await expect(aliasPopup.locator('input')).toHaveValue('Test Page Alias');
@@ -708,29 +717,36 @@ test.describe('Customize linked doc title and description', () => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
const { toolbar, switchViewBtn, inlineViewBtn, cardViewBtn } =
toolbarButtons(page);
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
// Copies link
await inlineToolbar.getByRole('button', { name: 'Copy link' }).click();
await toolbar.getByRole('button', { name: 'Copy link' }).click();
const url0 = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
await page.waitForTimeout(200);
await inlineLink.hover();
// Edits title
await inlineToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('Test Page Alias');
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
await inlineLink.hover();
// Switches to card view
await inlineToolbar.getByRole('button', { name: 'Switch view' }).click();
await inlineToolbar.getByRole('button', { name: 'Card view' }).click();
await switchViewBtn.click();
await cardViewBtn.click();
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardTitle = cardLink.locator(
@@ -739,12 +755,12 @@ test.describe('Customize linked doc title and description', () => {
const cardDescription = cardLink.locator(
'.affine-embed-linked-doc-content-note.alias'
);
const cardToolbar = page.locator('affine-embed-card-toolbar');
await cardLink.click();
await cardLink.click();
// Copies link
await cardToolbar.getByRole('button', { name: 'Copy link' }).click();
await toolbar.getByRole('button', { name: 'Copy link' }).click();
const url1 = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
@@ -753,12 +769,12 @@ test.describe('Customize linked doc title and description', () => {
expect(url1).not.toBeNull();
expect(url0).toBe(url1);
const docTitle = cardToolbar.getByRole('button', { name: 'Doc title' });
const docTitle = toolbar.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();
await toolbar.getByRole('button', { name: 'Edit' }).click();
const cardEditPopup = page.locator('embed-card-edit-modal');
@@ -772,13 +788,15 @@ test.describe('Customize linked doc title and description', () => {
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 expect(cardEditPopup).not.toBeVisible();
await cardLink.click();
await cardLink.click();
// Switches to inline view
{
await cardToolbar.getByRole('button', { name: 'Switch view' }).click();
await cardToolbar.getByRole('button', { name: 'Inline view' }).click();
await switchViewBtn.click();
await inlineViewBtn.click();
// Focuses inline editor
const bounds = (await inlineLink.boundingBox())!;
@@ -793,13 +811,14 @@ test.describe('Customize linked doc title and description', () => {
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 switchViewBtn.click();
await cardViewBtn.click();
}
await cardLink.click();
await cardLink.click();
await cardToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Resets
await cardEditPopup.getByRole('button', { name: 'Reset' }).click();
@@ -815,20 +834,25 @@ test.describe('Customize linked doc title and description', () => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
const { toolbar, switchViewBtn, inlineViewBtn, cardViewBtn, embedViewBtn } =
toolbarButtons(page);
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
// Copies link
await inlineToolbar.getByRole('button', { name: 'Copy link' }).click();
await toolbar.getByRole('button', { name: 'Copy link' }).click();
const url0 = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
await page.waitForTimeout(200);
await inlineLink.hover();
// Switches to card view
await inlineToolbar.getByRole('button', { name: 'Switch view' }).click();
await inlineToolbar.getByRole('button', { name: 'Card view' }).click();
await switchViewBtn.click();
await cardViewBtn.click();
const cardLink = page.locator('affine-embed-linked-doc-block');
const cardTitle = cardLink.locator(
@@ -837,31 +861,29 @@ test.describe('Customize linked doc title and description', () => {
const cardDescription = cardLink.locator(
'.affine-embed-linked-doc-content-note.alias'
);
const cardToolbar = page.locator('affine-embed-card-toolbar');
await cardLink.click();
await cardLink.click();
// Copies link
await cardToolbar.getByRole('button', { name: 'Copy link' }).click();
await toolbar.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();
await switchViewBtn.click();
await embedViewBtn.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');
// refocus the page
await embedLink.click();
await embedLink.click();
// Copies link
await embedToolbar.getByRole('button', { name: 'Copy link' }).click();
await toolbar.getByRole('button', { name: 'Copy link' }).click();
const url2 = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
@@ -873,7 +895,7 @@ test.describe('Customize linked doc title and description', () => {
expect(url1).toBe(url2);
// Edits title & description
await embedToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
const embedEditPopup = page.locator('embed-card-edit-modal');
@@ -890,7 +912,7 @@ test.describe('Customize linked doc title and description', () => {
await embedLink.click();
// Edits title & description
await embedToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('Test Page Alias');
@@ -909,22 +931,23 @@ test.describe('Customize linked doc title and description', () => {
await cardLink.click();
const docTitle = cardToolbar.getByRole('button', { name: 'Doc title' });
const docTitle = toolbar.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 switchViewBtn.click();
await embedViewBtn.click();
await expect(embedTitle).toHaveText('Test Page');
await embedLink.click();
await embedLink.click();
// Switches to inline view
{
await embedToolbar.getByRole('button', { name: 'Switch view' }).click();
await embedToolbar.getByRole('button', { name: 'Inline view' }).click();
await switchViewBtn.click();
await inlineViewBtn.click();
// Focuses inline editor
const bounds = (await inlineLink.boundingBox())!;
@@ -939,15 +962,15 @@ test.describe('Customize linked doc title and description', () => {
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 switchViewBtn.click();
await embedViewBtn.click();
}
await embedLink.click();
await expect(embedTitle).toHaveText('Test Page');
await expect(
embedToolbar.getByRole('button', { name: 'Doc title' })
toolbar.getByRole('button', { name: 'Doc title' })
).toBeHidden();
});
@@ -962,13 +985,13 @@ test.describe('Customize linked doc title and description', () => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
// Edits title
await inlineToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('🦀hello');
@@ -992,13 +1015,13 @@ test.describe('Customize linked doc title and description', () => {
await page.keyboard.press('Enter');
await createTodayPage(page);
const inlineLink = page.locator('affine-reference');
const inlineToolbar = page.locator('reference-popup');
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
// Edits title
await inlineToolbar.getByRole('button', { name: 'Edit' }).click();
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('🦀');

View File

@@ -123,12 +123,10 @@ test('ctrl click embedded doc link and open in new tab', async ({ page }) => {
// hover on the reference node and change it to embedded card mode
await referenceNode.hover();
const referencePopup = page.locator(
'reference-popup .affine-reference-popover-container'
);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(referencePopup).toBeVisible();
await referencePopup.getByRole('button', { name: 'Switch view' }).click();
await expect(toolbar).toBeVisible();
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await page.getByRole('button', { name: 'Card view' }).click();
const embededDocBlock = page.locator('affine-embed-linked-doc-block');

View File

@@ -23,17 +23,12 @@ test('can open peek view via link popover', async ({ page }) => {
await page.locator('affine-reference').hover();
await expect(
page.locator('.affine-reference-popover-container')
).toBeVisible();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
// click more button
await page
.locator('editor-menu-button editor-icon-button[aria-label="Open doc"]')
.click();
await page
.locator('editor-menu-action:has-text("Open in center peek")')
.click();
await toolbar.getByLabel('Open doc').click();
await toolbar.getByLabel('Open in center peek').click();
// verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
@@ -64,16 +59,12 @@ test('can open peek view via db+click link card', async ({ page }) => {
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
await expect(
page.locator('.affine-reference-popover-container')
).toBeVisible();
await page
.locator('editor-menu-button editor-icon-button[aria-label="Switch view"]')
.click();
await page
.locator('editor-menu-button editor-menu-action[aria-label="Card view"]')
.click();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Card view').click();
await expect(
page.locator('affine-embed-linked-doc-block:has-text("Test Page")')

View File

@@ -441,6 +441,7 @@ test('disable quick search when the link-popup is visitable', async ({
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
await withCtrlOrMeta(page, () => page.keyboard.press('k'));
await expect(quickSearch).toBeHidden();
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill(specialTitle);
@@ -544,7 +545,11 @@ test('can use slash menu to insert a newly created doc card', async ({
const testTitle = 'test title';
await page.locator('[cmdk-input]').fill(testTitle);
await page.keyboard.press('Enter');
const addNewPage = page.locator(
`[cmdk-item] >> text=New "${testTitle}" Page`
);
await addNewPage.click();
await expect(page.locator('affine-embed-linked-doc-block')).toBeVisible();
await expect(

View File

@@ -4,7 +4,6 @@ import { expect, type Locator, type Page } from '@playwright/test';
declare type _GLOBAL_ = typeof BlocksuiteBlocks;
const AFFINE_FORMAT_BAR_WIDGET = 'affine-format-bar-widget';
const EDGELESS_ELEMENT_TOOLBAR_WIDGET = 'edgeless-element-toolbar-widget';
const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
@@ -67,7 +66,7 @@ export async function focusDocTitle(page: Page, editorIndex = 0) {
// ================== Page ==================
export function locateFormatBar(page: Page, editorIndex = 0) {
return locateEditorContainer(page, editorIndex).locator(
AFFINE_FORMAT_BAR_WIDGET
'affine-toolbar-widget editor-toolbar'
);
}