mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
Closes: [BS-1475](https://linear.app/affine-design/issue/BS-1475/颜色主题更新) [BS-1803](https://linear.app/affine-design/issue/BS-1803/fill-color色板影响的yuan素) [BS-1804](https://linear.app/affine-design/issue/BS-1804/border-color色板影响的yuan素) [BS-1815](https://linear.app/affine-design/issue/BS-1815/连线文字配色略瞎) ### What's Changed * refactor `EdgelessLineWidthPanel` component, the previous width is fixed and cannot be used in the new design * refactor `EdgelessColorPanel` and `EdgelessColorButton` components, make them simple and reusable * delete redundant `EdgelessOneRowColorPanel` component * unity and update color palette, if the previously set color is not in the latest color palette, the custom color button will be selected
1346 lines
36 KiB
TypeScript
1346 lines
36 KiB
TypeScript
import './declare-test-window.js';
|
|
|
|
import type {
|
|
AffineInlineEditor,
|
|
NoteBlockModel,
|
|
RichText,
|
|
RootBlockModel,
|
|
} from '@blocks/index.js';
|
|
import {
|
|
DEFAULT_NOTE_BACKGROUND_COLOR,
|
|
DEFAULT_NOTE_HEIGHT,
|
|
DEFAULT_NOTE_WIDTH,
|
|
} from '@blocksuite/affine-model';
|
|
import type { BlockComponent, EditorHost } from '@blocksuite/block-std';
|
|
import { BLOCK_ID_ATTR } from '@blocksuite/block-std';
|
|
import { assertExists } from '@blocksuite/global/utils';
|
|
import type { InlineRootElement } from '@inline/inline-editor.js';
|
|
import { expect, type Locator, type Page } from '@playwright/test';
|
|
import { COLLECTION_VERSION, PAGE_VERSION } from '@store/consts.js';
|
|
import type { BlockModel } from '@store/index.js';
|
|
import type { JSXElement } from '@store/utils/jsx.js';
|
|
import {
|
|
format as prettyFormat,
|
|
plugins as prettyFormatPlugins,
|
|
} from 'pretty-format';
|
|
|
|
import {
|
|
getCanvasElementsCount,
|
|
getConnectorPath,
|
|
getContainerChildIds,
|
|
getContainerIds,
|
|
getContainerOfElements,
|
|
getEdgelessElementBound,
|
|
getNoteRect,
|
|
getSelectedBound,
|
|
getSortedIdsInViewport,
|
|
getZoomLevel,
|
|
toIdCountMap,
|
|
toModelCoord,
|
|
} from './actions/edgeless.js';
|
|
import {
|
|
pressArrowLeft,
|
|
pressArrowRight,
|
|
pressBackspace,
|
|
redoByKeyboard,
|
|
SHORT_KEY,
|
|
type,
|
|
undoByKeyboard,
|
|
} from './actions/keyboard.js';
|
|
import {
|
|
captureHistory,
|
|
getClipboardCustomData,
|
|
getCurrentEditorDocId,
|
|
getCurrentThemeCSSPropertyValue,
|
|
getEditorLocator,
|
|
inlineEditorInnerTextToString,
|
|
} from './actions/misc.js';
|
|
import { getStringFromRichText } from './inline-editor.js';
|
|
import { currentEditorIndex } from './multiple-editor.js';
|
|
|
|
export { assertExists };
|
|
|
|
export const defaultStore = {
|
|
meta: {
|
|
pages: [
|
|
{
|
|
id: 'doc:home',
|
|
title: '',
|
|
tags: [],
|
|
},
|
|
],
|
|
blockVersions: {
|
|
'affine:paragraph': 1,
|
|
'affine:page': 2,
|
|
'affine:database': 3,
|
|
'affine:data-view': 1,
|
|
'affine:list': 1,
|
|
'affine:note': 1,
|
|
'affine:divider': 1,
|
|
'affine:embed-youtube': 1,
|
|
'affine:embed-figma': 1,
|
|
'affine:embed-github': 1,
|
|
'affine:embed-loom': 1,
|
|
'affine:embed-html': 1,
|
|
'affine:embed-linked-doc': 1,
|
|
'affine:embed-synced-doc': 1,
|
|
'affine:image': 1,
|
|
'affine:latex': 1,
|
|
'affine:frame': 1,
|
|
'affine:code': 1,
|
|
'affine:surface': 5,
|
|
'affine:bookmark': 1,
|
|
'affine:attachment': 1,
|
|
'affine:surface-ref': 1,
|
|
'affine:edgeless-text': 1,
|
|
},
|
|
workspaceVersion: COLLECTION_VERSION,
|
|
pageVersion: PAGE_VERSION,
|
|
},
|
|
spaces: {
|
|
'doc:home': {
|
|
blocks: {
|
|
'0': {
|
|
'prop:title': '',
|
|
'sys:id': '0',
|
|
'sys:flavour': 'affine:page',
|
|
'sys:children': ['1'],
|
|
'sys:version': 2,
|
|
},
|
|
'1': {
|
|
'sys:flavour': 'affine:note',
|
|
'sys:id': '1',
|
|
'sys:children': ['2'],
|
|
'sys:version': 1,
|
|
'prop:xywh': `[0,0,${DEFAULT_NOTE_WIDTH}, ${DEFAULT_NOTE_HEIGHT}]`,
|
|
'prop:background': DEFAULT_NOTE_BACKGROUND_COLOR,
|
|
'prop:index': 'a0',
|
|
'prop:hidden': false,
|
|
'prop:displayMode': 'both',
|
|
'prop:edgeless': {
|
|
style: {
|
|
borderRadius: 8,
|
|
borderSize: 4,
|
|
borderStyle: 'none',
|
|
shadowType: '--affine-note-shadow-box',
|
|
},
|
|
},
|
|
},
|
|
'2': {
|
|
'sys:flavour': 'affine:paragraph',
|
|
'sys:id': '2',
|
|
'sys:children': [],
|
|
'sys:version': 1,
|
|
'prop:text': 'hello',
|
|
'prop:type': 'text',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export type Bound = [x: number, y: number, w: number, h: number];
|
|
|
|
export async function assertEmpty(page: Page) {
|
|
await assertRichTexts(page, ['']);
|
|
}
|
|
|
|
export async function assertTitle(page: Page, text: string) {
|
|
const editor = getEditorLocator(page);
|
|
const inlineEditor = editor.locator('.doc-title-container').first();
|
|
const vText = inlineEditorInnerTextToString(await inlineEditor.innerText());
|
|
expect(vText).toBe(text);
|
|
}
|
|
|
|
export async function assertInlineEditorDeltas(
|
|
page: Page,
|
|
deltas: unknown[],
|
|
i = 0
|
|
) {
|
|
const actual = await page.evaluate(i => {
|
|
const inlineRoot = document.querySelectorAll<InlineRootElement>(
|
|
'[data-v-root="true"]'
|
|
)[i];
|
|
return inlineRoot.inlineEditor.yTextDeltas;
|
|
}, i);
|
|
expect(actual).toEqual(deltas);
|
|
}
|
|
|
|
export async function assertRichTextInlineDeltas(
|
|
page: Page,
|
|
deltas: unknown[],
|
|
i = 0
|
|
) {
|
|
const actual = await page.evaluate(
|
|
([i, currentEditorIndex]) => {
|
|
const editorHost =
|
|
document.querySelectorAll('editor-host')[currentEditorIndex];
|
|
const inlineRoot = editorHost.querySelectorAll<InlineRootElement>(
|
|
'rich-text [data-v-root="true"]'
|
|
)[i];
|
|
return inlineRoot.inlineEditor.yTextDeltas;
|
|
},
|
|
[i, currentEditorIndex]
|
|
);
|
|
expect(actual).toEqual(deltas);
|
|
}
|
|
|
|
export async function assertText(page: Page, text: string, i = 0) {
|
|
const actual = await getStringFromRichText(page, i);
|
|
expect(actual).toBe(text);
|
|
}
|
|
|
|
export async function assertTextContain(page: Page, text: string, i = 0) {
|
|
const actual = await getStringFromRichText(page, i);
|
|
expect(actual).toContain(text);
|
|
}
|
|
|
|
export async function assertRichTexts(page: Page, texts: string[]) {
|
|
const actualTexts = await page.evaluate(currentEditorIndex => {
|
|
const editorHost =
|
|
document.querySelectorAll('editor-host')[currentEditorIndex];
|
|
const richTexts = Array.from(
|
|
editorHost?.querySelectorAll<RichText>('rich-text') ?? []
|
|
);
|
|
return richTexts.map(richText => {
|
|
const editor = richText.inlineEditor as AffineInlineEditor;
|
|
return editor.yText.toString();
|
|
});
|
|
}, currentEditorIndex);
|
|
expect(actualTexts).toEqual(texts);
|
|
}
|
|
|
|
export async function assertEdgelessCanvasText(page: Page, text: string) {
|
|
const actualTexts = await page.evaluate(() => {
|
|
const editor = document.querySelector(
|
|
[
|
|
'edgeless-text-editor',
|
|
'edgeless-shape-text-editor',
|
|
'edgeless-frame-title-editor',
|
|
'edgeless-group-title-editor',
|
|
'edgeless-connector-label-editor',
|
|
].join(',')
|
|
);
|
|
if (!editor) {
|
|
throw new Error('editor not found');
|
|
}
|
|
// @ts-ignore
|
|
const inlineEditor = editor.inlineEditor;
|
|
return inlineEditor?.yText.toString();
|
|
});
|
|
expect(actualTexts).toEqual(text);
|
|
}
|
|
|
|
export async function assertRichImage(page: Page, count: number) {
|
|
const editor = getEditorLocator(page);
|
|
await expect(editor.locator('.resizable-img')).toHaveCount(count);
|
|
}
|
|
|
|
export async function assertDivider(page: Page, count: number) {
|
|
await expect(page.locator('affine-divider')).toHaveCount(count);
|
|
}
|
|
|
|
export async function assertRichDragButton(page: Page) {
|
|
await expect(page.locator('.resize')).toHaveCount(4);
|
|
}
|
|
|
|
export async function assertImageSize(
|
|
page: Page,
|
|
size: { width: number; height: number }
|
|
) {
|
|
const actual = await page.locator('.resizable-img').boundingBox();
|
|
expect(size).toEqual({
|
|
width: Math.floor(actual?.width ?? NaN),
|
|
height: Math.floor(actual?.height ?? NaN),
|
|
});
|
|
}
|
|
|
|
export async function assertImageOption(page: Page) {
|
|
// const actual = await page.locator('.embed-editing-state').count();
|
|
// expect(actual).toEqual(1);
|
|
const locator = page.locator('.affine-image-toolbar-container');
|
|
await expect(locator).toBeVisible();
|
|
}
|
|
|
|
export async function assertDocTitleFocus(page: Page) {
|
|
const locator = page.locator('doc-title .inline-editor').nth(0);
|
|
await expect(locator).toBeFocused();
|
|
}
|
|
|
|
export async function assertListPrefix(
|
|
page: Page,
|
|
predict: (string | RegExp)[],
|
|
range?: [number, number]
|
|
) {
|
|
const prefixs = page.locator('.affine-list-block__prefix');
|
|
|
|
let start = 0;
|
|
let end = await prefixs.count();
|
|
if (range) {
|
|
[start, end] = range;
|
|
}
|
|
|
|
for (let i = start; i < end; i++) {
|
|
const prefix = await prefixs.nth(i).innerText();
|
|
expect(prefix).toContain(predict[i]);
|
|
}
|
|
}
|
|
|
|
export async function assertBlockCount(
|
|
page: Page,
|
|
flavour: string,
|
|
count: number
|
|
) {
|
|
await expect(page.locator(`affine-${flavour}`)).toHaveCount(count);
|
|
}
|
|
export async function assertRowCount(page: Page, count: number) {
|
|
await expect(page.locator('.affine-database-block-row')).toHaveCount(count);
|
|
}
|
|
|
|
export async function assertVisibleBlockCount(
|
|
page: Page,
|
|
flavour: string,
|
|
count: number
|
|
) {
|
|
// not only count, but also check if all the blocks are visible
|
|
const locator = page.locator(`affine-${flavour}`);
|
|
let visibleCount = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
if (await locator.nth(i).isVisible()) {
|
|
visibleCount++;
|
|
}
|
|
}
|
|
expect(visibleCount).toEqual(count);
|
|
}
|
|
|
|
export async function assertRichTextInlineRange(
|
|
page: Page,
|
|
richTextIndex: number,
|
|
rangeIndex: number,
|
|
rangeLength = 0
|
|
) {
|
|
const actual = await page.evaluate(
|
|
([richTextIndex, currentEditorIndex]) => {
|
|
const editorHost =
|
|
document.querySelectorAll('editor-host')[currentEditorIndex];
|
|
const richText = editorHost?.querySelectorAll('rich-text')[richTextIndex];
|
|
const inlineEditor = richText.inlineEditor;
|
|
return inlineEditor?.getInlineRange();
|
|
},
|
|
[richTextIndex, currentEditorIndex]
|
|
);
|
|
expect(actual).toEqual({ index: rangeIndex, length: rangeLength });
|
|
}
|
|
|
|
export async function assertNativeSelectionRangeCount(
|
|
page: Page,
|
|
count: number
|
|
) {
|
|
const actual = await page.evaluate(() => {
|
|
const selection = window.getSelection();
|
|
return selection?.rangeCount;
|
|
});
|
|
expect(actual).toEqual(count);
|
|
}
|
|
|
|
export async function assertNoteXYWH(
|
|
page: Page,
|
|
expected: [number, number, number, number]
|
|
) {
|
|
const actual = await page.evaluate(() => {
|
|
const rootModel = window.doc.root as RootBlockModel;
|
|
const note = rootModel.children.find(
|
|
x => x.flavour === 'affine:note'
|
|
) as NoteBlockModel;
|
|
return JSON.parse(note.xywh) as number[];
|
|
});
|
|
expect(actual[0]).toBeCloseTo(expected[0]);
|
|
expect(actual[1]).toBeCloseTo(expected[1]);
|
|
expect(actual[2]).toBeCloseTo(expected[2]);
|
|
expect(actual[3]).toBeCloseTo(expected[3]);
|
|
}
|
|
|
|
export async function assertTextFormat(
|
|
page: Page,
|
|
richTextIndex: number,
|
|
index: number,
|
|
resultObj: unknown
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ richTextIndex, index, currentEditorIndex }) => {
|
|
const editorHost =
|
|
document.querySelectorAll('editor-host')[currentEditorIndex];
|
|
const richText = editorHost.querySelectorAll('rich-text')[richTextIndex];
|
|
const inlineEditor = richText.inlineEditor;
|
|
if (!inlineEditor) {
|
|
throw new Error('Inline editor is undefined');
|
|
}
|
|
|
|
const result = inlineEditor.getFormat({
|
|
index,
|
|
length: 0,
|
|
});
|
|
return result;
|
|
},
|
|
{ richTextIndex, index, currentEditorIndex }
|
|
);
|
|
expect(actual).toEqual(resultObj);
|
|
}
|
|
|
|
export async function assertRichTextModelType(
|
|
page: Page,
|
|
type: string,
|
|
index = 0
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ index, BLOCK_ID_ATTR, currentEditorIndex }) => {
|
|
const editorHost =
|
|
document.querySelectorAll('editor-host')[currentEditorIndex];
|
|
const richText = editorHost.querySelectorAll('rich-text')[index];
|
|
const block = richText.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
|
|
|
if (!block) {
|
|
throw new Error('block component is undefined');
|
|
}
|
|
return (block.model as BlockModel<{ type: string }>).type;
|
|
},
|
|
{ index, BLOCK_ID_ATTR, currentEditorIndex }
|
|
);
|
|
expect(actual).toEqual(type);
|
|
}
|
|
|
|
export async function assertTextFormats(page: Page, resultObj: unknown[]) {
|
|
const actual = await page.evaluate(index => {
|
|
const editorHost = document.querySelectorAll('editor-host')[index];
|
|
const elements = editorHost.querySelectorAll('rich-text');
|
|
return Array.from(elements).map(el => {
|
|
const inlineEditor = el.inlineEditor;
|
|
if (!inlineEditor) {
|
|
throw new Error('Inline editor is undefined');
|
|
}
|
|
|
|
const result = inlineEditor.getFormat({
|
|
index: 0,
|
|
length: inlineEditor.yText.length,
|
|
});
|
|
return result;
|
|
});
|
|
}, currentEditorIndex);
|
|
expect(actual).toEqual(resultObj);
|
|
}
|
|
|
|
export async function assertStore(
|
|
page: Page,
|
|
expected: Record<string, unknown>
|
|
) {
|
|
const actual = await page.evaluate(() => {
|
|
const json = window.collection.doc.toJSON();
|
|
delete json.meta.pages[0].createDate;
|
|
return json;
|
|
});
|
|
expect(actual).toEqual(expected);
|
|
}
|
|
|
|
export async function assertBlockChildrenIds(
|
|
page: Page,
|
|
blockId: string,
|
|
ids: string[]
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ blockId }) => {
|
|
const element = document.querySelector(`[data-block-id="${blockId}"]`);
|
|
// @ts-ignore
|
|
const model = element.model as BlockModel;
|
|
return model.children.map(child => child.id);
|
|
},
|
|
{ blockId }
|
|
);
|
|
expect(actual).toEqual(ids);
|
|
}
|
|
|
|
export async function assertBlockChildrenFlavours(
|
|
page: Page,
|
|
blockId: string,
|
|
flavours: string[]
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ blockId }) => {
|
|
const element = document.querySelector(`[data-block-id="${blockId}"]`);
|
|
// @ts-ignore
|
|
const model = element.model as BlockModel;
|
|
return model.children.map(child => child.flavour);
|
|
},
|
|
{ blockId }
|
|
);
|
|
expect(actual).toEqual(flavours);
|
|
}
|
|
|
|
export async function assertParentBlockId(
|
|
page: Page,
|
|
blockId: string,
|
|
parentId: string
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ blockId }) => {
|
|
const model = window.doc?.getBlock(blockId)?.model;
|
|
if (!model) {
|
|
throw new Error(`Block with id ${blockId} not found`);
|
|
}
|
|
return model.doc.getParent(model)?.id;
|
|
},
|
|
{ blockId }
|
|
);
|
|
expect(actual).toEqual(parentId);
|
|
}
|
|
|
|
export async function assertParentBlockFlavour(
|
|
page: Page,
|
|
blockId: string,
|
|
flavour: string
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ blockId }) => {
|
|
const model = window.doc?.getBlock(blockId)?.model;
|
|
if (!model) {
|
|
throw new Error(`Block with id ${blockId} not found`);
|
|
}
|
|
return model.doc.getParent(model)?.flavour;
|
|
},
|
|
{ blockId }
|
|
);
|
|
expect(actual).toEqual(flavour);
|
|
}
|
|
|
|
export async function assertClassName(
|
|
page: Page,
|
|
selector: string,
|
|
className: RegExp
|
|
) {
|
|
const locator = page.locator(selector);
|
|
await expect(locator).toHaveClass(className);
|
|
}
|
|
|
|
export async function assertTextContent(
|
|
page: Page,
|
|
selector: string,
|
|
text: RegExp
|
|
) {
|
|
const locator = page.locator(selector);
|
|
await expect(locator).toHaveText(text);
|
|
}
|
|
|
|
export async function assertBlockType(
|
|
page: Page,
|
|
id: string | number | null,
|
|
type: string
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ id }) => {
|
|
const element = document.querySelector<BlockComponent>(
|
|
`[data-block-id="${id}"]`
|
|
);
|
|
|
|
if (!element) {
|
|
throw new Error(`Element with id ${id} not found`);
|
|
}
|
|
|
|
const model = element.model;
|
|
// @ts-ignore
|
|
return model.type;
|
|
},
|
|
{ id }
|
|
);
|
|
expect(actual).toBe(type);
|
|
}
|
|
|
|
export async function assertBlockFlavour(
|
|
page: Page,
|
|
id: string | number,
|
|
flavour: BlockSuite.Flavour
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ id }) => {
|
|
const element = document.querySelector<BlockComponent>(
|
|
`[data-block-id="${id}"]`
|
|
);
|
|
|
|
if (!element) {
|
|
throw new Error(`Element with id ${id} not found`);
|
|
}
|
|
|
|
const model = element.model;
|
|
return model.flavour;
|
|
},
|
|
{ id }
|
|
);
|
|
expect(actual).toBe(flavour);
|
|
}
|
|
|
|
export async function assertBlockTextContent(
|
|
page: Page,
|
|
id: string | number,
|
|
str: string
|
|
) {
|
|
const actual = await page.evaluate(
|
|
({ id }) => {
|
|
const element = document.querySelector<BlockComponent>(
|
|
`[data-block-id="${id}"]`
|
|
);
|
|
|
|
if (!element) {
|
|
throw new Error(`Element with id ${id} not found`);
|
|
}
|
|
|
|
const model = element.model;
|
|
return model.text?.toString() ?? '';
|
|
},
|
|
{ id }
|
|
);
|
|
expect(actual).toBe(str);
|
|
}
|
|
|
|
export async function assertBlockProps(
|
|
page: Page,
|
|
id: string,
|
|
props: Record<string, unknown>
|
|
) {
|
|
const actual = await page.evaluate(
|
|
([id, props]) => {
|
|
const element = document.querySelector(`[data-block-id="${id}"]`);
|
|
// @ts-ignore
|
|
const model = element.model as BlockModel;
|
|
return Object.fromEntries(
|
|
// @ts-ignore
|
|
Object.keys(props).map(key => [key, (model[key] as unknown).toString()])
|
|
);
|
|
},
|
|
[id, props] as const
|
|
);
|
|
expect(actual).toEqual(props);
|
|
}
|
|
|
|
export async function assertBlockTypes(page: Page, blockTypes: string[]) {
|
|
const actual = await page.evaluate(index => {
|
|
const editor = document.querySelectorAll('affine-editor-container')[index];
|
|
const elements = editor?.querySelectorAll('[data-block-id]');
|
|
return (
|
|
Array.from(elements)
|
|
.slice(2)
|
|
// @ts-ignore
|
|
.map(el => el.model.type)
|
|
);
|
|
}, currentEditorIndex);
|
|
expect(actual).toEqual(blockTypes);
|
|
}
|
|
|
|
/**
|
|
* @example
|
|
* ```ts
|
|
* await assertMatchMarkdown(
|
|
* page,
|
|
* `title
|
|
* text1
|
|
* text2`
|
|
* );
|
|
* ```
|
|
* @deprecated experimental, use {@link assertStoreMatchJSX} instead
|
|
*/
|
|
export async function assertMatchMarkdown(page: Page, text: string) {
|
|
const jsonDoc = (await page.evaluate(() =>
|
|
window.collection.doc.toJSON()
|
|
)) as Record<string, Record<string, unknown>>;
|
|
const titleNode = jsonDoc['doc:home']['0'] as Record<string, unknown>;
|
|
|
|
const markdownVisitor = (node: Record<string, unknown>): string => {
|
|
// TODO use schema
|
|
if (node['sys:flavour'] === 'affine:page') {
|
|
return (node['prop:title'] as Text).toString() ?? '';
|
|
}
|
|
if (!('prop:type' in node)) {
|
|
return '[? unknown node]';
|
|
}
|
|
if (node['prop:type'] === 'text') {
|
|
return node['prop:text'] as string;
|
|
}
|
|
if (node['prop:type'] === 'bulleted') {
|
|
return `- ${node['prop:text']}`;
|
|
}
|
|
// TODO please fix this
|
|
return `[? ${node['prop:type']} node]`;
|
|
};
|
|
|
|
const INDENT_SIZE = 2;
|
|
const visitNodes = (
|
|
node: Record<string, unknown>,
|
|
visitor: (node: Record<string, unknown>) => string
|
|
): string[] => {
|
|
if (!('sys:children' in node) || !Array.isArray(node['sys:children'])) {
|
|
throw new Error("Failed to visit nodes: 'sys:children' is not an array");
|
|
// return visitor(node);
|
|
}
|
|
|
|
const children = node['sys:children'].map(id => jsonDoc['doc:home'][id]);
|
|
return [
|
|
visitor(node),
|
|
...children.flatMap(child =>
|
|
visitNodes(child as Record<string, unknown>, visitor).map(line => {
|
|
if (node['sys:flavour'] === 'affine:page') {
|
|
// Ad hoc way to remove the title indent
|
|
return line;
|
|
}
|
|
|
|
return ' '.repeat(INDENT_SIZE) + line;
|
|
})
|
|
),
|
|
];
|
|
};
|
|
const visitRet = visitNodes(titleNode, markdownVisitor);
|
|
const actual = visitRet.join('\n');
|
|
|
|
expect(actual).toEqual(text);
|
|
}
|
|
|
|
export async function assertStoreMatchJSX(
|
|
page: Page,
|
|
snapshot: string,
|
|
blockId?: string
|
|
) {
|
|
const docId = await getCurrentEditorDocId(page);
|
|
const element = (await page.evaluate(
|
|
([blockId, docId]) => window.collection.exportJSX(blockId, docId),
|
|
[blockId, docId]
|
|
)) as JSXElement;
|
|
|
|
// Fix symbol can not be serialized, we need to set $$typeof manually
|
|
// If the function passed to the page.evaluate(pageFunction[, arg]) returns a non-Serializable value,
|
|
// then page.evaluate(pageFunction[, arg]) resolves to undefined.
|
|
// See https://playwright.dev/docs/api/class-page#page-evaluate
|
|
const testSymbol = Symbol.for('react.test.json');
|
|
const markSymbol = (node: JSXElement) => {
|
|
node.$$typeof = testSymbol;
|
|
if (!node.children) {
|
|
return;
|
|
}
|
|
const propText = node.props['prop:text'];
|
|
if (propText && typeof propText === 'object') {
|
|
markSymbol(propText);
|
|
}
|
|
node.children.forEach(child => {
|
|
if (!(typeof child === 'object')) {
|
|
return;
|
|
}
|
|
markSymbol(child);
|
|
});
|
|
};
|
|
|
|
markSymbol(element);
|
|
|
|
// See https://github.com/facebook/jest/blob/main/packages/pretty-format
|
|
const formattedJSX = prettyFormat(element, {
|
|
plugins: [prettyFormatPlugins.ReactTestComponent],
|
|
printFunctionName: false,
|
|
});
|
|
expect(formattedJSX, formattedJSX).toEqual(snapshot.trimStart());
|
|
}
|
|
|
|
type MimeType = 'text/plain' | 'blocksuite/x-c+w' | 'text/html';
|
|
|
|
export function assertClipItems(_page: Page, _key: MimeType, _value: unknown) {
|
|
// FIXME: use original clipboard API
|
|
// const clipItems = await page.evaluate(() => {
|
|
// return document
|
|
// .getElementsByTagName('affine-editor-container')[0]
|
|
// .clipboard['_copy']['_getClipItems']();
|
|
// });
|
|
// const actual = clipItems.find(item => item.mimeType === key)?.data;
|
|
// expect(actual).toEqual(value);
|
|
return true;
|
|
}
|
|
|
|
export function assertAlmostEqual(
|
|
actual: number,
|
|
expected: number,
|
|
precision = 0.001
|
|
) {
|
|
expect(
|
|
Math.abs(actual - expected),
|
|
`expected: ${expected}, but actual: ${actual}`
|
|
).toBeLessThan(precision);
|
|
}
|
|
|
|
export function assertPointAlmostEqual(
|
|
actual: number[],
|
|
expected: number[],
|
|
precision = 0.001
|
|
) {
|
|
assertAlmostEqual(actual[0], expected[0], precision);
|
|
assertAlmostEqual(actual[1], expected[1], precision);
|
|
}
|
|
|
|
/**
|
|
* Assert the locator is visible in the viewport.
|
|
* It will check the bounding box of the locator is within the viewport.
|
|
*
|
|
* See also https://playwright.dev/docs/actionability#visible
|
|
*/
|
|
export async function assertLocatorVisible(
|
|
page: Page,
|
|
locator: Locator,
|
|
visible = true
|
|
) {
|
|
const bodyRect = await page.locator('body').boundingBox();
|
|
const rect = await locator.boundingBox();
|
|
expect(rect).toBeTruthy();
|
|
expect(bodyRect).toBeTruthy();
|
|
if (!rect || !bodyRect) {
|
|
throw new Error('Unreachable');
|
|
}
|
|
if (visible) {
|
|
// Assert the locator is **fully** visible
|
|
await expect(locator).toBeVisible();
|
|
expect(rect.x).toBeGreaterThanOrEqual(0);
|
|
expect(rect.y).toBeGreaterThanOrEqual(0);
|
|
expect(rect.x + rect.width).toBeLessThanOrEqual(
|
|
bodyRect.x + bodyRect.width
|
|
);
|
|
expect(rect.y + rect.height).toBeLessThanOrEqual(
|
|
bodyRect.x + bodyRect.height
|
|
);
|
|
} else {
|
|
// Assert the locator is **fully** invisible
|
|
const locatorIsVisible = await locator.isVisible();
|
|
if (!locatorIsVisible) {
|
|
// If the locator is invisible, we don't need to check the bounding box
|
|
return;
|
|
}
|
|
const isInVisible =
|
|
rect.x > bodyRect.x + bodyRect.width ||
|
|
rect.y > bodyRect.y + bodyRect.height ||
|
|
rect.x + rect.width < bodyRect.x ||
|
|
rect.y + rect.height < bodyRect.y;
|
|
expect(isInVisible).toBe(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert basic keyboard operation works in input
|
|
*
|
|
* NOTICE:
|
|
* - it will clear the input value.
|
|
* - it will pollute undo/redo history.
|
|
*/
|
|
export async function assertKeyboardWorkInInput(page: Page, locator: Locator) {
|
|
await expect(locator).toBeVisible();
|
|
await locator.focus();
|
|
// Clear input before test
|
|
await locator.clear();
|
|
// type/backspace
|
|
await type(page, '12/34');
|
|
await expect(locator).toHaveValue('12/34');
|
|
await captureHistory(page);
|
|
await pressBackspace(page);
|
|
await expect(locator).toHaveValue('12/3');
|
|
|
|
// undo/redo
|
|
await undoByKeyboard(page);
|
|
await expect(locator).toHaveValue('12/34');
|
|
await redoByKeyboard(page);
|
|
await expect(locator).toHaveValue('12/3');
|
|
|
|
// keyboard
|
|
await pressArrowLeft(page, 2);
|
|
await pressArrowRight(page, 1);
|
|
await pressBackspace(page);
|
|
await expect(locator).toHaveValue('123');
|
|
await pressBackspace(page);
|
|
await expect(locator).toHaveValue('13');
|
|
|
|
// copy/cut/paste
|
|
await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 });
|
|
await page.keyboard.press(`${SHORT_KEY}+c`, { delay: 50 });
|
|
await pressBackspace(page);
|
|
await expect(locator).toHaveValue('');
|
|
await page.keyboard.press(`${SHORT_KEY}+v`, { delay: 50 });
|
|
await expect(locator).toHaveValue('13');
|
|
await page.keyboard.press(`${SHORT_KEY}+a`, { delay: 50 });
|
|
await page.keyboard.press(`${SHORT_KEY}+x`, { delay: 50 });
|
|
await expect(locator).toHaveValue('');
|
|
}
|
|
|
|
export function assertSameColor(c1?: `#${string}`, c2?: `#${string}`) {
|
|
expect(c1?.toLowerCase()).toEqual(c2?.toLowerCase());
|
|
}
|
|
|
|
type Rect = { x: number; y: number; w: number; h: number };
|
|
|
|
export async function assertNoteRectEqual(
|
|
page: Page,
|
|
noteId: string,
|
|
expected: Rect
|
|
) {
|
|
const rect = await getNoteRect(page, noteId);
|
|
assertRectEqual(rect, expected);
|
|
}
|
|
|
|
export function assertRectEqual(a: Rect, b: Rect) {
|
|
expect(a.x).toBeCloseTo(b.x, 0);
|
|
expect(a.y).toBeCloseTo(b.y, 0);
|
|
expect(a.w).toBeCloseTo(b.w, 0);
|
|
expect(a.h).toBeCloseTo(b.h, 0);
|
|
}
|
|
|
|
export function assertDOMRectEqual(a: DOMRect, b: DOMRect) {
|
|
expect(a.x).toBeCloseTo(b.x, 0);
|
|
expect(a.y).toBeCloseTo(b.y, 0);
|
|
expect(a.width).toBeCloseTo(b.width, 0);
|
|
expect(a.height).toBeCloseTo(b.height, 0);
|
|
}
|
|
|
|
export async function assertEdgelessDraggingArea(page: Page, xywh: number[]) {
|
|
const [x, y, w, h] = xywh;
|
|
const editor = getEditorLocator(page);
|
|
const draggingArea = editor
|
|
.locator('edgeless-dragging-area-rect')
|
|
.locator('.affine-edgeless-dragging-area');
|
|
|
|
const box = await draggingArea.boundingBox();
|
|
if (!box) throw new Error('Missing edgeless dragging area');
|
|
|
|
expect(box.x).toBeCloseTo(x, 0);
|
|
expect(box.y).toBeCloseTo(y, 0);
|
|
expect(box.width).toBeCloseTo(w, 0);
|
|
expect(box.height).toBeCloseTo(h, 0);
|
|
}
|
|
|
|
export async function getSelectedRect(page: Page) {
|
|
const editor = getEditorLocator(page);
|
|
const selectedRect = editor
|
|
.locator('edgeless-selected-rect')
|
|
.locator('.affine-edgeless-selected-rect');
|
|
// FIXME: remove this timeout
|
|
await page.waitForTimeout(50);
|
|
const box = await selectedRect.boundingBox();
|
|
if (!box) throw new Error('Missing edgeless selected rect');
|
|
return box;
|
|
}
|
|
|
|
// Better to use xxSelectedModelRect
|
|
export async function assertEdgelessSelectedRect(page: Page, xywh: number[]) {
|
|
const [x, y, w, h] = xywh;
|
|
const box = await getSelectedRect(page);
|
|
|
|
expect(box.x).toBeCloseTo(x, 0);
|
|
expect(box.y).toBeCloseTo(y, 0);
|
|
expect(box.width).toBeCloseTo(w, 0);
|
|
expect(box.height).toBeCloseTo(h, 0);
|
|
}
|
|
|
|
export async function assertEdgelessSelectedModelRect(
|
|
page: Page,
|
|
xywh: number[]
|
|
) {
|
|
const [x, y, w, h] = xywh;
|
|
const box = await getSelectedRect(page);
|
|
const [mX, mY] = await toModelCoord(page, [box.x, box.y]);
|
|
|
|
expect(mX).toBeCloseTo(x, 0);
|
|
expect(mY).toBeCloseTo(y, 0);
|
|
expect(box.width).toBeCloseTo(w, 0);
|
|
expect(box.height).toBeCloseTo(h, 0);
|
|
}
|
|
|
|
export async function assertEdgelessSelectedElementHandleCount(
|
|
page: Page,
|
|
count: number
|
|
) {
|
|
const editor = getEditorLocator(page);
|
|
const handles = editor.locator('.element-handle');
|
|
await expect(handles).toHaveCount(count);
|
|
}
|
|
|
|
// Better to use xxSelectedModelRect
|
|
export async function assertEdgelessRemoteSelectedRect(
|
|
page: Page,
|
|
xywh: number[],
|
|
index = 0
|
|
) {
|
|
const [x, y, w, h] = xywh;
|
|
const editor = getEditorLocator(page);
|
|
const remoteSelectedRect = editor
|
|
.locator('affine-edgeless-remote-selection-widget')
|
|
.locator('.remote-rect')
|
|
.nth(index);
|
|
|
|
const box = await remoteSelectedRect.boundingBox();
|
|
if (!box) throw new Error('Missing edgeless remote selected rect');
|
|
|
|
expect(box.x).toBeCloseTo(x, 0);
|
|
expect(box.y).toBeCloseTo(y, 0);
|
|
expect(box.width).toBeCloseTo(w, 0);
|
|
expect(box.height).toBeCloseTo(h, 0);
|
|
}
|
|
|
|
export async function assertEdgelessRemoteSelectedModelRect(
|
|
page: Page,
|
|
xywh: number[],
|
|
index = 0
|
|
) {
|
|
const [x, y, w, h] = xywh;
|
|
const editor = getEditorLocator(page);
|
|
const remoteSelectedRect = editor
|
|
.locator('affine-edgeless-remote-selection-widget')
|
|
.locator('.remote-rect')
|
|
.nth(index);
|
|
|
|
const box = await remoteSelectedRect.boundingBox();
|
|
if (!box) throw new Error('Missing edgeless remote selected rect');
|
|
|
|
const [mX, mY] = await toModelCoord(page, [box.x, box.y]);
|
|
expect(mX).toBeCloseTo(x, 0);
|
|
expect(mY).toBeCloseTo(y, 0);
|
|
expect(box.width).toBeCloseTo(w, 0);
|
|
expect(box.height).toBeCloseTo(h, 0);
|
|
}
|
|
|
|
export async function assertEdgelessSelectedRectRotation(page: Page, deg = 0) {
|
|
const editor = getEditorLocator(page);
|
|
const selectedRect = editor
|
|
.locator('edgeless-selected-rect')
|
|
.locator('.affine-edgeless-selected-rect');
|
|
|
|
const transform = await selectedRect.evaluate(el => el.style.transform);
|
|
const r = new RegExp(`rotate\\(${deg}deg\\)`);
|
|
expect(transform).toMatch(r);
|
|
}
|
|
|
|
export async function assertEdgelessSelectedReactCursor(
|
|
page: Page,
|
|
expected: (
|
|
| {
|
|
mode: 'resize';
|
|
handle:
|
|
| 'top'
|
|
| 'right'
|
|
| 'bottom'
|
|
| 'left'
|
|
| 'top-left'
|
|
| 'top-right'
|
|
| 'bottom-right'
|
|
| 'bottom-left';
|
|
}
|
|
| {
|
|
mode: 'rotate';
|
|
handle: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left';
|
|
}
|
|
) & {
|
|
cursor: string;
|
|
}
|
|
) {
|
|
const editor = getEditorLocator(page);
|
|
const selectedRect = editor
|
|
.locator('edgeless-selected-rect')
|
|
.locator('.affine-edgeless-selected-rect');
|
|
|
|
const handle = selectedRect
|
|
.getByLabel(expected.handle, { exact: true })
|
|
.locator(`.${expected.mode}`);
|
|
|
|
await handle.hover();
|
|
await expect(handle).toHaveCSS('cursor', expected.cursor);
|
|
}
|
|
|
|
export async function assertEdgelessNonSelectedRect(page: Page) {
|
|
const rect = page.locator('edgeless-selected-rect');
|
|
await expect(rect).toBeHidden();
|
|
}
|
|
|
|
export async function assertSelectionInNote(
|
|
page: Page,
|
|
noteId: string,
|
|
blockNote: string = 'affine-note'
|
|
) {
|
|
const closestNoteId = await page.evaluate(blockNote => {
|
|
const selection = window.getSelection();
|
|
const note = selection?.anchorNode?.parentElement?.closest(blockNote);
|
|
return note?.getAttribute('data-block-id');
|
|
}, blockNote);
|
|
expect(closestNoteId).toEqual(noteId);
|
|
}
|
|
|
|
export async function assertEdgelessNoteBackground(
|
|
page: Page,
|
|
noteId: string,
|
|
color: string
|
|
) {
|
|
const editor = getEditorLocator(page);
|
|
const backgroundColor = await editor
|
|
.locator(`affine-edgeless-note[data-block-id="${noteId}"]`)
|
|
.evaluate(ele => {
|
|
const noteWrapper =
|
|
ele?.querySelector<HTMLDivElement>('.note-background');
|
|
if (!noteWrapper) {
|
|
throw new Error(`Could not find note: ${noteId}`);
|
|
}
|
|
return noteWrapper.style.backgroundColor;
|
|
});
|
|
|
|
expect(backgroundColor).toEqual(`var(${color})`);
|
|
}
|
|
|
|
function toHex(color: string) {
|
|
let r, g, b;
|
|
|
|
if (color.startsWith('#')) {
|
|
color = color.substr(1);
|
|
if (color.length === 3) {
|
|
color = color.replace(/./g, '$&$&');
|
|
}
|
|
[r, g, b] = color.match(/.{2}/g)?.map(hex => parseInt(hex, 16)) ?? [];
|
|
} else if (color.startsWith('rgba')) {
|
|
[r, g, b] = color.match(/\d+/g)?.map(Number) ?? [];
|
|
} else if (color.startsWith('rgb')) {
|
|
[r, g, b] = color.match(/\d+/g)?.map(Number) ?? [];
|
|
} else {
|
|
throw new Error('Invalid color format');
|
|
}
|
|
|
|
if (r === undefined || g === undefined || b === undefined) {
|
|
throw new Error('Invalid color format');
|
|
}
|
|
|
|
const hex = ((r << 16) | (g << 8) | b).toString(16);
|
|
return '#' + '0'.repeat(6 - hex.length) + hex;
|
|
}
|
|
|
|
export async function assertEdgelessColorSameWithHexColor(
|
|
page: Page,
|
|
edgelessColor: string,
|
|
hexColor: `#${string}`
|
|
) {
|
|
const themeColor = await getCurrentThemeCSSPropertyValue(page, edgelessColor);
|
|
expect(themeColor).toBeTruthy();
|
|
const edgelessHexColor = toHex(themeColor);
|
|
|
|
assertSameColor(hexColor, edgelessHexColor as `#${string}`);
|
|
}
|
|
|
|
export async function assertZoomLevel(page: Page, zoom: number) {
|
|
const z = await getZoomLevel(page);
|
|
expect(z).toBe(Math.ceil(zoom));
|
|
}
|
|
|
|
export async function assertConnectorPath(
|
|
page: Page,
|
|
path: number[][],
|
|
index = 0
|
|
) {
|
|
const actualPath = await getConnectorPath(page, index);
|
|
actualPath.every((p, i) => assertPointAlmostEqual(p, path[i], 0.1));
|
|
}
|
|
|
|
export function assertRectExist(
|
|
rect: { x: number; y: number; width: number; height: number } | null
|
|
): asserts rect is { x: number; y: number; width: number; height: number } {
|
|
expect(rect).not.toBe(null);
|
|
}
|
|
|
|
export async function assertEdgelessElementBound(
|
|
page: Page,
|
|
elementId: string,
|
|
bound: Bound
|
|
) {
|
|
const actual = await getEdgelessElementBound(page, elementId);
|
|
assertBound(actual, bound);
|
|
}
|
|
|
|
export async function assertSelectedBound(
|
|
page: Page,
|
|
expected: Bound,
|
|
index = 0
|
|
) {
|
|
const bound = await getSelectedBound(page, index);
|
|
assertBound(bound, expected);
|
|
}
|
|
|
|
/**
|
|
* asserts all groups and they children count at the same time
|
|
* @param page
|
|
* @param expected the expected group id and the count of of its children
|
|
*/
|
|
export async function assertContainerIds(
|
|
page: Page,
|
|
expected: Record<string, number>
|
|
) {
|
|
const ids = await getContainerIds(page);
|
|
const result = toIdCountMap(ids);
|
|
|
|
expect(result).toEqual(expected);
|
|
}
|
|
|
|
export async function assertSortedIds(page: Page, expected: string[]) {
|
|
const ids = await getSortedIdsInViewport(page);
|
|
expect(ids).toEqual(expected);
|
|
}
|
|
|
|
export async function assertContainerChildIds(
|
|
page: Page,
|
|
expected: Record<string, number>,
|
|
id: string
|
|
) {
|
|
const ids = await getContainerChildIds(page, id);
|
|
const result = toIdCountMap(ids);
|
|
|
|
expect(result).toEqual(expected);
|
|
}
|
|
|
|
export async function assertContainerOfElements(
|
|
page: Page,
|
|
elements: string[],
|
|
containerId: string | null
|
|
) {
|
|
const elementContainers = await getContainerOfElements(page, elements);
|
|
|
|
elementContainers.forEach(elementContainer => {
|
|
expect(elementContainer).toEqual(containerId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert the given container has the expected children count.
|
|
* And the children's container id should equal to the given container id.
|
|
* @param page
|
|
* @param containerId
|
|
* @param childrenCount
|
|
*/
|
|
export async function assertContainerChildCount(
|
|
page: Page,
|
|
containerId: string,
|
|
childrenCount: number
|
|
) {
|
|
const ids = await getContainerChildIds(page, containerId);
|
|
|
|
await assertContainerOfElements(page, ids, containerId);
|
|
expect(new Set(ids).size).toBe(childrenCount);
|
|
}
|
|
|
|
export async function assertCanvasElementsCount(page: Page, expected: number) {
|
|
const number = await getCanvasElementsCount(page);
|
|
expect(number).toEqual(expected);
|
|
}
|
|
export function assertBound(received: Bound, expected: Bound) {
|
|
expect(received[0]).toBeCloseTo(expected[0], 0);
|
|
expect(received[1]).toBeCloseTo(expected[1], 0);
|
|
expect(received[2]).toBeCloseTo(expected[2], 0);
|
|
expect(received[3]).toBeCloseTo(expected[3], 0);
|
|
}
|
|
|
|
export async function assertClipboardItem(
|
|
page: Page,
|
|
data: unknown,
|
|
type: string
|
|
) {
|
|
type Args = [type: string];
|
|
const dataInClipboard = await page.evaluate(
|
|
async ([type]: Args) => {
|
|
const clipItems = await navigator.clipboard.read();
|
|
const item = clipItems.find(item => item.types.includes(type));
|
|
const data = await item?.getType(type);
|
|
return data?.text();
|
|
},
|
|
[type] as Args
|
|
);
|
|
|
|
expect(dataInClipboard).toBe(data);
|
|
}
|
|
|
|
export async function assertClipboardCustomData(
|
|
page: Page,
|
|
type: string,
|
|
data: unknown
|
|
) {
|
|
const dataInClipboard = await getClipboardCustomData(page, type);
|
|
expect(dataInClipboard).toBe(data);
|
|
}
|
|
|
|
export function assertClipData(
|
|
clipItems: { mimeType: string; data: unknown }[],
|
|
expectClipItems: { mimeType: string; data: unknown }[],
|
|
type: string
|
|
) {
|
|
expect(clipItems.find(item => item.mimeType === type)?.data).toBe(
|
|
expectClipItems.find(item => item.mimeType === type)?.data
|
|
);
|
|
}
|
|
|
|
export async function assertHasClass(locator: Locator, className: string) {
|
|
expect(
|
|
(await locator.getAttribute('class'))?.split(' ').includes(className)
|
|
).toEqual(true);
|
|
}
|
|
|
|
export async function assertNotHasClass(locator: Locator, className: string) {
|
|
expect(
|
|
(await locator.getAttribute('class'))?.split(' ').includes(className)
|
|
).toEqual(false);
|
|
}
|
|
|
|
export async function assertNoteSequence(page: Page, expected: string) {
|
|
const actual = await page.locator('.page-visible-index-label').innerText();
|
|
expect(expected).toBe(actual);
|
|
}
|
|
|
|
export async function assertBlockSelections(page: Page, paths: string[]) {
|
|
const selections = await page.evaluate(() => {
|
|
const host = document.querySelector<EditorHost>('editor-host');
|
|
if (!host) {
|
|
throw new Error('editor-host host not found');
|
|
}
|
|
return host.selection.filter('block');
|
|
});
|
|
const actualPaths = selections.map(selection => selection.blockId);
|
|
expect(actualPaths).toEqual(paths);
|
|
}
|
|
|
|
export async function assertTextSelection(
|
|
page: Page,
|
|
from?: {
|
|
blockId: string;
|
|
index: number;
|
|
length: number;
|
|
},
|
|
to?: {
|
|
blockId: string;
|
|
index: number;
|
|
length: number;
|
|
}
|
|
) {
|
|
const selection = await page.evaluate(() => {
|
|
const host = document.querySelector<EditorHost>('editor-host');
|
|
if (!host) {
|
|
throw new Error('editor-host host not found');
|
|
}
|
|
return host.selection.find('text');
|
|
});
|
|
|
|
if (!from && !to) {
|
|
expect(selection).toBeUndefined();
|
|
return;
|
|
}
|
|
|
|
if (from) {
|
|
expect(selection?.from).toEqual(from);
|
|
}
|
|
if (to) {
|
|
expect(selection?.to).toEqual(to);
|
|
}
|
|
}
|
|
|
|
export async function assertConnectorStrokeColor(page: Page, color: string) {
|
|
const colorButton = page
|
|
.locator('edgeless-change-connector-button')
|
|
.locator('edgeless-color-panel')
|
|
.locator(`.color-unit[aria-label="${color}"]`);
|
|
|
|
expect(await colorButton.count()).toBe(1);
|
|
}
|