fix(editor): ime input error at empty line (#11636)

Close [BS-3106](https://linear.app/affine-design/issue/BS-3106/mac-chrom在空行使用ime输入,文档卡住)
This commit is contained in:
L-Sun
2025-04-11 10:39:16 +00:00
parent e1e5e8fc14
commit aabb09b31f
22 changed files with 148 additions and 110 deletions

View File

@@ -394,7 +394,7 @@ test(scoped`delete emoji forward`, async ({ page }) => {
});
test(
scoped`ZERO_WIDTH_SPACE should be counted by one cursor position`,
scoped`ZERO_WIDTH_FOR_EMPTY_LINE should be counted by one cursor position`,
async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);

View File

@@ -12,7 +12,7 @@ import {
getEditorLocator,
waitNextFrame,
} from '../utils/actions/misc.js';
import { ZERO_WIDTH_SPACE } from '../utils/inline-editor.js';
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '../utils/inline-editor.js';
export async function press(page: Page, content: string) {
await page.keyboard.press(content, { delay: 50 });
@@ -95,7 +95,7 @@ export async function assertDatabaseTitleColumnText(
const text = await selectCell1.innerText();
if (title === '') {
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`));
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_FOR_EMPTY_LINE}])$`));
} else {
expect(text).toBe(title);
}

View File

@@ -15,7 +15,7 @@ import {
initEmptyParagraphState,
} from '../utils/actions/misc.js';
import { assertRichTextInlineDeltas } from '../utils/asserts.js';
import { ZERO_WIDTH_SPACE } from '../utils/inline-editor.js';
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '../utils/inline-editor.js';
// FIXME(mirone): copy paste from framework/inline/__tests__/utils.ts
const defaultPlaygroundURL = new URL(
`http://localhost:${process.env.CI ? 4173 : 5173}/`
@@ -165,8 +165,8 @@ test('basic input', async ({ page, browserName }) => {
const editorAUndo = page.getByText('undo').nth(0);
const editorARedo = page.getByText('redo').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -177,8 +177,8 @@ test('basic input', async ({ page, browserName }) => {
await editorAUndo.click();
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await editorARedo.click();
@@ -244,12 +244,18 @@ test('basic input', async ({ page, browserName }) => {
await page.waitForTimeout(100);
await expect(editorA).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorB).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await expect(editorB).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await editorAUndo.click();
@@ -258,12 +264,18 @@ test('basic input', async ({ page, browserName }) => {
await editorARedo.click();
await expect(editorA).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorB).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await expect(editorB).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await focusInlineRichText(page);
await page.waitForTimeout(100);
@@ -274,12 +286,18 @@ test('basic input', async ({ page, browserName }) => {
await editorAUndo.click();
await expect(editorA).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorB).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nbbb', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await expect(editorB).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nbbb',
{
useInnerText: true, // for multi-line text
}
);
await editorARedo.click();
@@ -314,12 +332,18 @@ test('basic input', async ({ page, browserName }) => {
await press(page, 'Enter');
await press(page, 'Enter');
await expect(editorA).toHaveText('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd', {
useInnerText: true, // for multi-line text
});
await expect(editorB).toHaveText('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abbbc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\ndd',
{
useInnerText: true, // for multi-line text
}
);
await expect(editorB).toHaveText(
'abbbc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\ndd',
{
useInnerText: true, // for multi-line text
}
);
await editorAUndo.click();
@@ -328,12 +352,18 @@ test('basic input', async ({ page, browserName }) => {
await editorARedo.click();
await expect(editorA).toHaveText('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd', {
useInnerText: true, // for multi-line text
});
await expect(editorB).toHaveText('abbbc\n' + ZERO_WIDTH_SPACE + '\ndd', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abbbc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\ndd',
{
useInnerText: true, // for multi-line text
}
);
await expect(editorB).toHaveText(
'abbbc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\ndd',
{
useInnerText: true, // for multi-line text
}
);
});
test('chinese input', async ({ page, browserName }) => {
@@ -348,8 +378,8 @@ test('chinese input', async ({ page, browserName }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorB = page.locator('[data-v-root="true"]').nth(1);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
const client = await page.context().newCDPSession(page);
@@ -396,8 +426,8 @@ test('readonly mode', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorB = page.locator('[data-v-root="true"]').nth(1);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -441,8 +471,8 @@ test('basic styles', async ({ page }) => {
const editorAUndo = page.getByText('undo').nth(0);
const editorARedo = page.getByText('redo').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -696,8 +726,8 @@ test('overlapping styles', async ({ page }) => {
const editorAUndo = page.getByText('undo').nth(0);
const editorARedo = page.getByText('redo').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -868,8 +898,8 @@ test('input continuous spaces', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorB = page.locator('[data-v-root="true"]').nth(1);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -902,8 +932,8 @@ test('select from the start of line using shift+arrow', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorB = page.locator('[data-v-root="true"]').nth(1);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -961,8 +991,8 @@ test('getLine', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorB = page.locator('[data-v-root="true"]').nth(1);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorB).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await expect(editorB).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -1032,7 +1062,7 @@ test('embed', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorAEmbed = page.getByText('embed').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
@@ -1101,7 +1131,7 @@ test('delete embed when pressing backspace after embed', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
const editorAEmbed = page.getByText('embed').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
await type(page, 'ab');
await expect(editorA).toHaveText('ab');
@@ -1146,7 +1176,7 @@ test('triple click to select line', async ({ page }) => {
const editorA = page.locator('[data-v-root="true"]').nth(0);
await expect(editorA).toHaveText(ZERO_WIDTH_SPACE);
await expect(editorA).toHaveText(ZERO_WIDTH_FOR_EMPTY_LINE);
await page.waitForTimeout(100);
await type(page, 'abc\nabc abc abc\nabc');
@@ -1161,9 +1191,12 @@ test('triple click to select line', async ({ page }) => {
await assertSelection(page, 0, 4, 11);
await pressBackspace(page);
await expect(editorA).toHaveText('abc\n' + ZERO_WIDTH_SPACE + '\nabc', {
useInnerText: true, // for multi-line text
});
await expect(editorA).toHaveText(
'abc\n' + ZERO_WIDTH_FOR_EMPTY_LINE + '\nabc',
{
useInnerText: true, // for multi-line text
}
);
});
test('caret should move correctly when inline elements are exist', async ({

View File

@@ -24,7 +24,7 @@ import {
assertRichTextInlineDeltas,
assertRichTextInlineRange,
} from '../utils/asserts.js';
import { ZERO_WIDTH_SPACE } from '../utils/inline-editor.js';
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '../utils/inline-editor.js';
import { test } from '../utils/playwright.js';
test('add inline latex at the start of line', async ({ page }) => {
@@ -162,13 +162,13 @@ test('latex editor', async ({ page }) => {
);
await pressBackspaceWithShortKey(page, 2);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_FOR_EMPTY_LINE);
await undoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
);
await redoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_FOR_EMPTY_LINE);
await undoByKeyboard(page);
expect(await latexEditorLine.innerText()).toBe(
'ababababababababababababababababababababababababab'
@@ -200,7 +200,7 @@ test('latex editor', async ({ page }) => {
await selectAllByKeyboard(page);
await pressBackspace(page);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_SPACE);
expect(await latexEditorLine.innerText()).toBe(ZERO_WIDTH_FOR_EMPTY_LINE);
// highlight
await type(

View File

@@ -14,7 +14,7 @@ import { expect } from '@playwright/test';
import stringify from 'json-stable-stringify';
import lz from 'lz-string';
import { ZERO_WIDTH_SPACE } from '../inline-editor.js';
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '../inline-editor.js';
import { currentEditorIndex } from '../multiple-editor.js';
import {
pressArrowRight,
@@ -1054,7 +1054,7 @@ export async function getIndexCoordinate(
}
export function inlineEditorInnerTextToString(innerText: string): string {
return innerText.replace(ZERO_WIDTH_SPACE, '').trim();
return innerText.replace(ZERO_WIDTH_FOR_EMPTY_LINE, '').trim();
}
export async function focusTitle(page: Page) {

View File

@@ -22,4 +22,5 @@ export async function getStringFromRichText(
}
// Why? we can't import from `@blocksuite/affine/std/inline` because playwright will throw an error
export const ZERO_WIDTH_SPACE = '\u200C';
export const ZERO_WIDTH_FOR_EMPTY_LINE =
process.env.BROWSER === 'webkit' ? '\u200C' : '\u200B';