import { expect } from '@playwright/test'; import { dragBetweenCoords, enterPlaygroundRoom, focusDatabaseTitle, getBoundingBox, initDatabaseDynamicRowWithData, initDatabaseRowWithData, initEmptyDatabaseState, pressArrowLeft, pressArrowRight, pressBackspace, pressEnter, pressEscape, pressShiftEnter, redoByKeyboard, selectAllByKeyboard, setInlineRangeInInlineEditor, switchReadonly, type, undoByClick, undoByKeyboard, waitNextFrame, } from '../utils/actions/index.js'; import { assertBlockProps, assertInlineEditorDeltas, assertRowCount, } from '../utils/asserts.js'; import { test } from '../utils/playwright.js'; import { getFormatBar } from '../utils/query.js'; import { assertColumnWidth, assertDatabaseCellRichTexts, assertDatabaseSearching, assertDatabaseTitleText, blurDatabaseSearch, clickColumnType, clickDatabaseOutside, focusDatabaseHeader, focusDatabaseSearch, getDatabaseBodyCell, getDatabaseHeaderColumn, getFirstColumnCell, initDatabaseColumn, switchColumnType, } from './actions.js'; test('edit database block title and create new rows', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); const locator = page.locator('affine-database'); await expect(locator).toBeVisible(); const dbTitle = 'Database 1'; await assertBlockProps(page, '2', { title: dbTitle, }); await focusDatabaseTitle(page); await selectAllByKeyboard(page); await pressBackspace(page); const expected = 'hello'; await type(page, expected); await assertBlockProps(page, '2', { title: 'hello', }); await undoByClick(page); await assertBlockProps(page, '2', { title: 'Database 1', }); await initDatabaseRowWithData(page, ''); await initDatabaseRowWithData(page, ''); await assertRowCount(page, 2); await waitNextFrame(page, 100); await pressEscape(page); await undoByClick(page); await undoByClick(page); await assertRowCount(page, 0); }); test('edit column title', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page, '1'); // first added column const { column } = await getDatabaseHeaderColumn(page, 1); expect(await column.innerText()).toBe('1'); await undoByClick(page); expect(await column.innerText()).toBe('Column 1'); }); test('should modify the value when the input loses focus', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Number'); await initDatabaseDynamicRowWithData(page, '1', true); await clickDatabaseOutside(page); const cell = getFirstColumnCell(page, 'number'); const text = await cell.textContent(); expect(text?.trim()).toBe('1'); }); test('should rich-text column support soft enter', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '123', true); const cell = getFirstColumnCell(page, 'affine-database-rich-text'); await cell.click(); await pressArrowLeft(page); await pressEnter(page); await assertDatabaseCellRichTexts(page, { text: '123' }); await cell.click(); await pressArrowRight(page); await pressArrowLeft(page); await pressShiftEnter(page); await pressEnter(page); await assertDatabaseCellRichTexts(page, { text: '12\n3' }); }); test('should the multi-select mode work correctly', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await initDatabaseDynamicRowWithData(page, '1', true); await pressEscape(page); await initDatabaseDynamicRowWithData(page, '2'); await pressEscape(page); const cell = getFirstColumnCell(page, 'select-selected'); expect(await cell.count()).toBe(2); expect(await cell.nth(0).innerText()).toBe('1'); expect(await cell.nth(1).innerText()).toBe('2'); }); test('should database search work', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await initDatabaseRowWithData(page, 'text1'); await initDatabaseDynamicRowWithData(page, '123', false); await pressEscape(page); await initDatabaseRowWithData(page, 'text2'); await initDatabaseDynamicRowWithData(page, 'a', false); await pressEscape(page); await initDatabaseRowWithData(page, 'text3'); await initDatabaseDynamicRowWithData(page, '26', false); await pressEscape(page); // search for '2' await focusDatabaseSearch(page); await type(page, '2'); const rows = page.locator('.affine-database-block-row'); expect(await rows.count()).toBe(3); // search for '23' await type(page, '3'); expect(await rows.count()).toBe(1); const cell = page.locator('.select-selected'); expect(await cell.innerText()).toBe('123'); // clear search input const closeIcon = page.locator('.close-icon'); await closeIcon.click(); expect(await rows.count()).toBe(3); }); test('should database search input displayed correctly', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await focusDatabaseSearch(page); await blurDatabaseSearch(page); await assertDatabaseSearching(page, false); await focusDatabaseSearch(page); await type(page, '2'); await blurDatabaseSearch(page); await assertDatabaseSearching(page, true); await focusDatabaseSearch(page); await pressBackspace(page); await blurDatabaseSearch(page); await assertDatabaseSearching(page, false); await focusDatabaseSearch(page); await type(page, '2'); const closeIcon = page.locator('.close-icon'); await closeIcon.click(); await blurDatabaseSearch(page); await assertDatabaseSearching(page, false); await focusDatabaseSearch(page); await type(page, '2'); await pressEscape(page); await blurDatabaseSearch(page); await assertDatabaseSearching(page, false); }); test('should database title and rich-text support undo/redo', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '123', true); await undoByKeyboard(page); await assertDatabaseCellRichTexts(page, { text: '' }); await pressEscape(page); await redoByKeyboard(page); await assertDatabaseCellRichTexts(page, { text: '123' }); await focusDatabaseTitle(page); await type(page, 'abc'); await assertDatabaseTitleText(page, 'Database 1abc'); await undoByKeyboard(page); await assertDatabaseTitleText(page, 'Database 1'); await redoByKeyboard(page); await assertDatabaseTitleText(page, 'Database 1abc'); }); test('should support drag to change column width', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); const headerColumns = page.locator('.affine-database-column'); const titleColumn = headerColumns.nth(0); const normalColumn = headerColumns.nth(1); const dragDistance = 100; const titleColumnWidth = 260; const normalColumnWidth = 180; await assertColumnWidth(titleColumn, titleColumnWidth - 1); const box = await assertColumnWidth(normalColumn, normalColumnWidth - 1); await dragBetweenCoords( page, { x: box.x, y: box.y }, { x: box.x + dragDistance, y: box.y }, { steps: 50, beforeMouseUp: async () => { await waitNextFrame(page); }, } ); await assertColumnWidth(titleColumn, titleColumnWidth + dragDistance); await assertColumnWidth(normalColumn, normalColumnWidth - 1); await undoByClick(page); await assertColumnWidth(titleColumn, titleColumnWidth - 1); await assertColumnWidth(normalColumn, normalColumnWidth - 1); }); test('should display the add column button on the right side of database correctly', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); const normalColumn = page.locator('.affine-database-column').nth(1); const addColumnBtn = page.locator('.header-add-column-button'); const box = await getBoundingBox(normalColumn); await dragBetweenCoords( page, { x: box.x, y: box.y }, { x: box.x + 400, y: box.y }, { steps: 50, beforeMouseUp: async () => { await waitNextFrame(page); }, } ); await focusDatabaseHeader(page); await expect(addColumnBtn).toBeVisible(); }); test('should support drag and drop to move columns', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page, 'column1'); await initDatabaseColumn(page, 'column2'); await initDatabaseColumn(page, 'column3'); const column1 = await focusDatabaseHeader(page, 1); const moveIcon = column1.locator('.affine-database-column-move'); const moveIconBox = await getBoundingBox(moveIcon); const x = moveIconBox.x + moveIconBox.width / 2; const y = moveIconBox.y + moveIconBox.height / 2; await dragBetweenCoords( page, { x, y }, { x: x + 100, y }, { steps: 50, beforeMouseUp: async () => { await waitNextFrame(page); const indicator = page.locator('.vertical-indicator').first(); await expect(indicator).toBeVisible(); const { box } = await getDatabaseHeaderColumn(page, 2); const indicatorBox = await getBoundingBox(indicator); expect(box.x + box.width - indicatorBox.x < 10).toBe(true); }, } ); const { text } = await getDatabaseHeaderColumn(page, 2); expect(text).toBe('column1'); }); test('should title column support quick renaming', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await initDatabaseDynamicRowWithData(page, 'a', true); await pressEscape(page); await focusDatabaseHeader(page, 1); const { textElement } = await getDatabaseHeaderColumn(page, 1); await textElement.click(); await waitNextFrame(page); await selectAllByKeyboard(page); await type(page, '123'); await pressEnter(page); expect(await textElement.innerText()).toBe('123'); await undoByClick(page); expect(await textElement.innerText()).toBe('Column 1'); await textElement.click(); await waitNextFrame(page); await selectAllByKeyboard(page); await type(page, '123'); await pressEnter(page); expect(await textElement.innerText()).toBe('123'); }); test('should title column support quick changing of column type', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await initDatabaseDynamicRowWithData(page, 'a', true); await pressEscape(page); await initDatabaseDynamicRowWithData(page, 'b'); await pressEscape(page); await focusDatabaseHeader(page, 1); const { typeIcon } = await getDatabaseHeaderColumn(page, 1); await typeIcon.click(); await waitNextFrame(page); await clickColumnType(page, 'Select'); const cell = getFirstColumnCell(page, 'select-selected'); expect(await cell.count()).toBe(1); }); test('database format-bar in header and text column', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, 'column', true); await pressArrowLeft(page); await pressEnter(page); await type(page, 'header'); // Title | Column1 // ---------------- // header | column const formatBar = getFormatBar(page); await setInlineRangeInInlineEditor(page, { index: 1, length: 4 }, 1); expect(await formatBar.formatBar.isVisible()).toBe(true); // Title | Column1 // ---------------- // h|eade|r | column await assertInlineEditorDeltas( page, [ { insert: 'header', }, ], 1 ); await formatBar.boldBtn.click(); await assertInlineEditorDeltas( page, [ { insert: 'h', }, { insert: 'eade', attributes: { bold: true, }, }, { insert: 'r', }, ], 1 ); await pressEscape(page); await pressArrowRight(page); await pressEnter(page); await setInlineRangeInInlineEditor(page, { index: 2, length: 2 }, 2); expect(await formatBar.formatBar.isVisible()).toBe(true); // Title | Column1 // ---------------- // header | co|lu|mn await assertInlineEditorDeltas( page, [ { insert: 'column', }, ], 2 ); await formatBar.boldBtn.click(); await assertInlineEditorDeltas( page, [ { insert: 'co', }, { insert: 'lu', attributes: { bold: true, }, }, { insert: 'mn', }, ], 2 ); }); test.describe('readonly mode', () => { test('database title should not be edited in readonly mode', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); const locator = page.locator('affine-database'); await expect(locator).toBeVisible(); const dbTitle = 'Database 1'; await assertBlockProps(page, '2', { title: dbTitle, }); await focusDatabaseTitle(page); await selectAllByKeyboard(page); await pressBackspace(page); await type(page, 'hello'); await assertBlockProps(page, '2', { title: 'hello', }); await switchReadonly(page); await type(page, ' world'); await assertBlockProps(page, '2', { title: 'hello', }); await pressBackspace(page, 'hello world'.length); await assertBlockProps(page, '2', { title: 'hello', }); }); test('should rich-text not be edited in readonly mode', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '', true); const cell = getFirstColumnCell(page, 'affine-database-rich-text'); await cell.click(); await type(page, '123'); await assertDatabaseCellRichTexts(page, { text: '123' }); await switchReadonly(page); await pressBackspace(page); await type(page, '789'); await assertDatabaseCellRichTexts(page, { text: '123' }); }); test('should hide edit widget after switch to readonly mode', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '', true); const database = page.locator('affine-database'); await expect(database).toBeVisible(); const databaseMenu = database.locator('.database-ops'); await expect(databaseMenu).toBeVisible(); const addViewButton = database.getByTestId('database-add-view-button'); await expect(addViewButton).toBeVisible(); const titleHeader = page.locator('affine-database-header-column').filter({ hasText: 'Title', }); await titleHeader.hover(); const columnDragBar = titleHeader.locator('.control-r'); await expect(columnDragBar).toBeVisible(); const filter = database.locator('data-view-header-tools-filter'); const search = database.locator('data-view-header-tools-search'); const options = database.locator('data-view-header-tools-view-options'); const headerAddRow = database.locator('data-view-header-tools-add-row'); await database.hover(); await expect(filter).toBeVisible(); await expect(search).toBeVisible(); await expect(options).toBeVisible(); await expect(headerAddRow).toBeVisible(); const row = database.locator('data-view-table-row'); const rowOptions = row.locator('.row-op'); const rowDragBar = row.locator('.data-view-table-view-drag-handler>div'); await row.hover(); await expect(rowOptions).toHaveCount(2); await expect(rowOptions.nth(0)).toBeVisible(); await expect(rowOptions.nth(1)).toBeVisible(); await expect(rowDragBar).toBeVisible(); const addRow = database.locator('.data-view-table-group-add-row'); await expect(addRow).toBeVisible(); // Readonly Mode { await switchReadonly(page); await expect(databaseMenu).toBeHidden(); await expect(addViewButton).toBeHidden(); await titleHeader.hover(); await expect(columnDragBar).toBeHidden(); await database.hover(); await expect(filter).toBeHidden(); await expect(search).toBeVisible(); // Note the search should not be hidden await expect(options).toBeHidden(); await expect(headerAddRow).toBeHidden(); await row.hover(); await expect(rowOptions.nth(0)).toBeHidden(); await expect(rowOptions.nth(1)).toBeHidden(); await expect(rowDragBar).toBeHidden(); await expect(addRow).toBeHidden(); } }); test('should hide focus border after switch to readonly mode', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '', true); const database = page.locator('affine-database'); await expect(database).toBeVisible(); const cell = getFirstColumnCell(page, 'affine-database-rich-text'); await cell.click(); const focusBorder = database.locator( 'data-view-table-selection .database-focus' ); await expect(focusBorder).toBeVisible(); await switchReadonly(page); await expect(focusBorder).toBeHidden(); }); test('should hide selection after switch to readonly mode', async ({ page, }) => { await enterPlaygroundRoom(page); await initEmptyDatabaseState(page); await initDatabaseColumn(page); await switchColumnType(page, 'Text'); await initDatabaseDynamicRowWithData(page, '', true); const database = page.locator('affine-database'); await expect(database).toBeVisible(); const startCell = getDatabaseBodyCell(page, { rowIndex: 0, columnIndex: 0, }); const endCell = getDatabaseBodyCell(page, { rowIndex: 0, columnIndex: 1, }); const startBox = await getBoundingBox(startCell); const endBox = await getBoundingBox(endCell); const startX = startBox.x + startBox.width / 2; const startY = startBox.y + startBox.height / 2; const endX = endBox.x + endBox.width / 2; const endY = endBox.y + endBox.height / 2; await dragBetweenCoords( page, { x: startX, y: startY }, { x: endX, y: endY } ); const selection = database.locator( 'data-view-table-selection .database-selection' ); await expect(selection).toBeVisible(); await switchReadonly(page); await expect(selection).toBeHidden(); }); });