chore: standardize tsconfig (#9568)

This commit is contained in:
forehalo
2025-01-08 04:07:56 +00:00
parent 39f4b17315
commit c0ed74dfed
151 changed files with 1041 additions and 1566 deletions
+312
View File
@@ -0,0 +1,312 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForAllPagesLoad,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
import { faker } from '@faker-js/faker';
import { hash } from '@node-rs/argon2';
import type { BrowserContext, Cookie, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Assertions } from 'ava';
import { z } from 'zod';
export async function getCurrentMailMessageCount() {
const response = await fetch('http://localhost:8025/api/v2/messages');
const data = await response.json();
return data.total;
}
export async function getLatestMailMessage() {
const response = await fetch('http://localhost:8025/api/v2/messages');
const data = await response.json();
return data.items[0];
}
export async function getTokenFromLatestMailMessage<A extends Assertions>(
test?: A
) {
const tokenRegex = /token=3D([^"&]+)/;
const emailContent = await getLatestMailMessage();
const tokenMatch = emailContent.Content.Body.match(tokenRegex);
const token = tokenMatch
? decodeURIComponent(tokenMatch[1].replace(/=\r\n/, ''))
: null;
test?.truthy(token);
return token;
}
export async function getLoginCookie(
context: BrowserContext
): Promise<Cookie | undefined> {
return (await context.cookies()).find(c => c.name === 'sid');
}
const cloudUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
password: z.string(),
});
export const runPrisma = async <T>(
cb: (
prisma: InstanceType<
// oxlint-disable-next-line @typescript-eslint/consistent-type-imports
typeof import('../../../../packages/backend/server/node_modules/@prisma/client').PrismaClient
>
) => Promise<T>
): Promise<T> => {
const {
PrismaClient,
// oxlint-disable-next-line @typescript-eslint/consistent-type-imports
} = await import(
'../../../../packages/backend/server/node_modules/@prisma/client'
);
const client = new PrismaClient({
datasourceUrl:
process.env.DATABASE_URL ||
'postgresql://affine:affine@localhost:5432/affine',
});
await client.$connect();
try {
return await cb(client);
} finally {
await client.$disconnect();
}
};
export async function addUserToWorkspace(
workspaceId: string,
userId: string,
permission: number
) {
await runPrisma(async client => {
const workspace = await client.workspace.findUnique({
where: {
id: workspaceId,
},
});
if (workspace == null) {
throw new Error(`workspace ${workspaceId} not found`);
}
await client.workspaceUserPermission.create({
data: {
workspaceId: workspace.id,
userId,
accepted: true,
status: 'Accepted',
type: permission,
},
});
});
}
export async function createRandomUser(): Promise<{
name: string;
email: string;
password: string;
id: string;
}> {
const startTime = Date.now();
const user = {
name: faker.internet.username(),
email: faker.internet.email().toLowerCase(),
password: '123456',
};
const result = await runPrisma(async client => {
const featureId = await client.feature
.findFirst({
where: { feature: 'free_plan_v1' },
select: { id: true },
orderBy: { version: 'desc' },
})
.then(f => f!.id);
await client.user.create({
data: {
...user,
emailVerifiedAt: new Date(),
password: await hash(user.password),
features: {
create: {
reason: 'created by test case',
activated: true,
featureId,
},
},
},
});
return await client.user.findUnique({
where: {
email: user.email,
},
});
});
const endTime = Date.now();
console.log(`createRandomUser takes: ${endTime - startTime}ms`);
cloudUserSchema.parse(result);
return {
...result,
password: user.password,
} as any;
}
export async function createRandomAIUser(): Promise<{
name: string;
email: string;
password: string;
id: string;
}> {
const user = {
name: faker.internet.username(),
email: faker.internet.email().toLowerCase(),
password: '123456',
};
const result = await runPrisma(async client => {
const freeFeatureId = await client.feature
.findFirst({
where: { feature: 'free_plan_v1' },
select: { id: true },
orderBy: { version: 'desc' },
})
.then(f => f!.id);
const aiFeatureId = await client.feature
.findFirst({
where: { feature: 'unlimited_copilot' },
select: { id: true },
orderBy: { version: 'desc' },
})
.then(f => f!.id);
await client.user.create({
data: {
...user,
emailVerifiedAt: new Date(),
password: await hash(user.password),
features: {
create: [
{
reason: 'created by test case',
activated: true,
featureId: freeFeatureId,
},
{
reason: 'created by test case',
activated: true,
featureId: aiFeatureId,
},
],
},
},
});
return await client.user.findUnique({
where: {
email: user.email,
},
});
});
cloudUserSchema.parse(result);
return {
...result,
password: user.password,
} as any;
}
export async function deleteUser(email: string) {
await runPrisma(async client => {
await client.user.delete({
where: {
email,
},
});
});
}
export async function loginUser(
page: Page,
user: {
email: string;
password: string;
},
config?: {
isElectron?: boolean;
beforeLogin?: () => Promise<void>;
afterLogin?: () => Promise<void>;
}
) {
if (config?.isElectron !== true) {
await openHomePage(page);
await waitForEditorLoad(page);
}
await page.getByTestId('sidebar-user-avatar').click({
delay: 200,
});
await loginUserDirectly(page, user, config);
}
export async function loginUserDirectly(
page: Page,
user: {
email: string;
password: string;
},
config?: {
isElectron?: boolean;
beforeLogin?: () => Promise<void>;
afterLogin?: () => Promise<void>;
}
) {
await page.getByPlaceholder('Enter your email address').fill(user.email);
await page.getByTestId('continue-login-button').click({
delay: 200,
});
await page.getByTestId('password-input').fill(user.password);
if (config?.beforeLogin) {
await config.beforeLogin();
}
await page.waitForTimeout(200);
const signIn = page.getByTestId('sign-in-button');
await signIn.click();
await signIn.waitFor({ state: 'detached' });
await page.waitForTimeout(200);
if (config?.afterLogin) {
await config.afterLogin();
}
}
export async function enableCloudWorkspace(page: Page) {
await clickSideBarSettingButton(page);
await page.getByTestId('current-workspace-label').click();
await page.getByTestId('publish-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click();
// wait for upload and delete local workspace
await page.waitForTimeout(2000);
await waitForAllPagesLoad(page);
await clickNewPageButton(page);
}
export async function enableCloudWorkspaceFromShareButton(page: Page) {
const shareMenuButton = page.getByTestId('local-share-menu-button');
await expect(shareMenuButton).toBeVisible();
await shareMenuButton.click();
await expect(page.getByTestId('local-share-menu')).toBeVisible();
await page.getByTestId('share-menu-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click();
// wait for upload and delete local workspace
await page.waitForTimeout(2000);
await waitForEditorLoad(page);
await clickNewPageButton(page);
}
export async function enableShare(page: Page) {
await page.getByTestId('cloud-share-menu-button').click();
await page.getByTestId('share-link-menu-trigger').click();
await page.getByTestId('share-link-menu-enable-share').click();
}
+35
View File
@@ -0,0 +1,35 @@
import type { Page } from '@playwright/test';
export const dropFile = async (
page: Page,
selector: string,
fileContent: Buffer | string,
fileName: string,
fileType = ''
) => {
const buffer =
typeof fileContent === 'string'
? Buffer.from(fileContent, 'utf-8')
: fileContent;
const dataTransfer = await page.evaluateHandle(
async ({ bufferData, localFileName, localFileType }) => {
const dt = new DataTransfer();
const blobData = await fetch(bufferData).then(res => res.blob());
const file = new File([blobData], localFileName, { type: localFileType });
dt.items.add(file);
return dt;
},
{
bufferData: `data:application/octet-stream;base64,${buffer.toString(
'base64'
)}`,
localFileName: fileName,
localFileType: fileType,
}
);
await page.dispatchEvent(selector, 'drop', { dataTransfer });
};
+43
View File
@@ -0,0 +1,43 @@
import { expect, type Page } from '@playwright/test';
export function locateModeSwitchButton(
page: Page,
mode: 'page' | 'edgeless',
active?: boolean
) {
// switch is implemented as RadioGroup button,
// so we can use aria-checked to determine the active state
const checkedSelector = active ? '[aria-checked="true"]' : '';
return page.locator(
`[data-testid="switch-${mode}-mode-button"]${checkedSelector}`
);
}
export async function clickEdgelessModeButton(page: Page) {
await locateModeSwitchButton(page, 'edgeless').click({ delay: 50 });
await ensureInEdgelessMode(page);
}
export async function clickPageModeButton(page: Page) {
await locateModeSwitchButton(page, 'page').click({ delay: 50 });
await ensureInPageMode(page);
}
export async function ensureInPageMode(page: Page) {
await expect(locateModeSwitchButton(page, 'page', true)).toBeVisible();
}
export async function ensureInEdgelessMode(page: Page) {
await expect(locateModeSwitchButton(page, 'edgeless', true)).toBeVisible();
}
export async function getPageMode(page: Page): Promise<'page' | 'edgeless'> {
if (await locateModeSwitchButton(page, 'page', true).isVisible()) {
return 'page';
}
if (await locateModeSwitchButton(page, 'edgeless', true).isVisible()) {
return 'edgeless';
}
throw new Error('Unknown mode');
}
+146
View File
@@ -0,0 +1,146 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { clickNewPageButton, getBlockSuiteEditorTitle } from './page-logic';
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
export const createFirstFilter = async (page: Page, name: string) => {
await page.locator('[data-testid="create-first-filter"]').click();
await page
.locator('[data-testid="variable-select-item"]', { hasText: name })
.click();
await page.keyboard.press('Escape');
};
export const checkFilterName = async (page: Page, name: string) => {
const filterName = await page
.locator('[data-testid="filter-name"]')
.textContent();
expect(filterName).toBe(name);
};
const dateFormat = (date: Date) => {
const month = monthNames[date.getMonth()];
const day = date.getDate().toString().padStart(2, '0');
return `${month} ${day}`;
};
// fixme: there could be multiple page lists in the Page
export const getPagesCount = async (page: Page) => {
const locator = page.locator('[data-testid="virtualized-page-list"]');
const pageListCount = await locator.count();
if (pageListCount === 0) {
return 0;
}
// locator is not a HTMLElement, so we can't use dataset
// eslint-disable-next-line unicorn/prefer-dom-node-dataset
const count = await locator.getAttribute('data-total-count');
return count ? parseInt(count) : 0;
};
export const checkDatePicker = async (page: Page, date: Date) => {
expect(
await page
.locator('[data-testid="filter-arg"]')
.locator('input')
.inputValue()
).toBe(dateFormat(date));
};
export const clickDatePicker = async (page: Page) => {
await page.locator('[data-testid="filter-arg"]').locator('input').click();
};
const clickMonthPicker = async (page: Page) => {
await page.locator('[data-testid="month-picker-button"]').click();
};
export const fillDatePicker = async (page: Page, date: Date) => {
await page
.locator('[data-testid="filter-arg"]')
.locator('input')
.fill(dateFormat(date));
};
export const selectMonthFromMonthPicker = async (page: Page, date: Date) => {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
// Open the month picker popup
await clickMonthPicker(page);
const selectMonth = async (): Promise<void> => {
const selectedYear = +(await page
.getByTestId('month-picker-current-year')
.innerText());
if (selectedYear > year) {
await page.locator('[data-testid="date-picker-nav-prev"]').click();
return await selectMonth();
} else if (selectedYear < year) {
await page.locator('[data-testid="date-picker-nav-next"]').click();
return await selectMonth();
}
// Click on the day cell
const monthCell = page.locator(
`[data-is-month-cell][aria-label="${year}-${month}"]`
);
await monthCell.click();
};
await selectMonth();
};
export const checkDatePickerMonth = async (page: Page, date: Date) => {
expect(
await page.getByTestId('month-picker-button').evaluate(e => e.dataset.month)
).toBe(date.getMonth().toString());
};
const createTag = async (page: Page, name: string) => {
await page.keyboard.type(name);
await page.keyboard.press('ArrowUp');
await page.keyboard.press('Enter');
};
export const createPageWithTag = async (
page: Page,
options: {
title: string;
tags: string[];
}
) => {
await page.getByTestId('all-pages').click();
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page');
await page.getByTestId('page-info-collapse').click();
await page.locator('[data-testid="property-tags-value"]').click();
for (const name of options.tags) {
await createTag(page, name);
}
await page.keyboard.press('Escape');
};
export const changeFilter = async (page: Page, to: string) => {
await page.getByTestId('filter-name').click();
await page.getByTestId(`filler-tag-${to}`).click();
};
export async function selectTag(page: Page, name: string | RegExp) {
await page.getByTestId('filter-arg').click();
await page.getByTestId(`multi-select-${name}`).click();
await page.keyboard.press('Escape', { delay: 100 });
}
+24
View File
@@ -0,0 +1,24 @@
import { Path } from '@affine-tools/utils/path';
import type { Page } from '@playwright/test';
export async function importImage(page: Page, pathInFixtures: string) {
await page.evaluate(() => {
// Force fallback to input[type=file] in tests
// See https://github.com/microsoft/playwright/issues/8850
// @ts-expect-error allow
window.showOpenFilePicker = undefined;
});
const fileChooser = page.waitForEvent('filechooser');
// open slash menu
await page.keyboard.type('/image', { delay: 50 });
await page.keyboard.press('Enter');
await (
await fileChooser
).setFiles(
Path.dir(import.meta.url).join('../../../fixtures', pathInFixtures).value
);
// TODO(@catsjuice): wait for image to be loaded more reliably
await page.waitForTimeout(1000);
}
+94
View File
@@ -0,0 +1,94 @@
import type { Page } from '@playwright/test';
const IS_MAC = process.platform === 'darwin';
async function keyDownCtrlOrMeta(page: Page) {
if (IS_MAC) {
await page.keyboard.down('Meta');
} else {
await page.keyboard.down('Control');
}
}
async function keyUpCtrlOrMeta(page: Page) {
if (IS_MAC) {
await page.keyboard.up('Meta');
} else {
await page.keyboard.up('Control');
}
}
// It's not good enough, but better than calling keyDownCtrlOrMeta and keyUpCtrlOrMeta separately
export const withCtrlOrMeta = async (page: Page, fn: () => Promise<void>) => {
await keyDownCtrlOrMeta(page);
await fn();
await keyUpCtrlOrMeta(page);
};
export async function pressEnter(page: Page) {
// avoid flaky test by simulate real user input
await page.keyboard.press('Enter', { delay: 50 });
}
export async function pressTab(page: Page) {
await page.keyboard.press('Tab', { delay: 50 });
}
export async function pressShiftTab(page: Page) {
await page.keyboard.down('Shift');
await page.keyboard.press('Tab', { delay: 50 });
await page.keyboard.up('Shift');
}
export async function pressShiftEnter(page: Page) {
await page.keyboard.down('Shift');
await page.keyboard.press('Enter', { delay: 50 });
await page.keyboard.up('Shift');
}
export async function copyByKeyboard(page: Page) {
await keyDownCtrlOrMeta(page);
await page.keyboard.press('c', { delay: 50 });
await keyUpCtrlOrMeta(page);
}
export async function cutByKeyboard(page: Page) {
await keyDownCtrlOrMeta(page);
await page.keyboard.press('x', { delay: 50 });
await keyUpCtrlOrMeta(page);
}
export async function pasteByKeyboard(page: Page) {
await keyDownCtrlOrMeta(page);
await page.keyboard.press('v', { delay: 50 });
await keyUpCtrlOrMeta(page);
}
export async function selectAllByKeyboard(page: Page) {
await keyDownCtrlOrMeta(page);
await page.keyboard.press('a', { delay: 50 });
await keyUpCtrlOrMeta(page);
}
export async function writeTextToClipboard(page: Page, text: string) {
// paste the url
await page.evaluate(
async ([text]) => {
const clipData = {
'text/plain': text,
};
const e = new ClipboardEvent('paste', {
clipboardData: new DataTransfer(),
});
Object.defineProperty(e, 'target', {
writable: false,
value: document,
});
Object.entries(clipData).forEach(([key, value]) => {
e.clipboardData?.setData(key, value);
});
document.dispatchEvent(e);
},
[text]
);
}
+23
View File
@@ -0,0 +1,23 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
export let coreUrl = 'http://localhost:8080';
export function setCoreUrl(url: string) {
coreUrl = url;
}
export async function openHomePage(page: Page) {
await page.goto(coreUrl);
}
export async function open404Page(page: Page) {
await page.goto(`${coreUrl}/404`);
}
export async function openJournalsPage(page: Page) {
await page.getByTestId('slider-bar-journals-button').click();
await expect(
page.locator('.doc-title-container:has-text("Today")')
).toBeVisible();
}
+219
View File
@@ -0,0 +1,219 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
export function getAllPage(page: Page) {
const newPageButton = page.getByTestId('new-page-button-trigger');
const newPageDropdown = newPageButton.locator('svg');
const edgelessBlockCard = page.getByTestId('new-edgeless-button-in-all-page');
async function clickNewPageButton() {
const newPageButton = page.getByTestId('new-page-button-trigger');
return await newPageButton.click();
}
async function clickNewEdgelessDropdown() {
await newPageDropdown.click();
await edgelessBlockCard.click();
}
return { clickNewPageButton, clickNewEdgelessDropdown };
}
export async function waitForEditorLoad(page: Page) {
await page.waitForSelector('v-line', {
timeout: 20000,
});
}
export async function waitForAllPagesLoad(page: Page) {
// if filters tag is rendered, we believe all_pages is ready
await page.waitForSelector('[data-testid="create-first-filter"]', {
timeout: 20000,
});
}
export async function clickNewPageButton(page: Page, title?: string) {
// FiXME: when the page is in edgeless mode, clickNewPageButton will create a new edgeless page
const edgelessPage = page.locator('edgeless-editor');
if (await edgelessPage.isVisible()) {
await page.getByTestId('switch-page-mode-button').click({
delay: 100,
});
}
// fixme(himself65): if too fast, the page will crash
await page.getByTestId('sidebar-new-page-button').click({
delay: 100,
});
await waitForEmptyEditor(page);
if (title) {
await getBlockSuiteEditorTitle(page).fill(title);
}
}
export async function waitForEmptyEditor(page: Page) {
await expect(page.locator('.doc-title-container-empty')).toBeVisible();
}
export function getBlockSuiteEditorTitle(page: Page) {
return page.locator('doc-title .inline-editor').nth(0);
}
export async function type(page: Page, content: string, delay = 50) {
await page.keyboard.type(content, { delay });
}
export const createLinkedPage = async (page: Page, pageName?: string) => {
// fixme: workaround for @ popover not showing up when editor is not ready
await page.waitForTimeout(500);
await page.keyboard.type('@', { delay: 50 });
const linkedPagePopover = page.locator('.linked-doc-popover');
await expect(linkedPagePopover).toBeVisible();
await type(page, pageName || 'Untitled');
await linkedPagePopover
.locator(`icon-button`)
.filter({ hasText: `New "${pageName}" page` })
.click();
};
export async function clickPageMoreActions(page: Page) {
return page
.getByTestId('header')
.getByTestId('header-dropDownButton')
.click();
}
export const getPageOperationButton = (page: Page, id: string) => {
return getPageItem(page, id).getByTestId('page-list-operation-button');
};
export const getPageItem = (page: Page, id: string) => {
return page.locator(`[data-page-id="${id}"][data-testid="page-list-item"]`);
};
export const getPageByTitle = (page: Page, title: string) => {
return page.getByTestId('page-list-item').getByText(title);
};
export type DragLocation =
| 'top-left'
| 'top'
| 'bottom'
| 'center'
| 'left'
| 'right';
export const toPosition = (
rect: {
x: number;
y: number;
width: number;
height: number;
},
location: DragLocation
) => {
switch (location) {
case 'center':
return {
x: rect.width / 2,
y: rect.height / 2,
};
case 'top':
return { x: rect.width / 2, y: 1 };
case 'bottom':
return { x: rect.width / 2, y: rect.height - 1 };
case 'left':
return { x: 1, y: rect.height / 2 };
case 'right':
return { x: rect.width - 1, y: rect.height / 2 };
case 'top-left':
default:
return { x: 1, y: 1 };
}
};
export const dragTo = async (
page: Page,
locator: Locator,
target: Locator,
location: DragLocation = 'center'
) => {
await locator.hover();
const locatorElement = await locator.boundingBox();
if (!locatorElement) {
throw new Error('locator element not found');
}
const locatorCenter = toPosition(locatorElement, 'center');
await page.mouse.move(
locatorElement.x + locatorCenter.x,
locatorElement.y + locatorCenter.y
);
await page.mouse.down();
await page.waitForTimeout(100);
await page.mouse.move(
locatorElement.x + locatorCenter.x + 1,
locatorElement.y + locatorCenter.y + 1
);
await page.mouse.move(1, 1, {
steps: 10,
});
await target.hover();
const targetElement = await target.boundingBox();
if (!targetElement) {
throw new Error('target element not found');
}
const targetPosition = toPosition(targetElement, location);
await page.mouse.move(
targetElement.x + targetPosition.x,
targetElement.y + targetPosition.y,
{
steps: 10,
}
);
await page.waitForTimeout(100);
await page.mouse.up();
};
// sometimes editor loses focus, this function is to focus the editor
export const focusInlineEditor = async (page: Page) => {
await page
.locator(
`.affine-paragraph-rich-text-wrapper:has(.visible):has-text("Type '/' for commands")`
)
.locator('.inline-editor')
.focus();
};
export const addDatabase = async (page: Page, title?: string) => {
await page.keyboard.press('/');
await expect(page.locator('affine-slash-menu .slash-menu')).toBeVisible();
await page.keyboard.type('database');
await page.getByTestId('Table View').click();
if (title) {
await page.locator('affine-database-title').click();
await page
.locator(
'affine-database-title textarea[data-block-is-database-title="true"]'
)
.fill(title);
await page
.locator(
'affine-database-title textarea[data-block-is-database-title="true"]'
)
.blur();
}
};
export const addDatabaseRow = async (page: Page, databaseTitle: string) => {
const db = page.locator(`affine-database-table`, {
has: page.locator(`affine-database-title:has-text("${databaseTitle}")`),
});
await db.locator('.data-view-table-group-add-row-button').click();
};
+198
View File
@@ -0,0 +1,198 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
export const getPropertyValueLocator = (page: Page, property: string) => {
return page.locator(
`[data-testid="doc-property-name"]:has-text("${property}") + *`
);
};
export const ensurePagePropertiesVisible = async (page: Page) => {
if (
await page
.getByRole('button', {
name: 'Add property',
})
.isVisible()
) {
return;
}
await page.getByTestId('page-info-collapse').click();
};
export const clickPropertyValue = async (page: Page, property: string) => {
await getPropertyValueLocator(page, property).click();
};
export const openTagsEditor = async (page: Page) => {
await clickPropertyValue(page, 'tags');
await expect(page.getByTestId('tags-editor-popup')).toBeVisible();
};
export const closeTagsEditor = async (page: Page) => {
await page.keyboard.press('Escape');
await expect(page.getByTestId('tags-editor-popup')).not.toBeVisible();
};
export const clickTagFromSelector = async (page: Page, name: string) => {
// assume that the tags editor is already open
await page
.locator(`[data-testid="tag-selector-item"][data-tag-value="${name}"]`)
.click();
};
export const removeSelectedTag = async (page: Page, name: string) => {
await page
.locator(
`[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] [data-tag-value="${name}"] [data-testid="remove-tag-button"]`
)
.click();
};
export const filterTags = async (page: Page, filter: string) => {
await page
.locator(
'[data-testid="tags-editor-popup"] [data-testid="inline-tags-list"] input'
)
.fill(filter);
};
export const searchAndCreateTag = async (page: Page, name: string) => {
await filterTags(page, name);
await page
.locator(
'[data-testid="tags-editor-popup"] [data-testid="tag-selector-item"]:has-text("Create ")'
)
.click();
};
export const expectTagsVisible = async (
root: Locator | Page,
tags: string[]
) => {
const tagListPanel = root
.getByTestId('property-tags-value')
.getByTestId('inline-tags-list');
expect(await tagListPanel.locator('[data-tag-value]').count()).toBe(
tags.length
);
for (const tag of tags) {
await expect(
tagListPanel.locator(`[data-tag-value="${tag}"]`)
).toBeVisible();
}
};
export const clickAddPropertyButton = async (root: Locator | Page) => {
await root
.getByRole('button', {
name: 'Add property',
})
.click();
};
export const ensureAddPropertyButtonVisible = async (
page: Page,
root: Locator | Page
) => {
if (
await root
.getByRole('button', {
name: 'Add property',
})
.isVisible()
) {
return;
}
await togglePropertyListVisibility(page);
await page.waitForTimeout(500);
await expect(
root.getByRole('button', { name: 'Add property' })
).toBeVisible();
};
export const togglePropertyListVisibility = async (page: Page) => {
await page.getByTestId('property-collapsible-button').click();
};
export const addCustomProperty = async (
page: Page,
root: Locator | Page,
type: string
) => {
ensureAddPropertyButtonVisible(page, root);
await clickAddPropertyButton(root);
await page
.locator(
`[data-testid="${'create-property-menu-item'}"][data-property-type="${type}"]`
)
.click();
if (await page.getByTestId('edit-property-menu-item').isVisible()) {
// is edit property menu opened, close it
await page.keyboard.press('Escape');
}
await page.waitForTimeout(500);
};
export const expectPropertyOrdering = async (
page: Page,
properties: string[]
) => {
for (let i = 0; i < properties.length; i++) {
await expect(
page.locator(`[data-testid="page-property-row-name"])`).nth(i)
).toHaveText(properties[i]);
}
};
export const openWorkspaceProperties = async (page: Page) => {
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page
.locator('[data-testid="workspace-list-item"] .setting-name')
.click();
await page.getByTestId('workspace-list-item-workspace:properties').click();
};
export const selectVisibilitySelector = async (
page: Page,
name: string,
option: string
) => {
await page
.getByRole('menu')
.locator(
`[data-testid="page-properties-settings-menu-item"]:has-text("${name}")`
)
.getByRole('button')
.click();
await page
.getByRole('menu')
.last()
.getByRole('menuitem', {
name: option,
exact: true,
})
.click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
};
export const changePropertyVisibility = async (
page: Page,
name: string,
option: string
) => {
await page
.locator(`[data-testid="doc-property-name"]:has-text("${name}")`)
.click();
await page.locator(`[data-property-visibility="${option}"]`).click();
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
};
+52
View File
@@ -0,0 +1,52 @@
import type { Page } from '@playwright/test';
export async function clickCollaborationPanel(page: Page) {
await page.click('[data-tab-key="collaboration"]');
}
export async function clickPublishPanel(page: Page) {
await page.click('[data-tab-key="publish"]');
}
export async function openSettingModal(page: Page) {
await page.getByTestId('settings-modal-trigger').click();
}
export async function openAppearancePanel(page: Page) {
await page.getByTestId('appearance-panel-trigger').click();
}
export async function openEditorSetting(page: Page) {
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('editor-panel-trigger').click();
}
export async function openShortcutsPanel(page: Page) {
await page.getByTestId('shortcuts-panel-trigger').click();
}
export async function openAboutPanel(page: Page) {
await page.getByTestId('about-panel-trigger').click();
}
export async function openExperimentalFeaturesPanel(page: Page) {
await page.getByTestId('experimental-features-trigger').click();
}
export async function confirmExperimentalPrompt(page: Page) {
await page.getByTestId('experimental-prompt-disclaimer').click();
await page.getByTestId('experimental-confirm-button').click();
}
export async function openWorkspaceSettingPanel(
page: Page,
workspaceName: string
) {
await page.getByTestId('settings-sidebar').getByText(workspaceName).click();
}
export async function clickUserInfoCard(page: Page) {
await page.getByTestId('user-info-card').click({
delay: 50,
});
}
+21
View File
@@ -0,0 +1,21 @@
import type { Page } from '@playwright/test';
export async function clickSideBarSettingButton(page: Page) {
return page.getByTestId('slider-bar-workspace-setting-button').click();
}
export async function clickSideBarAllPageButton(page: Page) {
return page.getByTestId('all-pages').click();
}
export async function clickSideBarCurrentWorkspaceBanner(page: Page) {
return page.getByTestId('current-workspace-card').click();
}
export async function clickSideBarUseAvatar(page: Page) {
return page.getByTestId('sidebar-user-avatar').click();
}
export async function clickNewPageButton(page: Page) {
return page.getByTestId('sidebar-new-page-button').click();
}
+24
View File
@@ -0,0 +1,24 @@
import type { Page } from '@playwright/test';
export function getDocIdFromUrl(url: string) {
const pathname = new URL(url).pathname;
const match = pathname.match(/\/workspace\/([^/]+)\/([^/]+)\/?/);
if (match && match[2]) {
return match[2];
}
throw new Error('Failed to get doc id from url');
}
export function getCurrentDocIdFromUrl(page: Page) {
return getDocIdFromUrl(page.url());
}
export function getCurrentCollectionIdFromUrl(page: Page) {
const pathname = new URL(page.url()).pathname;
const match = pathname.match(/\/workspace\/([^/]+)\/collection\/([^/]+)\/?/);
if (match && match[2]) {
return match[2];
}
throw new Error('Failed to get collection id from url');
}
+40
View File
@@ -0,0 +1,40 @@
import { setTimeout } from 'node:timers/promises';
import type { Page } from '@playwright/test';
import fs from 'fs-extra';
export async function waitForLogMessage(
page: Page,
log: string
): Promise<boolean> {
return new Promise(resolve => {
page.on('console', msg => {
if (msg.type() === 'log' && msg.text() === log) {
resolve(true);
}
});
});
}
export async function removeWithRetry(
filePath: string,
maxRetries = 5,
delay = 500
) {
for (let i = 0; i < maxRetries; i++) {
try {
await fs.remove(filePath);
console.log(`File ${filePath} successfully deleted.`);
return true;
} catch (err: any) {
if (err.code === 'EBUSY' || err.code === 'EPERM') {
console.log(`File ${filePath} is busy or locked, retrying...`);
await setTimeout(delay);
} else {
console.error(`Failed to delete file ${filePath}:`, err);
}
}
}
// Add a return statement here to ensure that a value is always returned
return false;
}
+48
View File
@@ -0,0 +1,48 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { waitForEditorLoad } from './page-logic';
interface CreateWorkspaceParams {
name: string;
}
export async function openWorkspaceListModal(page: Page) {
await page.getByTestId('app-sidebar').getByTestId('workspace-name').click({
delay: 50,
});
}
export async function createLocalWorkspace(
params: CreateWorkspaceParams,
page: Page
) {
await openWorkspaceListModal(page);
// open create workspace modal
await page.getByTestId('new-workspace').click();
// const isDesktop: boolean = await page.evaluate(() => {
// return !!window.appInfo?.electron;
// }, []);
// input workspace name
await page.getByPlaceholder('Set a Workspace name').click();
await page.getByPlaceholder('Set a Workspace name').fill(params.name);
// click create button
await page.getByTestId('create-workspace-create-button').click({
delay: 500,
});
await expect(
page.getByTestId('create-workspace-create-button')
).not.toBeAttached();
await waitForEditorLoad(page);
await expect(page.getByTestId('workspace-name')).toHaveText(params.name);
// if (isDesktop) {
// await page.getByTestId('create-workspace-continue-button').click();
// }
}