593 lines
15 KiB
TypeScript
593 lines
15 KiB
TypeScript
import type {
|
|
RichTextCell,
|
|
RichTextCellEditing,
|
|
} from '@blocksuite/affine/blocks/database';
|
|
import { ZERO_WIDTH_SPACE } from '@blocksuite/affine/inline';
|
|
import { expect, type Locator, type Page } from '@playwright/test';
|
|
|
|
import {
|
|
pressEnter,
|
|
selectAllByKeyboard,
|
|
type,
|
|
} from '../utils/actions/keyboard.js';
|
|
import {
|
|
getBoundingBox,
|
|
getBoundingClientRect,
|
|
getEditorLocator,
|
|
waitNextFrame,
|
|
} from '../utils/actions/misc.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('.database-row');
|
|
}
|
|
|
|
export function getDatabaseBodyRow(page: Page, rowIndex = 0) {
|
|
const rows = getDatabaseBodyRows(page);
|
|
return rows.nth(rowIndex);
|
|
}
|
|
|
|
export function getDatabaseTableContainer(page: Page) {
|
|
const container = page.locator('.affine-database-table-container');
|
|
return container;
|
|
}
|
|
|
|
export async function assertDatabaseTitleColumnText(
|
|
page: Page,
|
|
title: string,
|
|
index = 0
|
|
) {
|
|
const text = await page.evaluate(index => {
|
|
const rowContainer = document.querySelector('.affine-database-block-rows');
|
|
const row = rowContainer?.querySelector(
|
|
`.database-row:nth-child(${index + 1})`
|
|
);
|
|
const titleColumnCell = row?.querySelector('.database-cell:nth-child(1)');
|
|
const titleSpan = titleColumnCell?.querySelector(
|
|
'.data-view-header-area-rich-text'
|
|
) as HTMLElement;
|
|
if (!titleSpan) throw new Error('Cannot find database title column editor');
|
|
return titleSpan.innerText;
|
|
}, index);
|
|
|
|
if (title === '') {
|
|
expect(text).toMatch(new RegExp(`^(|[${ZERO_WIDTH_SPACE}])$`));
|
|
} else {
|
|
expect(text).toBe(title);
|
|
}
|
|
}
|
|
|
|
export function getDatabaseBodyCell(
|
|
page: Page,
|
|
{
|
|
rowIndex,
|
|
columnIndex,
|
|
}: {
|
|
rowIndex: number;
|
|
columnIndex: number;
|
|
}
|
|
) {
|
|
const row = getDatabaseBodyRow(page, rowIndex);
|
|
const cell = row.locator('.database-cell').nth(columnIndex);
|
|
return cell;
|
|
}
|
|
|
|
export function getDatabaseBodyCellContent(
|
|
page: Page,
|
|
{
|
|
rowIndex,
|
|
columnIndex,
|
|
cellClass,
|
|
}: {
|
|
rowIndex: number;
|
|
columnIndex: number;
|
|
cellClass: string;
|
|
}
|
|
) {
|
|
const cell = getDatabaseBodyCell(page, { rowIndex, columnIndex });
|
|
const cellContent = cell.locator(`.${cellClass}`);
|
|
return cellContent;
|
|
}
|
|
|
|
export function getFirstColumnCell(page: Page, cellClass: string) {
|
|
const cellContent = getDatabaseBodyCellContent(page, {
|
|
rowIndex: 0,
|
|
columnIndex: 1,
|
|
cellClass,
|
|
});
|
|
return cellContent;
|
|
}
|
|
|
|
export async function clickSelectOption(page: Page, index = 0) {
|
|
await page.locator('.select-option-icon').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(
|
|
`affine-database-cell-container[data-row-index='${rowIndex}'][data-column-index='${columnIndex}']`
|
|
);
|
|
|
|
const cellEditing = cellContainer.locator(
|
|
'affine-database-rich-text-cell-editing'
|
|
);
|
|
const cell = cellContainer.locator('affine-database-rich-text-cell');
|
|
|
|
const richText = (await cellEditing.count()) === 0 ? cell : cellEditing;
|
|
const actualTexts = await richText.evaluate(ele => {
|
|
return (ele as RichTextCellEditing).inlineEditor?.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') ??
|
|
cell?.querySelector<RichTextCellEditing>(
|
|
'affine-database-link-cell-editing'
|
|
);
|
|
if (!richText) throw new Error('Missing database rich text cell');
|
|
return richText.inlineEditor!.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 getBoundingClientRect(
|
|
page,
|
|
'.affine-database-table'
|
|
);
|
|
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 = getDatabaseBodyCell(page, { rowIndex, 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 = getDatabaseBodyCell(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 = getDatabaseBodyCell(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 cardHeader.click();
|
|
}
|
|
|
|
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);
|
|
};
|