Files
AFFiNE-Mirror/blocksuite/tests-legacy/list.spec.ts
2024-12-20 11:08:21 +00:00

843 lines
23 KiB
TypeScript

import { expect, type Locator } from '@playwright/test';
import { getFormatBar } from 'utils/query.js';
import {
dragBetweenIndices,
enterPlaygroundRoom,
enterPlaygroundWithList,
focusRichText,
getPageSnapshot,
initEmptyEdgelessState,
initEmptyParagraphState,
initThreeLists,
pressArrowLeft,
pressArrowUp,
pressBackspace,
pressBackspaceWithShortKey,
pressEnter,
pressShiftEnter,
pressShiftTab,
pressSpace,
pressTab,
redoByClick,
switchEditorMode,
switchReadonly,
type,
undoByClick,
undoByKeyboard,
updateBlockType,
waitNextFrame,
} from './utils/actions/index.js';
import {
assertBlockChildrenFlavours,
assertBlockChildrenIds,
assertBlockCount,
assertBlockType,
assertListPrefix,
assertRichTextInlineRange,
assertRichTexts,
assertTextContent,
} from './utils/asserts.js';
import { test } from './utils/playwright.js';
async function isToggleIconVisible(toggleIcon: Locator) {
const connected = await toggleIcon.isVisible();
if (!connected) return false;
const element = await toggleIcon.elementHandle();
if (!element) return false;
const opacity = await element.evaluate(node => {
// https://stackoverflow.com/questions/11365296/how-do-i-get-the-opacity-of-an-element-using-javascript
return window.getComputedStyle(node).getPropertyValue('opacity');
});
if (!opacity || typeof opacity !== 'string') {
throw new Error('opacity is not a string');
}
const isVisible = opacity !== '0';
return isVisible;
}
test('add new bulleted list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'bulleted');
await focusRichText(page, 0);
await type(page, 'aa');
await pressEnter(page);
await type(page, 'aa');
await pressEnter(page);
await assertRichTexts(page, ['aa', 'aa', '']);
await assertBlockCount(page, 'list', 3);
});
test('add new todo list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'todo');
await focusRichText(page, 0);
await type(page, 'aa');
await assertRichTexts(page, ['aa']);
const checkBox = page.locator('.affine-list-block__prefix');
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
});
test('add new toggle list', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await updateBlockType(page, 'affine:list', 'toggle');
await focusRichText(page, 0);
await type(page, 'top');
await pressTab(page);
await pressEnter(page);
await type(page, 'kid 1');
await pressEnter(page);
await assertRichTexts(page, ['top', 'kid 1', '']);
await assertBlockCount(page, 'list', 3);
});
test('convert nested paragraph to list', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa\nbbb');
await pressTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await dragBetweenIndices(page, [0, 1], [1, 2]);
const { openParagraphMenu, bulletedBtn } = getFormatBar(page);
await openParagraphMenu();
await bulletedBtn.click();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('convert to numbered list block', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0); // created 0, 1, 2
await updateBlockType(page, 'affine:list', 'bulleted'); // replaced 2 to 3
await waitNextFrame(page);
await updateBlockType(page, 'affine:list', 'numbered');
await focusRichText(page, 0);
const listSelector = '.affine-list-rich-text-wrapper';
const bulletIconSelector = `${listSelector} > div`;
await assertTextContent(page, bulletIconSelector, /1\./);
await undoByClick(page);
// const numberIconSelector = `${listSelector} > svg`;
// await expect(page.locator(numberIconSelector)).toHaveCount(1);
await redoByClick(page);
await focusRichText(page, 0);
await type(page, 'aa');
await pressEnter(page); // created 4
await assertBlockType(page, '4', 'numbered');
await type(page, 'aa');
await pressEnter(page); // created 5
await assertBlockType(page, '5', 'numbered');
await page.keyboard.press('Tab');
await assertBlockType(page, '5', 'numbered');
});
test('indent list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await type(page, 'hello');
await assertRichTexts(page, ['', 'hello', '']);
await page.keyboard.press('Tab'); // 0(1(2(3)4))
await assertRichTexts(page, ['', 'hello', '']);
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
await undoByKeyboard(page); // 0(1(2,3,4))
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
});
test('unindent list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4))
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
await pressShiftTab(page); // 0(1(2,3,4))
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
await pressShiftTab(page);
await assertBlockChildrenIds(page, '1', ['2', '3', '4']);
});
test('remove all indent for a list block', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 1);
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3)4))
await focusRichText(page, 2);
await page.keyboard.press('Tab', { delay: 50 });
await page.keyboard.press('Tab', { delay: 50 }); // 0(1(2(3(4))))
await assertBlockChildrenIds(page, '3', ['4']);
await pressBackspaceWithShortKey(page); // 0(1(2(3)4))
await assertBlockChildrenIds(page, '1', ['2', '4']);
await assertBlockChildrenIds(page, '2', ['3']);
});
test('insert new list block by enter', async ({ page }) => {
await enterPlaygroundWithList(page);
await assertRichTexts(page, ['', '', '']);
await focusRichText(page, 1);
await type(page, 'hello');
await assertRichTexts(page, ['', 'hello', '']);
await pressEnter(page);
await type(page, 'world');
await assertRichTexts(page, ['', 'hello', 'world', '']);
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:list',
'affine:list',
'affine:list',
]);
});
test('delete at start of list block', async ({ page }) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 1);
await page.keyboard.press('Backspace');
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:paragraph',
'affine:list',
]);
await waitNextFrame(page, 200);
await assertRichTextInlineRange(page, 1, 0, 0);
await undoByClick(page);
await assertBlockChildrenFlavours(page, '1', [
'affine:list',
'affine:list',
'affine:list',
]);
await waitNextFrame(page);
//FIXME: it just failed in playwright
// await assertSelection(page, 1, 0, 0);
});
test('nested list blocks', async ({ page }, testInfo) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 0);
await type(page, '123');
await focusRichText(page, 1);
await pressTab(page);
await type(page, '456');
await focusRichText(page, 2);
await pressTab(page);
await pressTab(page);
await type(page, '789');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await focusRichText(page, 1);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('update numbered list block prefix', async ({ page }) => {
await enterPlaygroundWithList(page, ['', '', ''], 'numbered'); // 0(1(2,3,4))
await focusRichText(page, 1);
await type(page, 'lunatic');
await assertRichTexts(page, ['', 'lunatic', '']);
await assertListPrefix(page, ['1', '2', '3']);
await page.keyboard.press('Tab');
await assertListPrefix(page, ['1', 'a', '2']);
await page.keyboard.press('Shift+Tab');
await assertListPrefix(page, ['1', '2', '3']);
await waitNextFrame(page, 200);
await page.keyboard.press('Enter');
await assertListPrefix(page, ['1', '2', '3', '4']);
await waitNextFrame(page, 200);
await type(page, 'concorde');
await assertRichTexts(page, ['', 'lunatic', 'concorde', '']);
await page.keyboard.press('Tab');
await assertListPrefix(page, ['1', '2', 'a', '3']);
});
test('basic indent and unindent', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'text1');
await pressEnter(page);
await type(page, 'text2');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await page.keyboard.press('Tab');
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_tab.json`
);
await page.waitForTimeout(100);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_after_shift_tab.json`
);
});
test('should indent todo block preserve todo status', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'text1');
await pressEnter(page);
await type(page, '[x]');
await pressSpace(page);
await type(page, 'todo item');
await pressTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await pressShiftTab(page);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_final.json`
);
});
test('enter list block with empty text', async ({ page }, testInfo) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
/**
* -
* -
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await focusRichText(page, 1);
await pressTab(page);
await focusRichText(page, 2);
await pressTab(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
await pressEnter(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_2.json`
);
await pressEnter(page);
/**
* -
* -
* |
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_3.json`
);
await undoByClick(page);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
/**
* -
* -|
* -
*/
await focusRichText(page, 1);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
/**
* -
* -|
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_4.json`
);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
/**
* -|
* -
* -
*/
await focusRichText(page, 0);
await waitNextFrame(page);
await pressEnter(page);
await waitNextFrame(page);
/**
* |
* -
* -
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_5.json`
);
await undoByClick(page);
/**
* -
* -
* -|
*/
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_1.json`
);
});
test('enter list block with non-empty text', async ({ page }) => {
await enterPlaygroundWithList(page); // 0(1(2,3,4))
await focusRichText(page, 0);
await type(page, 'aa');
await focusRichText(page, 1);
await type(page, 'bb');
await pressTab(page);
await focusRichText(page, 2);
await type(page, 'cc');
await pressTab(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
await focusRichText(page, 1);
await pressEnter(page);
await assertBlockChildrenIds(page, '2', ['3', '5', '4']);
await undoByClick(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
await focusRichText(page, 0);
await pressEnter(page);
await assertBlockChildrenIds(page, '2', ['6', '3', '4']); // 0(1(2,(6,3,4)))
await waitNextFrame(page);
await undoByClick(page);
await assertBlockChildrenIds(page, '2', ['3', '4']); // 0(1(2,(3,4)))
});
test.describe('indent correctly when deleting list item', () => {
test('delete the child item in the middle position', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '- a');
await pressEnter(page);
await pressTab(page);
await type(page, 'b');
await pressEnter(page);
await type(page, 'c');
await pressEnter(page);
await type(page, 'd');
await pressArrowUp(page);
await pressArrowLeft(page);
await pressBackspace(page);
await pressBackspace(page);
await assertBlockChildrenIds(page, '3', ['4', '6']);
await assertRichTexts(page, ['a', 'bc', 'd']);
await assertRichTextInlineRange(page, 1, 1);
});
test('merge two lists', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '- a');
await pressEnter(page);
await pressTab(page);
await type(page, 'b');
await pressEnter(page);
await pressTab(page);
await type(page, 'c');
await pressEnter(page);
await pressBackspace(page, 3);
await assertRichTexts(page, ['a', 'b', 'c', '']);
await waitNextFrame(page);
await pressEnter(page);
await type(page, '- d');
await pressEnter(page);
await pressTab(page);
await type(page, 'e');
await pressEnter(page);
await pressTab(page);
await type(page, 'f');
await pressArrowUp(page, 3);
await pressBackspace(page, 2);
await waitNextFrame(page, 200);
await assertRichTexts(page, ['a', 'b', '', 'd', 'e', 'f']);
await assertBlockChildrenIds(page, '1', ['3', '9']);
await assertBlockChildrenIds(page, '3', ['4']);
await assertBlockChildrenIds(page, '4', ['5']);
await assertBlockChildrenIds(page, '10', ['11']);
});
});
test('delete list item with nested children items', async ({ page }) => {
await enterPlaygroundWithList(page);
await focusRichText(page, 0);
await type(page, '1');
await focusRichText(page, 1);
await pressTab(page);
await type(page, '2');
await focusRichText(page, 2);
await pressTab(page);
await pressTab(page);
await type(page, '3');
await pressEnter(page);
await type(page, '4');
await focusRichText(page, 1);
await pressArrowLeft(page);
// 1
// |2
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1
// |2 (transformed to paragraph)
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1
// |2
// 3
// 4
await pressBackspace(page);
await waitNextFrame(page);
// 1|2
// 3
// 4
await assertRichTextInlineRange(page, 0, 1);
await assertRichTexts(page, ['12', '3', '4']);
await assertBlockChildrenIds(page, '1', ['2', '4', '5']);
});
test('add number prefix to a todo item should not forcefully change it into numbered list, vice versa', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page, 0);
await type(page, '1. numberList');
await assertListPrefix(page, ['1']);
await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } });
await type(page, '[] ');
await assertListPrefix(page, ['1']);
await pressBackspace(page, 14);
await type(page, '[] todoList');
await assertListPrefix(page, ['']);
await focusRichText(page, 0, { clickPosition: { x: 0, y: 0 } });
await type(page, '1. ');
await assertListPrefix(page, ['']);
});
test('should not convert to a list when pressing space at the second line', async ({
page,
}) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
await type(page, 'aaa');
await pressShiftEnter(page);
await type(page, '-');
await pressSpace(page);
await type(page, 'bbb');
await assertRichTexts(page, ['aaa\n- bbb']);
});
test.describe('toggle list', () => {
test('click toggle icon should collapsed list', async ({
page,
}, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
const parentPrefix = prefixes.nth(1);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await parentPrefix.hover();
await waitNextFrame(page);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_toggle.json`
);
// Collapsed toggle icon should be show always
await page.mouse.move(0, 0);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).not.toBeVisible();
await toggleIcon.click();
await expect(listChildren).toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await page.mouse.move(0, 0);
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
});
test('indent item should expand toggle', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
await focusRichText(page, 2);
await pressEnter(page);
await pressEnter(page);
await type(page, '012');
const toggleIcon = page.locator('.toggle-icon');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_init.json`
);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_toggle.json`
);
await focusRichText(page, 3);
await pressTab(page);
await waitNextFrame(page, 200);
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_finial.json`
);
});
test('toggle icon should be show when hover', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const parentPrefix = prefixes.nth(1);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
await parentPrefix.hover();
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await page.mouse.move(0, 0);
await waitNextFrame(page, 300);
expect(await isToggleIconVisible(toggleIcon)).toBe(false);
});
});
test.describe('readonly', () => {
test('can expand toggle in readonly mode', async ({ page }, testInfo) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const prefixes = page.locator('.affine-list-block__prefix');
const listChildren = page
.locator('[data-block-id="4"] .affine-block-children-container')
.nth(0);
const parentPrefix = prefixes.nth(1);
await parentPrefix.hover();
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
await waitNextFrame(page, 200);
await switchReadonly(page);
await waitNextFrame(page, 200);
expect(await isToggleIconVisible(toggleIcon)).toBe(true);
await expect(listChildren).not.toBeVisible();
await toggleIcon.click();
await expect(listChildren).toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_before_readonly.json`
);
});
test('can not modify todo list in readonly mode', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyParagraphState(page);
await focusRichText(page);
const checkBox = page.locator('.affine-list-block__prefix');
{
await type(page, '[] todo');
await switchReadonly(page);
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(0);
}
{
await switchReadonly(page, false);
await checkBox.click();
await switchReadonly(page);
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
await checkBox.click();
await expect(page.locator('.affine-list--checked')).toHaveCount(1);
}
});
test('should render collapsed list correctly', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
// await switchEditorMode(page);
await initThreeLists(page);
const toggleIcon = page.locator('.toggle-icon');
const listChildren = page
.locator('[data-block-id="5"] .affine-block-children-container')
.nth(0);
await expect(listChildren).toBeVisible();
await toggleIcon.click();
await expect(listChildren).not.toBeVisible();
await switchReadonly(page);
// trick for render a readonly doc from scratch
await switchEditorMode(page);
await switchEditorMode(page);
await expect(listChildren).not.toBeVisible();
});
});