fix(core): ai replace selection (#11875)

### TL;DR

* Fix the issue of inaccurate content replacement in AI Replace Selection
* Optimize unit Tests utils

### What Changed
1. Fixed the issue of inaccurate content replacement in AI Replace Selection:
  - Convert the AI Answer into a Snapshot, then transform it into a sequence of Blocks ready for insertion.
   - Invoke the `replaceSelectedTextWithBlocks` command to replace the current selection with blocks (given the complexity of block combinations, this command uses [ts-pattern](https://github.com/gvergnaud/ts-pattern) implementation to ensure compile-time prevention of pattern handling omissions).
2. Optimized unit test assertions for commands, now allowing direct document content comparison using `toEqualDoc`.
```ts
const host = affine`
  <affine-page id="page">
    <affine-note id="note">
      <affine-paragraph id="paragraph-1">Hel<anchor />lo</affine-paragraph>
      <affine-paragraph id="paragraph-2">Wor<focus />ld</affine-paragraph>
    </affine-note>
  </affine-page>
`;

// ....

const expected = affine`
  <affine-page id="page">
    <affine-note id="note">
      <affine-paragraph id="paragraph-1">Hel111</affine-paragraph>
      <affine-code id="code"></affine-code>
      <affine-paragraph id="paragraph-2">222ld</affine-paragraph>
    </affine-note>
  </affine-page>
`;

expect(host.doc).toEqualDoc(expected.doc);
```
3. Added support for text cursors in unit test template syntax.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

> CLOSE BS-3278

- **New Features**
  - Introduced the ability to replace selected text in documents with blocks, supporting advanced merging and insertion scenarios for various block types.
- **Bug Fixes**
  - Improved handling of text selection and cursor placement in document templates used for testing.
- **Tests**
  - Added comprehensive tests for replacing selected text with blocks, including edge cases and complex selection scenarios.
  - Enhanced test utilities for document structure comparison and selection handling.
  - Updated end-to-end tests to verify correct replacement of selected text via AI-driven actions.
- **Chores**
  - Added and updated dependencies to support new features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
yoyoyohamapi
2025-05-08 11:48:18 +00:00
parent 6689bd1914
commit 6d012f093f
13 changed files with 1331 additions and 39 deletions

View File

@@ -1,3 +1,4 @@
import { IS_MAC } from '@blocksuite/global/env';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
@@ -62,13 +63,22 @@ test.describe('AIChatWith/Text', () => {
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('I Loev Apple');
// Select the word "Loev"
const SHORT_KEY = IS_MAC ? 'Alt' : 'Control';
await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`);
await page.keyboard.press('ArrowLeft');
await page.keyboard.press(`Shift+${SHORT_KEY}+ArrowLeft`);
const { fixSpelling } = await utils.editor.showAIMenu(page);
const { answer } = await fixSpelling();
await expect(answer).toHaveText(/Love/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apfel');
expect(content).toBe('I Love Apple');
});
test('should support continue in chat', async ({

View File

@@ -546,19 +546,7 @@ export class EditorUtils {
};
}
public static async askAIWithText(page: Page, text: string) {
await this.focusToEditor(page);
const texts = text.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
public static async showAIMenu(page: Page) {
const askAI = await page.locator('page-editor editor-toolbar ask-ai-icon');
await askAI.waitFor({
state: 'attached',
@@ -661,6 +649,22 @@ export class EditorUtils {
} as const;
}
public static async askAIWithText(page: Page, text: string) {
await this.focusToEditor(page);
const texts = text.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
return await this.showAIMenu(page);
}
public static async whatAreYourThoughts(page: Page, text: string) {
const textarea = page.locator(
'affine-ai-panel-widget .ai-panel-container textarea'