mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
835 lines
23 KiB
835 lines
23 KiB
1 year ago
|
import { expect, Locator } from '@playwright/test';
|
||
|
import { DocsOpenedPagePage } from '.';
|
||
|
import BasePage from '../../../Base';
|
||
|
|
||
|
export class TiptapPage extends BasePage {
|
||
|
readonly openedPage: DocsOpenedPagePage;
|
||
|
|
||
|
constructor(openedPage: DocsOpenedPagePage) {
|
||
|
super(openedPage.rootPage);
|
||
|
this.openedPage = openedPage;
|
||
|
}
|
||
|
|
||
|
get() {
|
||
|
return this.openedPage.get().getByTestId('docs-page-content').locator('.ProseMirror');
|
||
|
}
|
||
|
|
||
|
private async _click(locator: Locator) {
|
||
|
await (await locator.elementHandle()).waitForElementState('stable');
|
||
|
|
||
|
const box = await (await locator.elementHandle()).boundingBox();
|
||
|
return this.rootPage.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||
|
}
|
||
|
|
||
|
private async _hover(locator: Locator) {
|
||
|
// await (await locator.elementHandle()).waitForElementState('stable');
|
||
|
|
||
|
const box = await (await locator.elementHandle()).boundingBox();
|
||
|
await this.rootPage.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||
|
}
|
||
|
|
||
|
async selectNodes({
|
||
|
start,
|
||
|
startType,
|
||
|
end,
|
||
|
endType,
|
||
|
}: {
|
||
|
start: number;
|
||
|
startType?: string;
|
||
|
end: number;
|
||
|
endType?: string;
|
||
|
}) {
|
||
|
const startLocator = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${start + 1})`)
|
||
|
.locator(`${startType ? tiptapNodeToDomType[startType] : 'p'}`);
|
||
|
|
||
|
const endLocator = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${end + 1})`)
|
||
|
.locator(`${endType ? tiptapNodeToDomType[endType] : 'p'}`);
|
||
|
|
||
|
const startBox = await (await startLocator.elementHandle()).boundingBox();
|
||
|
const endBox = await (await endLocator.elementHandle()).boundingBox();
|
||
|
|
||
|
await this.rootPage.mouse.move(startBox.x, startBox.y);
|
||
|
await this.rootPage.mouse.down();
|
||
|
await this.rootPage.waitForTimeout(400);
|
||
|
await this.rootPage.mouse.move(endBox.x + endBox.width, endBox.y + endBox.height);
|
||
|
}
|
||
|
|
||
|
async openCommandMenu({ index }: { index?: number } = {}) {
|
||
|
let paragraph;
|
||
|
if (index !== undefined) {
|
||
|
paragraph = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator('p:nth-child(1)');
|
||
|
} else {
|
||
|
paragraph = this.get().locator('.draggable-block-wrapper:last-child').locator('p:nth-child(1)');
|
||
|
}
|
||
|
await this._click(paragraph);
|
||
|
|
||
|
await this.rootPage.waitForTimeout(400);
|
||
|
|
||
|
await this.rootPage.keyboard.press('/');
|
||
|
|
||
|
await this.rootPage.locator('.nc-docs-command-list').waitFor({ state: 'visible' });
|
||
|
}
|
||
|
|
||
|
async clickTextFormatButton(type: TextFormatType) {
|
||
|
await this.rootPage.getByTestId(`nc-docs-editor-${type}-button`).click();
|
||
|
}
|
||
|
|
||
|
async verifyTextFormatButtonActive({ type, active }: { type: TextFormatType; active: boolean }) {
|
||
|
await expect(this.rootPage.getByTestId(`nc-docs-editor-${type}-button`)).toHaveAttribute(
|
||
|
'aria-active',
|
||
|
active.toString()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
async addNewNode({
|
||
|
type,
|
||
|
index,
|
||
|
link,
|
||
|
filePath,
|
||
|
noVerify,
|
||
|
}: {
|
||
|
type: TipTapNodes;
|
||
|
index?: number;
|
||
|
link?: string;
|
||
|
filePath?: string;
|
||
|
noVerify?: boolean;
|
||
|
}) {
|
||
|
await this.openCommandMenu({ index });
|
||
|
if (type === 'Image') {
|
||
|
await this.attachFile({
|
||
|
filePath: [filePath],
|
||
|
filePickUIAction: this.rootPage.getByTestId(`nc-docs-command-list-item-${type}`).click(),
|
||
|
});
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
await this.rootPage.getByTestId(`nc-docs-command-list-item-${type}`).click();
|
||
|
|
||
|
if (type === 'Embed iframe') {
|
||
|
await this.rootPage.getByTestId('nc-docs-command-list-link-input').type(link);
|
||
|
await this.rootPage.getByTestId('nc-docs-command-list-link-input').press('Enter');
|
||
|
}
|
||
|
|
||
|
if (!noVerify) await this.rootPage.locator('.nc-docs-command-list').waitFor({ state: 'hidden' });
|
||
|
}
|
||
|
|
||
|
async verifyErrorCommandMenu({ error }: { error: string }) {
|
||
|
await expect(this.rootPage.getByTestId('nc-docs-command-list-link-input-error')).toHaveText(error);
|
||
|
}
|
||
|
|
||
|
async verifyHeaderNode({ index, type, content }: { index: number; type: TipTapNodes; content?: string }) {
|
||
|
const level = type.split(' ')[1];
|
||
|
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`)
|
||
|
.locator(`h${level}`)
|
||
|
).toBeVisible();
|
||
|
|
||
|
if (content) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`)
|
||
|
.locator(`h${level}`)
|
||
|
).toHaveText(content);
|
||
|
} else {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`)
|
||
|
.locator(`h${level}`)
|
||
|
).toHaveAttribute('data-placeholder', `Heading ${level}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async clickBackButtonLinkCommandMenu() {
|
||
|
await this._click(this.rootPage.getByTestId('nc-docs-command-list-link-back-btn'));
|
||
|
}
|
||
|
|
||
|
async verifyCommandMenuOpened({ isVisible }: { isVisible: boolean }) {
|
||
|
if (isVisible) {
|
||
|
await this.rootPage.locator('.nc-docs-command-list').waitFor({ state: 'visible' });
|
||
|
} else {
|
||
|
await this.rootPage.locator('.nc-docs-command-list').waitFor({ state: 'hidden' });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async dragToNode({
|
||
|
fromIndex,
|
||
|
toIndex,
|
||
|
withoutHandle,
|
||
|
}: {
|
||
|
fromIndex: number;
|
||
|
toIndex: number;
|
||
|
withoutHandle?: boolean;
|
||
|
}) {
|
||
|
await this.openedPage.waitForRender();
|
||
|
|
||
|
const fromLocator = this.get().locator(`.draggable-block-wrapper:nth-child(${fromIndex + 1})`);
|
||
|
const toLocator = this.get().locator(`.draggable-block-wrapper:nth-child(${toIndex + 1})`);
|
||
|
|
||
|
await this._hover(fromLocator);
|
||
|
let dragHandle = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${fromIndex + 1})`)
|
||
|
.locator('div[data-drag-handle="true"][tiptap-draghandle="true"]');
|
||
|
if (withoutHandle) {
|
||
|
dragHandle = fromLocator;
|
||
|
}
|
||
|
|
||
|
await dragHandle.dragTo(toLocator);
|
||
|
await this.rootPage.waitForTimeout(200);
|
||
|
}
|
||
|
|
||
|
async fillContent({
|
||
|
content,
|
||
|
index = 0,
|
||
|
waitForNetwork = true,
|
||
|
type = 'Paragraph',
|
||
|
}: {
|
||
|
content: string;
|
||
|
index?: number;
|
||
|
waitForNetwork?: boolean;
|
||
|
type?: TipTapNodes;
|
||
|
}) {
|
||
|
await this.openedPage.waitForRender();
|
||
|
await this.rootPage.waitForTimeout(1000);
|
||
|
|
||
|
let contentDomType;
|
||
|
switch (type) {
|
||
|
case 'Paragraph':
|
||
|
contentDomType = 'p';
|
||
|
break;
|
||
|
case 'Heading 1':
|
||
|
contentDomType = 'h1';
|
||
|
break;
|
||
|
case 'Heading 2':
|
||
|
contentDomType = 'h2';
|
||
|
break;
|
||
|
case 'Heading 3':
|
||
|
contentDomType = 'h3';
|
||
|
break;
|
||
|
default:
|
||
|
contentDomType = 'p';
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
const waitNetwork = waitForNetwork
|
||
|
? this.rootPage.waitForResponse(async response => {
|
||
|
return response.url().includes('api/v1/docs/page') && response.request().method() === 'PUT';
|
||
|
})
|
||
|
: Promise.resolve();
|
||
|
|
||
|
await this.get().click({
|
||
|
force: true,
|
||
|
});
|
||
|
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`${type ? tiptapNodeToDomType[type] : 'p'}`)
|
||
|
.click({
|
||
|
force: true,
|
||
|
});
|
||
|
|
||
|
for (const char of content) {
|
||
|
await this.rootPage.keyboard.type(char);
|
||
|
}
|
||
|
|
||
|
await waitNetwork;
|
||
|
}
|
||
|
|
||
|
async scrollToNode({ index }: { index: number }) {
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.scrollIntoViewIfNeeded();
|
||
|
}
|
||
|
|
||
|
async clickLastNode({ start }) {
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:last-child`)
|
||
|
.locator('.node-view-drag-content')
|
||
|
.click({
|
||
|
force: true,
|
||
|
position: start
|
||
|
? {
|
||
|
x: 0,
|
||
|
y: 0,
|
||
|
}
|
||
|
: undefined,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async clickNode({ index, start }: { index: number; start: boolean }) {
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator('.node-view-drag-content')
|
||
|
.click({
|
||
|
force: true,
|
||
|
position: start
|
||
|
? {
|
||
|
x: 0,
|
||
|
y: 0,
|
||
|
}
|
||
|
: undefined,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async toggleTaskNode({ index }: { index: number }) {
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator('input[type="checkbox"]')
|
||
|
.click({
|
||
|
force: true,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private async verifyHistory({
|
||
|
node,
|
||
|
history,
|
||
|
nodeType,
|
||
|
content,
|
||
|
}: {
|
||
|
node: Locator;
|
||
|
nodeType: TipTapNodes;
|
||
|
content: string;
|
||
|
history: {
|
||
|
added?: boolean;
|
||
|
removed?: boolean;
|
||
|
};
|
||
|
}) {
|
||
|
const diffType = history.added ? 'ins' : 'del';
|
||
|
|
||
|
let found = false;
|
||
|
// In case of text diff, section node diff attribute is not set
|
||
|
for (let i = 0; i < 3; i++) {
|
||
|
if ((await node.getAttribute('data-diff-node')) === diffType) {
|
||
|
found = true;
|
||
|
break;
|
||
|
}
|
||
|
await this.rootPage.waitForTimeout(200);
|
||
|
}
|
||
|
|
||
|
// As text diff is shown in a different way, we need to check if it's visible
|
||
|
if (!found) {
|
||
|
const textDiffHtmlTag = diffType;
|
||
|
if (nodeType === 'Paragraph') {
|
||
|
await expect(node.locator(textDiffHtmlTag)).toBeVisible();
|
||
|
await expect(node.locator(textDiffHtmlTag)).toHaveText(content);
|
||
|
} else {
|
||
|
await expect(node.locator(`[data-diff-node="${textDiffHtmlTag}"]`)).toBeVisible();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
await expect(node).toHaveAttribute('data-diff-node', diffType);
|
||
|
}
|
||
|
|
||
|
async verifyNode({
|
||
|
index,
|
||
|
type,
|
||
|
content,
|
||
|
childParagraphCount,
|
||
|
childParagraph,
|
||
|
isUploading,
|
||
|
placeholder,
|
||
|
history,
|
||
|
}: {
|
||
|
index: number;
|
||
|
type?: TipTapNodes;
|
||
|
content?: string;
|
||
|
childParagraphCount?: number;
|
||
|
childParagraph?: { index: number; content: string };
|
||
|
isUploading?: boolean;
|
||
|
placeholder?: string;
|
||
|
history?: {
|
||
|
added?: boolean;
|
||
|
removed?: boolean;
|
||
|
} | null;
|
||
|
}) {
|
||
|
type = type || 'Paragraph';
|
||
|
const node = this.get().locator(`.draggable-block-wrapper:nth-child(${index + 1})`);
|
||
|
|
||
|
if (history) {
|
||
|
await this.verifyHistory({ node, history, nodeType: type, content });
|
||
|
}
|
||
|
|
||
|
if (type === 'Embed iframe') {
|
||
|
await expect(node.locator('.external-content-wrapper').locator('iframe')).toHaveAttribute('src', content);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (isUploading) {
|
||
|
await expect(
|
||
|
node.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`).locator(tiptapNodeToDomType[type])
|
||
|
).toHaveAttribute('isuploading', 'true');
|
||
|
await expect(
|
||
|
node.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`).locator('.image-uploading-wrapper')
|
||
|
).toContainText('Uploading...');
|
||
|
}
|
||
|
|
||
|
await expect(
|
||
|
node.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`).locator(tiptapNodeToDomType[type])
|
||
|
).toBeVisible();
|
||
|
|
||
|
if (content) {
|
||
|
await expect(node).toContainText(content);
|
||
|
}
|
||
|
|
||
|
if (type) {
|
||
|
await expect(node.locator('.node-view-drag-content')).toHaveAttribute(
|
||
|
'data-testid',
|
||
|
`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (childParagraphCount) {
|
||
|
await expect(node.locator('.node-view-drag-content').locator('p')).toHaveCount(childParagraphCount);
|
||
|
}
|
||
|
|
||
|
if (childParagraph) {
|
||
|
await expect(
|
||
|
node.locator('.node-view-drag-content').locator(`p:nth-child(${childParagraph.index + 1})`)
|
||
|
).toHaveText(childParagraph.content);
|
||
|
}
|
||
|
|
||
|
if (placeholder) {
|
||
|
await expect(
|
||
|
node.getByTestId(`nc-docs-tiptap-wrapper-${tiptapNodeLabels[type]}`).locator(tiptapNodeToDomType[type])
|
||
|
).toHaveAttribute('data-placeholder', placeholder);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async verifyContent({ content }: { content: string }) {
|
||
|
await expect(this.get()).toHaveText(content);
|
||
|
}
|
||
|
|
||
|
async verifyNodeSelected({ index }: { index: number }) {
|
||
|
await expect(this.get().locator(`.draggable-block-wrapper:nth-child(${index + 1})`)).toHaveClass(/focused/);
|
||
|
}
|
||
|
|
||
|
async verifyListNode({
|
||
|
index,
|
||
|
level,
|
||
|
type,
|
||
|
content,
|
||
|
nestedIndex,
|
||
|
checked,
|
||
|
}: {
|
||
|
index: number;
|
||
|
level: number;
|
||
|
type: TipTapNodes;
|
||
|
content: string;
|
||
|
nestedIndex?: number;
|
||
|
checked?: boolean;
|
||
|
}) {
|
||
|
await this.verifyNode({ index, type, content });
|
||
|
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`[data-type="${tiptapNodeLabels[type]}"]`)
|
||
|
).toHaveCSS('padding-left', `${level * 16}px`);
|
||
|
|
||
|
if (nestedIndex && type === 'Numbered List') {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`.tiptap-list-item-start > span`)
|
||
|
).toHaveAttribute('data-number', `${nestedIndex + 1}`);
|
||
|
}
|
||
|
|
||
|
if (checked !== undefined && type === 'Task List') {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`input[type="checkbox"]`)
|
||
|
).toBeChecked({ checked });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async fillTableCell({
|
||
|
index,
|
||
|
row,
|
||
|
column,
|
||
|
content,
|
||
|
}: {
|
||
|
index: number;
|
||
|
row: number;
|
||
|
column: number;
|
||
|
content: string;
|
||
|
}) {
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${row + 1})`)
|
||
|
.locator(`td:nth-child(${column + 1})`)
|
||
|
.click({
|
||
|
force: true,
|
||
|
});
|
||
|
|
||
|
await this.rootPage.keyboard.type(content);
|
||
|
}
|
||
|
|
||
|
async addTableRow({ index, rowIndex, kind }: { index: number; rowIndex?: number; kind: 'above' | 'below' | 'end' }) {
|
||
|
if (kind === 'end') {
|
||
|
const addNewRowButton = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.tiptap-create-row-btn`);
|
||
|
|
||
|
await addNewRowButton.hover();
|
||
|
await this._click(addNewRowButton);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const rowLocator = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`);
|
||
|
await this._hover(rowLocator);
|
||
|
|
||
|
const rowDragHandle = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-row-drag-handle-wrapper');
|
||
|
await this._click(rowDragHandle);
|
||
|
|
||
|
const insertRowLocator = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`)
|
||
|
.getByTestId(kind === 'above' ? 'nc-docs-table-row-insert-above' : 'nc-docs-table-row-insert-below');
|
||
|
await this._click(insertRowLocator);
|
||
|
}
|
||
|
|
||
|
async addTableColumn({
|
||
|
index,
|
||
|
columnIndex,
|
||
|
kind,
|
||
|
}: {
|
||
|
index: number;
|
||
|
columnIndex?: number;
|
||
|
kind: 'left' | 'right' | 'end';
|
||
|
}) {
|
||
|
if (kind === 'end') {
|
||
|
const addNewColumnButton = this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.tiptap-create-column-btn`);
|
||
|
|
||
|
await addNewColumnButton.hover();
|
||
|
await addNewColumnButton.click();
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
.hover();
|
||
|
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-column-drag-handle-wrapper')
|
||
|
.click({
|
||
|
force: true,
|
||
|
});
|
||
|
await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
.getByTestId(kind === 'left' ? 'nc-docs-table-column-insert-left' : 'nc-docs-table-column-insert-right')
|
||
|
.click({
|
||
|
force: true,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async deleteTableRow({ index, rowIndex }: { index: number; rowIndex?: number }) {
|
||
|
await this._hover(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`)
|
||
|
);
|
||
|
|
||
|
await this._click(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-row-drag-handle-wrapper')
|
||
|
);
|
||
|
|
||
|
await this._click(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${rowIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-row-delete')
|
||
|
);
|
||
|
}
|
||
|
|
||
|
async deleteTableColumn({ index, columnIndex }: { index: number; columnIndex?: number }) {
|
||
|
await this._hover(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
);
|
||
|
|
||
|
await this._click(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-column-drag-handle-wrapper')
|
||
|
);
|
||
|
|
||
|
await this._click(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td:nth-child(${columnIndex + 1})`)
|
||
|
.getByTestId('nc-docs-table-column-delete')
|
||
|
);
|
||
|
}
|
||
|
|
||
|
async verifyTextFormatting({
|
||
|
index,
|
||
|
text,
|
||
|
formatType,
|
||
|
}: {
|
||
|
index: number;
|
||
|
text: string;
|
||
|
formatType: 'bold' | 'italic' | 'strike' | 'underline' | 'code';
|
||
|
}) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`${tiptapTextFormatToDomType[formatType]}`)
|
||
|
).toHaveText(text);
|
||
|
}
|
||
|
|
||
|
async verifyTableNode({
|
||
|
index,
|
||
|
cells,
|
||
|
rowCount,
|
||
|
columnCount,
|
||
|
}: {
|
||
|
index: number;
|
||
|
cells: {
|
||
|
row: number;
|
||
|
column: number;
|
||
|
content: string;
|
||
|
}[];
|
||
|
rowCount?: number;
|
||
|
columnCount?: number;
|
||
|
}) {
|
||
|
await this.verifyNode({ index, type: 'Table' });
|
||
|
|
||
|
for (const cell of cells) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(${cell.row + 1})`)
|
||
|
.locator(`td:nth-child(${cell.column + 1})`)
|
||
|
).toHaveText(cell.content);
|
||
|
}
|
||
|
|
||
|
if (rowCount) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr`)
|
||
|
).toHaveCount(rowCount);
|
||
|
}
|
||
|
|
||
|
if (columnCount) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`tr:nth-child(1)`)
|
||
|
.locator(`td`)
|
||
|
).toHaveCount(columnCount);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async verifyLinkNode({ index, placeholder, url }: { index: number; placeholder: string; url?: string }) {
|
||
|
if (url) {
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`a`)
|
||
|
).toHaveAttribute('href', url);
|
||
|
}
|
||
|
|
||
|
await expect(
|
||
|
this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`a`)
|
||
|
).toHaveText(placeholder);
|
||
|
}
|
||
|
|
||
|
async verifyLinkOptionVisible({ visible }: { visible: boolean }) {
|
||
|
if (visible) {
|
||
|
await expect(this.rootPage.getByTestId('nc-docs-link-options')).toBeVisible();
|
||
|
} else {
|
||
|
await expect(this.rootPage.getByTestId('nc-docs-link-options')).toBeHidden();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async verifyLinkOptionSearchResults({ titles, selectedTitle }: { titles?: string[]; selectedTitle?: string }) {
|
||
|
if (titles) {
|
||
|
for (const title of titles) {
|
||
|
await expect(this.rootPage.getByTestId(`nc-docs-link-option-searched-page-${title}`)).toContainText(title);
|
||
|
}
|
||
|
|
||
|
const count = titles.length;
|
||
|
await expect(this.rootPage.getByTestId('nc-docs-link-option-searched-pages').locator('.page-item')).toHaveCount(
|
||
|
count
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (selectedTitle) {
|
||
|
await expect(this.rootPage.getByTestId(`nc-docs-link-option-searched-page-${selectedTitle}`)).toHaveClass(
|
||
|
/selected/
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async gotoStoredLink({ index }: { index: number }) {
|
||
|
const linkHref = await this.get()
|
||
|
.locator(`.draggable-block-wrapper:nth-child(${index + 1})`)
|
||
|
.locator(`.node-view-drag-content`)
|
||
|
.locator(`a`)
|
||
|
.getAttribute('href');
|
||
|
|
||
|
await this.rootPage.goto(linkHref);
|
||
|
await this.openedPage.waitForRender();
|
||
|
}
|
||
|
|
||
|
async clickLinkDeleteButton() {
|
||
|
await this.rootPage.getByTestId('nc-docs-link-options-open-delete').click();
|
||
|
}
|
||
|
|
||
|
async clearContent() {
|
||
|
await this.openedPage.waitForRender();
|
||
|
|
||
|
await this.get().click();
|
||
|
|
||
|
const firstParagraph = this.get()
|
||
|
.locator('.draggable-block-wrapper:nth-child(1)')
|
||
|
.locator('.node-view-drag-content');
|
||
|
await firstParagraph.click();
|
||
|
await this.rootPage.waitForTimeout(500);
|
||
|
await this.rootPage.keyboard.press('Meta+A');
|
||
|
await this.rootPage.keyboard.press('Backspace');
|
||
|
|
||
|
// TODO: fix this
|
||
|
await this.rootPage.waitForTimeout(500);
|
||
|
|
||
|
// await this.waitForResponse({
|
||
|
// uiAction: () => firstParagraph.clear(),
|
||
|
// httpMethodsToMatch: ['PUT'],
|
||
|
// requestUrlPathToMatch: `api/v1/docs/page`,
|
||
|
// });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export type TipTapNodes =
|
||
|
| 'Heading 1'
|
||
|
| 'Heading 2'
|
||
|
| 'Heading 3'
|
||
|
| 'Paragraph'
|
||
|
| 'Quote'
|
||
|
| 'Code'
|
||
|
| 'Bullet List'
|
||
|
| 'Numbered List'
|
||
|
| 'Task List'
|
||
|
| 'Divider'
|
||
|
| 'Image'
|
||
|
| 'Table'
|
||
|
| 'Link'
|
||
|
| 'Emoji'
|
||
|
| 'Info notice'
|
||
|
| 'Warning notice'
|
||
|
| 'Tip notice'
|
||
|
| 'Embed iframe';
|
||
|
|
||
|
export type TextFormatType =
|
||
|
| 'bold'
|
||
|
| 'italic'
|
||
|
| 'underline'
|
||
|
| 'strike'
|
||
|
| 'task'
|
||
|
| 'bullet'
|
||
|
| 'ordered'
|
||
|
| 'link'
|
||
|
| 'expand';
|
||
|
|
||
|
const tiptapNodeLabels: Record<TipTapNodes, string> = {
|
||
|
'Info notice': 'infoCallout',
|
||
|
'Warning notice': 'warningCallout',
|
||
|
'Tip notice': 'tipCallout',
|
||
|
Code: 'codeBlock',
|
||
|
Paragraph: 'paragraph',
|
||
|
'Heading 1': 'heading',
|
||
|
'Heading 2': 'heading',
|
||
|
'Heading 3': 'heading',
|
||
|
Divider: 'divider',
|
||
|
Image: 'image',
|
||
|
'Bullet List': 'bullet',
|
||
|
'Numbered List': 'ordered',
|
||
|
'Task List': 'task',
|
||
|
Table: 'table',
|
||
|
Quote: 'quote',
|
||
|
};
|
||
|
|
||
|
const tiptapNodeToDomType: Record<TipTapNodes, string> = {
|
||
|
'Heading 1': 'h1',
|
||
|
'Heading 2': 'h2',
|
||
|
'Heading 3': 'h3',
|
||
|
'Info notice': 'div.info-callout',
|
||
|
'Warning notice': 'div.warning-callout',
|
||
|
'Tip notice': 'div.tip-callout',
|
||
|
Paragraph: 'p',
|
||
|
Divider: 'hr',
|
||
|
'Embed iframe': 'iframe',
|
||
|
Image: 'img',
|
||
|
'Bullet List': 'div[data-type="bullet"]',
|
||
|
'Numbered List': 'div[data-type="ordered"]',
|
||
|
'Task List': 'div[data-type="task"]',
|
||
|
Table: 'div.tiptap-table-wrapper',
|
||
|
Code: 'pre',
|
||
|
Quote: 'quote',
|
||
|
};
|
||
|
|
||
|
const tiptapTextFormatToDomType: Record<'bold' | 'italic' | 'underline' | 'strike' | 'code', string> = {
|
||
|
bold: 'strong',
|
||
|
italic: 'em',
|
||
|
underline: 'u',
|
||
|
strike: 's',
|
||
|
code: 'code',
|
||
|
};
|