mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
chore: standardize tsconfig (#9568)
This commit is contained in:
184
tests/kit/src/electron.ts
Normal file
184
tests/kit/src/electron.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { Package } from '@affine-tools/utils/workspace';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
import fs from 'fs-extra';
|
||||
import type { ElectronApplication } from 'playwright';
|
||||
import { _electron as electron } from 'playwright';
|
||||
|
||||
import { test as base, testResultDir } from './playwright';
|
||||
import { removeWithRetry } from './utils/utils';
|
||||
|
||||
const electronRoot = new Package('@affine/electron').path;
|
||||
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
type RoutePath = 'setting';
|
||||
|
||||
const getPageId = async (page: Page) => {
|
||||
return page.evaluate(() => {
|
||||
return (window.__appInfo as any)?.viewId as string;
|
||||
});
|
||||
};
|
||||
|
||||
const isActivePage = async (page: Page) => {
|
||||
return page.evaluate(async () => {
|
||||
return await (window as any).__apis?.ui.isActiveTab();
|
||||
});
|
||||
};
|
||||
|
||||
const getActivePage = async (pages: Page[]) => {
|
||||
for (const page of pages) {
|
||||
if (await isActivePage(page)) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const test = base.extend<{
|
||||
electronApp: ElectronApplication;
|
||||
shell: Page;
|
||||
appInfo: {
|
||||
appPath: string;
|
||||
appData: string;
|
||||
sessionData: string;
|
||||
};
|
||||
views: {
|
||||
getActive: () => Promise<Page>;
|
||||
};
|
||||
router: {
|
||||
goto: (path: RoutePath) => Promise<void>;
|
||||
};
|
||||
}>({
|
||||
shell: async ({ electronApp }, use) => {
|
||||
await expect.poll(() => electronApp.windows().length > 1).toBeTruthy();
|
||||
|
||||
for (const page of electronApp.windows()) {
|
||||
const viewId = await getPageId(page);
|
||||
if (viewId === 'shell') {
|
||||
await use(page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
page: async ({ electronApp }, use) => {
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
return electronApp.windows().length > 1;
|
||||
},
|
||||
{
|
||||
timeout: 50000,
|
||||
}
|
||||
)
|
||||
.toBeTruthy();
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const page = await getActivePage(electronApp.windows());
|
||||
return !!page;
|
||||
},
|
||||
{
|
||||
timeout: 50000,
|
||||
}
|
||||
)
|
||||
.toBeTruthy();
|
||||
|
||||
const page = await getActivePage(electronApp.windows());
|
||||
|
||||
if (!page) {
|
||||
throw new Error('No active page found');
|
||||
}
|
||||
|
||||
// wait for blocksuite to be loaded
|
||||
await page.waitForSelector('v-line');
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.setItem('dismissAiOnboarding', 'true');
|
||||
window.localStorage.setItem('dismissAiOnboardingEdgeless', 'true');
|
||||
window.localStorage.setItem('dismissAiOnboardingLocal', 'true');
|
||||
});
|
||||
|
||||
await page.reload({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// wait until the page is stable enough
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await use(page as Page);
|
||||
},
|
||||
views: async ({ electronApp, page }, use) => {
|
||||
void page; // makes sure page is a dependency
|
||||
await use({
|
||||
getActive: async () => {
|
||||
const view = await getActivePage(electronApp.windows());
|
||||
return view || page;
|
||||
},
|
||||
});
|
||||
},
|
||||
// oxlint-disable-next-line no-empty-pattern
|
||||
electronApp: async ({}, use) => {
|
||||
try {
|
||||
// a random id to avoid conflicts between tests
|
||||
const id = generateUUID();
|
||||
const dist = electronRoot.join('dist').value;
|
||||
const clonedDist = electronRoot.join('e2e-dist-' + id).value;
|
||||
await fs.copy(dist, clonedDist);
|
||||
const packageJson = await fs.readJSON(
|
||||
electronRoot.join('package.json').value
|
||||
);
|
||||
// overwrite the app name
|
||||
packageJson.name = 'affine-test-' + id;
|
||||
// overwrite the path to the main script
|
||||
packageJson.main = './main.js';
|
||||
// write to the cloned dist
|
||||
await fs.writeJSON(clonedDist + '/package.json', packageJson);
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
env.DEBUG = 'pw:browser';
|
||||
|
||||
env.SKIP_ONBOARDING = '1';
|
||||
|
||||
const electronApp = await electron.launch({
|
||||
args: [clonedDist],
|
||||
env,
|
||||
cwd: clonedDist,
|
||||
recordVideo: {
|
||||
dir: testResultDir,
|
||||
},
|
||||
colorScheme: 'light',
|
||||
});
|
||||
|
||||
await use(electronApp);
|
||||
console.log('Cleaning up...');
|
||||
const pages = electronApp.windows();
|
||||
for (const page of pages) {
|
||||
await page.close();
|
||||
}
|
||||
await electronApp.close();
|
||||
await removeWithRetry(clonedDist);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
appInfo: async ({ electronApp }, use) => {
|
||||
const appInfo = await electronApp.evaluate(async ({ app }) => {
|
||||
return {
|
||||
appPath: app.getAppPath(),
|
||||
appData: app.getPath('appData'),
|
||||
sessionData: app.getPath('sessionData'),
|
||||
};
|
||||
});
|
||||
await use(appInfo);
|
||||
},
|
||||
});
|
||||
24
tests/kit/src/mobile.ts
Normal file
24
tests/kit/src/mobile.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test as baseTest } from './playwright';
|
||||
|
||||
type CurrentDocCollection = {
|
||||
meta: { id: string; flavour: string };
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<{
|
||||
workspace: {
|
||||
current: () => Promise<CurrentDocCollection>;
|
||||
};
|
||||
}>({
|
||||
page: async ({ page }, use) => {
|
||||
await page.goto('/');
|
||||
await expect(
|
||||
page.locator('.affine-page-viewport[data-mode="edgeless"]')
|
||||
).toBeVisible({
|
||||
timeout: 30 * 1000,
|
||||
});
|
||||
await page.goto('/');
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
112
tests/kit/src/playwright.ts
Normal file
112
tests/kit/src/playwright.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import { Path, ProjectRoot } from '@affine-tools/utils/path';
|
||||
import type { BrowserContext } from '@playwright/test';
|
||||
import { test as baseTest } from '@playwright/test';
|
||||
|
||||
export { Path, ProjectRoot };
|
||||
export const testResultDir = ProjectRoot.join('test-results').value;
|
||||
export const istanbulTempDir = process.env.ISTANBUL_TEMP_DIR
|
||||
? path.resolve(process.env.ISTANBUL_TEMP_DIR)
|
||||
: ProjectRoot.join('.nyc_output').value;
|
||||
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export const enableCoverage = !!process.env.CI || !!process.env.COVERAGE;
|
||||
|
||||
type CurrentDocCollection = {
|
||||
meta: { id: string; flavour: string };
|
||||
};
|
||||
|
||||
export const skipOnboarding = async (context: BrowserContext) => {
|
||||
await context.addInitScript(() => {
|
||||
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
||||
window.localStorage.setItem('dismissAiOnboarding', 'true');
|
||||
window.localStorage.setItem('dismissAiOnboardingEdgeless', 'true');
|
||||
window.localStorage.setItem('dismissAiOnboardingLocal', 'true');
|
||||
});
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<{
|
||||
workspace: {
|
||||
current: () => Promise<CurrentDocCollection>;
|
||||
};
|
||||
}>({
|
||||
workspace: async ({ page }, use) => {
|
||||
await use({
|
||||
current: async () => {
|
||||
return await page.evaluate(async () => {
|
||||
if (!(globalThis as any).currentWorkspace) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
globalThis.addEventListener(
|
||||
'affine:workspace:change',
|
||||
() => resolve(),
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
setTimeout(() => reject(new Error('timeout')), 5000);
|
||||
});
|
||||
}
|
||||
const currentWorkspace = (globalThis as any).currentWorkspace;
|
||||
return {
|
||||
meta: currentWorkspace.meta,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
page: async ({ page, context }, use) => {
|
||||
if (process.env.CPU_THROTTLE) {
|
||||
const cdpSession = await context.newCDPSession(page);
|
||||
await cdpSession.send('Emulation.setCPUThrottlingRate', {
|
||||
rate: parseInt(process.env.CPU_THROTTLE),
|
||||
});
|
||||
}
|
||||
await use(page);
|
||||
},
|
||||
context: async ({ context }, use) => {
|
||||
// workaround for skipping onboarding redirect on the web
|
||||
await skipOnboarding(context);
|
||||
|
||||
if (enableCoverage) {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
// @ts-expect-error
|
||||
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
|
||||
)
|
||||
);
|
||||
|
||||
await fs.promises.mkdir(istanbulTempDir, { recursive: true });
|
||||
await context.exposeFunction(
|
||||
'collectIstanbulCoverage',
|
||||
(coverageJSON?: string) => {
|
||||
if (coverageJSON)
|
||||
fs.writeFileSync(
|
||||
path.join(
|
||||
istanbulTempDir,
|
||||
`playwright_coverage_${generateUUID()}.json`
|
||||
),
|
||||
coverageJSON
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await use(context);
|
||||
|
||||
if (enableCoverage) {
|
||||
for (const page of context.pages()) {
|
||||
await page.evaluate(() =>
|
||||
// @ts-expect-error
|
||||
window.collectIstanbulCoverage(JSON.stringify(window.__coverage__))
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
312
tests/kit/src/utils/cloud.ts
Normal file
312
tests/kit/src/utils/cloud.ts
Normal 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
tests/kit/src/utils/drop-file.ts
Normal file
35
tests/kit/src/utils/drop-file.ts
Normal 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
tests/kit/src/utils/editor.ts
Normal file
43
tests/kit/src/utils/editor.ts
Normal 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
tests/kit/src/utils/filter.ts
Normal file
146
tests/kit/src/utils/filter.ts
Normal 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
tests/kit/src/utils/image.ts
Normal file
24
tests/kit/src/utils/image.ts
Normal 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
tests/kit/src/utils/keyboard.ts
Normal file
94
tests/kit/src/utils/keyboard.ts
Normal 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
tests/kit/src/utils/load-page.ts
Normal file
23
tests/kit/src/utils/load-page.ts
Normal 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
tests/kit/src/utils/page-logic.ts
Normal file
219
tests/kit/src/utils/page-logic.ts
Normal 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
tests/kit/src/utils/properties.ts
Normal file
198
tests/kit/src/utils/properties.ts
Normal 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
tests/kit/src/utils/setting.ts
Normal file
52
tests/kit/src/utils/setting.ts
Normal 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
tests/kit/src/utils/sidebar.ts
Normal file
21
tests/kit/src/utils/sidebar.ts
Normal 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
tests/kit/src/utils/url.ts
Normal file
24
tests/kit/src/utils/url.ts
Normal 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
tests/kit/src/utils/utils.ts
Normal file
40
tests/kit/src/utils/utils.ts
Normal 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
tests/kit/src/utils/workspace.ts
Normal file
48
tests/kit/src/utils/workspace.ts
Normal 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();
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user