snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
555
tests/blocksuite/e2e/database/actions.ts
Normal file
555
tests/blocksuite/e2e/database/actions.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import type { RichTextCell } from '@blocksuite/affine/blocks/database';
|
||||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
pressEnter,
|
||||
selectAllByKeyboard,
|
||||
type,
|
||||
} from '../utils/actions/keyboard.js';
|
||||
import {
|
||||
getBoundingBox,
|
||||
getEditorLocator,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/misc.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 });
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
export async function initDatabaseColumn(page: Page, title = '') {
|
||||
const editor = getEditorLocator(page);
|
||||
await editor.locator('affine-data-view-table-group').first().hover();
|
||||
const columnAddBtn = editor.locator('.header-add-column-button');
|
||||
await columnAddBtn.click();
|
||||
await waitNextFrame(page, 200);
|
||||
|
||||
if (title) {
|
||||
await selectAllByKeyboard(page);
|
||||
await type(page, title);
|
||||
await waitNextFrame(page);
|
||||
await pressEnter(page);
|
||||
} else {
|
||||
await pressEnter(page);
|
||||
}
|
||||
}
|
||||
|
||||
export const renameColumn = async (page: Page, name: string) => {
|
||||
const column = page.locator('affine-database-header-column', {
|
||||
hasText: name,
|
||||
});
|
||||
await column.click();
|
||||
};
|
||||
|
||||
export async function performColumnAction(
|
||||
page: Page,
|
||||
name: string,
|
||||
action: string
|
||||
) {
|
||||
await renameColumn(page, name);
|
||||
|
||||
const actionMenu = page.locator(`.affine-menu-button`, { hasText: action });
|
||||
await actionMenu.click();
|
||||
}
|
||||
|
||||
export async function switchColumnType(
|
||||
page: Page,
|
||||
columnType: string,
|
||||
columnIndex = 1
|
||||
) {
|
||||
const { typeIcon } = await getDatabaseHeaderColumn(page, columnIndex);
|
||||
await typeIcon.click();
|
||||
|
||||
await clickColumnType(page, columnType);
|
||||
}
|
||||
|
||||
export function clickColumnType(page: Page, columnType: string) {
|
||||
const typeMenu = page.locator(`.affine-menu-button`, {
|
||||
hasText: new RegExp(`${columnType}`),
|
||||
});
|
||||
return typeMenu.click();
|
||||
}
|
||||
|
||||
export function getDatabaseBodyRows(page: Page) {
|
||||
const rowContainer = page.locator('.affine-database-block-rows');
|
||||
return rowContainer.locator('data-view-table-row');
|
||||
}
|
||||
|
||||
export function getDatabaseBodyRow(page: Page, rowIndex = 0) {
|
||||
const rows = getDatabaseBodyRows(page);
|
||||
return rows.nth(rowIndex);
|
||||
}
|
||||
|
||||
export async function assertDatabaseTitleColumnText(
|
||||
page: Page,
|
||||
title: string,
|
||||
rowIndex = 0,
|
||||
columnIndex = 0
|
||||
) {
|
||||
const selectCell1 = getDatabaseCell(page, {
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: columnIndex,
|
||||
});
|
||||
const text = await selectCell1.innerText();
|
||||
|
||||
if (title === '') {
|
||||
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_FOR_EMPTY_LINE}])$`));
|
||||
} else {
|
||||
expect(text).toBe(title);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDatabaseCell(
|
||||
page: Page,
|
||||
{
|
||||
rowIndex,
|
||||
columnType,
|
||||
columnIndex,
|
||||
}: {
|
||||
rowIndex?: number;
|
||||
columnType?: string;
|
||||
columnIndex?: number;
|
||||
}
|
||||
) {
|
||||
const row = getDatabaseBodyRow(page, rowIndex);
|
||||
const index = columnIndex ?? 0;
|
||||
const columns = columnType
|
||||
? row.getByTestId(columnType)
|
||||
: row.locator('dv-table-view-cell-container');
|
||||
return columns.nth(index);
|
||||
}
|
||||
|
||||
export const getDatabaseColumnCells = (page: Page, columnIndex: number) => {
|
||||
return page.locator(
|
||||
`dv-table-view-cell-container[data-column-index="${columnIndex}"]`
|
||||
);
|
||||
};
|
||||
|
||||
export async function clickSelectOption(page: Page, index = 0) {
|
||||
await page.getByTestId('option-more').nth(index).click();
|
||||
}
|
||||
|
||||
export async function performSelectColumnTagAction(
|
||||
page: Page,
|
||||
name: string,
|
||||
index = 0
|
||||
) {
|
||||
await clickSelectOption(page, index);
|
||||
await page
|
||||
.locator('.affine-menu-button', { hasText: new RegExp(name) })
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function assertSelectedStyle(
|
||||
page: Page,
|
||||
key: keyof CSSStyleDeclaration,
|
||||
value: string
|
||||
) {
|
||||
const style = await getElementStyle(page, '.select-selected', key);
|
||||
expect(style).toBe(value);
|
||||
}
|
||||
|
||||
export async function clickDatabaseOutside(page: Page) {
|
||||
const docTitle = page.locator('.doc-title-container');
|
||||
await docTitle.click();
|
||||
}
|
||||
|
||||
export async function assertColumnWidth(locator: Locator, width: number) {
|
||||
const box = await getBoundingBox(locator);
|
||||
expect(box.width).toBe(width + 1);
|
||||
return box;
|
||||
}
|
||||
|
||||
export async function assertDatabaseCellRichTexts(
|
||||
page: Page,
|
||||
{
|
||||
rowIndex = 0,
|
||||
columnIndex = 1,
|
||||
text,
|
||||
}: {
|
||||
rowIndex?: number;
|
||||
columnIndex?: number;
|
||||
text: string;
|
||||
}
|
||||
) {
|
||||
const cellContainer = page.locator(
|
||||
`dv-table-view-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']`
|
||||
);
|
||||
|
||||
const cell = cellContainer.locator('affine-database-rich-text-cell');
|
||||
|
||||
const actualTexts = await cell.evaluate(ele => {
|
||||
return (ele as RichTextCell).inlineEditor$.value?.yTextString;
|
||||
});
|
||||
expect(actualTexts).toEqual(text);
|
||||
}
|
||||
|
||||
export async function assertDatabaseCellNumber(
|
||||
page: Page,
|
||||
{
|
||||
rowIndex = 0,
|
||||
columnIndex = 1,
|
||||
text,
|
||||
}: {
|
||||
rowIndex?: number;
|
||||
columnIndex?: number;
|
||||
text: string;
|
||||
}
|
||||
) {
|
||||
const actualText = await page
|
||||
.locator('.affine-database-block-rows')
|
||||
.locator('.database-row')
|
||||
.nth(rowIndex)
|
||||
.locator('.database-cell')
|
||||
.nth(columnIndex)
|
||||
.locator('.number')
|
||||
.textContent();
|
||||
expect(actualText?.trim()).toEqual(text);
|
||||
}
|
||||
|
||||
export async function assertDatabaseCellLink(
|
||||
page: Page,
|
||||
{
|
||||
rowIndex = 0,
|
||||
columnIndex = 1,
|
||||
text,
|
||||
}: {
|
||||
rowIndex?: number;
|
||||
columnIndex?: number;
|
||||
text: string;
|
||||
}
|
||||
) {
|
||||
const actualTexts = await page.evaluate(
|
||||
({ rowIndex, columnIndex }) => {
|
||||
const rows = document.querySelector('.affine-database-block-rows');
|
||||
const row = rows?.querySelector(
|
||||
`.database-row:nth-child(${rowIndex + 1})`
|
||||
);
|
||||
const cell = row?.querySelector(
|
||||
`.database-cell:nth-child(${columnIndex + 1})`
|
||||
);
|
||||
const richText = cell?.querySelector<RichTextCell>(
|
||||
'affine-database-link-cell'
|
||||
);
|
||||
if (!richText) throw new Error('Missing database rich text cell');
|
||||
return richText.inlineEditor$.value!.yText.toString();
|
||||
},
|
||||
{ rowIndex, columnIndex }
|
||||
);
|
||||
expect(actualTexts).toEqual(text);
|
||||
}
|
||||
|
||||
export async function assertDatabaseTitleText(page: Page, text: string) {
|
||||
const dbTitle = page.locator('[data-block-is-database-title="true"]');
|
||||
expect(await dbTitle.inputValue()).toEqual(text);
|
||||
}
|
||||
|
||||
export async function waitSearchTransitionEnd(page: Page) {
|
||||
await waitNextFrame(page, 400);
|
||||
}
|
||||
|
||||
export async function assertDatabaseSearching(
|
||||
page: Page,
|
||||
isSearching: boolean
|
||||
) {
|
||||
const searchExpand = page.locator('.search-container-expand');
|
||||
const count = await searchExpand.count();
|
||||
expect(count).toBe(isSearching ? 1 : 0);
|
||||
}
|
||||
|
||||
export async function focusDatabaseSearch(page: Page) {
|
||||
await (await getDatabaseMouse(page)).mouseOver();
|
||||
|
||||
const searchExpand = page.locator('.search-container-expand');
|
||||
const count = await searchExpand.count();
|
||||
if (count === 1) {
|
||||
const input = page.locator('.affine-database-search-input');
|
||||
await input.click();
|
||||
} else {
|
||||
const searchIcon = page.locator('.affine-database-search-input-icon');
|
||||
await searchIcon.click();
|
||||
await waitSearchTransitionEnd(page);
|
||||
}
|
||||
}
|
||||
|
||||
export async function blurDatabaseSearch(page: Page) {
|
||||
const dbTitle = page.locator('[data-block-is-database-title="true"]');
|
||||
await dbTitle.click();
|
||||
}
|
||||
|
||||
export async function focusDatabaseHeader(page: Page, columnIndex = 0) {
|
||||
const column = page.locator('.affine-database-column').nth(columnIndex);
|
||||
const box = await getBoundingBox(column);
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await waitNextFrame(page);
|
||||
return column;
|
||||
}
|
||||
|
||||
export async function getDatabaseMouse(page: Page) {
|
||||
const databaseRect = await page.getByTestId('dv-table-view').boundingBox();
|
||||
if (!databaseRect) throw new Error('Cannot find database rect');
|
||||
return {
|
||||
mouseOver: async () => {
|
||||
await page.mouse.move(databaseRect.x, databaseRect.y);
|
||||
},
|
||||
mouseLeave: async () => {
|
||||
await page.mouse.move(databaseRect.x - 1, databaseRect.y - 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDatabaseHeaderColumn(page: Page, index = 0) {
|
||||
const column = page.locator('.affine-database-column').nth(index);
|
||||
const box = await getBoundingBox(column);
|
||||
const textElement = column.locator('.affine-database-column-text-input');
|
||||
const text = await textElement.innerText();
|
||||
const typeIcon = column.locator('.affine-database-column-type-icon');
|
||||
|
||||
return {
|
||||
column,
|
||||
box,
|
||||
text,
|
||||
textElement,
|
||||
typeIcon,
|
||||
};
|
||||
}
|
||||
|
||||
export async function assertRowsSelection(
|
||||
page: Page,
|
||||
rowIndexes: [start: number, end: number]
|
||||
) {
|
||||
const rows = page.locator('data-view-table-row');
|
||||
const startIndex = rowIndexes[0];
|
||||
const endIndex = rowIndexes[1];
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const row = rows.nth(i);
|
||||
await row.locator('.row-select-checkbox .selected').isVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertCellsSelection(
|
||||
page: Page,
|
||||
cellIndexes: {
|
||||
start: [rowIndex: number, columnIndex: number];
|
||||
end?: [rowIndex: number, columnIndex: number];
|
||||
}
|
||||
) {
|
||||
const { start, end } = cellIndexes;
|
||||
|
||||
if (!end) {
|
||||
// single cell
|
||||
const focus = page.locator('.database-focus');
|
||||
const focusBox = await getBoundingBox(focus);
|
||||
|
||||
const [rowIndex, columnIndex] = start;
|
||||
const cell = getDatabaseCell(page, { rowIndex, columnIndex: columnIndex });
|
||||
const cellBox = await getBoundingBox(cell);
|
||||
expect(focusBox).toEqual({
|
||||
x: cellBox.x,
|
||||
y: cellBox.y - 1,
|
||||
height: cellBox.height + 2,
|
||||
width: cellBox.width + 1,
|
||||
});
|
||||
} else {
|
||||
// multi cells
|
||||
const selection = page.locator('.database-selection');
|
||||
const selectionBox = await getBoundingBox(selection);
|
||||
|
||||
const [startRowIndex, startColumnIndex] = start;
|
||||
const [endRowIndex, endColumnIndex] = end;
|
||||
|
||||
const rowIndexStart = Math.min(startRowIndex, endRowIndex);
|
||||
const rowIndexEnd = Math.max(startRowIndex, endRowIndex);
|
||||
const columnIndexStart = Math.min(startColumnIndex, endColumnIndex);
|
||||
const columnIndexEnd = Math.max(startColumnIndex, endColumnIndex);
|
||||
|
||||
let height = 0;
|
||||
let width = 0;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (let i = rowIndexStart; i <= rowIndexEnd; i++) {
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: i,
|
||||
columnIndex: columnIndexStart,
|
||||
});
|
||||
const box = await getBoundingBox(cell);
|
||||
height += box.height + 1;
|
||||
if (i === rowIndexStart) {
|
||||
y = box.y;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = columnIndexStart; j <= columnIndexEnd; j++) {
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: rowIndexStart,
|
||||
columnIndex: j,
|
||||
});
|
||||
const box = await getBoundingBox(cell);
|
||||
width += box.width;
|
||||
if (j === columnIndexStart) {
|
||||
x = box.x;
|
||||
}
|
||||
}
|
||||
|
||||
expect(selectionBox).toEqual({
|
||||
x,
|
||||
y,
|
||||
height,
|
||||
width: width + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getElementStyle(
|
||||
page: Page,
|
||||
selector: string,
|
||||
key: keyof CSSStyleDeclaration
|
||||
) {
|
||||
const style = await page.evaluate(
|
||||
({ key, selector }) => {
|
||||
const el = document.querySelector<HTMLElement>(selector);
|
||||
if (!el) throw new Error(`Missing ${selector} tag`);
|
||||
// @ts-ignore
|
||||
return el.style[key];
|
||||
},
|
||||
{
|
||||
key,
|
||||
selector,
|
||||
}
|
||||
);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
export async function focusKanbanCardHeader(page: Page, index = 0) {
|
||||
const cardHeader = page.locator('data-view-header-area-text').nth(index);
|
||||
await cardHeader.click();
|
||||
}
|
||||
|
||||
export async function clickKanbanCardHeader(page: Page, index = 0) {
|
||||
const cardHeader = page.locator('data-view-header-area-text').nth(index);
|
||||
await cardHeader.click();
|
||||
await pressEnter(page);
|
||||
}
|
||||
|
||||
export async function assertKanbanCardHeaderText(
|
||||
page: Page,
|
||||
text: string,
|
||||
index = 0
|
||||
) {
|
||||
const cardHeader = page.locator('data-view-header-area-text').nth(index);
|
||||
|
||||
await expect(cardHeader).toHaveText(text);
|
||||
}
|
||||
|
||||
export async function assertKanbanCellSelected(
|
||||
page: Page,
|
||||
{
|
||||
groupIndex,
|
||||
cardIndex,
|
||||
cellIndex,
|
||||
}: {
|
||||
groupIndex: number;
|
||||
cardIndex: number;
|
||||
cellIndex: number;
|
||||
}
|
||||
) {
|
||||
const border = await page.evaluate(
|
||||
({ groupIndex, cardIndex, cellIndex }) => {
|
||||
const group = document.querySelector(
|
||||
`affine-data-view-kanban-group:nth-child(${groupIndex + 1})`
|
||||
);
|
||||
const card = group?.querySelector(
|
||||
`affine-data-view-kanban-card:nth-child(${cardIndex + 1})`
|
||||
);
|
||||
const cells = Array.from(
|
||||
card?.querySelectorAll<HTMLElement>(`affine-data-view-kanban-cell`) ??
|
||||
[]
|
||||
);
|
||||
const cell = cells[cellIndex];
|
||||
if (!cell) throw new Error(`Missing cell tag`);
|
||||
return cell.style.border;
|
||||
},
|
||||
{
|
||||
groupIndex,
|
||||
cardIndex,
|
||||
cellIndex,
|
||||
}
|
||||
);
|
||||
|
||||
expect(border).toEqual('1px solid var(--affine-primary-color)');
|
||||
}
|
||||
|
||||
export async function assertKanbanCardSelected(
|
||||
page: Page,
|
||||
{
|
||||
groupIndex,
|
||||
cardIndex,
|
||||
}: {
|
||||
groupIndex: number;
|
||||
cardIndex: number;
|
||||
}
|
||||
) {
|
||||
const border = await page.evaluate(
|
||||
({ groupIndex, cardIndex }) => {
|
||||
const group = document.querySelector(
|
||||
`affine-data-view-kanban-group:nth-child(${groupIndex + 1})`
|
||||
);
|
||||
const card = group?.querySelector<HTMLElement>(
|
||||
`affine-data-view-kanban-card:nth-child(${cardIndex + 1})`
|
||||
);
|
||||
if (!card) throw new Error(`Missing card tag`);
|
||||
return card.style.border;
|
||||
},
|
||||
{
|
||||
groupIndex,
|
||||
cardIndex,
|
||||
}
|
||||
);
|
||||
|
||||
expect(border).toEqual('1px solid var(--affine-primary-color)');
|
||||
}
|
||||
|
||||
export function getKanbanCard(
|
||||
page: Page,
|
||||
{
|
||||
groupIndex,
|
||||
cardIndex,
|
||||
}: {
|
||||
groupIndex: number;
|
||||
cardIndex: number;
|
||||
}
|
||||
) {
|
||||
const group = page.locator('affine-data-view-kanban-group').nth(groupIndex);
|
||||
const card = group.locator('affine-data-view-kanban-card').nth(cardIndex);
|
||||
return card;
|
||||
}
|
||||
|
||||
export const moveToCenterOf = async (page: Page, locator: Locator) => {
|
||||
const box = (await locator.boundingBox())!;
|
||||
expect(box).toBeDefined();
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
};
|
||||
export const changeColumnType = async (
|
||||
page: Page,
|
||||
column: number,
|
||||
name: string
|
||||
) => {
|
||||
await waitNextFrame(page);
|
||||
await page.locator('affine-database-header-column').nth(column).click();
|
||||
await waitNextFrame(page, 200);
|
||||
await pressKey(page, 'Escape');
|
||||
await pressKey(page, 'ArrowDown');
|
||||
await pressKey(page, 'Enter');
|
||||
await type(page, name);
|
||||
await pressKey(page, 'ArrowDown');
|
||||
await pressKey(page, 'Enter');
|
||||
};
|
||||
export const pressKey = async (page: Page, key: string, count: number = 1) => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await waitNextFrame(page);
|
||||
await press(page, key);
|
||||
}
|
||||
await waitNextFrame(page);
|
||||
};
|
||||
487
tests/blocksuite/e2e/database/calendar.spec.ts
Normal file
487
tests/blocksuite/e2e/database/calendar.spec.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
import type { DatabaseBlockModel } from '@blocksuite/affine/model';
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
getEditorHostLocator,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/index.js';
|
||||
import { scoped, test } from '../utils/playwright.js';
|
||||
|
||||
const currentMonthAnchor = () => {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(1);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const timestampFromMonthAnchor = ({
|
||||
monthAnchor,
|
||||
day,
|
||||
}: {
|
||||
monthAnchor: number;
|
||||
day: number;
|
||||
}) => {
|
||||
const date = new Date(monthAnchor);
|
||||
date.setDate(day);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const getMonthTimestamp = (page: Page, monthAnchor: number, day: number) =>
|
||||
page.evaluate(timestampFromMonthAnchor, { monthAnchor, day });
|
||||
|
||||
const createCalendarDatabase = async (
|
||||
page: Page,
|
||||
options?: {
|
||||
withDateColumn?: boolean;
|
||||
mapDateColumn?: boolean;
|
||||
readonly?: boolean;
|
||||
rowCount?: number;
|
||||
linkedDocTitle?: string;
|
||||
withEndDateColumn?: boolean;
|
||||
}
|
||||
) => {
|
||||
const monthAnchor = await page.evaluate(currentMonthAnchor);
|
||||
const dateTimestamp = await getMonthTimestamp(page, monthAnchor, 15);
|
||||
const endDateTimestamp = await getMonthTimestamp(page, monthAnchor, 17);
|
||||
|
||||
return page.evaluate(
|
||||
({
|
||||
withDateColumn,
|
||||
mapDateColumn,
|
||||
readonly,
|
||||
rowCount,
|
||||
linkedDocTitle,
|
||||
withEndDateColumn,
|
||||
dateTimestamp,
|
||||
endDateTimestamp,
|
||||
monthAnchor,
|
||||
}) => {
|
||||
const { doc } = window;
|
||||
const rows = rowCount ?? 1;
|
||||
const linkedDocId = linkedDocTitle ? `doc:${linkedDocTitle}` : undefined;
|
||||
doc.captureSync();
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new window.$blocksuite.store.Text(),
|
||||
});
|
||||
const noteId = doc.addBlock('affine:note', {}, rootId);
|
||||
const databaseId = doc.addBlock(
|
||||
'affine:database',
|
||||
{
|
||||
title: new window.$blocksuite.store.Text('Calendar database'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
if (linkedDocId && linkedDocTitle) {
|
||||
const linkedDoc = window.collection.createDoc(linkedDocId).getStore();
|
||||
linkedDoc.load();
|
||||
const linkedRootId = linkedDoc.addBlock('affine:page', {
|
||||
title: new window.$blocksuite.store.Text(linkedDocTitle),
|
||||
});
|
||||
linkedDoc.addBlock('affine:surface', {}, linkedRootId);
|
||||
linkedDoc.addBlock('affine:note', {}, linkedRootId);
|
||||
}
|
||||
const rowIds = Array.from({ length: rows }, (_, index) => {
|
||||
const text =
|
||||
index === 0 && linkedDocId
|
||||
? new window.$blocksuite.store.Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: linkedDocId,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown as DeltaInsert[])
|
||||
: new window.$blocksuite.store.Text(`Task ${index + 1}`);
|
||||
return doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
type: 'text',
|
||||
text,
|
||||
},
|
||||
databaseId
|
||||
);
|
||||
});
|
||||
const rowId = rowIds[0];
|
||||
const model = doc.getModelById(databaseId) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(model);
|
||||
const dateColumnId = withDateColumn
|
||||
? datasource.propertyAdd('end', {
|
||||
type: 'date',
|
||||
name: 'Date',
|
||||
})
|
||||
: undefined;
|
||||
const endDateColumnId = withEndDateColumn
|
||||
? datasource.propertyAdd('end', {
|
||||
type: 'date',
|
||||
name: 'End Date',
|
||||
})
|
||||
: undefined;
|
||||
for (const id of rowIds) {
|
||||
if (dateColumnId) {
|
||||
datasource.cellValueChange(id, dateColumnId, dateTimestamp);
|
||||
}
|
||||
if (endDateColumnId) {
|
||||
datasource.cellValueChange(id, endDateColumnId, endDateTimestamp);
|
||||
}
|
||||
}
|
||||
const viewId = datasource.viewManager.viewAdd('calendar');
|
||||
datasource.viewManager.setCurrentView(viewId);
|
||||
if (dateColumnId && mapDateColumn) {
|
||||
const view = datasource.viewManager.viewGet(viewId) as
|
||||
| {
|
||||
setDateColumn: (propertyId: string) => void;
|
||||
setEndDateColumn: (propertyId: string) => void;
|
||||
}
|
||||
| undefined;
|
||||
view?.setDateColumn(dateColumnId);
|
||||
if (endDateColumnId) {
|
||||
view?.setEndDateColumn(endDateColumnId);
|
||||
}
|
||||
}
|
||||
if (readonly) {
|
||||
doc.readonly = true;
|
||||
}
|
||||
doc.captureSync();
|
||||
return {
|
||||
databaseId,
|
||||
rowId,
|
||||
rowIds,
|
||||
dateColumnId,
|
||||
endDateColumnId,
|
||||
viewId,
|
||||
linkedDocId,
|
||||
monthAnchor,
|
||||
};
|
||||
},
|
||||
{ ...options, dateTimestamp, endDateTimestamp, monthAnchor }
|
||||
);
|
||||
};
|
||||
|
||||
test(scoped`database calendar setup and row interactions`, async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: false,
|
||||
});
|
||||
|
||||
const editorHost = getEditorHostLocator(page);
|
||||
await expect(editorHost.getByTestId('dv-calendar-view')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Select or create date property' })
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Select or create date property' })
|
||||
.click();
|
||||
await page.getByText('Date', { exact: true }).click();
|
||||
await expect(
|
||||
page.locator('.calendar-entry').filter({ hasText: 'Task 1' })
|
||||
).toBeVisible();
|
||||
|
||||
await page.locator('.calendar-entry').filter({ hasText: 'Task 1' }).click();
|
||||
await expect(page.locator('affine-data-view-record-detail')).toBeVisible();
|
||||
|
||||
await page.evaluate(({ databaseId, dateColumnId }) => {
|
||||
if (!dateColumnId) {
|
||||
throw new Error('dateColumnId is required');
|
||||
}
|
||||
const model = window.doc.getModelById(databaseId) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(model);
|
||||
datasource.propertyDelete(dateColumnId);
|
||||
}, ids);
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Select or create date property' })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
scoped`database calendar creates date property from setup`,
|
||||
async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await createCalendarDatabase(page);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Select or create date property' })
|
||||
.click();
|
||||
await page.getByText('Create date property', { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Select or create date property' })
|
||||
).toBeHidden();
|
||||
}
|
||||
);
|
||||
|
||||
test(scoped`database calendar creates row from empty day`, async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
});
|
||||
|
||||
const targetDay = page
|
||||
.locator('.calendar-day')
|
||||
.filter({ has: page.locator('.calendar-day-number', { hasText: '20' }) })
|
||||
.first();
|
||||
await targetDay.hover();
|
||||
await targetDay.getByRole('button', { name: '+ New row' }).click();
|
||||
|
||||
await expect(page.locator('affine-data-view-record-detail')).toBeVisible();
|
||||
|
||||
const expectedDate = await getMonthTimestamp(page, ids.monthAnchor, 20);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
({ databaseId, dateColumnId, expectedDate }) => {
|
||||
if (!dateColumnId) {
|
||||
throw new Error('dateColumnId is required');
|
||||
}
|
||||
const model = window.doc.getModelById(
|
||||
databaseId
|
||||
) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(
|
||||
model
|
||||
);
|
||||
return datasource.rows$.value.some(
|
||||
rowId =>
|
||||
datasource.cellValueGet(rowId, dateColumnId) === expectedDate
|
||||
);
|
||||
},
|
||||
{ ...ids, expectedDate }
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test(
|
||||
scoped`database calendar opens linked doc row via row detail`,
|
||||
async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
linkedDocTitle: 'Calendar linked doc',
|
||||
});
|
||||
|
||||
await page.evaluate(({ databaseId }) => {
|
||||
const database = document.querySelector(
|
||||
`affine-database[data-block-id="${databaseId}"]`
|
||||
) as any;
|
||||
database.dataViewRootLogic.value.openDetailPanel = ({
|
||||
rowId,
|
||||
}: {
|
||||
rowId: string;
|
||||
}) => {
|
||||
(window as any).__calendarRowOpen = rowId;
|
||||
};
|
||||
}, ids);
|
||||
|
||||
const entry = page.locator('.calendar-entry.row').first();
|
||||
await expect(entry).toContainText('Calendar linked doc');
|
||||
await entry.click();
|
||||
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => (window as any).__calendarRowOpen))
|
||||
.toBe(ids.rowId);
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
scoped`database calendar hides setup create action in readonly`,
|
||||
async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await createCalendarDatabase(page, { readonly: true });
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Select or create date property' })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('Create date property', { exact: true })
|
||||
).toBeHidden();
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.doc.readonly = false;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test(scoped`database calendar shows all day entries`, async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
rowCount: 4,
|
||||
});
|
||||
|
||||
const may15 = page
|
||||
.locator('.calendar-day')
|
||||
.filter({ has: page.locator('.calendar-day-number', { hasText: '15' }) })
|
||||
.first();
|
||||
|
||||
await expect(may15.locator('.calendar-entry')).toHaveCount(4);
|
||||
await expect(may15.locator('.calendar-overflow')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test(
|
||||
scoped`database calendar drag updates date cell and readonly disables drag`,
|
||||
async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
});
|
||||
await waitNextFrame(page);
|
||||
|
||||
const entry = page.locator('.calendar-entry').filter({ hasText: 'Task 1' });
|
||||
const targetDay = page
|
||||
.locator('.calendar-day')
|
||||
.filter({ has: page.locator('.calendar-day-number', { hasText: '20' }) })
|
||||
.first();
|
||||
await entry.dragTo(targetDay);
|
||||
|
||||
const expectedDate = await getMonthTimestamp(page, ids.monthAnchor, 20);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(({ databaseId, rowId, dateColumnId }) => {
|
||||
if (!dateColumnId) {
|
||||
throw new Error('dateColumnId is required');
|
||||
}
|
||||
const model = window.doc.getModelById(
|
||||
databaseId
|
||||
) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(
|
||||
model
|
||||
);
|
||||
return datasource.cellValueGet(rowId, dateColumnId);
|
||||
}, ids)
|
||||
)
|
||||
.toBe(expectedDate);
|
||||
|
||||
const readonlyIds = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
readonly: true,
|
||||
});
|
||||
await waitNextFrame(page);
|
||||
const readonlyCalendar = page.getByTestId('dv-calendar-view').last();
|
||||
await expect(
|
||||
readonlyCalendar
|
||||
.locator('.calendar-entry')
|
||||
.filter({ hasText: 'Task 1' })
|
||||
.first()
|
||||
).toHaveAttribute('draggable', 'false');
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.doc.readonly = false;
|
||||
});
|
||||
expect(readonlyIds.databaseId).toBeTruthy();
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
scoped`database calendar drags row range preserving duration`,
|
||||
async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
withEndDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
});
|
||||
await waitNextFrame(page);
|
||||
|
||||
const entry = page
|
||||
.locator('.calendar-entry.row')
|
||||
.filter({ hasText: 'Task 1' })
|
||||
.first();
|
||||
const targetDay = page
|
||||
.locator('.calendar-day')
|
||||
.filter({ has: page.locator('.calendar-day-number', { hasText: '20' }) })
|
||||
.first();
|
||||
await entry.dragTo(targetDay);
|
||||
|
||||
const expectedStart = await getMonthTimestamp(page, ids.monthAnchor, 20);
|
||||
const expectedEnd = await getMonthTimestamp(page, ids.monthAnchor, 22);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
({ databaseId, rowId, dateColumnId, endDateColumnId }) => {
|
||||
if (!dateColumnId || !endDateColumnId) {
|
||||
throw new Error('date columns are required');
|
||||
}
|
||||
const model = window.doc.getModelById(
|
||||
databaseId
|
||||
) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(
|
||||
model
|
||||
);
|
||||
return [
|
||||
datasource.cellValueGet(rowId, dateColumnId),
|
||||
datasource.cellValueGet(rowId, endDateColumnId),
|
||||
];
|
||||
},
|
||||
ids
|
||||
)
|
||||
)
|
||||
.toEqual([expectedStart, expectedEnd]);
|
||||
}
|
||||
);
|
||||
|
||||
test(scoped`database calendar resizes row range end date`, async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
const ids = await createCalendarDatabase(page, {
|
||||
withDateColumn: true,
|
||||
withEndDateColumn: true,
|
||||
mapDateColumn: true,
|
||||
});
|
||||
await waitNextFrame(page);
|
||||
|
||||
const entry = page
|
||||
.locator('.calendar-entry.row')
|
||||
.filter({ hasText: 'Task 1' })
|
||||
.filter({ has: page.locator('.calendar-resize-handle.right') })
|
||||
.first();
|
||||
await entry.hover();
|
||||
const handle = entry.locator('.calendar-resize-handle.right');
|
||||
const targetDay = page
|
||||
.locator('.calendar-day')
|
||||
.filter({ has: page.locator('.calendar-day-number', { hasText: '20' }) })
|
||||
.first();
|
||||
const handleBox = await handle.boundingBox();
|
||||
const targetBox = await targetDay.boundingBox();
|
||||
if (!handleBox || !targetBox) {
|
||||
throw new Error('resize target is not visible');
|
||||
}
|
||||
|
||||
await page.mouse.move(
|
||||
handleBox.x + handleBox.width / 2,
|
||||
handleBox.y + handleBox.height / 2
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + 16);
|
||||
await page.mouse.up();
|
||||
|
||||
const expectedEnd = await getMonthTimestamp(page, ids.monthAnchor, 20);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(({ databaseId, rowId, endDateColumnId }) => {
|
||||
if (!endDateColumnId) {
|
||||
throw new Error('endDateColumnId is required');
|
||||
}
|
||||
const model = window.doc.getModelById(databaseId) as DatabaseBlockModel;
|
||||
const datasource =
|
||||
new window.$blocksuite.blocks.database.DatabaseBlockDataSource(model);
|
||||
return datasource.cellValueGet(rowId, endDateColumnId);
|
||||
}, ids)
|
||||
)
|
||||
.toBe(expectedEnd);
|
||||
});
|
||||
176
tests/blocksuite/e2e/database/clipboard.spec.ts
Normal file
176
tests/blocksuite/e2e/database/clipboard.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { dragBetweenCoords } from '../utils/actions/drag.js';
|
||||
import {
|
||||
copyByKeyboard,
|
||||
pasteByKeyboard,
|
||||
pressArrowDown,
|
||||
pressArrowUp,
|
||||
pressEnter,
|
||||
pressEscape,
|
||||
type,
|
||||
} from '../utils/actions/keyboard.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
focusRichText,
|
||||
getBoundingBox,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initDatabaseRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
initEmptyDatabaseWithParagraphState,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/misc.js';
|
||||
import { assertRichTexts } from '../utils/asserts.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import {
|
||||
assertDatabaseTitleColumnText,
|
||||
getDatabaseCell,
|
||||
getElementStyle,
|
||||
initDatabaseColumn,
|
||||
switchColumnType,
|
||||
} from './actions.js';
|
||||
|
||||
test.describe('copy&paste when editing', () => {
|
||||
test.skip('should support copy&paste of the title column', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseWithParagraphState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseRowWithData(page, 'abc123');
|
||||
await pressEscape(page);
|
||||
|
||||
await pressEnter(page);
|
||||
await page.keyboard.down('Shift');
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
}
|
||||
await page.keyboard.up('Shift');
|
||||
await copyByKeyboard(page);
|
||||
|
||||
const bgValue = await getElementStyle(page, '.database-focus', 'boxShadow');
|
||||
expect(bgValue).not.toBe('unset');
|
||||
|
||||
await focusRichText(page, 1);
|
||||
await pasteByKeyboard(page);
|
||||
await assertRichTexts(page, ['Database 1', 'c123']);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('copy&paste when selecting', () => {
|
||||
test.skip('should support copy&paste of a single cell', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseRowWithData(page, 'abc123');
|
||||
await initDatabaseRowWithData(page, '');
|
||||
await pressEscape(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await pressArrowUp(page);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
await pressArrowDown(page);
|
||||
await pasteByKeyboard(page, false);
|
||||
await waitNextFrame(page);
|
||||
await assertDatabaseTitleColumnText(page, 'abc123', 1);
|
||||
});
|
||||
|
||||
test.skip('should support copy&paste of multi cells', 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, '');
|
||||
await initDatabaseRowWithData(page, '');
|
||||
|
||||
const startCell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 0,
|
||||
});
|
||||
const startCellBox = await getBoundingBox(startCell);
|
||||
const endCell = getDatabaseCell(page, { rowIndex: 1, columnIndex: 1 });
|
||||
const endCellBox = await getBoundingBox(endCell);
|
||||
const startX = startCellBox.x + startCellBox.width / 2;
|
||||
const startY = startCellBox.y + startCellBox.height / 2;
|
||||
const endX = endCellBox.x + endCellBox.width / 2;
|
||||
const endY = endCellBox.y + endCellBox.height / 2;
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: startX, y: startY },
|
||||
{ x: endX, y: endY },
|
||||
{
|
||||
steps: 50,
|
||||
}
|
||||
);
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await pressArrowDown(page);
|
||||
await pressArrowDown(page);
|
||||
await waitNextFrame(page);
|
||||
await pasteByKeyboard(page, false);
|
||||
|
||||
await assertDatabaseTitleColumnText(page, 'text1', 2);
|
||||
await assertDatabaseTitleColumnText(page, 'text2', 3);
|
||||
const selectCell21 = getDatabaseCell(page, {
|
||||
rowIndex: 2,
|
||||
columnIndex: 1,
|
||||
});
|
||||
const selectCell31 = getDatabaseCell(page, {
|
||||
rowIndex: 3,
|
||||
columnIndex: 1,
|
||||
});
|
||||
expect(await selectCell21.innerText()).toBe('123');
|
||||
expect(await selectCell31.innerText()).toBe('a');
|
||||
});
|
||||
|
||||
test.skip('should support copy&paste of a single row', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseRowWithData(page, 'text1');
|
||||
await pressEscape(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await initDatabaseDynamicRowWithData(page, 'abc', false);
|
||||
await pressEscape(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await initDatabaseColumn(page);
|
||||
await switchColumnType(page, 'Number', 2);
|
||||
const numberCell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 2,
|
||||
});
|
||||
await numberCell.click();
|
||||
await waitNextFrame(page);
|
||||
await type(page, '123');
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await initDatabaseRowWithData(page, '');
|
||||
await pressEscape(page);
|
||||
await pasteByKeyboard(page, false);
|
||||
await waitNextFrame(page);
|
||||
|
||||
await assertDatabaseTitleColumnText(page, 'text1', 1);
|
||||
const selectCell = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
columnIndex: 1,
|
||||
});
|
||||
expect(await selectCell.innerText()).toBe('abc');
|
||||
const selectNumberCell = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
columnIndex: 2,
|
||||
});
|
||||
expect(await selectNumberCell.innerText()).toBe('123');
|
||||
});
|
||||
});
|
||||
638
tests/blocksuite/e2e/database/column.spec.ts
Normal file
638
tests/blocksuite/e2e/database/column.spec.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
assertDatabaseColumnOrder,
|
||||
dragBetweenCoords,
|
||||
enterPlaygroundRoom,
|
||||
getBoundingBox,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
pressArrowRight,
|
||||
pressArrowUp,
|
||||
pressArrowUpWithShiftKey,
|
||||
pressBackspace,
|
||||
pressEnter,
|
||||
pressEscape,
|
||||
redoByClick,
|
||||
selectAllByKeyboard,
|
||||
type,
|
||||
undoByClick,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/index.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import {
|
||||
assertDatabaseCellNumber,
|
||||
assertDatabaseCellRichTexts,
|
||||
assertSelectedStyle,
|
||||
changeColumnType,
|
||||
clickDatabaseOutside,
|
||||
clickSelectOption,
|
||||
getDatabaseCell,
|
||||
getDatabaseHeaderColumn,
|
||||
initDatabaseColumn,
|
||||
performColumnAction,
|
||||
performSelectColumnTagAction,
|
||||
switchColumnType,
|
||||
} from './actions.js';
|
||||
|
||||
test.describe('column operations', () => {
|
||||
test('should support rename column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, 'abc');
|
||||
|
||||
const { textElement } = await getDatabaseHeaderColumn(page, 1);
|
||||
expect(await textElement.innerText()).toBe('abc');
|
||||
await textElement.click();
|
||||
await waitNextFrame(page, 200);
|
||||
await pressArrowRight(page);
|
||||
await type(page, '123');
|
||||
await pressEnter(page);
|
||||
expect(await textElement.innerText()).toBe('abc123');
|
||||
|
||||
await undoByClick(page);
|
||||
expect(await textElement.innerText()).toBe('abc');
|
||||
await redoByClick(page);
|
||||
expect(await textElement.innerText()).toBe('abc123');
|
||||
});
|
||||
|
||||
test('should support add new column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await pressEscape(page);
|
||||
const { text: title1 } = await getDatabaseHeaderColumn(page, 1);
|
||||
expect(title1).toBe('Column');
|
||||
|
||||
const selected = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'multi-select',
|
||||
});
|
||||
expect(await selected.innerText()).toBe('123');
|
||||
|
||||
await initDatabaseColumn(page, 'abc');
|
||||
const { text: title2 } = await getDatabaseHeaderColumn(page, 2);
|
||||
expect(title2).toBe('abc');
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
const { text: title3 } = await getDatabaseHeaderColumn(page, 3);
|
||||
expect(title3).toBe('Column 2');
|
||||
});
|
||||
|
||||
test('should support right insert column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
|
||||
await performColumnAction(page, '1', 'Insert right');
|
||||
await selectAllByKeyboard(page);
|
||||
await type(page, '2');
|
||||
await pressEnter(page);
|
||||
const columns = page.locator('.affine-database-column');
|
||||
expect(await columns.count()).toBe(3);
|
||||
|
||||
await assertDatabaseColumnOrder(page, ['1', '2']);
|
||||
});
|
||||
|
||||
test('should support left insert column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
|
||||
await performColumnAction(page, '1', 'Insert left');
|
||||
await selectAllByKeyboard(page);
|
||||
await type(page, '2');
|
||||
await pressEnter(page);
|
||||
const columns = page.locator('.affine-database-column');
|
||||
expect(await columns.count()).toBe(3);
|
||||
|
||||
await assertDatabaseColumnOrder(page, ['2', '1']);
|
||||
});
|
||||
|
||||
test('should support delete column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
|
||||
const columns = page.locator('.affine-database-column');
|
||||
expect(await columns.count()).toBe(2);
|
||||
|
||||
await performColumnAction(page, '1', 'Delete');
|
||||
expect(await columns.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('should support duplicate column', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await pressEscape(page);
|
||||
await performColumnAction(page, '1', 'duplicate');
|
||||
await pressEscape(page);
|
||||
const cells = page.locator('affine-database-multi-select-cell');
|
||||
expect(await cells.count()).toBe(2);
|
||||
|
||||
const secondCell = cells.nth(1);
|
||||
const selected = secondCell.locator('.select-selected');
|
||||
expect(await selected.innerText()).toBe('123');
|
||||
});
|
||||
|
||||
test('should support move column right', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await pressEscape(page);
|
||||
await initDatabaseColumn(page, '2');
|
||||
await initDatabaseDynamicRowWithData(page, 'abc', false, 1);
|
||||
await pressEscape(page);
|
||||
await assertDatabaseColumnOrder(page, ['1', '2']);
|
||||
await waitNextFrame(page, 350);
|
||||
await performColumnAction(page, '1', 'Move right');
|
||||
await assertDatabaseColumnOrder(page, ['2', '1']);
|
||||
|
||||
await undoByClick(page);
|
||||
const { column } = await getDatabaseHeaderColumn(page, 2);
|
||||
await column.click();
|
||||
const moveLeft = page.locator('.action', { hasText: 'Move right' });
|
||||
expect(await moveLeft.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('should support move column left', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page, '1');
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await pressEscape(page);
|
||||
await initDatabaseColumn(page, '2');
|
||||
await initDatabaseDynamicRowWithData(page, 'abc', false, 1);
|
||||
await pressEscape(page);
|
||||
await assertDatabaseColumnOrder(page, ['1', '2']);
|
||||
|
||||
const { column } = await getDatabaseHeaderColumn(page, 0);
|
||||
await column.click();
|
||||
const moveLeft = page.locator('.action', { hasText: 'Move left' });
|
||||
expect(await moveLeft.count()).toBe(0);
|
||||
await waitNextFrame(page, 200);
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
|
||||
await performColumnAction(page, '2', 'Move left');
|
||||
await assertDatabaseColumnOrder(page, ['2', '1']);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('switch column type', () => {
|
||||
test('switch to number', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123abc', true);
|
||||
await pressEscape(page);
|
||||
await changeColumnType(page, 1, 'Number');
|
||||
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'number',
|
||||
});
|
||||
await assertDatabaseCellNumber(page, {
|
||||
text: '',
|
||||
});
|
||||
await pressEnter(page);
|
||||
await type(page, '123abc');
|
||||
await pressEscape(page);
|
||||
expect((await cell.textContent())?.trim()).toBe('123');
|
||||
});
|
||||
|
||||
test('switch to rich-text', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123abc', true);
|
||||
await pressEscape(page);
|
||||
await switchColumnType(page, 'Text');
|
||||
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'rich-text',
|
||||
});
|
||||
await cell.isVisible();
|
||||
await pressEnter(page);
|
||||
await type(page, '123');
|
||||
await pressEscape(page);
|
||||
await pressEnter(page);
|
||||
await type(page, 'abc');
|
||||
await pressEscape(page);
|
||||
await assertDatabaseCellRichTexts(page, { text: '123abc123abc' });
|
||||
});
|
||||
|
||||
test('switch between multi-select and select', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await type(page, 'abc');
|
||||
await pressEnter(page);
|
||||
await pressEscape(page);
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 1,
|
||||
});
|
||||
const tags = cell.getByTestId('tag-selected');
|
||||
expect(await tags.count()).toBe(2);
|
||||
|
||||
await switchColumnType(page, 'Select', 1);
|
||||
expect(await tags.count()).toBe(1);
|
||||
expect(await tags.innerText()).toBe('123');
|
||||
|
||||
await pressEnter(page);
|
||||
await type(page, 'def');
|
||||
await pressEnter(page);
|
||||
expect(await tags.innerText()).toBe('def');
|
||||
|
||||
await switchColumnType(page, 'Multi-select');
|
||||
await pressEnter(page);
|
||||
await type(page, '666');
|
||||
await pressEnter(page);
|
||||
await pressEscape(page);
|
||||
expect(await tags.count()).toBe(2);
|
||||
expect(await tags.nth(0).innerText()).toBe('def');
|
||||
expect(await tags.nth(1).innerText()).toBe('666');
|
||||
|
||||
await switchColumnType(page, 'Select');
|
||||
expect(await tags.count()).toBe(1);
|
||||
expect(await tags.innerText()).toBe('def');
|
||||
|
||||
await pressEnter(page);
|
||||
await type(page, '888');
|
||||
await pressEnter(page);
|
||||
expect(await tags.innerText()).toBe('888');
|
||||
});
|
||||
|
||||
test('switch between number and rich-text', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await switchColumnType(page, 'Number');
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '123abc', true);
|
||||
await assertDatabaseCellNumber(page, {
|
||||
text: '123',
|
||||
});
|
||||
|
||||
await switchColumnType(page, 'Text');
|
||||
await pressEnter(page);
|
||||
await type(page, 'abc');
|
||||
await pressEscape(page);
|
||||
await assertDatabaseCellRichTexts(page, { text: '123abc' });
|
||||
|
||||
await switchColumnType(page, 'Number');
|
||||
await assertDatabaseCellNumber(page, {
|
||||
text: '123',
|
||||
});
|
||||
});
|
||||
|
||||
test('switch number to select', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await switchColumnType(page, 'Number');
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'number',
|
||||
});
|
||||
expect((await cell.textContent())?.trim()).toBe('123');
|
||||
|
||||
await switchColumnType(page, 'Select');
|
||||
await initDatabaseDynamicRowWithData(page, 'abc');
|
||||
const selectCell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'select',
|
||||
});
|
||||
expect(await selectCell.innerText()).toBe('abc');
|
||||
|
||||
await switchColumnType(page, 'Number');
|
||||
await assertDatabaseCellNumber(page, {
|
||||
text: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('switch to checkbox', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
await changeColumnType(page, 1, 'Checkbox');
|
||||
|
||||
const checkbox = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'checkbox',
|
||||
}).locator('.checkbox');
|
||||
await expect(checkbox).not.toHaveClass('checked');
|
||||
|
||||
await waitNextFrame(page, 500);
|
||||
await checkbox.click();
|
||||
await expect(checkbox).toHaveClass(/checked/);
|
||||
|
||||
await undoByClick(page);
|
||||
await expect(checkbox).not.toHaveClass('checked');
|
||||
});
|
||||
|
||||
test('checkbox to text', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
await changeColumnType(page, 1, 'Checkbox');
|
||||
|
||||
let checkbox = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'checkbox',
|
||||
});
|
||||
await expect(checkbox).not.toHaveClass('checked');
|
||||
|
||||
// checked
|
||||
await checkbox.click();
|
||||
await changeColumnType(page, 1, 'Text');
|
||||
await clickDatabaseOutside(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await assertDatabaseCellRichTexts(page, { text: 'Yes' });
|
||||
await clickDatabaseOutside(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await changeColumnType(page, 1, 'Checkbox');
|
||||
checkbox = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'checkbox',
|
||||
}).locator('.checkbox');
|
||||
await expect(checkbox).toHaveClass(/checked/);
|
||||
|
||||
// not checked
|
||||
await checkbox.click();
|
||||
await changeColumnType(page, 1, 'Text');
|
||||
await clickDatabaseOutside(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await assertDatabaseCellRichTexts(page, { text: 'No' });
|
||||
await clickDatabaseOutside(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await changeColumnType(page, 1, 'Checkbox');
|
||||
checkbox = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'checkbox',
|
||||
});
|
||||
await expect(checkbox).not.toHaveClass('checked');
|
||||
});
|
||||
|
||||
test('switch to progress', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
await switchColumnType(page, 'Progress');
|
||||
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'progress',
|
||||
});
|
||||
const progress = cell.getByTestId('progress');
|
||||
expect((await progress.textContent())?.trim()).toBe('0');
|
||||
await waitNextFrame(page, 500);
|
||||
const progressBg = cell.getByTestId('progress-background');
|
||||
const {
|
||||
x: progressBgX,
|
||||
y: progressBgY,
|
||||
width: progressBgWidth,
|
||||
} = await getBoundingBox(progressBg);
|
||||
await page.mouse.move(progressBgX, progressBgY);
|
||||
await page.mouse.click(progressBgX, progressBgY);
|
||||
const dragHandle = cell.getByTestId('progress-drag-handle');
|
||||
const {
|
||||
x: dragX,
|
||||
y: dragY,
|
||||
width,
|
||||
height,
|
||||
} = await getBoundingBox(dragHandle);
|
||||
const dragCenterX = dragX + width / 2;
|
||||
const dragCenterY = dragY + height / 2;
|
||||
await page.mouse.move(dragCenterX, dragCenterY);
|
||||
|
||||
const endX = dragCenterX + progressBgWidth;
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: dragCenterX, y: dragCenterY },
|
||||
{ x: endX, y: dragCenterY }
|
||||
);
|
||||
expect(await progress.textContent()).toBe('100');
|
||||
await pressEscape(page);
|
||||
await undoByClick(page);
|
||||
expect(await progress.textContent()).toBe('0');
|
||||
});
|
||||
|
||||
test('switch to link', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await switchColumnType(page, 'Link');
|
||||
|
||||
const linkText = 'http://example.com';
|
||||
const cell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'link',
|
||||
});
|
||||
await pressEnter(page);
|
||||
await type(page, linkText);
|
||||
await pressEscape(page);
|
||||
const link = cell.getByTestId('property-link-a');
|
||||
await expect(link).toHaveAttribute('href', linkText);
|
||||
expect(await link.textContent()).toBe(linkText);
|
||||
|
||||
// not link text
|
||||
await cell.hover();
|
||||
const linkEdit = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'link',
|
||||
}).getByTestId('edit-link-button');
|
||||
await linkEdit.click();
|
||||
await waitNextFrame(page);
|
||||
await selectAllByKeyboard(page);
|
||||
await type(page, 'abc');
|
||||
await pressEnter(page);
|
||||
await expect(link).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select column tag action', () => {
|
||||
test('should support select tag renaming', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await type(page, 'abc');
|
||||
await pressEnter(page);
|
||||
await clickSelectOption(page);
|
||||
await waitNextFrame(page);
|
||||
await pressArrowRight(page);
|
||||
await type(page, '4567abc00');
|
||||
await pressEnter(page);
|
||||
const options = page.getByTestId('tag-option-list').getByTestId('tag-name');
|
||||
expect(await options.nth(0).innerText()).toBe('abc4567abc00');
|
||||
expect(await options.nth(1).innerText()).toBe('123');
|
||||
});
|
||||
|
||||
test('should select tag renaming support shortcut key', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await clickSelectOption(page);
|
||||
await waitNextFrame(page);
|
||||
await pressArrowRight(page);
|
||||
await type(page, '456');
|
||||
// esc
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
const options = page.getByTestId('tag-option-list').getByTestId('tag-name');
|
||||
const option1 = options.nth(0);
|
||||
expect(await option1.innerText()).toBe('123456');
|
||||
});
|
||||
|
||||
test('should support select tag deletion', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await performSelectColumnTagAction(page, 'Delete');
|
||||
const options = page.locator('.select-option-name');
|
||||
expect(await options.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('should support modifying select tag color', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await performSelectColumnTagAction(page, 'Red');
|
||||
await pressEscape(page);
|
||||
await assertSelectedStyle(
|
||||
page,
|
||||
'backgroundColor',
|
||||
'var(--affine-v2-chip-label-red)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('drag-to-fill', () => {
|
||||
test('should show when cell in focus and hide on blur', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
|
||||
await pressEscape(page);
|
||||
|
||||
const dragToFillHandle = page.locator('.drag-to-fill');
|
||||
|
||||
await expect(dragToFillHandle).toBeVisible();
|
||||
|
||||
await pressEscape(page);
|
||||
|
||||
await expect(dragToFillHandle).toBeHidden();
|
||||
});
|
||||
|
||||
test('should not show in multi (row or column) selection', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
|
||||
const dragToFillHandle = page.locator('.drag-to-fill');
|
||||
|
||||
await expect(dragToFillHandle).toBeVisible();
|
||||
|
||||
await pressArrowUpWithShiftKey(page);
|
||||
|
||||
await expect(dragToFillHandle).toBeHidden();
|
||||
await pressArrowUp(page);
|
||||
|
||||
await expect(dragToFillHandle).toBeVisible();
|
||||
});
|
||||
|
||||
test('should fill columns with data', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, 'thing', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressBackspace(page);
|
||||
await type(page, 'aaa');
|
||||
await pressEnter(page);
|
||||
await pressEnter(page);
|
||||
|
||||
await pressEscape(page);
|
||||
await pressArrowUp(page);
|
||||
|
||||
const cells = page.locator('affine-database-multi-select-cell');
|
||||
|
||||
expect(await cells.nth(0).innerText()).toBe('thing');
|
||||
expect(await cells.nth(1).innerText()).toBe('aaa');
|
||||
|
||||
const dragToFillHandle = page.locator('.drag-to-fill');
|
||||
|
||||
await expect(dragToFillHandle).toBeVisible();
|
||||
|
||||
const bbox = await getBoundingBox(dragToFillHandle);
|
||||
|
||||
if (!bbox) throw new Error('Expected a bounding box');
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 },
|
||||
{ x: bbox.x, y: bbox.y + 200 }
|
||||
);
|
||||
|
||||
expect(await cells.nth(0).innerText()).toBe('thing');
|
||||
expect(await cells.nth(1).innerText()).toBe('thing');
|
||||
});
|
||||
});
|
||||
689
tests/blocksuite/e2e/database/database.spec.ts
Normal file
689
tests/blocksuite/e2e/database/database.spec.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
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,
|
||||
getDatabaseCell,
|
||||
getDatabaseHeaderColumn,
|
||||
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 undoByKeyboard(page);
|
||||
await assertBlockProps(page, '2', {
|
||||
title: '',
|
||||
});
|
||||
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');
|
||||
});
|
||||
|
||||
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 = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: '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 = getDatabaseCell(page, {
|
||||
// rowIndex: 0,
|
||||
// columnType: 'rich-text',
|
||||
// });
|
||||
// await cell.click();
|
||||
await pressEnter(page);
|
||||
await pressArrowLeft(page);
|
||||
await pressEnter(page);
|
||||
await assertDatabaseCellRichTexts(page, { text: '123' });
|
||||
|
||||
await pressEnter(page);
|
||||
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 tags = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'multi-select',
|
||||
}).getByTestId('tag-selected');
|
||||
expect(await tags.count()).toBe(2);
|
||||
expect(await tags.nth(0).innerText()).toBe('1');
|
||||
expect(await tags.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');
|
||||
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 = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: 'select',
|
||||
});
|
||||
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);
|
||||
|
||||
await pressEnter(page);
|
||||
await type(page, '123');
|
||||
await waitNextFrame(page, 100);
|
||||
await pressEnter(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await assertDatabaseCellRichTexts(page, { text: '123' });
|
||||
|
||||
await switchReadonly(page);
|
||||
await waitNextFrame(page);
|
||||
await pressEnter(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.getByTestId('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 = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnType: '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 = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 0,
|
||||
});
|
||||
const endCell = getDatabaseCell(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();
|
||||
});
|
||||
});
|
||||
564
tests/blocksuite/e2e/database/selection.spec.ts
Normal file
564
tests/blocksuite/e2e/database/selection.spec.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { dragBetweenCoords } from '../utils/actions/drag.js';
|
||||
import { shiftClick } from '../utils/actions/edgeless.js';
|
||||
import {
|
||||
pressArrowDown,
|
||||
pressArrowDownWithShiftKey,
|
||||
pressArrowLeft,
|
||||
pressArrowRight,
|
||||
pressArrowUp,
|
||||
pressArrowUpWithShiftKey,
|
||||
pressBackspace,
|
||||
pressEnter,
|
||||
pressEscape,
|
||||
type,
|
||||
} from '../utils/actions/keyboard.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
getBoundingBox,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initDatabaseRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
initKanbanViewState,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/misc.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import {
|
||||
assertCellsSelection,
|
||||
assertDatabaseTitleColumnText,
|
||||
assertKanbanCardHeaderText,
|
||||
assertKanbanCardSelected,
|
||||
assertKanbanCellSelected,
|
||||
assertRowsSelection,
|
||||
clickKanbanCardHeader,
|
||||
focusKanbanCardHeader,
|
||||
getDatabaseCell,
|
||||
getKanbanCard,
|
||||
initDatabaseColumn,
|
||||
switchColumnType,
|
||||
} from './actions.js';
|
||||
|
||||
test.describe('focus', () => {
|
||||
test('should support move focus by arrow key', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await pressEscape(page);
|
||||
await waitNextFrame(page, 100);
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
|
||||
test('should support multi row selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
await switchColumnType(page, 'Number');
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
|
||||
const selectColumn = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
columnIndex: 1,
|
||||
});
|
||||
|
||||
const endBox = await getBoundingBox(selectColumn);
|
||||
const endX = endBox.x + endBox.width / 2;
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: endX, y: endBox.y },
|
||||
{ x: endX, y: endBox.y + endBox.height }
|
||||
);
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 1]);
|
||||
});
|
||||
|
||||
test('should support row selection with dynamic height', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await type(page, '456456');
|
||||
await pressEnter(page);
|
||||
await type(page, 'abcabc');
|
||||
await pressEnter(page);
|
||||
await type(page, 'defdef');
|
||||
await pressEnter(page);
|
||||
await pressEscape(page);
|
||||
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('row-level selection', () => {
|
||||
test('should support title selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseRowWithData(page, 'title');
|
||||
await waitNextFrame(page, 100);
|
||||
await assertCellsSelection(page, {
|
||||
start: [0, 0],
|
||||
});
|
||||
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
|
||||
test('should support pressing esc to trigger row selection', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await waitNextFrame(page, 100);
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
|
||||
test('should support multi row selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await switchColumnType(page, 'Number');
|
||||
await initDatabaseDynamicRowWithData(page, 'asd', true);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
|
||||
const selectColumn = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
columnIndex: 1,
|
||||
});
|
||||
|
||||
const endBox = await getBoundingBox(selectColumn);
|
||||
const endX = endBox.x + endBox.width / 2;
|
||||
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{ x: endX, y: endBox.y },
|
||||
{ x: endX, y: endBox.y + endBox.height }
|
||||
);
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 1]);
|
||||
});
|
||||
|
||||
test('should support row selection with dynamic height', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await type(page, '456456');
|
||||
await pressEnter(page);
|
||||
await type(page, 'abcabc');
|
||||
await pressEnter(page);
|
||||
await type(page, 'defdef');
|
||||
await pressEnter(page);
|
||||
await pressEscape(page);
|
||||
|
||||
await pressEscape(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
|
||||
test('move row selection with (up | down)', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
|
||||
// add two rows
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await pressEscape(page); // switch to row selection
|
||||
|
||||
await assertRowsSelection(page, [1, 1]);
|
||||
|
||||
await pressArrowUp(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
|
||||
// should not allow under selection
|
||||
await pressArrowUp(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
|
||||
await pressArrowDown(page);
|
||||
await assertRowsSelection(page, [1, 1]);
|
||||
|
||||
// should not allow over selection
|
||||
await pressArrowDown(page);
|
||||
await assertRowsSelection(page, [1, 1]);
|
||||
});
|
||||
|
||||
test('increment decrement row selection with shift+(up | down)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
|
||||
// add two rows
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await initDatabaseDynamicRowWithData(page, '123123', true);
|
||||
await pressEscape(page);
|
||||
|
||||
await pressEscape(page); // switch to row selection
|
||||
|
||||
await pressArrowUpWithShiftKey(page);
|
||||
await assertRowsSelection(page, [0, 1]);
|
||||
|
||||
await pressArrowDownWithShiftKey(page);
|
||||
await assertRowsSelection(page, [1, 1]); // should decrement back
|
||||
|
||||
await pressArrowUp(page); // go to first row
|
||||
|
||||
await pressArrowDownWithShiftKey(page);
|
||||
await assertRowsSelection(page, [0, 1]);
|
||||
|
||||
await pressArrowUpWithShiftKey(page);
|
||||
await assertRowsSelection(page, [0, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('cell-level selection', () => {
|
||||
test('should support multi cell selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseDynamicRowWithData(page, '', true);
|
||||
await pressEscape(page);
|
||||
await switchColumnType(page, 'Number');
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
|
||||
const startCell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 0,
|
||||
});
|
||||
const endCell = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
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 }
|
||||
);
|
||||
|
||||
await assertCellsSelection(page, {
|
||||
start: [0, 0],
|
||||
end: [1, 1],
|
||||
});
|
||||
});
|
||||
|
||||
test("should support backspace key to delete cell's content", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
await initDatabaseColumn(page);
|
||||
await initDatabaseRowWithData(page, 'row1');
|
||||
await initDatabaseDynamicRowWithData(page, 'abc', false);
|
||||
await pressEscape(page);
|
||||
await initDatabaseRowWithData(page, 'row2');
|
||||
await initDatabaseDynamicRowWithData(page, '123', false);
|
||||
await pressEscape(page);
|
||||
|
||||
const startCell = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 0,
|
||||
});
|
||||
const endCell = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
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 }
|
||||
);
|
||||
|
||||
await pressBackspace(page);
|
||||
await assertDatabaseTitleColumnText(page, '', 0);
|
||||
await assertDatabaseTitleColumnText(page, '', 1);
|
||||
const selectCell1 = getDatabaseCell(page, {
|
||||
rowIndex: 0,
|
||||
columnIndex: 1,
|
||||
});
|
||||
expect(await selectCell1.innerText()).toBe('');
|
||||
const selectCell2 = getDatabaseCell(page, {
|
||||
rowIndex: 1,
|
||||
columnIndex: 1,
|
||||
});
|
||||
expect(await selectCell2.innerText()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('kanban view selection', () => {
|
||||
test("should support move cell's focus by arrow key(up&down) within a card", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1'],
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
value: [true],
|
||||
},
|
||||
{
|
||||
type: 'rich-text',
|
||||
value: ['text'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
cellIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowDown(page, 3);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
cellIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowUp(page);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
cellIndex: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("should support move cell's focus by arrow key(up&down) within multi cards", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1', 'row2'],
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
// Both rows are checked so they stay in the same group.
|
||||
value: [true, true],
|
||||
},
|
||||
{
|
||||
type: 'rich-text',
|
||||
value: ['text'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
await pressArrowUp(page);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 1,
|
||||
cellIndex: 2,
|
||||
});
|
||||
|
||||
await pressArrowDown(page);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
cellIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("should support move cell's focus by arrow key(left&right)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1', 'row2'],
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
value: [true, false],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
|
||||
await pressArrowRight(page, 2);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
cellIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowLeft(page);
|
||||
await assertKanbanCellSelected(page, {
|
||||
groupIndex: 1,
|
||||
cardIndex: 0,
|
||||
cellIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("should support move card's focus by arrow key(up&down)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1', 'row2', 'row3'],
|
||||
columns: [
|
||||
{
|
||||
type: 'number',
|
||||
value: [undefined, undefined, undefined],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowDown(page, 3);
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowUp(page);
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("should support move card's focus by arrow key(left&right)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1', 'row2'],
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
value: [true, false],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
|
||||
await pressArrowRight(page, 2);
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
});
|
||||
|
||||
await pressArrowLeft(page);
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 1,
|
||||
cardIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('should support multi card selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1', 'row2'],
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
value: [true, false],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await focusKanbanCardHeader(page);
|
||||
await pressEscape(page);
|
||||
await pressEscape(page);
|
||||
|
||||
const card = getKanbanCard(page, {
|
||||
groupIndex: 1,
|
||||
cardIndex: 0,
|
||||
});
|
||||
const box = await getBoundingBox(card);
|
||||
await shiftClick(page, {
|
||||
x: box.x + box.width / 2,
|
||||
y: box.y + box.height / 2,
|
||||
});
|
||||
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 0,
|
||||
cardIndex: 0,
|
||||
});
|
||||
await assertKanbanCardSelected(page, {
|
||||
groupIndex: 1,
|
||||
cardIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("should support move cursor in card's title by arrow key(left&right)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initKanbanViewState(page, {
|
||||
rows: ['row1'],
|
||||
columns: [
|
||||
{
|
||||
type: 'rich-text',
|
||||
value: ['text'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await clickKanbanCardHeader(page);
|
||||
await type(page, 'abc');
|
||||
await pressArrowLeft(page, 2);
|
||||
await pressArrowRight(page);
|
||||
await pressBackspace(page);
|
||||
await pressEscape(page);
|
||||
|
||||
await assertKanbanCardHeaderText(page, 'row1ac');
|
||||
});
|
||||
});
|
||||
112
tests/blocksuite/e2e/database/sort.spec.ts
Normal file
112
tests/blocksuite/e2e/database/sort.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { expect, type Locator } from '@playwright/test';
|
||||
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/index.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import { initDatabaseColumn, switchColumnType } from './actions.js';
|
||||
|
||||
test('database sort with multiple rules', async ({ page }) => {
|
||||
// Initialize database
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
|
||||
// Add test columns: Name (text) and Age (number)
|
||||
await initDatabaseColumn(page, 'Name');
|
||||
await switchColumnType(page, 'Text', 1);
|
||||
await initDatabaseColumn(page, 'Age');
|
||||
await switchColumnType(page, 'Number', 2);
|
||||
|
||||
// Add test data
|
||||
const testData = [
|
||||
{ name: 'Alice', age: '25' },
|
||||
{ name: 'Bob', age: '30' },
|
||||
{ name: 'Alice', age: '20' },
|
||||
{ name: 'Charlie', age: '25' },
|
||||
];
|
||||
|
||||
for (const data of testData) {
|
||||
await initDatabaseDynamicRowWithData(page, data.name, true, 0);
|
||||
await initDatabaseDynamicRowWithData(page, data.age, false, 1);
|
||||
}
|
||||
|
||||
// Open sort menu
|
||||
const sortButton = page.locator('data-view-header-tools-sort');
|
||||
await sortButton.click();
|
||||
|
||||
// Add first sort rule: Name ascending
|
||||
await page.locator('affine-menu').getByText('Name').click();
|
||||
await waitNextFrame(page);
|
||||
|
||||
// Add second sort rule: Age ascending
|
||||
await page.getByText('Add sort').click();
|
||||
await page.locator('affine-menu').getByText('Age').click();
|
||||
await waitNextFrame(page);
|
||||
|
||||
// Get all rows after sorting
|
||||
const rows = await page.locator('affine-database-row').all();
|
||||
const getCellText = async (row: Locator, index: number) => {
|
||||
const cell = row.locator('.cell').nth(index);
|
||||
return cell.innerText();
|
||||
};
|
||||
|
||||
// Verify sorting results
|
||||
// Should be sorted by Name first, then by Age
|
||||
const expectedOrder = [
|
||||
{ name: 'Alice', age: '20' },
|
||||
{ name: 'Alice', age: '25' },
|
||||
{ name: 'Bob', age: '30' },
|
||||
{ name: 'Charlie', age: '25' },
|
||||
];
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const name = await getCellText(rows[i], 1);
|
||||
const age = await getCellText(rows[i], 2);
|
||||
expect(name).toBe(expectedOrder[i].name);
|
||||
expect(age).toBe(expectedOrder[i].age);
|
||||
}
|
||||
|
||||
// Change sort order of Name to descending
|
||||
await page.locator('.sort-item').first().getByText('Ascending').click();
|
||||
await page.getByText('Descending').click();
|
||||
await waitNextFrame(page);
|
||||
|
||||
// Verify new sorting results
|
||||
const expectedOrderDesc = [
|
||||
{ name: 'Charlie', age: '25' },
|
||||
{ name: 'Bob', age: '30' },
|
||||
{ name: 'Alice', age: '20' },
|
||||
{ name: 'Alice', age: '25' },
|
||||
];
|
||||
|
||||
const rowsAfterDesc = await page.locator('affine-database-row').all();
|
||||
for (let i = 0; i < rowsAfterDesc.length; i++) {
|
||||
const name = await getCellText(rowsAfterDesc[i], 1);
|
||||
const age = await getCellText(rowsAfterDesc[i], 2);
|
||||
expect(name).toBe(expectedOrderDesc[i].name);
|
||||
expect(age).toBe(expectedOrderDesc[i].age);
|
||||
}
|
||||
|
||||
// Remove first sort rule
|
||||
await page.locator('.sort-item').first().getByRole('img').last().click();
|
||||
await waitNextFrame(page);
|
||||
|
||||
// Verify sorting now only by Age
|
||||
const expectedOrderAgeOnly = [
|
||||
{ name: 'Alice', age: '20' },
|
||||
{ name: 'Alice', age: '25' },
|
||||
{ name: 'Charlie', age: '25' },
|
||||
{ name: 'Bob', age: '30' },
|
||||
];
|
||||
|
||||
const rowsAfterRemove = await page.locator('affine-database-row').all();
|
||||
for (let i = 0; i < rowsAfterRemove.length; i++) {
|
||||
const name = await getCellText(rowsAfterRemove[i], 1);
|
||||
const age = await getCellText(rowsAfterRemove[i], 2);
|
||||
expect(name).toBe(expectedOrderAgeOnly[i].name);
|
||||
expect(age).toBe(expectedOrderAgeOnly[i].age);
|
||||
}
|
||||
});
|
||||
107
tests/blocksuite/e2e/database/statistics.spec.ts
Normal file
107
tests/blocksuite/e2e/database/statistics.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import { type } from '../utils/actions/index.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
getAddRow,
|
||||
initEmptyDatabaseState,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/misc.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import {
|
||||
changeColumnType,
|
||||
moveToCenterOf,
|
||||
press,
|
||||
pressKey,
|
||||
} from './actions.js';
|
||||
|
||||
const addRow = async (page: Page, count: number = 1) => {
|
||||
await waitNextFrame(page);
|
||||
const addRow = getAddRow(page);
|
||||
for (let i = 0; i < count; i++) {
|
||||
await addRow.click();
|
||||
}
|
||||
await press(page, 'Escape');
|
||||
await waitNextFrame(page);
|
||||
};
|
||||
const insertRightColumn = async (page: Page, index = 0) => {
|
||||
await waitNextFrame(page);
|
||||
await page.locator('affine-database-header-column').nth(index).click();
|
||||
await waitNextFrame(page, 200);
|
||||
await pressKey(page, 'Escape');
|
||||
const menu = page.locator('.affine-menu-button', {
|
||||
hasText: new RegExp('Insert Right'),
|
||||
});
|
||||
await menu.click();
|
||||
await waitNextFrame(page, 200);
|
||||
await pressKey(page, 'Enter');
|
||||
};
|
||||
const menuSelect = async (page: Page, selectors: string[]) => {
|
||||
await waitNextFrame(page);
|
||||
for (const name of selectors) {
|
||||
const menu = page.locator('.affine-menu-button', {
|
||||
hasText: new RegExp(name),
|
||||
});
|
||||
await menu.click();
|
||||
}
|
||||
};
|
||||
test.describe('title', () => {
|
||||
test('empty count', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
await addRow(page, 3);
|
||||
const statCell = page.locator('affine-database-column-stats-cell').nth(0);
|
||||
await moveToCenterOf(page, statCell);
|
||||
await statCell.click();
|
||||
await menuSelect(page, ['Count', 'Count Empty']);
|
||||
const value = statCell.locator('.value');
|
||||
expect((await value.textContent())?.trim()).toBe('3');
|
||||
await page.locator('dv-table-view-cell-container').nth(0).click();
|
||||
await pressKey(page, 'Enter');
|
||||
await type(page, 'asd');
|
||||
await pressKey(page, 'Escape');
|
||||
expect((await value.textContent())?.trim()).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('rich-text', () => {
|
||||
test('empty count', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
await addRow(page, 3);
|
||||
await insertRightColumn(page);
|
||||
await changeColumnType(page, 1, 'text');
|
||||
const statCell = page.locator('affine-database-column-stats-cell').nth(1);
|
||||
await moveToCenterOf(page, statCell);
|
||||
await statCell.click();
|
||||
await menuSelect(page, ['Count', 'Count Empty']);
|
||||
const value = statCell.locator('.value');
|
||||
expect((await value.textContent())?.trim()).toBe('3');
|
||||
await page.locator('dv-table-view-cell-container').nth(1).click();
|
||||
await pressKey(page, 'Enter');
|
||||
await type(page, 'asd');
|
||||
await pressKey(page, 'Escape');
|
||||
expect((await value.textContent())?.trim()).toBe('2');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select', () => {
|
||||
test('empty count', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
await addRow(page, 3);
|
||||
await insertRightColumn(page);
|
||||
await changeColumnType(page, 1, 'select');
|
||||
const statCell = page.locator('affine-database-column-stats-cell').nth(1);
|
||||
await moveToCenterOf(page, statCell);
|
||||
await statCell.click();
|
||||
await menuSelect(page, ['Count', 'Count Empty']);
|
||||
const value = statCell.locator('.value');
|
||||
expect((await value.textContent())?.trim()).toBe('3');
|
||||
await page.locator('dv-table-view-cell-container').nth(1).click();
|
||||
await pressKey(page, 'Enter');
|
||||
await type(page, 'select');
|
||||
await pressKey(page, 'Enter');
|
||||
expect((await value.textContent())?.trim()).toBe('2');
|
||||
});
|
||||
});
|
||||
19
tests/blocksuite/e2e/database/title.spec.ts
Normal file
19
tests/blocksuite/e2e/database/title.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
initDatabaseDynamicRowWithData,
|
||||
initEmptyDatabaseState,
|
||||
} from '../utils/actions/misc.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
import { press } from './actions.js';
|
||||
|
||||
test.describe('title', () => {
|
||||
test('should able to link doc by press @', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyDatabaseState(page);
|
||||
await initDatabaseDynamicRowWithData(page, '123', true);
|
||||
await press(page, '@');
|
||||
await expect(page.locator('.linked-doc-popover')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user