snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 04:01:20 +05:00
commit 702cc698a7
9990 changed files with 1073618 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Click ai-land icon', async ({ page }) => {
test.skip(process.env.CI !== undefined, 'Skip test in CI');
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await page.locator('[data-testid=ai-island]').click();
await expect(page.getByTestId('chat-panel-input-container')).toBeVisible();
});

View File

@@ -0,0 +1,225 @@
/* oxlint-disable unicorn/prefer-dom-node-dataset */
import { test } from '@affine-test/kit/playwright';
import { getPagesCount } from '@affine-test/kit/utils/filter';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getAllPage,
getBlockSuiteEditorTitle,
waitForAllPagesLoad,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test';
test('all page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickSideBarAllPageButton(page);
});
test('all page can create new page', async ({ page }) => {
const { clickNewPageButton } = getAllPage(page);
await openHomePage(page);
await waitForEditorLoad(page);
await clickSideBarAllPageButton(page);
await clickNewPageButton();
const title = getBlockSuiteEditorTitle(page);
await title.fill('this is a new page');
await clickSideBarAllPageButton(page);
const cell = page.getByRole('cell', { name: 'this is a new page' });
expect(cell).not.toBeUndefined();
});
test('all page can create new edgeless page', async ({ page }) => {
const { clickNewEdgelessDropdown } = getAllPage(page);
await openHomePage(page);
await waitForEditorLoad(page);
await clickSideBarAllPageButton(page);
await clickNewEdgelessDropdown();
await expect(page.locator('affine-edgeless-root')).toBeVisible();
});
test('enable selection and use ESC to disable selection', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
// there should be no checkbox in the page list by default
expect(
await page
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
.count()
).toBe(0);
// by shift + clicking [data-testid="doc-list-item"], checkboxes should appear
await page
.locator('[data-testid="doc-list-item"]')
.first()
.click({
modifiers: ['Shift'],
});
// wait for 500ms
await page.waitForTimeout(500);
// there should be checkboxes in the page list now
expect(
await page
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
.count()
).toBeGreaterThan(0);
// esc again, checkboxes should disappear
await page.keyboard.press('Escape');
// wait for 500ms
await page.waitForTimeout(500);
expect(
await page
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
.count()
).toBe(0);
});
test('select two pages and delete', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
const pageCount = await getPagesCount(page);
// by shift + clicking [data-testid="doc-list-item"], checkboxes should appear, and first doc be selected
await page
.locator('[data-testid="doc-list-item"]')
.first()
.click({
modifiers: ['Shift'],
});
// select the first two pages
await page.locator('[data-testid="doc-list-item"]').nth(1).click();
// the floating popover should appear
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
'2 doc(s) selected'
);
// click delete button
await page.locator('[data-testid="list-toolbar-delete"]').click();
// the confirm dialog should appear
await expect(page.getByText('Delete 2 docs?')).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
// check the page count again
await page.waitForTimeout(300);
await expect(
page.locator('[data-testid="floating-toolbar"]')
).not.toBeVisible();
expect(await getPagesCount(page)).toBe(pageCount - 2);
});
test('select three pages with shiftKey and delete', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickNewPageButton(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
const pageCount = await getPagesCount(page);
await page.keyboard.down('Shift');
await page.locator('[data-testid="doc-list-item"]').nth(0).click();
await page.locator('[data-testid="doc-list-item"]').nth(2).click();
await page.keyboard.up('Shift');
// the floating popover should appear
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
'3 doc(s) selected'
);
// click delete button
await page.locator('[data-testid="list-toolbar-delete"]').click();
// the confirm dialog should appear
await expect(page.getByText('Delete 3 docs?')).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
// check the page count again
await page.waitForTimeout(300);
await expect(
page.locator('[data-testid="floating-toolbar"]')
).not.toBeVisible();
expect(await getPagesCount(page)).toBe(pageCount - 3);
});
test('select all and delete closes the floating toolbar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
const pageCount = await getPagesCount(page);
const selectAllButton = page
.getByRole('button', { name: 'Select all' })
.last();
await selectAllButton.hover();
await selectAllButton.click();
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
await page.locator('[data-testid="list-toolbar-delete"]').click();
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForTimeout(300);
await expect(
page.locator('[data-testid="floating-toolbar"]')
).not.toBeVisible();
expect(await getPagesCount(page)).toBeLessThan(pageCount);
});
test('create a tag and delete it', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-tags-button').click();
// create a tag
await page.getByTestId('all-tags-new-button').click();
await expect(page.getByTestId('edit-tag-modal')).toBeVisible();
await page.getByTestId('edit-tag-input').fill('test-tag');
await page.getByTestId('save-tag').click();
// check the tag is created
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
await page.getByTestId('workspace-tags-button').click();
const cell = page.getByTestId('tag-list-item').getByText('test-tag');
await expect(cell).toBeVisible();
// delete the tag
await page.getByTestId('tag-item-operation-button').click();
await page.getByTestId('delete-tag').click();
await page.getByTestId('confirm-modal-confirm').getByText('Delete').click();
await page.waitForURL(url => url.pathname.endsWith('tag'));
const newCell = page.getByTestId('tag-list-item').getByText('test-tag');
await expect(newCell).not.toBeVisible();
});

View File

@@ -0,0 +1,462 @@
import { test } from '@affine-test/kit/playwright';
import { importAttachment } from '@affine-test/kit/utils/attachment';
import { locateToolbar } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import {
confirmExperimentalPrompt,
openEditorSetting,
openExperimentalFeaturesPanel,
} from '@affine-test/kit/utils/setting';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
async function clickPeekViewControl(page: Page, n = 0) {
await page.getByTestId('peek-view-control').nth(n).click();
await page.waitForTimeout(500);
}
async function enablePDFEmbedView(page: Page) {
// Opens settings panel
await openEditorSetting(page);
await openExperimentalFeaturesPanel(page);
await confirmExperimentalPrompt(page);
const settingModal = page.locator('[data-testid=setting-modal-content]');
const item = settingModal.locator('div').getByText('PDF embed preview');
await item.waitFor({ state: 'attached' });
await expect(item).toBeVisible();
const button = item.locator('label');
const isChecked = await button.locator('input').isChecked();
if (!isChecked) {
await button.click();
}
// Closes settings panel
await page.keyboard.press('Escape');
}
test('attachment preview should be shown', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
await page.locator('affine-attachment').first().dblclick();
const attachmentViewer = page.getByTestId('pdf-viewer');
await expect(attachmentViewer).toBeVisible();
await page.waitForTimeout(500);
const pageCursor = attachmentViewer.locator('.page-cursor');
expect(await pageCursor.textContent()).toBe('1');
const pageCount = attachmentViewer.locator('.page-count');
expect(await pageCount.textContent()).toBe('3');
const thumbnails = attachmentViewer.locator('.thumbnails');
await thumbnails.locator('button').click();
await page.waitForTimeout(500);
expect(
await thumbnails
.getByTestId('virtuoso-item-list')
.locator('[data-item-index]')
.count()
).toBe(3);
await clickPeekViewControl(page);
await expect(attachmentViewer).not.toBeVisible();
});
test('attachment preview can be expanded', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
await page.locator('affine-attachment').first().dblclick();
const attachmentViewer = page.getByTestId('pdf-viewer');
await page.waitForTimeout(500);
await expect(attachmentViewer).toBeVisible();
await clickPeekViewControl(page, 1);
await page.waitForTimeout(500);
const pageCursor = attachmentViewer.locator('.page-cursor');
expect(await pageCursor.textContent()).toBe('1');
const pageCount = attachmentViewer.locator('.page-count');
expect(await pageCount.textContent()).toBe('3');
const thumbnails = attachmentViewer.locator('.thumbnails');
await thumbnails.locator('button').click();
await page.waitForTimeout(500);
expect(
await thumbnails
.getByTestId('virtuoso-item-list')
.locator('[data-item-index]')
.count()
).toBe(3);
});
test('should preview PDF in embed view', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.type('PDF preview');
await enablePDFEmbedView(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await title.click();
await page.keyboard.type('PDF page');
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
await page.waitForTimeout(500);
const portal = attachment.locator('lit-react-portal');
await expect(portal).toBeVisible();
await attachment.click();
await page.waitForTimeout(500);
const pageCursor = portal.locator('.page-cursor');
expect(await pageCursor.textContent()).toBe('1');
const pageCount = portal.locator('.page-count');
expect(await pageCount.textContent()).toBe('3');
const prevButton = portal.getByRole('button', { name: 'Prev' });
const nextButton = portal.getByRole('button', { name: 'Next' });
await nextButton.click();
expect(await pageCursor.textContent()).toBe('2');
await nextButton.click();
expect(await pageCursor.textContent()).toBe('3');
await prevButton.click();
expect(await pageCursor.textContent()).toBe('2');
// Title alias
{
await page.keyboard.press('Enter');
await page.keyboard.press('@');
const doc0 = page.locator('.linked-doc-popover').getByText('PDF preview');
await doc0.click();
await page.keyboard.press('@');
const doc1 = page.locator('.linked-doc-popover').getByText('PDF page');
await doc1.click();
const inlineLink = page.locator('affine-reference').nth(0);
const inlineTitle = inlineLink.locator('.affine-reference-title');
await expect(inlineTitle).toHaveText('PDF preview');
const bouding = await inlineLink.boundingBox();
expect(bouding).not.toBeNull();
await page.mouse.move(bouding!.x - 50, bouding!.y + bouding!.height / 2);
await page.waitForTimeout(500);
await page.mouse.click(bouding!.x - 50, bouding!.y + bouding!.height / 2);
await inlineLink.hover();
// Edits title
await toolbar.getByRole('button', { name: 'Edit' }).click();
// Title alias
await page.keyboard.type('PDF embed preview');
await page.keyboard.press('Enter');
await expect(inlineTitle).toHaveText('PDF embed preview');
}
// PDF embed view should not be re-rendered
expect(await pageCursor.textContent()).toBe('2');
expect(await pageCount.textContent()).toBe('3');
// Chagnes origin title
{
const inlineLink = page.locator('affine-reference').nth(1);
const inlineTitle = inlineLink.locator('.affine-reference-title');
await expect(inlineTitle).toHaveText('PDF page');
await title.click();
await page.keyboard.type(' preview');
await expect(inlineTitle).toHaveText('PDF page preview');
}
// PDF embed view should not be re-rendered
expect(await pageCursor.textContent()).toBe('2');
expect(await pageCount.textContent()).toBe('3');
});
test('should sync name in pdf embed view', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await enablePDFEmbedView(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const attachmentTitle = attachment.locator(
'.affine-attachment-content-title-text'
);
await expect(attachmentTitle).toHaveText('lorem-ipsum.pdf');
// Renames
await toolbar.getByRole('button', { name: 'Rename' }).click();
const input = page
.locator('.affine-attachment-rename-input-wrapper')
.locator('input');
await input.fill('What is Lorem Ipsum');
await page.keyboard.press('Enter');
await expect(attachmentTitle).toHaveText('What is Lorem Ipsum.pdf');
await attachment.click();
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
await page.waitForTimeout(500);
const portal = attachment.locator('lit-react-portal');
const portalName = portal.locator('.pdf-name');
await expect(portal).toBeVisible();
await page.waitForTimeout(500);
await expect(portalName).toHaveText('What is Lorem Ipsum.pdf');
await attachment.click();
// Renames
await toolbar.getByRole('button', { name: 'Rename' }).click();
await input.fill('lorem-ipsum');
await page.keyboard.press('Enter');
await expect(portalName).toHaveText('lorem-ipsum.pdf');
});
test('should enable pointer event in pdf viewer', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.type('PDF preview');
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const attachmentSelection = attachment.locator(
'.affine-attachment-container.focused'
);
const toolbar = locateToolbar(page);
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
await attachment
.locator('.affine-attachment-embed-container')
.waitFor({ state: 'visible' });
const rect = await attachment.boundingBox();
expect(rect).not.toBeNull();
const { x, y, width, height } = rect!;
const startPoint = [x - 10, y - 10];
const endPoint = [x + width / 2, y + height / 2];
await page.mouse.move(startPoint[0], startPoint[1]);
await page.mouse.down();
await page.mouse.move(endPoint[0], endPoint[1]);
await page.mouse.move(
endPoint[0] + width / 2 + 10,
endPoint[1] + height / 2 + 10
);
await expect(attachmentSelection).toBeVisible();
await page.mouse.move(startPoint[0], startPoint[1]);
await expect(attachmentSelection).toBeHidden();
await page.mouse.move(startPoint[0] - 50, startPoint[1] - 50);
await page.mouse.up();
await expect(attachmentSelection).toBeHidden();
});
test('should re-render pdf viewer', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const toolbar = locateToolbar(page);
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
const portal = attachment.locator('lit-react-portal');
await expect(portal).toBeVisible();
const portalId = await portal.getAttribute('portalid');
expect(portalId).not.toBeNull();
const moreMenu = toolbar.getByLabel('More menu');
await moreMenu.click();
await toolbar.getByRole('button', { name: 'Reload' }).click();
const newPortalId = await portal.getAttribute('portalid');
expect(newPortalId).not.toBeNull();
expect(portalId).not.toEqual(newPortalId);
});
test('should display status when an error is thrown in peek view', async ({
context,
page,
}) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const toolbar = locateToolbar(page);
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
await context.setOffline(true);
await attachment.dblclick();
// Peek view
const pdfViewer = page.getByTestId('pdf-viewer');
await expect(pdfViewer).toBeHidden();
const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
await expect(statusWrapper).toBeVisible();
});
test('should display 404 when attachment is not found', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment');
await attachment.click();
const toolbar = locateToolbar(page);
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
await attachment.dblclick();
// Peek view
const pdfViewer = page.getByTestId('pdf-viewer');
await expect(pdfViewer).toBeVisible();
const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
await expect(statusWrapper).toBeHidden();
await clickPeekViewControl(page, 1);
await page.goto(page.url() + 'test');
await expect(pdfViewer).toBeHidden();
await expect(page.getByTestId('not-found')).toBeVisible();
});

View File

@@ -0,0 +1,28 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test('at menu should not be close when press arrow key', async ({ page }) => {
await page.keyboard.press('Enter');
await type(page, '@');
const atMenu = page.locator('.linked-doc-popover');
await expect(atMenu).toBeVisible();
await page.keyboard.press('ArrowRight');
await expect(atMenu).toBeVisible();
await page.keyboard.press('ArrowLeft');
await expect(atMenu).toBeVisible();
});

View File

@@ -0,0 +1,118 @@
import { test } from '@affine-test/kit/playwright';
import {
importAttachment,
importFile,
} from '@affine-test/kit/utils/attachment';
import {
clickEdgelessModeButton,
clickView,
locateToolbar,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
});
test.describe('Replaces attachment', () => {
test('should replace attachment in page', async ({ page }) => {
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment').first();
await attachment.click();
const name = attachment.locator('.affine-attachment-content-title-text');
await expect(name).toHaveText('lorem-ipsum.pdf');
const toolbar = locateToolbar(page);
const replaceButton = toolbar.getByLabel('Replace attachment');
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
await replaceButton.click({ delay: 50 });
});
await expect(attachment).toBeVisible();
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
});
test('should replace attachment in edgeless', async ({ page }) => {
await clickEdgelessModeButton(page);
const button = page.locator('edgeless-mindmap-tool-button');
await button.click();
const menu = page.locator('edgeless-mindmap-menu');
const mediaItem = menu.locator('.media-item');
await mediaItem.click();
await importFile(page, 'lorem-ipsum.pdf', async () => {
await toViewCoord(page, [100, 250]);
await clickView(page, [100, 250]);
});
const attachment = page.locator('affine-edgeless-attachment').first();
await attachment.click();
const name = attachment.locator('.affine-attachment-content-title-text');
await expect(name).toHaveText('lorem-ipsum.pdf');
const toolbar = locateToolbar(page);
const replaceButton = toolbar.getByLabel('Replace attachment');
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
await replaceButton.click({ delay: 50 });
});
await expect(attachment).toBeVisible();
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
});
test('should fall back to card view when file type does not support embed view', async ({
page,
}) => {
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importAttachment(page, 'lorem-ipsum.pdf');
const attachment = page.locator('affine-attachment').first();
await attachment.click();
const toolbar = locateToolbar(page);
// Switches to embed view
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
const portal = attachment.locator('lit-react-portal');
await expect(portal).toBeVisible();
const replaceButton = toolbar.getByLabel('Replace attachment');
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
await replaceButton.click({ delay: 50 });
});
await expect(portal).toBeHidden();
const name = attachment.locator('.affine-attachment-content-title-text');
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
});
});

View File

@@ -0,0 +1,76 @@
import {
pressArrowDown,
pressArrowUp,
pressBackspace,
pressEnter,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await page.locator('affine-paragraph v-line div').click();
});
test('add callout block using slash menu and change emoji', async ({
page,
}) => {
await type(page, '/callout\naaaa\nbbbb');
const callout = page.locator('affine-callout');
const emoji = page.locator('affine-callout').getByTestId('callout-emoji');
await expect(callout).toBeVisible();
await expect(emoji).toContainText('💡');
const paragraph = page.locator('affine-callout affine-paragraph');
await expect(paragraph).toHaveCount(2);
const vLine = page.locator('affine-callout v-line');
await expect(vLine).toHaveCount(2);
expect(await vLine.nth(0).innerText()).toBe('aaaa');
expect(await vLine.nth(1).innerText()).toBe('bbbb');
});
test('press backspace after callout block', async ({ page }) => {
await pressEnter(page);
await pressArrowUp(page);
await type(page, '/callout\n');
await pressArrowDown(page);
const paragraph = page.locator('affine-paragraph');
const callout = page.locator('affine-callout');
expect(await paragraph.count()).toBe(3);
expect(await callout.count()).toBe(1);
await pressBackspace(page);
expect(await paragraph.count()).toBe(3);
expect(await callout.count()).toBe(1);
await pressBackspace(page);
await expect(paragraph).toHaveCount(2);
await expect(callout).toHaveCount(0);
});
test('press backspace in callout block', async ({ page }) => {
const paragraph = page.locator('affine-paragraph');
const callout = page.locator('affine-callout');
await type(page, '/callout\n');
expect(await paragraph.count()).toBe(2);
expect(await callout.count()).toBe(1);
await pressBackspace(page);
await expect(paragraph).toHaveCount(1);
await expect(callout).toHaveCount(1);
await pressBackspace(page);
await expect(paragraph).toHaveCount(1);
await expect(callout).toHaveCount(0);
});

View File

@@ -0,0 +1,581 @@
import { test } from '@affine-test/kit/playwright';
import { importFile } from '@affine-test/kit/utils/attachment';
import { pasteContent } from '@affine-test/kit/utils/clipboard';
import {
clickEdgelessModeButton,
clickPageModeButton,
clickView,
getCodeBlockIds,
getParagraphIds,
locateEditorContainer,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import {
copyByKeyboard,
cutByKeyboard,
pasteByKeyboard,
pressEnter,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { setSelection } from '@affine-test/kit/utils/selection';
import type { CodeBlockComponent } from '@blocksuite/affine-block-code';
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import type { PageRootBlockComponent } from '@blocksuite/affine-block-root';
import type { BlockComponent } from '@blocksuite/std';
import { expect, type Page } from '@playwright/test';
const paragraphLocator = 'affine-note affine-paragraph';
const codeBlockLocator = 'affine-note affine-code';
// Helper function to create paragraph blocks with text
async function createParagraphBlocks(page: Page, texts: string[]) {
for (const text of texts) {
await pressEnter(page);
await type(page, text);
}
}
// Helper function to verify block text content
async function verifyBlockContent<T extends BlockComponent>(
page: Page,
selector: string,
index: number,
expectedText: string
) {
expect(
await page
.locator(selector)
.nth(index)
.evaluate((block: T, expected: string) => {
const model = block.model;
// Check if model has text property
if (!('text' in model)) return false;
const text = model.text;
// Check if text exists and has toString method
return text && text.toString() === expected;
}, expectedText)
).toBeTruthy();
}
// Helper functions using the generic verifyBlockContent
async function verifyParagraphContent(
page: Page,
index: number,
expectedText: string
) {
await verifyBlockContent<ParagraphBlockComponent>(
page,
paragraphLocator,
index,
expectedText
);
}
async function verifyCodeBlockContent(
page: Page,
index: number,
expectedText: string
) {
await verifyBlockContent<CodeBlockComponent>(
page,
codeBlockLocator,
index,
expectedText
);
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'Clipboard Test');
await waitForEditorLoad(page);
});
test.describe('paste in multiple blocks text selection', () => {
test('paste plain text', async ({ page }) => {
const texts = ['hello world', 'hello world', 'hello world'];
await createParagraphBlocks(page, texts);
const { blockIds: paragraphIds } = await getParagraphIds(page);
/**
* select text cross the 3 blocks:
* hello |world
* hello world
* hello| world
*/
await setSelection(page, paragraphIds[0], 6, paragraphIds[2], 5);
await pasteContent(page, { 'text/plain': 'test' });
/**
* after paste:
* hello test world
*/
await verifyParagraphContent(page, 0, 'hello test world');
});
test('paste snapshot', async ({ page }) => {
// Create initial test blocks
await createParagraphBlocks(page, ['test', 'test']);
// Create target blocks
await createParagraphBlocks(page, [
'hello world',
'hello world',
'hello world',
]);
/**
* before paste:
* test
* test
* hello world
* hello world
* hello world
*/
const { blockIds: paragraphIds } = await getParagraphIds(page);
/**
* select the first 2 blocks:
* |test
* test|
* hello world
* hello world
* hello world
*/
await setSelection(page, paragraphIds[0], 0, paragraphIds[1], 4);
// copy the first 2 blocks
await copyByKeyboard(page);
/**
* select the last 3 blocks:
* test
* test
* hello |world
* hello world
* hello| world
*/
await setSelection(page, paragraphIds[2], 6, paragraphIds[4], 5);
// paste the first 2 blocks
await pasteByKeyboard(page);
await page.waitForTimeout(100);
/**
* after paste:
* test
* test
* hello test
* test world
*/
await verifyParagraphContent(page, 2, 'hello test');
await verifyParagraphContent(page, 3, 'test world');
});
});
test.describe('surface-ref block', () => {
async function setupSurfaceRefBlock(page: Page) {
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
// add a shape
await page.keyboard.press('s');
await container.click({ position: { x: 100, y: 300 } });
await page.waitForTimeout(50);
// add a frame
await page.keyboard.press('f');
await page.waitForTimeout(50);
// click on the frame title to trigger the change frame button toolbar
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await page.waitForTimeout(50);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
const insertIntoPageButton = toolbar.getByLabel('Insert into Page');
await insertIntoPageButton.click();
await clickPageModeButton(page);
await waitForEditorLoad(page);
await container.click();
return { container };
}
test('paste surface-ref block to another doc as embed-linked-doc block', async ({
page,
}) => {
await setupSurfaceRefBlock(page);
// copy surface-ref block
const surfaceRefBlock = page.locator('affine-surface-ref');
await surfaceRefBlock.click();
await page.waitForSelector('affine-surface-ref .focused');
await copyByKeyboard(page);
// paste to another doc
await clickNewPageButton(page, 'page2');
await pressEnter(page);
// paste the surface-ref block
await pasteByKeyboard(page);
await page.waitForTimeout(50);
const embedLinkedDocBlock = page.locator('affine-embed-linked-doc-block');
await expect(embedLinkedDocBlock).toBeVisible();
const embedLinkedDocBlockTitle = embedLinkedDocBlock.locator(
'.affine-embed-linked-doc-content-title-text'
);
await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test');
});
test('cut and paste surface-ref block to same doc should remain surface-ref block', async ({
page,
}) => {
const { container } = await setupSurfaceRefBlock(page);
// cut surface-ref block
const surfaceRefBlock = page.locator('affine-surface-ref');
await surfaceRefBlock.click();
await page.waitForSelector('affine-surface-ref .focused');
await cutByKeyboard(page);
// focus on the editor
await container.click();
// paste the surface-ref block
await pasteByKeyboard(page);
await page.waitForTimeout(50);
await expect(surfaceRefBlock).toHaveCount(1);
await expect(surfaceRefBlock).toBeVisible();
});
});
test.describe('paste to code block', () => {
test('should replace the selected text when pasting plain text', async ({
page,
}) => {
// press enter to focus on the first block of the editor
await pressEnter(page);
await addCodeBlock(page);
await type(page, 'hello world hello');
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
await pasteContent(page, { 'text/plain': 'test' });
await verifyCodeBlockContent(page, 0, 'hello test hello');
});
test('should replace the selected text when pasting single snapshot', async ({
page,
}) => {
// Create initial test blocks
// add a paragraph block
await createParagraphBlocks(page, ['test']);
// add a code block
await pressEnter(page);
await addCodeBlock(page);
await type(page, 'hello world hello');
// select the paragraph content
const { blockIds: paragraphIds } = await getParagraphIds(page);
await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4);
// copy the paragraph content
await copyByKeyboard(page);
// select 'world' in the code block
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
// paste to the code block
await pasteByKeyboard(page);
await page.waitForTimeout(100);
await verifyCodeBlockContent(page, 0, 'hello test hello');
});
test('should replace the selected text when pasting multiple snapshots', async ({
page,
}) => {
// Create initial test blocks
// add three paragraph blocks
await createParagraphBlocks(page, ['test', 'test', 'test']);
// add a code block
await pressEnter(page);
await addCodeBlock(page);
await type(page, 'hello world hello');
// select all paragraph content
const { blockIds: paragraphIds } = await getParagraphIds(page);
await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4);
// copy the paragraph content
await copyByKeyboard(page);
// select 'world' in the code block
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
// paste to the code block
await pasteByKeyboard(page);
await page.waitForTimeout(100);
await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello');
});
test('should preserve indentation when pasting code with spaces into code block', async ({
page,
}) => {
await pressEnter(page);
await addCodeBlock(page);
// Sample code with proper indentation for text/plain
const plainTextCode = [
'const fibonacci = (n: number): number => {',
' if (n <= 1) return n;',
' return fibonacci(n - 1) + fibonacci(n - 2);',
'}',
'function generateSequence(length: number) {',
' const sequence = [];',
' for (let i = 0; i < length; i++) {',
' sequence.push(fibonacci(i));',
' }',
' return sequence;',
'}',
].join('\n');
const htmlCode =
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
await pasteContent(page, {
'text/plain': plainTextCode,
'text/html': htmlCode,
});
await page.waitForTimeout(100);
// Verify the pasted code maintains indentation
await verifyCodeBlockContent(page, 0, plainTextCode);
});
test('html tag should be treated as plain text when pasting', async ({
page,
}) => {
await pressEnter(page);
await addCodeBlock(page);
const textWithHtmlTags =
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
await pasteContent(page, { 'text/plain': textWithHtmlTags });
await page.waitForTimeout(100);
// Verify the pasted code maintains indentation
await verifyCodeBlockContent(page, 0, textWithHtmlTags);
});
test('should not wrap line in brackets when pasting code', async ({
page,
}) => {
await pressEnter(page);
await addCodeBlock(page);
const plainTextCode = [
' model: anthropic("claude-sonnet-4-5-20250929"),',
' prompt: How many people will live in the world in 2040?',
' providerOptions: {',
' anthropic: {',
' thinking: { type: enabled, budgetTokens: 12000 },',
' } satisfies AnthropicProviderOptions,',
' },',
].join('\n');
await pasteContent(page, { 'text/plain': plainTextCode });
await page.waitForTimeout(100);
// Verify the pasted code maintains indentation
await verifyCodeBlockContent(page, 0, plainTextCode);
});
test('should paste markdown text as plain text', async ({ page }) => {
await pressEnter(page);
await addCodeBlock(page);
const markdownText = [
'# Heading 1',
'',
'## Heading 2 with **bold** and *italic*',
'',
'### Lists:',
'- Item 1',
' - Nested item with `inline code`',
' - Another nested item',
'- Item 2 with [link](https://example.com)',
'',
'```typescript',
'const code = "block";',
'console.log(code);',
'```',
'',
'> This is a blockquote with **bold** text',
'> Multiple lines in blockquote',
'',
'| Table | Header |',
'|-------|--------|',
'| Cell 1 | Cell 2 |',
'$This is a inline latex$',
].join('\n');
await pasteContent(page, { 'text/plain': markdownText });
await page.waitForTimeout(100);
// Verify the pasted code maintains indentation
await verifyCodeBlockContent(page, 0, markdownText);
});
});
test.describe('paste in readonly mode', () => {
test('should not paste content when document is in readonly mode', async ({
page,
}) => {
await createParagraphBlocks(page, ['This is a test paragraph']);
const { blockIds } = await getParagraphIds(page);
const initialParagraphCount = blockIds.length;
await page.evaluate(() => {
const pageRoot = document.querySelector(
'affine-page-root'
) as PageRootBlockComponent;
pageRoot.store.readonly = true;
});
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
await pasteContent(page, {
'text/plain': ' - Added text that should not appear',
});
await verifyParagraphContent(page, 0, 'This is a test paragraph');
await pressEnter(page);
await pasteContent(page, { 'text/plain': 'This should not be pasted' });
const { blockIds: afterParagraphIds } = await getParagraphIds(page);
expect(afterParagraphIds.length).toBe(initialParagraphCount);
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
await pasteByKeyboard(page);
await verifyParagraphContent(page, 0, 'This is a test paragraph');
});
});
test.describe('cross document clipboard regression', () => {
test('copy and paste paragraph content between docs', async ({ page }) => {
const container = locateEditorContainer(page);
await container.click();
const sourceText = "Cross-doc paste can't fail again";
await type(page, sourceText);
const { blockIds } = await getParagraphIds(page);
await setSelection(page, blockIds[0], 0, blockIds[0], sourceText.length);
await copyByKeyboard(page);
await clickNewPageButton(page, 'Clipboard Destination');
await waitForEditorLoad(page);
const destination = locateEditorContainer(page);
await destination.click();
await pasteByKeyboard(page);
await page.waitForTimeout(100);
const pastedTexts = await page.locator(paragraphLocator).allTextContents();
expect(pastedTexts.some(text => text.includes(sourceText))).toBe(true);
});
test('copied content remains available to external clipboard consumers', async ({
page,
}) => {
const container = locateEditorContainer(page);
await container.click();
const textForExternal = 'External clipboard visibility check';
await type(page, textForExternal);
const { blockIds } = await getParagraphIds(page);
await setSelection(
page,
blockIds[0],
0,
blockIds[0],
textForExternal.length
);
await copyByKeyboard(page);
const plainText = await page.evaluate(() => navigator.clipboard.readText());
expect(plainText).toBe(textForExternal);
});
test('copy and paste within a single document still duplicates content', async ({
page,
}) => {
const container = locateEditorContainer(page);
await container.click();
const intraDocText = 'Same doc paste regression guard';
await type(page, intraDocText);
const { blockIds } = await getParagraphIds(page);
await setSelection(page, blockIds[0], 0, blockIds[0], intraDocText.length);
await copyByKeyboard(page);
await pressEnter(page);
await pasteByKeyboard(page);
await page.waitForTimeout(100);
await verifyParagraphContent(page, 1, intraDocText);
});
});
test('should copy single image from edgeless and paste to page', async ({
page,
}) => {
await clickEdgelessModeButton(page);
const button = page.locator('edgeless-mindmap-tool-button');
await button.click();
const menu = page.locator('edgeless-mindmap-menu');
const mediaItem = menu.locator('.media-item');
await mediaItem.click();
await importFile(page, 'large-image.png', async () => {
await toViewCoord(page, [100, 250]);
await clickView(page, [100, 250]);
});
const image = page.locator('affine-edgeless-image').first();
await image.click();
await copyByKeyboard(page);
await clickPageModeButton(page);
await waitForEditorLoad(page);
const container = locateEditorContainer(page);
await container.click();
await pasteByKeyboard(page);
await expect(page.locator('affine-page-image')).toBeVisible();
});

View File

@@ -0,0 +1,115 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { type, waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
import {
createNewPage,
gotoContentFromTitle,
initCodeBlockByOneStep,
} from './utils';
test.describe('Code Block Autocomplete Operations', () => {
test('angle brackets are not supported', async ({ page }) => {
// open the home page and insert the code block
await initCodeBlockByOneStep(page);
await page.keyboard.type('<');
const codeUnit = page.locator('affine-code-unit');
await expect(codeUnit).toHaveText('<');
});
});
test.describe('Code Block Preview', () => {
test('enable html preview', async ({ page }) => {
const code = page.locator('affine-code');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```html aaa');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(
page
.locator('iframe[title="HTML Preview"]')
.contentFrame()
.getByText('aaa')
).toBeVisible();
});
test('enable mermaid preview', async ({ page }) => {
const code = page.locator('affine-code');
const mermaidSvg = page.locator('mermaid-preview .mermaid-preview-svg svg');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```mermaid graph TD;A-->B');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(mermaidSvg).toBeVisible();
});
test('enable typst preview', async ({ page }) => {
const code = page.locator('affine-code');
const typstPreview = page.locator('typst-preview');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```typst = Title');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(typstPreview).toBeVisible();
});
test('change lang without preview', async ({ page }) => {
const code = page.locator('affine-code');
const preview = page.locator('affine-code .affine-code-block-preview');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```html aaa');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(preview).toBeVisible();
// change to lang without preview support
await page.getByTestId('lang-button').click();
await page.getByRole('button', { name: 'ABAP' }).click();
await expect(preview).toBeHidden();
// change back to html
await page.getByTestId('lang-button').click();
await page.getByRole('button', { name: 'HTML', exact: true }).click();
await expect(preview).toBeVisible();
});
});

View File

@@ -0,0 +1,192 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import {
closeSettingModal,
openEditorSetting,
} from '@affine-test/kit/utils/setting';
import { expect, type Page } from '@playwright/test';
import { initCodeBlockByOneStep, openCodeBlockMoreMenu } from './utils';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Opens Editor Settings and returns a locator for the
* "Show line numbers in code blocks" toggle switch.
*/
async function openLineNumbersSetting(page: Page) {
await openEditorSetting(page);
return page.getByTestId('code-block-line-numbers-trigger');
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Global code block line numbers setting', () => {
test('line numbers are visible by default', async ({ page }) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
});
test('global toggle hides line numbers on all existing code blocks', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
// Turn the global setting OFF
const toggle = await openLineNumbersSetting(page);
await expect(toggle).toBeVisible();
await toggle.click();
await closeSettingModal(page);
// Line numbers must now be hidden
await expect(lineNumber).toBeHidden();
});
test('turning global toggle back on restores line numbers', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// ON again
const toggle2 = await openLineNumbersSetting(page);
await toggle2.click();
await closeSettingModal(page);
await expect(lineNumber).toBeVisible();
});
test('per-block toggle shows line numbers when global is off', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// Turn global setting OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// Per-block: explicitly enable for this block
const { lineNumberButton } = await openCodeBlockMoreMenu(page);
await lineNumberButton.click();
await expect(lineNumber).toBeVisible();
});
test('per-block toggle can hide line numbers when global is on', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// Global is ON by default
await expect(lineNumber).toBeVisible();
// Per-block: hide for this block
const { cancelLineNumberButton } = await openCodeBlockMoreMenu(page);
await cancelLineNumberButton.click();
// Click away to dismiss menu
await page.mouse.click(300, 300);
await expect(lineNumber).toBeHidden();
// Turn global setting OFF and then ON - per-block override persists
const toggle = await openLineNumbersSetting(page);
await toggle.click(); // OFF
await toggle.click(); // ON
await closeSettingModal(page);
// Per-block explicitly set to false - still hidden
await expect(lineNumber).toBeHidden();
});
test('global setting is reflected on newly created code blocks', async ({
page,
}) => {
// Start on the home page and disable line numbers globally first
await openHomePage(page);
await waitForEditorLoad(page);
const toggle = await openLineNumbersSetting(page);
await toggle.click(); // OFF
await closeSettingModal(page);
// Now create a fresh code block
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeHidden();
});
test('global toggle affects all code blocks on the page', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
// Exit the code block (Mod+Enter creates a paragraph below and moves focus there)
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+Enter' : 'Control+Enter');
// Add a second code block in the new paragraph
await addCodeBlock(page);
// Both blocks should show line numbers by default
const lineNumbers = page.locator('affine-code .line-number');
await expect(lineNumbers).toHaveCount(2);
await expect(lineNumbers.first()).toBeVisible();
await expect(lineNumbers.last()).toBeVisible();
// Turn global OFF - both must hide
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumbers.first()).toBeHidden();
await expect(lineNumbers.last()).toBeHidden();
// Turn global ON - both must show again
const toggle2 = await openLineNumbersSetting(page);
await toggle2.click();
await closeSettingModal(page);
await expect(lineNumbers.first()).toBeVisible();
await expect(lineNumbers.last()).toBeVisible();
});
test('global setting persists across page reload', async ({ page }) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
// Turn global OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// Reload the page - setting must survive
await page.reload();
await waitForEditorLoad(page);
await expect(lineNumber).toBeHidden();
});
});

View File

@@ -0,0 +1,46 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { Page } from '@playwright/test';
export const gotoContentFromTitle = async (page: Page) => {
await page.keyboard.press('Enter');
};
export const createNewPage = async (page: Page) => {
await clickNewPageButton(page);
};
export const initCodeBlockByOneStep = async (page: Page) => {
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await addCodeBlock(page);
};
/**
* Opens the "More" menu on the first code block and returns locators for
* the line-number toggle buttons inside it.
* Uses .first() to stay deterministic when multiple code blocks are present.
*/
export const openCodeBlockMoreMenu = async (page: Page) => {
const codeBlock = page.locator('affine-code').first();
await codeBlock.hover();
const moreButton = page
.locator('affine-code-toolbar')
.getByRole('button', { name: 'More' });
await moreButton.click();
const menu = page.locator('.more-popup-menu');
const lineNumberButton = menu.getByRole('button', { name: 'Line number' });
const cancelLineNumberButton = menu.getByRole('button', {
name: 'Cancel line number',
});
return { menu, lineNumberButton, cancelLineNumberButton };
};

View File

@@ -0,0 +1,111 @@
import { test } from '@affine-test/kit/playwright';
import { importFile } from '@affine-test/kit/utils/attachment';
import {
clickEdgelessModeButton,
dragView,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test('should keep color on custom color button', async ({ page }) => {
await page.getByTestId('header-dropDownButton').click();
await page.getByTestId('editor-option-menu-import').click();
const importDialog = page.getByTestId('import-dialog');
await expect(importDialog).toBeVisible();
const importSnapshot = importDialog.getByTestId(
'editor-option-menu-import-snapshot'
);
const completeButton = importDialog.getByRole('button', { name: 'Complete' });
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
await importSnapshot.click();
});
await completeButton.click();
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await expect(title).toContainText('Getting Started');
await clickEdgelessModeButton(page);
const toolbar = locateToolbar(page);
const colorPicker = toolbar.locator('edgeless-color-picker-button');
// frame
{
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await expect(toolbar).toBeVisible();
const backgroundButton = toolbar.getByLabel('Background');
const colorButton = backgroundButton
.locator('edgeless-color-button')
.first();
const color = await colorButton.locator('svg').getAttribute('fill');
expect(color).not.toBeNull();
await backgroundButton.click();
const customColorButton = colorPicker.locator(
'edgeless-color-custom-button'
);
const keptColor = await customColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
expect(keptColor.startsWith('#')).toBe(true);
expect(keptColor.length).toBe(7);
expect(color).toBe(keptColor);
}
// shape
{
await dragView(page, [5425 - 10, 2658 - 10], [5425 + 20, 2658 + 20]);
await expect(toolbar).toBeVisible();
const colorButton = toolbar.getByLabel(/^Color$/);
await colorButton.click();
const fillCustomColorButton = colorPicker
.locator('edgeless-color-custom-button')
.first();
const strokeCustomColorButton = colorPicker
.locator('edgeless-color-custom-button')
.last();
const fillKeptColor = await fillCustomColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
const strokeKeptColor = await strokeCustomColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
expect(fillKeptColor.length).toBe(7);
expect(fillKeptColor.startsWith('#')).toBe(true);
expect(strokeKeptColor.length).toBe(7);
expect(strokeKeptColor.startsWith('#')).toBe(true);
}
});

View File

@@ -0,0 +1,69 @@
import { test } from '@affine-test/kit/playwright';
import {
addColumn,
addRows,
initDatabaseByOneStep,
pasteString,
selectCell,
verifyCellContents,
} from './utils';
test.describe('Database Clipboard Operations', () => {
test('paste tab-separated data from Excel into database', async ({
page,
}) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with two rows
await addRows(page, 2);
// Select the first cell and paste data
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify cell contents
await verifyCellContents(page, [
'Cell 1A',
'Cell 1B',
'Cell 2A',
'Cell 2B',
]);
});
test('handle empty cells when pasting tab-separated data', async ({
page,
}) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with two rows
await addRows(page, 2);
// Select the first cell and paste data with empty cells
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\t\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify cell contents including empty cells
await verifyCellContents(page, ['Cell 1A', '', 'Cell 2A', 'Cell 2B']);
});
test('handle pasting data larger than selected area', async ({ page }) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with one row
await addRows(page, 1);
// Select the first cell and paste data larger than table
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify only the cells that exist are filled
await verifyCellContents(page, ['Cell 1A', 'Cell 1B']);
});
});

View File

@@ -0,0 +1,99 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
import {
addColumn,
createDatabaseBlock,
createNewPage,
gotoContentFromTitle,
selectCell,
} from './utils';
test.describe('Database Rich Text Column', () => {
test('paste document link into rich text cell', async ({ page }) => {
// Step 1: Open home page
await openHomePage(page);
await waitForEditorLoad(page);
// Step 2: Create a new page
await createNewPage(page);
await gotoContentFromTitle(page);
// Step 3: Create a database in the page
await createDatabaseBlock(page);
// Step 4: Add a text column
await addColumn(page, 'Text');
// Step 5: Create a new page to get its link
await clickNewPageButton(page, 'Test Page');
const pageUrl = page.url();
// Step 6: Go back to database page
await page.goBack();
await waitForEditorLoad(page);
// Step 7: Select and edit the rich text cell
const richTextCell = await selectCell(page, 2);
// Step 8: Paste the document link
await page.evaluate(url => {
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', url);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData,
bubbles: true,
cancelable: true,
});
document.activeElement?.dispatchEvent(pasteEvent);
}, pageUrl);
// Step 9: Verify the result
const referenceTitle = richTextCell.locator('.affine-reference-title');
await expect(referenceTitle).toBeVisible();
await expect(referenceTitle).toContainText('Test Page');
});
test('add document link via @ in rich text cell', async ({ page }) => {
// Step 1: Open home page
await openHomePage(page);
await waitForEditorLoad(page);
// Step 2: Create a new page
await createNewPage(page);
await gotoContentFromTitle(page);
// Step 3: Create a database in the page
await createDatabaseBlock(page);
// Step 4: Add a text column
await addColumn(page, 'Text');
// Step 5: Create a new page as reference target
await clickNewPageButton(page, 'Reference Target');
await page.goBack();
await waitForEditorLoad(page);
// Step 6: Select and edit the rich text cell
const richTextCell = await selectCell(page, 2);
await richTextCell.click();
await page.keyboard.type('@');
// Step 7: Wait for reference picker and select the page
const linkedDocPopover = page.locator('.linked-doc-popover');
await expect(linkedDocPopover).toBeVisible();
const targetPage = linkedDocPopover.getByText('Reference Target');
await targetPage.click();
// Step 8: Verify the result
const referenceTitle = richTextCell.locator('.affine-reference-title');
await expect(referenceTitle).toBeVisible();
await expect(referenceTitle).toContainText('Reference Target');
});
});

View File

@@ -0,0 +1,122 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addDatabase,
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
export async function createNewPage(page: Page) {
await clickNewPageButton(page);
}
export const gotoContentFromTitle = async (page: Page) => {
await page.keyboard.press('Enter');
};
export async function createDatabaseBlock(page: Page) {
await addDatabase(page);
}
export async function addRows(page: Page, rowCount: number) {
for (let i = 0; i < rowCount; i++) {
await addDatabaseRow(page);
}
}
export async function addDatabaseRow(page: Page) {
const addButton = page.locator('.data-view-table-group-add-row');
await addButton.click();
}
export async function pasteString(page: Page, data: string) {
await page.evaluate(data => {
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', data);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData,
bubbles: true,
cancelable: true,
});
const activeElement = document.activeElement;
if (activeElement) {
pasteEvent.preventDefault();
activeElement.dispatchEvent(pasteEvent);
}
}, data);
}
export async function selectCell(page: Page, nth: number, editing = true) {
const firstCell = page.locator('dv-table-view-cell-container').nth(nth);
// First click for focus
await firstCell.click({ delay: 100 });
// Second click for edit mode
if (editing) {
await firstCell.click({ delay: 100 });
}
return firstCell;
}
export async function verifyCellContents(
page: Page,
expectedContents: string[]
) {
const cells = page.locator('dv-table-view-cell-container');
for (let i = 0; i < expectedContents.length; i++) {
const cell = cells.nth(i);
await expect(cell.locator('uni-lit > *:first-child')).toHaveText(
expectedContents[i]
);
}
}
export async function selectColumnType(
page: Page,
columnType: string,
nth: number = 1
) {
const typeMenu = page.locator('affine-menu').getByText('Type');
await page.waitForTimeout(100);
await typeMenu.hover();
await page.mouse.move(0, 0);
await page.waitForTimeout(100);
for (const char of columnType.split('')) {
await page.keyboard.type(char);
await page.waitForTimeout(10);
}
await page.waitForTimeout(100);
for (let i = 0; i < nth; i++) {
await page.keyboard.press('ArrowDown');
}
await page.waitForTimeout(100);
await page.keyboard.press('Enter');
await page.waitForTimeout(100);
}
export async function addColumn(page: Page, type: string, nth: number = 1) {
await clickAddColumnButton(page);
await selectColumnType(page, type, nth);
}
export async function clickAddColumnButton(page: Page) {
const addColumnButton = page.locator('.header-add-column-button');
await addColumnButton.click();
}
export async function changeColumnType(
page: Page,
columnIndex: number,
columnType: string
) {
const header = page.locator('affine-database-header-column').nth(columnIndex);
await header.click();
await selectColumnType(page, columnType);
}
export const initDatabaseByOneStep = async (page: Page) => {
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await createDatabaseBlock(page);
};

View File

@@ -0,0 +1,100 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should hide toolbar when creating connector', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const autoComplete = page.locator('edgeless-auto-complete');
const rightArrowButton = autoComplete
.locator('.edgeless-auto-complete-arrow')
.nth(0);
const bounds = await rightArrowButton.boundingBox();
expect(bounds).not.toBeNull();
const { x, y, width: w, height: h } = bounds!;
const startPos = [x + w / 2, y + h / 2];
const endPos = [startPos[0] + 100, startPos[1] + 100];
await page.mouse.move(startPos[0], startPos[1]);
await page.mouse.down();
await page.mouse.move(endPos[0], endPos[1]);
await expect(toolbar).toBeHidden();
await page.mouse.up();
const autoCompletePanel = page
.locator('edgeless-auto-complete-panel')
.locator('.auto-complete-panel-container');
await expect(toolbar).toBeHidden();
await expect(autoCompletePanel).toBeVisible();
});
test('should toggle toolbar when connecting target', async ({ page }) => {
await setEdgelessTool(page, 'shape');
// source
await dragView(page, [100, 300], [200, 400]);
// target
await setEdgelessTool(page, 'shape');
await dragView(page, [300, 300], [400, 400]);
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const autoComplete = page.locator('edgeless-auto-complete');
const rightArrowButton = autoComplete
.locator('.edgeless-auto-complete-arrow')
.nth(0);
const bounds = await rightArrowButton.boundingBox();
expect(bounds).not.toBeNull();
const { x, y, width: w, height: h } = bounds!;
const startPos = [x + w / 2, y + h / 2];
const endPos = await toViewCoord(page, [300, 350]);
await page.mouse.move(startPos[0], startPos[1]);
await page.mouse.down();
await page.mouse.move(endPos[0], endPos[1]);
await expect(toolbar).toBeHidden();
await page.mouse.up();
await expect(toolbar).toBeVisible();
});

View File

@@ -0,0 +1,197 @@
import { test } from '@affine-test/kit/playwright';
import {
type CanvasRendererPerfSnapshot,
deleteEdgelessElements,
getCanvasRendererPerfSnapshot,
resetCanvasRendererPerfMetrics,
seedEdgelessPerfScene,
} from '@affine-test/kit/utils/edgeless-perf';
import {
clickEdgelessModeButton,
dragView,
fitViewportToContent,
getEdgelessSelectedIds,
getSelectedXYWH,
locateEditorContainer,
setEdgelessTool,
setViewportZoom,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
const PERF_ENV = 'AFFINE_RUN_PERF_E2E';
const perfEnabled = process.env[PERF_ENV] === '1';
const modKey = process.platform === 'darwin' ? 'Meta' : 'Control';
type PerfScenarioResult = {
name: string;
snapshot: CanvasRendererPerfSnapshot;
};
test.describe.serial('canvas renderer perf probes', () => {
test.skip(!perfEnabled, `Set ${PERF_ENV}=1 to run manual perf probes`);
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
await locateEditorContainer(page).click();
});
test('collect metrics for common edgeless canvas scenarios', async ({
page,
}, testInfo) => {
test.slow();
const results: PerfScenarioResult[] = [];
let addedShapeIds: string[] = [];
const selectWholePerfScene = async () => {
await setEdgelessTool(page, 'default');
await dragView(page, [80, 140], [2300, 1500]);
await expect
.poll(async () => (await getEdgelessSelectedIds(page)).length)
.toBeGreaterThan(0);
};
const recordScenario = async (
name: string,
action: () => Promise<void>
) => {
await resetCanvasRendererPerfMetrics(page);
await action();
await page.waitForTimeout(400);
const snapshot = await getCanvasRendererPerfSnapshot(page);
results.push({ name, snapshot });
console.log(
`[canvas-perf] ${name}: ${JSON.stringify(snapshot.metrics, null, 2)}`
);
};
const initial = await seedEdgelessPerfScene(page, {
shapeCount: 120,
rowLength: 12,
startX: 120,
startY: 180,
width: 160,
height: 120,
});
addedShapeIds = initial.shapeIds;
await fitViewportToContent(page);
await page.waitForTimeout(500);
await recordScenario('add-shapes', async () => {
const seeded = await seedEdgelessPerfScene(page, {
shapeCount: 40,
rowLength: 10,
startX: 160,
startY: 1720,
width: 160,
height: 120,
});
addedShapeIds = addedShapeIds.concat(seeded.shapeIds);
await fitViewportToContent(page);
});
await recordScenario('delete-shapes', async () => {
await deleteEdgelessElements(page, addedShapeIds.slice(-20));
});
await recordScenario('box-select', async () => {
await selectWholePerfScene();
});
await recordScenario('group-selection', async () => {
await selectWholePerfScene();
await page.keyboard.press(`${modKey}+g`);
});
await recordScenario('ungroup-selection', async () => {
await page.keyboard.press(`${modKey}+Shift+g`);
});
await recordScenario('large-drag-selection', async () => {
await selectWholePerfScene();
const [x, y, w, h] = await getSelectedXYWH(page);
const center: [number, number] = [x + w / 2, y + h / 2];
await dragView(page, center, [center[0] + 1200, center[1] + 900]);
});
await recordScenario('large-pan', async () => {
await setEdgelessTool(page, 'pan');
await dragView(page, [1200, 900], [200, 180]);
});
await recordScenario('large-zoom', async () => {
await setViewportZoom(page, 0.25);
await page.waitForTimeout(200);
await setViewportZoom(page, 2.2);
await page.waitForTimeout(200);
await fitViewportToContent(page);
});
const finalSnapshot = await getCanvasRendererPerfSnapshot(page);
expect(finalSnapshot.rendererType).toBe('CanvasRenderer');
expect(results.length).toBeGreaterThanOrEqual(7);
await testInfo.attach('canvas-renderer-perf-scenarios.json', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
});
test('collect metrics for interleaved block and canvas layers', async ({
page,
}, testInfo) => {
test.slow();
await seedEdgelessPerfScene(page, {
interleaved: true,
noteCount: 21,
shapeCount: 20,
rowLength: 1,
startX: 120,
startY: 180,
width: 180,
height: 120,
});
await fitViewportToContent(page);
await page.waitForTimeout(500);
const snapshot = await getCanvasRendererPerfSnapshot(page);
const metrics = snapshot.metrics as {
canvasMemoryMegabytes?: number;
lastRenderMetrics?: {
renderByBoundCallCount?: number;
};
stackingCanvasCount?: number;
visibleStackingCanvasCount?: number;
} | null;
console.log(
`[canvas-perf] interleaved-layers: ${JSON.stringify(snapshot, null, 2)}`
);
expect(snapshot.rendererType).toBe('CanvasRenderer');
expect(metrics).not.toBeNull();
expect(metrics?.stackingCanvasCount ?? 0).toBeGreaterThan(0);
expect(
metrics?.lastRenderMetrics?.renderByBoundCallCount ?? 0
).toBeGreaterThan(1);
expect(metrics?.visibleStackingCanvasCount ?? 0).toBeGreaterThan(0);
expect(metrics?.canvasMemoryMegabytes ?? 0).toBeLessThan(5);
await testInfo.attach('canvas-renderer-layering.json', {
body: JSON.stringify(snapshot, null, 2),
contentType: 'application/json',
});
});
});

View File

@@ -0,0 +1,65 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dblclickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
switchEdgelessTheme,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should correct text color on toolbar', async ({ page }) => {
await setEdgelessTool(page, 'connector');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-connector-label-editor rich-text')
).toBeVisible();
await page.keyboard.type('label');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(textColorContainer).toBeVisible();
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor).toBe('#000000');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor2 = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor2).toBe('#ffffff');
});

View File

@@ -0,0 +1,60 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
dblclickNoteBody,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { isContainedInBoundingBox } from '@affine-test/kit/utils/utils';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should close embed editing modal when editor switching to page mode by short cut', async ({
page,
}) => {
await page.keyboard.press('@');
await page.getByTestId('cmdk-label').getByText('Getting Started').click();
const toolbar = locateToolbar(page);
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Card view').click();
await toolbar.getByLabel('Edit').click();
const editingModal = page.locator('embed-card-edit-modal');
await expect(editingModal).toBeVisible();
await page.keyboard.press('Alt+s');
await waitForEditorLoad(page);
await expect(editingModal).toBeHidden();
});
test('embed card should not overflow the edgeless note', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await dblclickNoteBody(page);
await type(page, '/github');
await pressEnter(page);
await page
.locator('.embed-card-modal-input')
.fill('https://github.com/toeverything/AFFiNE/pull/10442');
await pressEnter(page);
const embedCard = page.locator('affine-embed-github-block');
await embedCard
.locator('.affine-embed-github-block:not(.loading)')
.waitFor({ state: 'visible' });
expect(await isContainedInBoundingBox(note, embedCard, true)).toBe(true);
});

View File

@@ -0,0 +1,64 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
dragView,
locateEditorContainer,
locateToolbar,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
await selectAllByKeyboard(page);
await pressBackspace(page);
});
test('should update zindex of element when moving it into frame', async ({
page,
}) => {
const toolbar = locateToolbar(page);
// create a top frame
await page.keyboard.press('f');
await dragView(page, [0, 100], [300, 400]);
await toolbar.getByLabel('Background').click();
await toolbar.getByLabel('LightRed').click();
await pressEscape(page);
// create a note
await createEdgelessNoteBlock(page, [500, 500]);
await clickView(page, [0, 100]);
await clickView(page, [500, 500]);
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Send to Back').click();
await pressEscape(page);
await dragView(page, [500, 500], [100, 200]);
const point = await toViewCoord(page, [100, 200]);
const isNoteAboveFrame = await page.evaluate(point => {
return !!document
.elementFromPoint(point[0], point[1])
?.closest('affine-edgeless-note');
}, point);
expect(isNoteAboveFrame).toBe(true);
});

View File

@@ -0,0 +1,85 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should add highlighter', async ({ page }) => {
await setEdgelessTool(page, 'highlighter');
await dragView(page, [100, 300], [200, 400]);
await setEdgelessTool(page, 'default');
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await page.waitForTimeout(250);
await expect(toolbar).toBeVisible();
const lineWidthButton = toolbar
.locator('affine-slider')
.locator('.point-button[data-selected]')
.last();
const defaultLineWidth = await lineWidthButton.getAttribute('aria-label');
expect(defaultLineWidth).toBe('22');
});
test('should exit drawing tools menu when Escape is pressed', async ({
page,
}) => {
await setEdgelessTool(page, 'highlighter');
const drawingToolsMenu = page.locator('edgeless-pen-menu');
await expect(drawingToolsMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(drawingToolsMenu).toBeHidden();
});
test('should enter highlighter tool when `Shift + P` is pressed', async ({
page,
}) => {
const drawingToolButton = page.locator('.edgeless-pen-button');
const drawingToolsMenu = page.locator('edgeless-pen-menu');
await expect(drawingToolButton).toHaveAttribute('data-drawing-tool', 'brush');
await expect(drawingToolsMenu).toBeHidden();
await page.keyboard.press('Shift+P');
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
await expect(drawingToolsMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(drawingToolsMenu).toBeHidden();
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
});

View File

@@ -0,0 +1,535 @@
import { test } from '@affine-test/kit/playwright';
import {
assertTitle,
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
getEdgelessSelectedIds,
getPageMode,
getSelectedXYWH,
isDocTitleFocused,
locateEditorContainer,
locateModeSwitchButton,
locateToolbar,
moveToView,
resizeElementByHandle,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import {
pasteByKeyboard,
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickLocatorByRatio } from '@affine-test/kit/utils/utils';
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
import type { IVec } from '@blocksuite/affine/global/gfx';
import type { NoteBlockModel } from '@blocksuite/affine/model';
import { expect, type Page } from '@playwright/test';
const title = 'Edgeless Note Header Test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, title);
await page.keyboard.press('Enter');
await page.keyboard.type('Hello');
await page.keyboard.press('Enter');
await page.keyboard.type('World');
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
// the first note block is called page block
test.describe('edgeless page block', () => {
const locateHeaderToolbar = (page: Page) =>
page.getByTestId('edgeless-page-block-header');
test('only first note block has header toolbar and its element toolbar', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
await expect(toolbar).toHaveCount(1);
await expect(toolbar).toBeVisible();
await createEdgelessNoteBlock(page, [100, 100]);
await expect(toolbar).toHaveCount(1);
await expect(toolbar).toBeVisible();
});
test('should shrink note block when clicking on the toggle button', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const toolBox = await toolbar.boundingBox();
const noteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!noteBox || !toolBox) throw new Error('Bounding box not found');
expect(noteBox.height).toBeGreaterThan(toolBox.height);
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
await toggleButton.click();
const newNoteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!newNoteBox) throw new Error('Bounding box not found');
expect(newNoteBox.height).toBe(toolBox.height);
await toggleButton.click();
const newNoteBox2 = await page
.locator('affine-edgeless-note')
.boundingBox();
if (!newNoteBox2) throw new Error('Bounding box not found');
expect(newNoteBox2).toEqual(noteBox);
});
test('page title in toolbar should be displayed when page block is collapsed and hidden when page block is not collapsed', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const toolbarTitle = toolbar.getByTestId('edgeless-note-title');
await expect(toolbarTitle).toHaveText('');
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
await toggleButton.click();
await expect(toolbarTitle).toHaveText(title);
await toggleButton.click();
await expect(toolbarTitle).toHaveText('');
});
test('should switch to page mode when expand button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const viewInPageButton = toolbar.getByTestId(
'edgeless-note-view-in-page-button'
);
await viewInPageButton.click();
expect(await getPageMode(page)).toBe('page');
});
test('should open doc properties dialog when info button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
await infoButton.click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
});
test('should copy note edgeless link to clipboard when link button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
await selectAllByKeyboard(page);
const noteId = (await getEdgelessSelectedIds(page))[0];
const linkButton = toolbar.getByTestId('edgeless-note-link-button');
await linkButton.click();
const url = page.url();
const link = await page.evaluate(() => navigator.clipboard.readText());
expect(link).toBe(`${url}&blockIds=${noteId}`);
});
test('info button should hidden in peek view', async ({ page }) => {
const url = page.url();
await page.evaluate(url => navigator.clipboard.writeText(url), url);
await clickNewPageButton(page);
await page.keyboard.press('Enter');
await pasteByKeyboard(page);
const reference = page.locator('affine-reference');
await reference.click({ modifiers: ['Shift'] });
const toolbar = locateHeaderToolbar(page);
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
await expect(toolbar).toBeVisible();
await expect(infoButton).toBeHidden();
});
test('caret on focusing', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await note.click(); // focus note
// click on title's rear
const docTitle = note.locator('edgeless-page-block-title');
await expect(docTitle).toBeVisible();
await clickLocatorByRatio(page, docTitle, { xRatio: 0.9, yRatio: 0.8 });
const hasCaretInTitle = await page.evaluate(
hasCaretIn,
'edgeless-page-block-title'
);
expect(hasCaretInTitle).toBe(true);
await clickLocatorByRatio(page, note, { xRatio: 1.1, yRatio: 0.1 }); // cancel note focus
await note.click(); // focus note again
// click on firstParagraph's rear
const firstParagraph = note.locator('affine-paragraph:first-child');
await expect(firstParagraph).toBeVisible();
await clickLocatorByRatio(page, firstParagraph, {
xRatio: 0.9,
yRatio: 0.5,
});
const hasCaretInParagraph = await page.evaluate(
hasCaretIn,
'affine-paragraph'
);
expect(hasCaretInParagraph).toBe(true);
function hasCaretIn(elemSelector: string) {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return false;
const startContainer = sel.getRangeAt(0).startContainer;
const selContainer =
startContainer.nodeType === Node.TEXT_NODE
? startContainer.parentElement
: startContainer;
if (!selContainer) return false;
const closestDstElem = (selContainer as HTMLElement)?.closest(
elemSelector
);
if (!closestDstElem) return false;
return true;
}
});
test('page title should be editable', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
const docTitle = note.locator('edgeless-page-block-title');
await expect(docTitle).toBeVisible();
await assertTitle(page, title);
await note.dblclick();
await docTitle.click();
// clear the title
await selectAllByKeyboard(page);
await pressBackspace(page);
await assertTitle(page, '');
// type new title
await type(page, 'New Title');
await assertTitle(page, 'New Title');
// cursor could move between doc title and note content
await page.keyboard.press('ArrowDown');
await type(page, 'xx');
const paragraphs = note.locator('affine-paragraph v-line');
const numParagraphs = await paragraphs.count();
await expect(paragraphs.first()).toHaveText('xxHello');
await page.keyboard.press('ArrowUp');
await type(page, 'yy');
await assertTitle(page, 'yyNew Title');
await pressEnter(page);
await assertTitle(page, 'yy');
await expect(paragraphs).toHaveCount(numParagraphs + 1);
await expect(paragraphs.nth(0)).toHaveText('New Title');
await expect(paragraphs.nth(1)).toHaveText('xxHello');
});
});
test.describe('edgeless note element toolbar', () => {
test('the toolbar of page block should not contains auto-height button and display in page button', async ({
page,
}) => {
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible();
await expect(autoHeight).toHaveCount(0);
await expect(displayInPage).toHaveCount(0);
});
test('the toolbar of note block should contains auto-height button and display in page button', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [100, 100]);
await page.waitForSelector('.affine-paragraph-placeholder.visible');
await clickView(page, [0, 0]);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible();
await expect(autoHeight).toBeVisible();
await expect(displayInPage).toBeVisible();
});
test('display in page button', async ({ page }) => {
const editorContainer = locateEditorContainer(page);
const notes = editorContainer.locator('affine-note');
await createEdgelessNoteBlock(page, [100, 100]);
await page.waitForSelector('.affine-paragraph-placeholder.visible');
await page.keyboard.type('Note 2');
await clickView(page, [0, 300]);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
const displayInPage = toolbar.getByTestId('display-in-page');
await displayInPage.click();
await locateModeSwitchButton(page, 'page').click();
await expect(notes).toHaveCount(2);
await clickEdgelessModeButton(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await locateModeSwitchButton(page, 'page').click();
await waitForEditorLoad(page);
await expect(notes).toHaveCount(1);
const undoButton = page.getByTestId('notification-card-undo');
const viewTocButton = page.getByTestId('view-in-toc');
await clickEdgelessModeButton(page);
await waitForEditorLoad(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await expect(undoButton).toBeVisible();
await expect(viewTocButton).toBeVisible();
await undoButton.click();
await expect(undoButton).toBeHidden();
await locateModeSwitchButton(page, 'page').click();
await waitForEditorLoad(page);
await expect(notes).toHaveCount(1);
await clickEdgelessModeButton(page);
await waitForEditorLoad(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await undoByKeyboard(page);
await page.waitForTimeout(500);
await expect(
undoButton,
'the toast should be hidden immediately when undo by keyboard'
).toBeHidden();
await displayInPage.click();
await viewTocButton.click();
const toc = page.locator('affine-outline-panel');
await toc.waitFor({ state: 'visible' });
const highlightNoteCards = toc.locator(
'affine-outline-note-card > [data-status="selected"]'
);
await expect(highlightNoteCards).toHaveCount(1);
});
test('note edgeless styles', async ({ page }) => {
const getNoteEdgelessProps = async (page: Page, noteId: string) => {
const container = locateEditorContainer(page);
return await container.evaluate((container: HTMLElement, noteId) => {
const root = container.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const note = root.gfx.getElementById(noteId) as NoteBlockModel;
return note.props.edgeless;
}, noteId);
};
const toolbar = locateToolbar(page);
await selectAllByKeyboard(page);
const noteId = (await getEdgelessSelectedIds(page))[0];
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 4,
borderStyle: 'none',
shadowType: '--affine-note-shadow-box',
},
});
await toolbar.getByRole('button', { name: 'Note Style' }).click();
const noteStylePanel = page.locator('edgeless-note-style-panel');
await noteStylePanel.getByTestId('affine-note-shadow-film').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 4,
borderStyle: 'none',
shadowType: '--affine-note-shadow-film',
},
});
const borderStylePanel = noteStylePanel.getByTestId(
'affine-note-border-style-panel'
);
await borderStylePanel.locator('.mode-solid').click();
await borderStylePanel.locator('affine-slider').getByLabel('8').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
const cornerPanel = noteStylePanel.getByTestId(
'affine-note-corner-radius-panel'
);
await cornerPanel.locator('affine-slider').getByLabel('24').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 24,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
await pressEscape(page);
const headerToolbar = page.getByTestId('edgeless-page-block-header');
const toggleButton = headerToolbar.getByTestId(
'edgeless-note-toggle-button'
);
await toggleButton.click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
collapse: true,
collapsedHeight: 48,
style: {
borderRadius: 24,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
});
});
test.describe('note block rendering', () => {
test('collapsed content rendering', async ({ page }) => {
await createEdgelessNoteBlock(page, [50, 50]);
await type(page, 'paragraph 1');
for (let i = 0; i < 5; i++) {
await pressEnter(page);
}
await type(page, 'paragraph 2');
await pressEscape(page, 3);
await clickView(page, [50, 50]);
await resizeElementByHandle(page, [0, -50], 'bottom-right');
const xywh = await getSelectedXYWH(page);
const center: IVec = [xywh[0] + xywh[2] / 2, xywh[1] + xywh[3] / 2];
const note = page
.locator('affine-edgeless-note')
.getByTestId('edgeless-note-clip-container')
.nth(1);
await expect(note, 'should hide collapsed content').toHaveCSS(
'overflow-y',
'clip'
);
await moveToView(page, center);
await expect(note, 'should show collapsed content when hover').toHaveCSS(
'overflow-y',
'visible'
);
const [x1, y1] = await toViewCoord(page, center);
const [x2, y2] = await toViewCoord(page, [center[0], center[1] + 25]);
const [x3, y3] = await toViewCoord(page, [center[0], center[1] + 50]);
await page.mouse.move(x1, y1);
await page.mouse.down();
await page.mouse.move(x2, y2, { steps: 10 });
await expect(
note,
'should hide collapsed content during dragging'
).toHaveCSS('overflow-y', 'clip');
await page.mouse.move(x3, y3, { steps: 10 });
await page.mouse.up();
await page.mouse.move(x3, y3);
await expect(
note,
'should show collapsed content when dragging is finished'
).toHaveCSS('overflow-y', 'visible');
});
test('cursor should not jump to page block title from note block', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [50, 50]);
await pressBackspace(page);
expect(await isDocTitleFocused(page)).toBeFalsy();
});
});
test('should convert note block to linked doc when clicking turn into linked doc button', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '# Two Questions');
await pressEnter(page);
await type(page, 'Is this the real life?');
await pressEnter(page);
await type(page, 'Is this just fantasy?');
await pressEscape(page, 3);
const note = page.locator('affine-edgeless-note', {
hasText: 'Is this just fantasy?',
});
await note.click();
const toolbar = locateToolbar(page);
const moreBtn = toolbar.getByRole('button', { name: 'More' });
await moreBtn.click();
const turnIntoLinkedDocBtn = toolbar.getByRole('button', {
name: 'Turn into linked doc',
});
await turnIntoLinkedDocBtn.click();
const inputModal = page.getByRole('dialog').locator('input');
await expect(inputModal).toHaveValue('Two Questions');
await type(page, 'Bohemian Rhapsody');
const confirmBtn = page.getByTestId('confirm-modal-confirm');
await confirmBtn.click();
const syncedDoc = page.locator('affine-embed-edgeless-synced-doc-block');
await expect(syncedDoc).toBeVisible();
const noteInSyncedDoc = syncedDoc.locator('affine-note');
await expect(noteInSyncedDoc).toBeVisible();
const paragraphs = noteInSyncedDoc.locator('affine-paragraph');
await expect(paragraphs.nth(0)).toContainText('Two Questions');
await expect(paragraphs.nth(1)).toContainText('Is this the real life?');
await expect(paragraphs.nth(2)).toContainText('Is this just fantasy?');
});

View File

@@ -0,0 +1,110 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dblclickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
switchEdgelessTheme,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should add text to shape, default to pure black', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(textColorContainer).toBeVisible();
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor).toBe('#000000');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor2 = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor2).toBe('#000000');
});
test('should add text to shape with pure white', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
let currentColor = await textColorBtn
.locator('svg rect')
.getAttribute('fill');
expect(currentColor).toBe('#000000');
await textColorBtn.click();
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(blackBtn).toHaveCount(1);
const whiteBtn = textColorContainer
.locator('edgeless-color-button')
.getByLabel('White');
await whiteBtn.click();
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
});

View File

@@ -0,0 +1,102 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Locator } from '@playwright/test';
function getEdgelessTextColor(text: Locator) {
return text
.locator('.affine-block-children-container')
.first()
.evaluate(e => e.style.getPropertyValue('--edgeless-text-color'));
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should update color of edgeless text when switching theme', async ({
page,
}) => {
const container = locateEditorContainer(page);
await container.dblclick();
await page.waitForSelector('affine-edgeless-text');
await type(page, 'text color');
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
const text = page.locator('affine-edgeless-text');
await text.click();
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const colorPicker = toolbar.locator('edgeless-color-picker-button');
const colorButton = toolbar.getByLabel('Text color');
await colorButton.click();
const pickedColorButton = colorPicker.locator(
'edgeless-color-button[active]'
);
let pickedColor = await pickedColorButton.locator('svg').getAttribute('fill');
let textColor = await getEdgelessTextColor(text);
await expect(pickedColorButton.getByLabel('Black')).toHaveCount(1);
expect(pickedColor).toBe(textColor);
const blackColorButton = colorPicker
.locator('edgeless-color-button')
.filter({
has: page.locator('.color-unit'),
})
.filter({
has: page.getByLabel('Black'),
});
await blackColorButton.click();
pickedColor = await blackColorButton.locator('svg').getAttribute('fill');
textColor = await getEdgelessTextColor(text);
expect(pickedColor).toBe(textColor);
expect(pickedColor).toBe('#000000');
await page.getByTestId('header-info-button').click();
await page
.locator('[data-info-id="edgelessTheme"]')
.locator('[data-property-value="true"]')
.locator('button[value="dark"]')
.click();
await page.keyboard.press('Escape');
await expect(page.getByTestId('property-collapsible-section')).toBeHidden();
await colorButton.click();
pickedColor = await blackColorButton.locator('svg').getAttribute('fill');
textColor = await getEdgelessTextColor(text);
expect(pickedColor).toBe(textColor);
expect(pickedColor).toBe('#ffffff');
});

View File

@@ -0,0 +1,99 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { test } from '@affine-test/kit/playwright';
import { locateEditorContainer } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addDatabase,
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('database is useable', async ({ page }) => {
test.slow();
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('test title');
await page.keyboard.press('Enter');
expect(await title.innerText()).toBe('test title');
await addDatabase(page);
const database = page.locator('affine-database');
await expect(database).toBeVisible();
await page.reload();
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title2 = getBlockSuiteEditorTitle(page);
await title2.pressSequentially('test title2');
await page.waitForTimeout(500);
expect(await title2.innerText()).toBe('test title2');
await page.keyboard.press('Enter');
await addDatabase(page);
const database2 = page.locator('affine-database');
await expect(database2).toBeVisible();
});
test('link page is useable', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('page1');
await page.keyboard.press('Enter');
expect(await title.innerText()).toBe('page1');
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title2 = getBlockSuiteEditorTitle(page);
await title2.pressSequentially('page2');
await page.keyboard.press('Enter');
expect(await title2.innerText()).toBe('page2');
await page.keyboard.press('@', { delay: 50 });
await page.keyboard.press('p');
await page.keyboard.press('a');
await page.keyboard.press('g');
await page.keyboard.press('e');
await page.keyboard.press('1');
await waitNextFrame(page);
await page.locator('icon-button:has-text("page1")').first().click();
const link = page.locator('.affine-reference');
await expect(link).toBeVisible();
await page.click('.affine-reference');
await page.waitForTimeout(500);
await expect(
page.locator('.doc-title-container:has-text("page1")')
).toBeVisible();
});
test('append paragraph when click editor gap', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('test title');
await page.keyboard.press('ArrowDown');
await page.keyboard.insertText('test content');
const paragraph = page.locator('affine-paragraph');
const numParagraphs = await paragraph.count();
await page.locator('[data-testid=page-editor-blank]').click();
expect(await paragraph.count()).toBe(numParagraphs + 1);
await page.locator('[data-testid=page-editor-blank]').click();
expect(
await paragraph.count(),
'click the gap again, should not append another paragraph'
).toBe(numParagraphs + 1);
const editorContainer = locateEditorContainer(page);
expect(
await editorContainer.evaluate(el => el.contains(document.activeElement)),
'editor should should keep being focused'
).toBe(true);
});

View File

@@ -0,0 +1,263 @@
import {
clickEdgelessModeButton,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Page, test } from '@playwright/test';
const TEST_SPOTIFY_URL =
'https://open.spotify.com/episode/7makk4oTQel546B0PZlDM5';
const TEST_AFFINE_URL = 'https://app.affine.pro/';
const EMBED_IFRAME_BLOCK = 'affine-embed-iframe-block';
const EMBED_EDGELESS_IFRAME_BLOCK = 'affine-embed-edgeless-iframe-block';
const IDLE_CARD = 'embed-iframe-idle-card';
const LINK_INPUT_POPUP = 'embed-iframe-link-input-popup';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await page.locator('affine-paragraph v-line div').click();
});
test.describe('embed iframe block', () => {
async function addEmbedIframeBlock(page: Page) {
await type(page, '/embed');
const embedItem = page.getByTestId('Embed');
await embedItem.click();
const embedIframeBlock = page.locator(EMBED_IFRAME_BLOCK);
await expect(embedIframeBlock).toBeVisible();
const embedIframeIdleCard = page.locator(IDLE_CARD);
await expect(embedIframeIdleCard).toBeVisible();
const embedIframeLinkInputPopup = page.locator(LINK_INPUT_POPUP);
await expect(embedIframeLinkInputPopup).toBeVisible();
const input = embedIframeLinkInputPopup.locator('input');
await expect(input).toBeFocused();
return {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
};
}
async function openToolbarAndSwitchView(page: Page) {
// expect toolbar to be visible
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
// switch to inline view
await page.getByRole('button', { name: 'Switch view' }).click();
}
test('add embed iframe block using slash menu', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
await page.waitForTimeout(100);
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
});
test('add bookmark when the url cannot be embedded', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
await input.fill(TEST_AFFINE_URL);
await input.press('Enter');
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
// expect the embed iframe block count is 0
await expect(embedIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkBlock = page.locator('affine-bookmark');
await expect(bookmarkBlock).toHaveCount(1);
});
test('add embed iframe url by clicking the idle card', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
// click somewhere else to close the link input popup
// click position below the popup
const popupBoundingBox = await embedIframeLinkInputPopup.boundingBox();
expect(popupBoundingBox).not.toBeNull();
if (!popupBoundingBox) return;
await page.mouse.click(popupBoundingBox.x - 50, popupBoundingBox.y + 100);
await page.waitForTimeout(100);
await expect(embedIframeLinkInputPopup).not.toBeVisible();
// click the idle card
await embedIframeIdleCard.click();
await expect(embedIframeLinkInputPopup).toBeVisible();
// fill the url
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
await expect(embedIframeBlock).toHaveCount(1);
});
test.describe('conversions', () => {
async function setupEmbedIframe(page: Page) {
const { embedIframeBlock, input } = await addEmbedIframeBlock(page);
// fill the url
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
// wait until iframe is loaded
await expect(embedIframeBlock).toBeVisible();
// click to select embed iframe block
await embedIframeBlock.click();
return { embedIframeBlock };
}
test('embed iframe block to link text', async ({ page }) => {
const { embedIframeBlock } = await setupEmbedIframe(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Inline view' }).click();
const affineLink = page.locator('affine-link');
await expect(affineLink).toBeVisible();
// hover affine link
await affineLink.hover();
await page.waitForTimeout(100);
// convert back to embed iframe block
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
// expect the link text content is the url
await expect(affineLink).toHaveCount(0);
});
test('embed iframe block to bookmark card', async ({ page }) => {
const { embedIframeBlock } = await setupEmbedIframe(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Card view' }).click();
// expect the embed iframe block count is 0
await expect(embedIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkBlock = page.locator('affine-bookmark');
await expect(bookmarkBlock).toHaveCount(1);
// click the bookmark block
await bookmarkBlock.click();
// convert back to embed iframe block
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
await expect(bookmarkBlock).toHaveCount(0);
});
});
test.describe('edgeless mode', () => {
// add embed iframe block to edgeless mode by cmdk menu
async function addEmbedEdgelessIframeBlock(page: Page) {
// switch to edgeless mode
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
// press @ to trigger the menu
await page.keyboard.press('@');
// type spotify url
await page.keyboard.type(TEST_SPOTIFY_URL);
// click item which data-testid='cmdk-label'
await page
.locator(
`[data-testid="cmdk-label"][data-value="external-link:${TEST_SPOTIFY_URL}"]`
)
.click();
// expect the embed iframe block count is 1
const embedEdgelessIframeBlock = page.locator(
EMBED_EDGELESS_IFRAME_BLOCK
);
await expect(embedEdgelessIframeBlock).toHaveCount(1);
}
test('insert embed iframe block to edgeless directly', async ({ page }) => {
await addEmbedEdgelessIframeBlock(page);
});
test('convert between embed view and card view', async ({ page }) => {
await addEmbedEdgelessIframeBlock(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Card view' }).click();
// expect the embed iframe block count is 0
const embedEdgelessIframeBlock = page.locator(
EMBED_EDGELESS_IFRAME_BLOCK
);
await expect(embedEdgelessIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkEdgelessBlock = page.locator('affine-edgeless-bookmark');
await expect(bookmarkEdgelessBlock).toHaveCount(1);
// click the bookmark block
await bookmarkEdgelessBlock.click();
// convert back to embed view
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedEdgelessIframeBlock).toHaveCount(1);
});
});
});

View File

@@ -0,0 +1,170 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
dblclickNoteBody,
fitViewportToContent,
focusDocTitle,
getSelectedXYWH,
locateEditorContainer,
resizeElementByHandle,
scaleElementByHandle,
} from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
const title = 'Synced Block Test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, title);
await pressEnter(page);
await page.keyboard.type('test content');
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should not show hidden note in embed view page mode', async ({
page,
}) => {
await dblclickNoteBody(page);
await page.keyboard.type('visible content');
await createEdgelessNoteBlock(page, [100, 100]);
await page.keyboard.press('Enter');
await page.keyboard.type('hidden content');
await page.keyboard.press('Enter');
// create a new page and navigate
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
await inlineLink.dblclick();
// reference the previous page
await focusDocTitle(page);
await page.keyboard.press('Enter');
await page.keyboard.type('@' + title);
const docPopover = page.locator('.linked-doc-popover');
await docPopover.getByText(/^Synced Block Test$/).click();
// switch to embed view
await inlineLink.hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
// check the content
const embedLink = page.locator('affine-embed-synced-doc-block');
await expect(embedLink.getByText(/visible content/)).toBeVisible();
await expect(embedLink.getByText(/hidden content/)).toBeHidden();
});
test.describe('edgeless', () => {
test.beforeEach(async ({ page }) => {
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
await clickView(page, [0, 0]);
await page.keyboard.type('@' + title);
await page
.getByTestId('cmdk-quick-search')
.getByText(/^Synced Block Test$/)
.click();
await fitViewportToContent(page);
});
test.describe('header of edgeless embed synced doc', () => {
test('should fold button works', async ({ page }) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
const content = embedBlock.locator('editor-host');
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
await foldButton.click();
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
});
test('should show title in header', async ({ page }) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const headerTitle = embedBlock.getByTestId(
'edgeless-embed-synced-doc-title'
);
await expect(headerTitle).toHaveText(title);
});
});
test.describe('size adjustment of embed synced doc', () => {
test.beforeEach(async ({ page }) => {
await scaleElementByHandle(page, [10, 10], 'bottom-right');
});
test('should fold embed synced doc when adjust height to smallest', async ({
page,
}) => {
const [, , , h] = await getSelectedXYWH(page);
await resizeElementByHandle(page, [0, -(h - 10)], 'bottom-right');
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
const content = embedBlock.locator('editor-host');
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await expect(content).not.toBeInViewport();
await foldButton.click();
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
await embedBlock.click();
const [, , , h2] = await getSelectedXYWH(page);
expect(
h2,
'should recover height when unfold embed synced doc which was resized to smallest height directly'
).toEqual(h);
});
test('should be able to adjust height the folded embed synced doc', async ({
page,
}) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const content = embedBlock.locator('editor-host');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
await foldButton.click();
await resizeElementByHandle(page, [50, 0], 'bottom-right');
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await resizeElementByHandle(page, [-50, 0], 'bottom-right');
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await resizeElementByHandle(page, [0, 50], 'bottom-right');
await expect(
content,
'should unfold the embed synced doc when adjust height to greater'
).toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
});
});
});

View File

@@ -0,0 +1,119 @@
import { toolbarButtons } from '@affine-test/kit/bs/linked-toolbar';
import { test } from '@affine-test/kit/playwright';
import {
pressArrowUp,
pressEnter,
pressTab,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'List Test');
await waitForEditorLoad(page);
});
test.describe('split list', () => {
test('split at the start of the list', async ({ page }) => {
await pressEnter(page);
await type(page, '1. aaa\nbbb\nccc\nddd');
await pressArrowUp(page, 2);
await pressTab(page);
await pressArrowUp(page, 2);
const listLocator = page.locator('affine-list');
/**
* 1. |aaa
* a. bbb
* 2. ccc
* 3. ddd
*/
await expect(listLocator.nth(0).locator('rich-text')).toHaveText([
'aaa',
'bbb',
]);
await expect(
listLocator.nth(0).locator('.affine-list-block__numbered')
).toHaveText(['1.', 'a.']);
await expect(listLocator.nth(2).locator('rich-text')).toHaveText('ccc');
await expect(
listLocator.nth(2).locator('.affine-list-block__numbered')
).toHaveText('2.');
await expect(listLocator.nth(3).locator('rich-text')).toHaveText('ddd');
await expect(
listLocator.nth(3).locator('.affine-list-block__numbered')
).toHaveText('3.');
await pressEnter(page);
/**
* 1.
* 2. aaa
* a. bbb
* 3. ccc
* 4. ddd
*/
await expect(
listLocator.nth(0).locator('.affine-list-block__numbered')
).toHaveText('1.');
await expect(listLocator.nth(1).locator('rich-text')).toHaveText([
'aaa',
'bbb',
]);
await expect(
listLocator.nth(1).locator('.affine-list-block__numbered')
).toHaveText(['2.', 'a.']);
await expect(listLocator.nth(3).locator('rich-text')).toHaveText('ccc');
await expect(
listLocator.nth(3).locator('.affine-list-block__numbered')
).toHaveText('3.');
await expect(listLocator.nth(4).locator('rich-text')).toHaveText('ddd');
await expect(
listLocator.nth(4).locator('.affine-list-block__numbered')
).toHaveText('4.');
});
test('tab in list should not propagate out of editor', async ({ page }) => {
await pressEnter(page);
await type(page, '1. aaa');
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
const { switchViewBtn, cardViewBtn } = toolbarButtons(page);
const list = page.locator('affine-list');
const card = page.locator('affine-embed-linked-doc-block');
const pressWithCount = async (key: string, count: number) => {
for (let i = 0; i < count; i++) {
await new Promise(resolve => setTimeout(resolve, 5));
await page.keyboard.press(key);
}
};
await inlineLink.hover();
await switchViewBtn.click();
await cardViewBtn.click();
await expect(list.filter({ has: card })).toHaveCount(1);
await page.keyboard.press('Shift+Tab');
await expect(list.filter({ hasNot: card })).toHaveCount(1);
await pressWithCount('Tab', 5);
await expect(list.filter({ has: card })).toHaveCount(1);
await pressWithCount('Shift+Tab', 5);
await expect(list.filter({ hasNot: card })).toHaveCount(1);
});
});

View File

@@ -0,0 +1,565 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickPageModeButton,
clickView,
createEdgelessNoteBlock,
focusDocTitle,
getEdgelessSelectedIds,
getViewportCenter,
locateToolbar,
setViewportCenter,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import {
closeSettingModal,
confirmExperimentalPrompt,
openExperimentalFeaturesPanel,
openSettingModal,
} from '@affine-test/kit/utils/setting';
import { openRightSideBar } from '@affine-test/kit/utils/sidebar';
import { expect, type Locator, type Page } from '@playwright/test';
import {
createHeadings,
createTitle,
getVerticalCenterFromLocator,
} from './utils';
async function openTocPanel(page: Page) {
const toc = page.locator('affine-outline-panel');
if (await toc.isVisible()) return toc;
await openRightSideBar(page, 'outline');
await toc.waitFor({ state: 'visible' });
return toc;
}
function getTocHeading(panel: Locator, level: number) {
return panel.getByTestId(`outline-block-preview-h${level}`).locator('span');
}
// locate cards in outline panel
// ! Please note that when any card mode changed, the locator will be mutated
function locateCards(toc: Locator, mode?: 'both' | 'page' | 'edgeless') {
const cards = toc.locator('affine-outline-note-card');
return mode
? cards.locator(`>div[data-visibility="${mode}"]`)
: cards.locator('>div');
}
function locateSortingButton(panel: Locator) {
return panel.getByTestId('toggle-notes-sorting-button');
}
async function changeNoteDisplayMode(
card: Locator,
mode: 'both' | 'doc' | 'edgeless'
) {
await card.hover();
await card.getByTestId('display-mode-button').click();
await card.locator(`note-display-mode-panel .item.${mode}`).click();
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test.describe('TOC display', () => {
test('should display title and headings when there are non-empty headings in editor', async ({
page,
}) => {
await createTitle(page);
await createHeadings(page);
const toc = await openTocPanel(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeVisible();
for (let i = 1; i <= 6; i++) {
await expect(getTocHeading(toc, i)).toBeVisible();
await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}`);
}
});
test('should display placeholder when no headings', async ({ page }) => {
const toc = await openTocPanel(page);
const noHeadingPlaceholder = toc.getByTestId('empty-panel-placeholder');
await createTitle(page);
await pressEnter(page);
await type(page, 'hello world');
await expect(noHeadingPlaceholder).toBeVisible();
});
test('should not display headings when there are only empty headings', async ({
page,
}) => {
await createTitle(page);
// create empty headings
for (let i = 1; i <= 6; i++) {
await type(page, `${'#'.repeat(i)} `);
await pressEnter(page);
}
const toc = await openTocPanel(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden();
for (let i = 1; i <= 6; i++) {
await expect(getTocHeading(toc, i)).toBeHidden();
}
});
test('should update panel when modify or clear title or headings', async ({
page,
}) => {
const title = await createTitle(page);
const headings = await createHeadings(page);
const toc = await openTocPanel(page);
await title.scrollIntoViewIfNeeded();
await title.click();
await type(page, 'xxx');
await expect(toc.getByTestId('outline-block-preview-title')).toContainText([
'Titlexxx',
]);
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden();
for (let i = 1; i <= 6; i++) {
await headings[i - 1].click();
await type(page, 'xxx');
await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}xxx`);
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(getTocHeading(toc, i)).toBeHidden();
}
});
test('should update panel when switch doc', async ({ page }) => {
const toc = await openTocPanel(page);
await focusDocTitle(page);
await page.keyboard.press('ArrowDown');
await type(page, '# Heading 1');
await clickNewPageButton(page);
await expect(getTocHeading(toc, 1)).toBeHidden();
await page.goBack();
await expect(getTocHeading(toc, 1)).toBeVisible();
});
test('should add padding to sub-headings', async ({ page }) => {
await createHeadings(page);
const toc = await openTocPanel(page);
let prev = getTocHeading(toc, 1);
for (let i = 2; i <= 6; i++) {
const curr = getTocHeading(toc, i);
const prevRect = await prev.boundingBox();
const currRect = await curr.boundingBox();
expect(prevRect).not.toBeNull();
expect(currRect).not.toBeNull();
expect(prevRect!.x).toBeLessThan(currRect!.x);
prev = curr;
}
});
test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# Heading 1');
const toc = await openTocPanel(page);
const sortingButton = locateSortingButton(toc);
await expect(sortingButton).not.toHaveClass(/active/);
await expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
await clickEdgelessModeButton(page);
await expect(sortingButton).toHaveClass(/active/);
await expect(toc.locator('[data-sortable="true"]')).toHaveCount(1);
await sortingButton.click();
await expect(sortingButton).not.toHaveClass(/active/);
await expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
});
test('should notify user when there are some page only notes and sorting is disabled', async ({
page,
}) => {
await clickEdgelessModeButton(page);
const toc = await openTocPanel(page);
const viewportCenter = await getViewportCenter(page);
await createEdgelessNoteBlock(page, [viewportCenter.x, viewportCenter.y]);
const card = locateCards(toc, 'edgeless');
await changeNoteDisplayMode(card, 'doc');
const notification = toc.getByTestId('affine-outline-notice');
await expect(notification).toBeHidden();
const sortingButton = locateSortingButton(toc);
await sortingButton.click();
await expect(notification).toBeVisible();
await notification.getByTestId('outline-notice-sort-button').click();
await expect(notification).toBeHidden();
await expect(sortingButton).toHaveClass(/active/);
await sortingButton.click();
await expect(notification).toBeVisible();
await notification.getByTestId('outline-notice-close-button').click();
await expect(notification).toBeHidden();
});
});
test.describe('TOC and editor scroll', () => {
test('should highlight heading when scroll to area before viewport center', async ({
page,
}) => {
const title = await createTitle(page);
for (let i = 0; i < 3; i++) {
await pressEnter(page);
}
const headings = await createHeadings(page, 10);
await title.scrollIntoViewIfNeeded();
const toc = await openTocPanel(page);
const viewportCenter = await getVerticalCenterFromLocator(
page.locator('body')
);
const activeHeadingContainer = toc.locator(
'affine-outline-panel-body .active'
);
await title.click();
await expect(activeHeadingContainer).toContainText('Title');
for (let i = 0; i < headings.length; i++) {
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20);
await page.waitForTimeout(10);
await expect(activeHeadingContainer).toContainText(`Heading ${i + 1}`);
}
});
test('should scroll to heading and highlight heading when click item in outline panel', async ({
page,
}) => {
const headings = await createHeadings(page, 10);
const toc = await openTocPanel(page);
const activeHeadingContainer = toc.locator(
'affine-outline-panel-body .active'
);
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
getTocHeading(toc, i + 1)
);
await headingsInPanel[2].click();
await expect(headings[2]).toBeVisible();
await expect(activeHeadingContainer).toContainText('Heading 3');
});
test('should scroll to title when click title in outline panel', async ({
page,
}) => {
const title = await createTitle(page);
await pressEnter(page);
await createHeadings(page, 10);
const toc = await openTocPanel(page);
const titleInPanel = toc.getByTestId('outline-block-preview-title');
await expect(title).not.toBeInViewport();
await titleInPanel.click();
await expect(title).toBeVisible();
});
});
test.describe('TOC and edgeless selection', () => {
test('should select note blocks when selecting cards in TOC', async ({
page,
}) => {
const toc = await openTocPanel(page);
await clickEdgelessModeButton(page);
const cards = locateCards(toc);
await createEdgelessNoteBlock(page, [100, 100]);
await changeNoteDisplayMode(cards.last(), 'doc');
await createEdgelessNoteBlock(page, [200, 200]);
await cards.nth(0).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1);
await cards.nth(0).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(0);
await cards.nth(1).click();
expect(
await getEdgelessSelectedIds(page),
'should not select doc only note'
).toHaveLength(0);
await cards.nth(2).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1);
await cards.nth(2).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(0);
await cards.nth(0).click({ modifiers: ['Shift'] });
await cards.nth(1).click({ modifiers: ['Shift'] });
await cards.nth(2).click({ modifiers: ['Shift'] });
expect(await getEdgelessSelectedIds(page)).toHaveLength(2);
});
test('should select note cards when select note blocks in canvas', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setViewportCenter(page, [0, 0]);
await selectAllByKeyboard(page);
await pressBackspace(page);
await createEdgelessNoteBlock(page, [100, 100]);
await createEdgelessNoteBlock(page, [200, 200]);
await clickView(page, [0, 0]);
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await clickView(page, [100, 100]);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'selected');
await clickView(page, [200, 200]);
await expect(cards.nth(1)).toHaveAttribute('data-status', 'selected');
await selectAllByKeyboard(page);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'selected');
await expect(cards.nth(1)).toHaveAttribute('data-status', 'selected');
await pressEscape(page);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'normal');
await expect(cards.nth(1)).toHaveAttribute('data-status', 'normal');
});
});
test.describe('drag and drop note in outline panel', () => {
const dragCard = async (
from: Locator,
to: Locator,
position: 'before' | 'after' = 'before'
) => {
const fromRect = await from.boundingBox();
const fromCenter = {
x: fromRect!.width / 2,
y: fromRect!.height / 2,
};
const toRect = await to.boundingBox();
const toCenter = {
x: toRect!.width / 2,
y: toRect!.height / 2,
};
await from.dragTo(to, {
sourcePosition: fromCenter,
targetPosition:
position === 'before'
? { x: toCenter.x, y: toCenter.y - 10 }
: { x: toCenter.x, y: toCenter.y + 10 },
});
};
// create 2 both cards, 2 page cards and 2 edgeless cards
test.beforeEach(async ({ page }) => {
const toc = await openTocPanel(page);
const edgelessCards = locateCards(toc, 'edgeless');
// 2 both cards
{
await focusDocTitle(page);
await page.keyboard.press('ArrowDown');
await type(page, '0');
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '1');
await changeNoteDisplayMode(edgelessCards.first(), 'both');
}
// 2 page cards
{
await createEdgelessNoteBlock(page, [150, 150]);
await type(page, '2');
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
await createEdgelessNoteBlock(page, [200, 200]);
await type(page, '3');
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
}
// 2 edgeless cards
{
await createEdgelessNoteBlock(page, [250, 250]);
await type(page, '4');
await createEdgelessNoteBlock(page, [300, 300]);
await type(page, '5');
}
});
test('should reorder notes when drag and drop a note in outline panel', async ({
page,
}) => {
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await dragCard(cards.nth(3), cards.nth(1));
await clickPageModeButton(page);
const paragraphs = page
.locator('affine-paragraph')
.locator('[data-v-text="true"]');
await expect(paragraphs).toHaveCount(4);
await expect(paragraphs.nth(0)).toContainText('0');
await expect(paragraphs.nth(1)).toContainText('3');
await expect(paragraphs.nth(2)).toContainText('1');
await expect(paragraphs.nth(3)).toContainText('2');
// Note card should be able to drag and drop in page mode
await locateSortingButton(toc).click();
await dragCard(cards.nth(3), cards.nth(1));
await expect(paragraphs.nth(0)).toContainText('0');
await expect(paragraphs.nth(1)).toContainText('2');
await expect(paragraphs.nth(2)).toContainText('3');
await expect(paragraphs.nth(3)).toContainText('1');
});
test('multiple selected note cards can be dragged and dropped at same time', async ({
page,
}) => {
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await cards.nth(2).click({ modifiers: ['Shift'] });
await cards.nth(3).click({ modifiers: ['Shift'] });
await dragCard(cards.nth(2), cards.nth(0));
await clickPageModeButton(page);
const paragraphs = page
.locator('affine-paragraph')
.locator('[data-v-text="true"]');
await expect(paragraphs).toHaveCount(4);
await expect(paragraphs.nth(0)).toContainText('2');
await expect(paragraphs.nth(1)).toContainText('3');
await expect(paragraphs.nth(2)).toContainText('0');
await expect(paragraphs.nth(3)).toContainText('1');
});
});
test.describe('advanced visibility control', () => {
test.beforeEach(async ({ page }) => {
await openSettingModal(page);
await openExperimentalFeaturesPanel(page);
await confirmExperimentalPrompt(page);
await page.getByTestId('enable_advanced_block_visibility').click();
await closeSettingModal(page);
await page.reload();
});
test('should update notes when change note display mode from note toolbar', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setViewportCenter(page, [0, 0]);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, 'hello');
await clickView(page, [200, 200]);
const toc = await openTocPanel(page);
const bothCard = locateCards(toc, 'both');
const edgelessCard = locateCards(toc, 'edgeless');
await expect(bothCard).toHaveCount(1);
await expect(edgelessCard).toHaveCount(1);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
await toolbar.getByRole('button', { name: 'Mode' }).click();
await toolbar.locator('note-display-mode-panel .item.both').click();
await expect(bothCard).toHaveCount(2);
await expect(edgelessCard).toHaveCount(0);
});
test('should update notes after slicing note', async ({ page }) => {
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [200, 100]);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await pressEscape(page, 3);
await createEdgelessNoteBlock(page, [200, 400]);
const toc = await openTocPanel(page);
const bothCard = locateCards(toc, 'both');
const edgelessCards = locateCards(toc, 'edgeless');
await expect(bothCard).toHaveCount(1);
await expect(edgelessCards).toHaveCount(2);
while ((await edgelessCards.count()) > 0) {
await edgelessCards.first().hover();
await edgelessCards.first().getByTestId('display-mode-button').click();
await edgelessCards
.first()
.locator('note-display-mode-panel .item.both')
.click();
}
await expect(bothCard).toHaveCount(3);
await clickView(page, [200, 100]);
const toolbar = locateToolbar(page);
await toolbar.getByRole('button', { name: 'Slicer' }).click();
await expect(page.locator('.note-slicer-button')).toBeVisible();
await page.locator('.note-slicer-button').click();
await expect(bothCard).toHaveCount(4);
await expect(
bothCard.nth(1),
'the sliced note card should keep the order in its parent'
).toContainText('hello');
await expect(
bothCard.nth(2),
'the sliced note card should keep the order in its parent'
).toContainText('world');
});
});

View File

@@ -0,0 +1,279 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickPageModeButton,
createEdgelessNoteBlock,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEnter,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Locator, type Page } from '@playwright/test';
import {
createHeadings,
createTitle,
getVerticalCenterFromLocator,
} from './utils';
function getIndicators(container: Page | Locator) {
return container.locator('affine-outline-viewer .outline-viewer-indicator');
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test('should display indicators when non-empty headings exists', async ({
page,
}) => {
const indicators = getIndicators(page);
await createHeadings(page);
await expect(indicators).toHaveCount(6);
for (let i = 0; i < 6; i++) {
await expect(indicators.nth(i)).toBeVisible();
}
});
test('should be hidden when only empty headings exists', async ({ page }) => {
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(0);
for (let i = 1; i <= 6; i++) {
// empty heading
await type(page, `${'#'.repeat(i)} `);
await pressEnter(page);
}
await expect(indicators).toHaveCount(0);
});
test('should update indicator when clear title or headings', async ({
page,
}) => {
const indicators = getIndicators(page);
const title = await createTitle(page);
const headings = await createHeadings(page);
await expect(indicators).toHaveCount(7);
await title.scrollIntoViewIfNeeded();
await title.click();
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(indicators).toHaveCount(6);
for (let i = 1; i <= 6; i++) {
await headings[i - 1].click();
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(indicators).toHaveCount(6 - i);
}
});
test('should display simple outline panel when hovering over indicators', async ({
page,
}) => {
const indicators = getIndicators(page);
await createTitle(page);
await createHeadings(page);
await indicators.first().hover({ force: true });
const items = page.locator('.outline-viewer-item');
await expect(items).toHaveCount(8);
await expect(items.nth(0)).toContainText(['Table of Contents']);
await expect(items.nth(1)).toContainText(['Title']);
for (let i = 2; i <= 7; i++) {
await expect(items.nth(i)).toContainText([`Heading ${i - 1}`]);
}
});
test('should highlight indicator when scrolling', async ({ page }) => {
const indicators = getIndicators(page);
const title = await createTitle(page);
for (let i = 1; i <= 3; i++) {
await pressEnter(page);
}
const headings = await createHeadings(page, 10);
await title.scrollIntoViewIfNeeded();
const viewportCenter = await getVerticalCenterFromLocator(
page.locator('body')
);
for (let i = 0; i < headings.length; i++) {
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
await expect(indicators.nth(i)).toHaveClass(/active/);
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20);
await page.waitForTimeout(10);
}
});
test('should highlight indicator when click item in outline panel', async ({
page,
}) => {
const viewer = page.locator('affine-outline-viewer');
const indicators = getIndicators(page);
const headings = await createHeadings(page, 10);
await indicators.first().hover({ force: true });
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
viewer.getByTestId(`outline-block-preview-h${i + 1}`)
);
await headingsInPanel[2].click();
await expect(headings[2]).toBeVisible();
await expect(indicators.nth(2)).toHaveClass(/active/);
});
test('outline viewer should hide in edgeless mode', async ({ page }) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# ');
await type(page, 'Heading 1');
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(2);
await clickEdgelessModeButton(page);
await expect(indicators).toHaveCount(0);
await clickPageModeButton(page);
await expect(indicators).toHaveCount(2);
});
test('should hide edgeless-only note headings', async ({ page }) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# Heading 1');
await pressEnter(page);
await type(page, '## Heading 2');
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '# Edgeless');
await clickPageModeButton(page);
await waitForEditorLoad(page);
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(3);
await indicators.first().hover({ force: true });
const viewer = page.locator('affine-outline-viewer');
await expect(viewer).toBeVisible();
const h1InPanel = viewer
.getByTestId('outline-block-preview-h1')
.locator('span');
await h1InPanel.waitFor({ state: 'visible' });
await expect(h1InPanel).toContainText(['Heading 1']);
});
test('outline viewer should update after change heading in edgeless mode', async ({
page,
}) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# ');
await type(page, 'Heading 1');
await clickEdgelessModeButton(page);
const note = page.locator('affine-edgeless-note');
await note.dblclick();
await type(page, '# New Heading');
await clickPageModeButton(page);
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(3);
});
test('outline viewer should be useable in doc peek preview', async ({
page,
}) => {
await pressEnter(page);
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel(/^Open doc with$/).click();
await toolbar
.getByLabel('Open doc menu')
.getByLabel('Open in center peek')
.click();
const peekView = page.getByTestId('peek-view-modal');
await expect(peekView).toBeVisible();
const title = peekView.locator('doc-title .inline-editor');
await title.click();
await pressEnter(page);
await type(page, '# Heading 1');
for (let i = 0; i < 10; i++) {
await pressEnter(page);
}
await type(page, '## Heading 2');
const outlineViewer = peekView.locator('affine-outline-viewer');
const outlineViewerBound = await outlineViewer.boundingBox();
expect(outlineViewerBound).not.toBeNull();
const indicators = getIndicators(peekView);
await expect(indicators).toHaveCount(3);
await expect(indicators.nth(0)).toBeVisible();
await expect(indicators.nth(1)).toBeVisible();
await expect(indicators.nth(2)).toBeVisible();
await indicators.first().hover({ force: true });
const viewer = peekView.locator('affine-outline-viewer');
await expect(viewer).toBeVisible();
// position of outline viewer should be fixed
{
const headingButtons = peekView.locator(
'affine-outline-viewer .outline-viewer-item:not(.outline-viewer-header)'
);
await expect(headingButtons).toHaveCount(3);
await expect(headingButtons.nth(0)).toBeVisible();
await expect(headingButtons.nth(1)).toBeVisible();
await expect(headingButtons.nth(2)).toBeVisible();
await headingButtons.last().click();
await page.mouse.move(0, 0);
await headingButtons.last().waitFor({ state: 'hidden' });
const currentOutlineViewerBound = await outlineViewer.boundingBox();
expect(currentOutlineViewerBound).not.toBeNull();
expect(outlineViewerBound).toEqual(currentOutlineViewerBound);
}
// outline viewer should be hidden when clicking the outline panel toggle button
{
await indicators.first().hover({ force: true });
const toggleButton = peekView.locator(
'.outline-viewer-header edgeless-tool-icon-button'
);
await toggleButton.click();
await page.waitForTimeout(500);
await expect(peekView).toBeHidden();
await expect(viewer).toBeHidden();
await expect(page.locator('affine-outline-panel')).toBeVisible();
}
});

View File

@@ -0,0 +1,34 @@
import { locateEditorContainer } from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import {
getBlockSuiteEditorTitle,
type,
} from '@affine-test/kit/utils/page-logic';
import type { Locator, Page } from '@playwright/test';
export async function createTitle(page: Page) {
const title = getBlockSuiteEditorTitle(page);
await title.scrollIntoViewIfNeeded();
await title.click();
await type(page, 'Title');
await pressEnter(page);
return title;
}
export async function createHeadings(page: Page, gap = 0) {
const editorContainer = locateEditorContainer(page);
const headings: Locator[] = [];
await pressEnter(page, gap + 1);
for (let i = 1; i <= 6; i++) {
await type(page, `${'#'.repeat(i)} Heading ${i}`);
headings.push(editorContainer.locator(`.h${i}`));
await pressEnter(page, gap + 1);
}
return headings;
}
export async function getVerticalCenterFromLocator(locator: Locator) {
const rect = await locator.boundingBox();
return rect!.y + rect!.height / 2;
}

View File

@@ -0,0 +1,460 @@
import { test } from '@affine-test/kit/playwright';
import { locateToolbar } from '@affine-test/kit/utils/editor';
import {
pressEnter,
pressShiftTab,
pressTab,
selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import { expect, type Locator } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'Paragraph Test');
await waitForEditorLoad(page);
});
async function expectParagraphState(
paragraphs: Locator,
index: number,
expectedType: string,
expectedText: string
) {
await expect
.poll(async () => {
return await paragraphs
.nth(index)
.evaluate(
(
block: ParagraphBlockComponent,
expected: { type: string; text: string }
) =>
block.model.props.type === expected.type &&
block.model.props.text.toString() === expected.text,
{
type: expectedType,
text: expectedText,
}
);
})
.toBeTruthy();
}
async function expectParagraphVisibility(
paragraphs: Locator,
index: number,
visible: boolean
) {
await expect
.poll(async () => {
return await paragraphs.nth(index).isVisible();
})
.toBe(visible);
}
test('heading icon should be updated after change heading level', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('Hello');
// Hello|
// empty paragraph
const paragraph = page.locator('affine-note affine-paragraph').nth(0);
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
await toolbar.getByLabel('Conversions').click();
await toolbar.getByLabel('Heading 1').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeVisible();
await selectAllByKeyboard(page);
await toolbar.getByLabel('Conversions').click();
await toolbar.getByLabel('Heading 2').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeHidden();
await expect(page.getByTestId('heading-icon-2')).toBeVisible();
});
test('basic heading collapsed', async ({ page }) => {
await pressEnter(page);
/**
* # aaa
* bbb
* ## ccc
* ddd
* ### eee
* fff
* ## ggg
* hhh
* # iii
* jjj
*/
await type(
page,
'# aaa\nbbb\n## ccc\nddd\n### eee\nfff\n## ggg\nhhh\n# iii\njjj'
);
const a = page.locator('affine-note affine-paragraph').nth(0);
const b = page.locator('affine-note affine-paragraph').nth(1);
const c = page.locator('affine-note affine-paragraph').nth(2);
const d = page.locator('affine-note affine-paragraph').nth(3);
const e = page.locator('affine-note affine-paragraph').nth(4);
const f = page.locator('affine-note affine-paragraph').nth(5);
const g = page.locator('affine-note affine-paragraph').nth(6);
const h = page.locator('affine-note affine-paragraph').nth(7);
const i = page.locator('affine-note affine-paragraph').nth(8);
const j = page.locator('affine-note affine-paragraph').nth(9);
const assertInitState = async () => {
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeTruthy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
};
await assertInitState();
// fold h1 aaa
await a.hover();
await page.waitForTimeout(100);
await a.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeFalsy();
expect(await c.isVisible()).toBeFalsy();
expect(await d.isVisible()).toBeFalsy();
expect(await e.isVisible()).toBeFalsy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeFalsy();
expect(await h.isVisible()).toBeFalsy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h2 ccc
await c.hover();
await page.waitForTimeout(100);
await c.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeFalsy();
expect(await e.isVisible()).toBeFalsy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h3 eee
await e.hover();
await page.waitForTimeout(100);
await e.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h2 ggg
await g.hover();
await page.waitForTimeout(100);
await g.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeTruthy();
expect(await g.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
});
test('add new heading when press enter at the end of collapsed heading', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa');
await page.keyboard.press('ArrowDown');
await type(page, 'bbb');
await page.keyboard.press('ArrowUp');
await page.keyboard.press('ArrowRight');
const paragraph = page.locator('affine-note affine-paragraph');
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await pressEnter(page);
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === ''
)
).toBeTruthy();
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await undoByKeyboard(page);
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(0).click();
expect(await paragraph.count()).toBe(2);
await pressEnter(page);
expect(await paragraph.count()).toBe(3);
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === ''
)
).toBeTruthy();
});
test('unfold collapsed heading when some block indented to be its child', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa\nbbb\n# ccc');
const paragraph = page.locator('affine-note affine-paragraph');
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(2).click();
await pressTab(page);
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
});
test('unfold collapsed heading when its siblings changed to text type from heading type', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa\nbbb\n# ccc');
const paragraph = page.locator('affine-note affine-paragraph');
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(2).click();
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
await page.keyboard.press('Backspace');
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
});
test('also move children when dedent collapsed heading', async ({ page }) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await pressTab(page);
await type(page, '# bbb\nccc');
const paragraph = page.locator('affine-note affine-paragraph');
const subParagraph = paragraph.nth(0).locator('affine-paragraph');
await expect.poll(() => subParagraph.count()).toBe(2);
await expectParagraphState(subParagraph, 0, 'h1', 'bbb');
await expectParagraphState(subParagraph, 1, 'text', 'ccc');
expect(await subParagraph.nth(1).isVisible()).toBeTruthy();
await subParagraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await subParagraph.nth(1).isVisible()).toBeFalsy();
await subParagraph.nth(0).click();
await pressShiftTab(page);
expect(await subParagraph.count()).toBe(0);
await expectParagraphState(paragraph, 1, 'h1', 'bbb');
await expectParagraphState(paragraph, 2, 'text', 'ccc');
await expectParagraphVisibility(paragraph, 2, false);
await paragraph
.nth(1)
.locator('blocksuite-toggle-button .toggle-icon')
.click({
position: {
x: 5,
y: 5,
},
});
await expectParagraphVisibility(paragraph, 2, true);
});
test('also move collapsed siblings when indent collapsed heading', async ({
page,
}) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await type(page, '# bbb\nccc');
await page.keyboard.press('ArrowUp');
const paragraph = page.locator('affine-note affine-paragraph');
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(0);
await pressTab(page);
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(1);
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(0)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await undoByKeyboard(page);
expect(await paragraph.nth(2).isVisible()).toBeTruthy();
await paragraph
.nth(1)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(2).isVisible()).toBeFalsy();
await paragraph.nth(1).click();
await pressTab(page);
expect(await paragraph.nth(2).isVisible()).toBeFalsy();
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(2);
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(0)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
});
test('unfold collapsed heading when its other blocks indented to be its sibling', async ({
page,
}) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await type(page, '# bbb\nddd');
await page.keyboard.press('ArrowUp');
await pressTab(page);
const paragraph = page.locator('affine-note affine-paragraph');
const subParagraph = paragraph.nth(0).locator('affine-paragraph');
await expect.poll(() => subParagraph.count()).toBe(1);
await expectParagraphState(subParagraph, 0, 'h1', 'bbb');
await subParagraph.nth(0).click();
await page.keyboard.press('End');
await pressEnter(page);
await type(page, 'ccc');
/**
* aaa
* # bbb
* ccc
* ddd
*/
await expectParagraphVisibility(paragraph, 2, true);
await expectParagraphState(paragraph, 2, 'text', 'ccc');
await paragraph.locator('blocksuite-toggle-button .toggle-icon').click();
await expectParagraphVisibility(paragraph, 2, false);
await paragraph.nth(3).click(); // ddd
await expectParagraphVisibility(paragraph, 2, false);
await expect
.poll(() => paragraph.nth(0).locator('affine-paragraph').count())
.toBe(2);
await pressTab(page);
await expect
.poll(() => paragraph.nth(0).locator('affine-paragraph').count())
.toBe(3);
await expectParagraphVisibility(paragraph, 2, true);
});

View File

@@ -0,0 +1,126 @@
import {
clickDeleteButtonInTableMenu,
createTable,
getCellText,
inputToCell,
} from '@affine-test/kit/bs/table';
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('should table collab work', async ({ page: pageA, context }) => {
await openHomePage(pageA);
await clickNewPageButton(pageA);
await waitForEditorLoad(pageA);
await pageA.keyboard.press('Enter');
/**
* | Cell1 | Cell2 |
* | Cell3 | Cell4 |
*/
await createTable(pageA);
const cellsInA = pageA.locator('affine-table-cell');
const cellCountInA = await cellsInA.count();
expect(cellCountInA).toBe(4);
const currentUrl = pageA.url();
const pageB = await context.newPage();
await pageB.goto(currentUrl);
await waitForEditorLoad(pageB);
await pageB.keyboard.press('Enter');
const tableInB = pageB.locator('affine-table');
await expect(tableInB).toBeVisible();
const cellsInB = tableInB.locator('affine-table-cell');
expect(await cellsInB.count()).toBe(cellCountInA);
expect(await getCellText(pageB, 0)).toBe('Cell1');
expect(await getCellText(pageB, 3)).toBe('Cell4');
await cellsInB.last().hover();
const addColumnButton = tableInB.locator('data-testid=add-column-button');
expect(await addColumnButton.isVisible()).toBe(true);
await addColumnButton.click();
expect(await cellsInA.count()).toBe(6);
expect(await cellsInB.count()).toBe(6);
// new created cells should be synced to both page
/**
* | Cell1 | Cell2 | Cell5 |
* | Cell3 | Cell4 | Cell6 |
*/
await inputToCell(pageA, 2, 'Cell5');
await inputToCell(pageA, 5, 'Cell6');
expect(await getCellText(pageB, 2)).toBe('Cell5');
expect(await getCellText(pageB, 5)).toBe('Cell6');
await cellsInB.last().hover();
const addRowButton = tableInB.locator('data-testid=add-row-button');
expect(await addRowButton.isVisible()).toBe(true);
await addRowButton.click();
expect(await cellsInA.count()).toBe(9);
expect(await cellsInB.count()).toBe(9);
// new created cells should be synced to both page
/**
* | Cell1 | Cell2 | Cell5 |
* | Cell3 | Cell4 | Cell6 |
* | Cell7 | Cell8 | Cell9 |
*/
await inputToCell(pageA, 6, 'Cell7');
await inputToCell(pageA, 7, 'Cell8');
await inputToCell(pageA, 8, 'Cell9');
expect(await getCellText(pageB, 6)).toBe('Cell7');
expect(await getCellText(pageB, 7)).toBe('Cell8');
expect(await getCellText(pageB, 8)).toBe('Cell9');
// delete a column
await cellsInA.nth(1).hover();
const dragColumnHandle = cellsInA
.nth(1)
.locator('data-testid=drag-column-handle');
expect(await dragColumnHandle.isVisible()).toBe(true);
await dragColumnHandle.click();
await clickDeleteButtonInTableMenu(pageA);
/**
* | Cell1 | Cell5 |
* | Cell3 | Cell6 |
* | Cell7 | Cell9 |
*/
expect(await cellsInA.count()).toBe(6);
expect(await cellsInB.count()).toBe(6);
expect(await getCellText(pageB, 0)).toBe('Cell1');
expect(await getCellText(pageB, 1)).toBe('Cell5');
expect(await getCellText(pageB, 2)).toBe('Cell3');
expect(await getCellText(pageB, 3)).toBe('Cell6');
expect(await getCellText(pageB, 4)).toBe('Cell7');
expect(await getCellText(pageB, 5)).toBe('Cell9');
// delete a row
await cellsInB.nth(0).hover();
const dragRowHandle = cellsInB.nth(0).locator('data-testid=drag-row-handle');
expect(await dragRowHandle.isVisible()).toBe(true);
await dragRowHandle.click();
await clickDeleteButtonInTableMenu(pageB);
/**
* | Cell3 | Cell6 |
* | Cell7 | Cell9 |
*/
expect(await cellsInA.count()).toBe(4);
expect(await cellsInB.count()).toBe(4);
expect(await getCellText(pageB, 0)).toBe('Cell3');
expect(await getCellText(pageB, 1)).toBe('Cell6');
expect(await getCellText(pageB, 2)).toBe('Cell7');
expect(await getCellText(pageB, 3)).toBe('Cell9');
});

View File

@@ -0,0 +1,200 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { createTable, getCellText } from '@affine-test/kit/bs/table';
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
/**
* This test suite validates the edge case functionality of table insertion operations.
* It tests insertion at boundaries of the table, such as:
* - Insert Left at the first column
* - Insert Above at the first row
* - Insert Right at the last column
* - Insert Below at the last row
*/
test.describe('Table edge case insertion operations', () => {
test('should correctly insert at edges', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Wait for table rendering to complete
await waitNextFrame(page);
// Initialize all cell contents
const cells = table.locator('affine-table-cell');
expect(await cells.count()).toBe(4);
// Clear and set cell contents
for (let i = 0; i < 4; i++) {
const cell = cells.nth(i);
await cell.click({ clickCount: 3 }); // Select all content in the cell
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type(`Cell${i + 1}`); // Input new content
await waitNextFrame(page);
}
// Verify initial cell contents
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('Cell2');
// Edge case 1: Insert to the left of the first column
// Hover over the first column to make the column options button visible
await cells.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button to open the column options menu
const columnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.first();
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Left" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Left').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (3x2 table)
const cellsAfterLeftEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterLeftEdgeInsert.count()).toBe(6);
// Input text to verify insertion position
const leftEdgeCell = cellsAfterLeftEdgeInsert.nth(0);
await leftEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Left Edge');
await waitNextFrame(page);
// Verify first row values: Left Edge, Cell1, Cell2
expect(await getCellText(page, 0)).toBe('Left Edge');
// Fix: Re-get and verify second column content
const secondCell = cellsAfterLeftEdgeInsert.nth(1);
await secondCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Cell1'); // Re-input
await waitNextFrame(page);
expect(await getCellText(page, 1)).toBe('Cell1');
// Fix: Re-get and verify third column content
const thirdCell = cellsAfterLeftEdgeInsert.nth(2);
await thirdCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Cell2'); // Re-input
await waitNextFrame(page);
expect(await getCellText(page, 2)).toBe('Cell2');
// Edge case 2: Insert above the first row
// Hover over the first row to make the row options button visible
await cellsAfterLeftEdgeInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button to open the row options menu
const rowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.first();
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Above" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Above').click();
await waitNextFrame(page);
// Verify: Now we should have 9 cells (3x3 table)
const cellsAfterAboveEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterAboveEdgeInsert.count()).toBe(9);
// Input text to verify insertion position
const aboveEdgeCell = cellsAfterAboveEdgeInsert.nth(0);
await aboveEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Above Edge');
await waitNextFrame(page);
// Verify first row first column value
expect(await getCellText(page, 0)).toBe('Above Edge');
// Verify second row first column value (previously the first row first column)
expect(await getCellText(page, 3)).toBe('Left Edge');
// Edge case 3: Insert to the right of the last column
// Hover over the last column to make the column options button visible
const lastColumnCell = cellsAfterAboveEdgeInsert.nth(2);
await lastColumnCell.hover();
await waitNextFrame(page);
// Click on the last column's options button
const lastColumnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.nth(2);
await expect(lastColumnOptionButton).toBeVisible();
await lastColumnOptionButton.click();
// Click on the "Insert Right" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Right').click();
await waitNextFrame(page);
// Verify: Now we should have 12 cells (4x3 table)
const cellsAfterRightEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterRightEdgeInsert.count()).toBe(12);
// Input text to verify insertion position
const rightEdgeCell = cellsAfterRightEdgeInsert.nth(3);
await rightEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Right Edge');
await waitNextFrame(page);
// Verify first row last column value
expect(await getCellText(page, 3)).toBe('Right Edge');
// Edge case 4: Insert below the last row
// Find the first cell of the last row
const lastRowCell = cellsAfterRightEdgeInsert.nth(8); // Row 3, Column 1
await lastRowCell.hover();
await waitNextFrame(page);
// Click on the last row's options button
const lastRowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.nth(2);
await expect(lastRowOptionButton).toBeVisible();
await lastRowOptionButton.click();
// Click on the "Insert Below" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Below').click();
await waitNextFrame(page);
// Verify: Now we should have 16 cells (4x4 table)
const cellsAfterBelowEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterBelowEdgeInsert.count()).toBe(16);
// Input text to verify insertion position
const belowEdgeCell = cellsAfterBelowEdgeInsert.nth(12); // Row 4, Column 1
await belowEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Below Edge');
await waitNextFrame(page);
// Verify last row first column value
expect(await getCellText(page, 12)).toBe('Below Edge');
});
});

View File

@@ -0,0 +1,198 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { createTable, getCellText } from '@affine-test/kit/bs/table';
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
/**
* This test suite validates the correct functionality of table insertion operations.
* It tests Insert Left, Insert Right, Insert Above, and Insert Below operations
* to verify they correctly insert rows and columns in the expected positions.
*/
test.describe('Table insertion operations', () => {
test('should correctly insert columns', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Verify initial table structure (2x2)
const initialCells = table.locator('affine-table-cell');
expect(await initialCells.count()).toBe(4);
// Hover over the first cell to make the column options button visible
await initialCells.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button (three dots icon) to open the column options menu
const columnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.first();
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Right" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Right').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (3x2 table)
const cellsAfterRightInsert = table.locator('affine-table-cell');
expect(await cellsAfterRightInsert.count()).toBe(6);
// Input text in the newly inserted column to verify its position
// After inserting right of column 0, our new cell should be at position 1
await getCellText(page, 0); // Wait for cell to be ready
const newColumnCell = cellsAfterRightInsert.nth(1);
await newColumnCell.hover();
await waitNextFrame(page);
await newColumnCell.click();
await page.keyboard.type('New Right');
// Verify values in row 0: Cell1, New Right, Cell2
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('New Right');
expect(await getCellText(page, 2)).toBe('Cell2');
// Hover over the first column again to make the column options button visible
await cellsAfterRightInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button to open the column options menu
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Left" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Left').click();
await waitNextFrame(page);
// Verify: Now we should have 8 cells (4x2 table)
const cellsAfterLeftInsert = table.locator('affine-table-cell');
expect(await cellsAfterLeftInsert.count()).toBe(8);
// Input text in the newly inserted column
await getCellText(page, 0); // Wait for cell to be ready
const leftColumnCell = cellsAfterLeftInsert.nth(0);
await leftColumnCell.hover();
await waitNextFrame(page);
await leftColumnCell.click();
await page.keyboard.type('New Left');
// Verify values in row 0: New Left, Cell1, New Right, Cell2
expect(await getCellText(page, 0)).toBe('New Left');
expect(await getCellText(page, 1)).toBe('Cell1');
expect(await getCellText(page, 2)).toBe('New Right');
expect(await getCellText(page, 3)).toBe('Cell2');
});
test('should correctly insert rows', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Verify initial table structure (2x2)
const initialCells = table.locator('affine-table-cell');
expect(await initialCells.count()).toBe(4);
// Hover over the first cell to make the row options button visible
await initialCells.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button (three dots icon) to open the row options menu
const rowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.first();
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Below" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Below').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (2x3 table)
const cellsAfterBelowInsert = table.locator('affine-table-cell');
expect(await cellsAfterBelowInsert.count()).toBe(6);
// Input text in the newly inserted row
await getCellText(page, 2); // Wait for cell to be ready
const newRowCell1 = cellsAfterBelowInsert.nth(2);
await newRowCell1.hover();
await waitNextFrame(page);
await newRowCell1.click();
await page.keyboard.type('New Below 1');
await getCellText(page, 3); // Wait for cell to be ready
const newRowCell2 = cellsAfterBelowInsert.nth(3);
await newRowCell2.hover();
await waitNextFrame(page);
await newRowCell2.click();
await page.keyboard.type('New Below 2');
// Verify values: First row: Cell1, Cell2; Second row: New Below 1, New Below 2; Third row: Cell3, Cell4
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('Cell2');
expect(await getCellText(page, 2)).toBe('New Below 1');
expect(await getCellText(page, 3)).toBe('New Below 2');
expect(await getCellText(page, 4)).toBe('Cell3');
expect(await getCellText(page, 5)).toBe('Cell4');
// Hover over the first row again to make the row options button visible
await cellsAfterBelowInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button to open the row options menu
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Above" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Above').click();
await waitNextFrame(page);
// Verify: Now we should have 8 cells (2x4 table)
const cellsAfterAboveInsert = table.locator('affine-table-cell');
expect(await cellsAfterAboveInsert.count()).toBe(8);
// Input text in the newly inserted row
await getCellText(page, 0); // Wait for cell to be ready
const aboveRowCell1 = cellsAfterAboveInsert.nth(0);
await aboveRowCell1.hover();
await waitNextFrame(page);
await aboveRowCell1.click();
await page.keyboard.type('New Above 1');
await getCellText(page, 1); // Wait for cell to be ready
const aboveRowCell2 = cellsAfterAboveInsert.nth(1);
await aboveRowCell2.hover();
await waitNextFrame(page);
await aboveRowCell2.click();
await page.keyboard.type('New Above 2');
// Verify the first row contains our new values
expect(await getCellText(page, 0)).toBe('New Above 1');
expect(await getCellText(page, 1)).toBe('New Above 2');
expect(await getCellText(page, 2)).toBe('Cell1');
expect(await getCellText(page, 3)).toBe('Cell2');
});
});

View File

@@ -0,0 +1,458 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
dragView,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { importImage } from '@affine-test/kit/utils/image';
import {
selectAllByKeyboard,
writeTextToClipboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
function hexToRGB(hex: string) {
hex = hex.replace(/^#/, '');
const len = hex.length;
let arr: string[] = [];
if (len === 3 || len === 4) {
arr = hex.split('').map(s => s.repeat(2));
} else if (len === 6 || len === 8) {
arr = Array.from<number>({ length: len / 2 })
.fill(0)
.map((n, i) => n + i * 2)
.map(n => hex.substring(n, n + 2));
}
const values = arr
.map(s => parseInt(s, 16))
.map((n, i) => (i === 3 ? (n % 255) / 255 : n));
return `rgb${values.length === 4 ? 'a' : ''}(${values.join(', ')})`;
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
});
test('should toggle toolbar when dragging page area', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('Roman');
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await expect(toolbar).toBeInViewport();
const paragraph = page.locator('affine-note affine-paragraph').nth(0);
const bounds = await paragraph.boundingBox();
expect(bounds).toBeTruthy();
const { x, y, width } = bounds!;
await page.mouse.move(x + width + 10, y - 10, { steps: 2 });
await page.mouse.down();
await page.mouse.move(x + width - 10, y + 10, { steps: 2 });
await expect(toolbar).toBeHidden();
await page.mouse.up();
await expect(toolbar).toBeVisible();
});
test.describe('Formatting', () => {
test('should change text color', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
const highlightButton = toolbar.locator('affine-highlight-duotone-icon');
await highlightButton.click();
const fgGreenButton = toolbar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
await expect(textSpan).toBeVisible();
await expect(textSpan).toHaveCSS('color', hexToRGB(fgColor));
});
test('should change text background color', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
const highlightButton = toolbar.locator('affine-highlight-duotone-icon');
await highlightButton.click();
const fgGreenButton = toolbar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
await page.waitForTimeout(200);
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan1 = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
await expect(textSpan1).toHaveCSS('color', hexToRGB(fgColor));
await page.keyboard.press('ArrowRight');
for (let i = 0; i < 5; i++) {
await page.keyboard.press('Shift+ArrowLeft');
}
await highlightButton.click();
const yellow = 'var(--affine-text-highlight-yellow)';
const bgYellowButton = toolbar.locator('[data-testid="background-yellow"]');
await bgYellowButton.click();
const textSpan2 = paragraph
.locator('affine-text:has-text("wo")')
.locator('span')
.first();
await expect(textSpan2).toBeVisible();
const bgColor1 = await textSpan1.evaluate(e => e.style.backgroundColor);
const bgColor2 = await textSpan2.evaluate(e => e.style.backgroundColor);
expect(yellow).toBe(bgColor1);
expect(yellow).toBe(bgColor2);
const bgColor = await bgYellowButton
.locator('affine-text-duotone-icon')
.evaluate(e =>
window.getComputedStyle(e).getPropertyValue('--background')
);
await expect(textSpan1).toHaveCSS('background-color', hexToRGB(bgColor));
await expect(textSpan2).toHaveCSS('background-color', hexToRGB(bgColor));
});
});
test('should not show toolbar when releasing spacebar and elements have been deleted', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await page.keyboard.down('Space');
await expect(toolbar).toBeHidden();
await page.keyboard.up('Space');
await expect(toolbar).toBeVisible();
await page.keyboard.press('Delete');
await expect(toolbar).toBeHidden();
await page.keyboard.down('Space');
await expect(toolbar).toBeHidden();
await page.keyboard.up('Space');
await expect(toolbar).toBeHidden();
});
test('should not show inner toolbar of surface-ref in note under edgeless', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('/frame');
await page.keyboard.press('Enter');
const toolbar = locateToolbar(page);
const surfaceRef = page.locator('affine-surface-ref');
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
await clickEdgelessModeButton(page);
const note = page.locator('affine-edgeless-note');
await note.click();
await note.click();
const edgelessSurfaceRef = note.locator('affine-edgeless-surface-ref');
await edgelessSurfaceRef.hover();
await expect(toolbar).toBeHidden();
const dragHandler = page.locator('.affine-drag-handle-grabber');
await dragHandler.hover();
await dragHandler.click();
await expect(
edgelessSurfaceRef.locator('.affine-edgeless-surface-ref-container')
).toHaveClass(/focused$/);
await expect(toolbar).toBeHidden();
});
test('should show toolbar when inline link is preceded by image or surface-ref', async ({
page,
}) => {
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
const image = page.locator('affine-image');
await image.click();
await page.keyboard.press('Enter');
const url = new URL(page.url());
await writeTextToClipboard(page, url.toString());
const toolbar = locateToolbar(page);
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
await expect(toolbar).toBeVisible();
await toolbar.hover();
await expect(toolbar).toBeVisible();
await image.hover();
await expect(toolbar).toBeVisible();
await expect(toolbar).toHaveAttribute('data-placement', 'inner');
await inlineLink.hover();
await expect(toolbar).toBeVisible();
await expect(toolbar).not.toHaveAttribute('data-placement', 'inner');
});
test('should focus on input of popover on toolbar', async ({ page }) => {
await clickEdgelessModeButton(page);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeHidden();
const note = page.locator('affine-edgeless-note').first();
await note.click();
await expect(toolbar).toBeVisible();
const scaleMenu = toolbar.locator('.scale-menu');
const scaleInput = scaleMenu.locator('input');
const scaleButton = scaleMenu.getByLabel('Scale');
await expect(scaleInput).toBeHidden();
await scaleButton.click();
await scaleInput.click();
await expect(scaleInput).toBeFocused();
await scaleInput.fill('150');
await expect(scaleInput).toBeFocused();
const scaleValue = await scaleInput.inputValue();
expect(scaleValue).toBe('150');
const stylePanelButton = toolbar.locator('edgeless-note-style-panel');
const cornersInput = stylePanelButton.locator(
'.edgeless-note-corner-radius-panel input'
);
await stylePanelButton.click();
await cornersInput.click();
await expect(cornersInput).toBeFocused();
await cornersInput.fill('36');
await expect(cornersInput).toBeFocused();
const cornersValue = await cornersInput.inputValue();
expect(cornersValue).toBe('36');
});
test('Dropdown menus should be closed automatically when toolbar is displayed', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('/frame');
await page.keyboard.press('Enter');
const toolbar = locateToolbar(page);
const surfaceRef = page.locator('affine-surface-ref');
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
const moreMenuContainer = toolbar.getByLabel('More menu');
const moreMenuButton = moreMenuContainer.getByLabel('More');
const moreMenu = moreMenuContainer.getByRole('menu');
await expect(moreMenu).toBeHidden();
await moreMenuButton.click();
await expect(moreMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(toolbar).toBeHidden();
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
await expect(moreMenu).toBeHidden();
});
test('should clear selection when switching doc mode', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await clickEdgelessModeButton(page);
await expect(toolbar).toBeHidden();
});
test.describe('Toolbar More Actions', () => {
test('should copy selected text as markdown', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('toolbar-copy-as-markdown');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('toolbar-copy-as-markdown');
});
test('should preserve heading syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '# toolbar-heading');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('# toolbar-heading');
});
test('should preserve bulleted list syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '- toolbar-list-item');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toMatch(/^\* toolbar-list-item/m);
});
test('should duplicate block', async ({ page }) => {
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
const images = page.locator('affine-page-image');
const firstImage = images.first();
const firstImageUrl = await firstImage.locator('img').getAttribute('src');
await firstImage.hover();
const toolbar = locateToolbar(page);
const moreMenu = toolbar.getByLabel('More menu');
await moreMenu.click();
const duplicateButton = toolbar.getByTestId('duplicate');
await duplicateButton.click();
await expect(images).toHaveCount(2);
const secondImage = images.nth(1);
const secondImageUrl = await secondImage.locator('img').getAttribute('src');
expect(firstImageUrl).not.toBeNull();
expect(firstImageUrl!.startsWith('blob:')).toBe(true);
expect(secondImageUrl).not.toBeNull();
expect(secondImageUrl!.startsWith('blob:')).toBe(true);
});
});

View File

@@ -0,0 +1,144 @@
import { test } from '@affine-test/kit/playwright';
import {
ensureInEdgelessMode,
ensureInPageMode,
getPageMode,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
waitForAllPagesLoad,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test';
test('Switch to edgeless by switch edgeless item', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const btn = page.getByTestId('switch-edgeless-mode-button');
await btn.click({ delay: 100 });
await ensureInEdgelessMode(page);
});
test('Quick Switch Doc Mode, Doc Mode should stable', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await ensureInPageMode(page);
await page.keyboard.down('Alt');
await page.keyboard.down('S');
await page.keyboard.up('S');
await page.keyboard.up('Alt');
await page.keyboard.down('Alt');
await page.keyboard.down('S');
await page.keyboard.up('S');
await page.keyboard.up('Alt');
await page.keyboard.down('Alt');
await page.keyboard.down('S');
await page.keyboard.up('S');
await page.keyboard.up('Alt');
await page.keyboard.down('Alt');
await page.keyboard.down('S');
await page.keyboard.up('S');
await page.keyboard.up('Alt');
await page.keyboard.down('Alt');
await page.keyboard.down('S');
await page.keyboard.up('S');
await page.keyboard.up('Alt');
await ensureInEdgelessMode(page);
await page.waitForTimeout(1000);
expect(await getPageMode(page)).toBe('edgeless');
});
test('default to edgeless by editor header items', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, 'this is a new page');
const title = getBlockSuiteEditorTitle(page);
expect(await title.textContent()).toBe('this is a new page');
await clickPageMoreActions(page);
const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless');
await menusEdgelessItem.click({ delay: 100 });
await clickSideBarAllPageButton(page);
await waitForAllPagesLoad(page);
const docItem = page.locator(
`[data-testid="doc-list-item"]:has-text("this is a new page")`
);
expect(docItem).not.toBeUndefined();
await docItem.click();
await waitForEditorLoad(page);
const edgeless = page.locator('affine-edgeless-root');
await expect(edgeless).toBeVisible();
});
test('Able to insert the title of an untitled page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const titleBarTextContent = page.getByTestId('title-edit-button');
await titleBarTextContent.dblclick({ delay: 100 });
const titleContent = page.getByTestId('title-content');
await titleContent.fill('test');
await titleContent.blur();
expect(await titleBarTextContent.textContent()).toBe('test');
});
test('Able to edit the title of an existing page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const titleBarTextContent = page.getByTestId('title-edit-button');
await titleBarTextContent.dblclick({ delay: 100 });
const titleContent = page.getByTestId('title-content');
await titleContent.fill('test');
await titleContent.blur();
expect(await titleBarTextContent.textContent()).toBe('test');
await titleBarTextContent.dblclick({ delay: 100 });
await titleContent.fill('Sample text 2');
await titleContent.blur();
expect(await titleBarTextContent.textContent()).toBe('Sample text 2');
});
test('Clearing out the title bar will remove the page title', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
const titleBarTextContent = page.getByTestId('title-edit-button');
await titleBarTextContent.dblclick({ delay: 100 });
const titleContent = page.getByTestId('title-content');
await titleContent.fill('test');
await titleContent.blur();
expect(await titleBarTextContent.textContent()).toBe('test');
await titleBarTextContent.dblclick({ delay: 100 });
await titleContent.fill('');
await titleContent.blur();
expect(await titleBarTextContent.textContent()).toBe('Untitled');
});
test('Rename by editor header items, save with shortcut', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickPageMoreActions(page);
const menusRenameItem = page.getByTestId('editor-option-menu-rename');
await menusRenameItem.click({ delay: 100 });
const titleBarTextContent = page.getByTestId('title-edit-button');
const titleContent = page.getByTestId('title-content');
await titleContent.fill('test');
await page.keyboard.press('Enter');
expect(await titleBarTextContent.textContent()).toBe('test');
await clickPageMoreActions(page);
await menusRenameItem.click({ delay: 100 });
await titleContent.fill('');
await page.keyboard.press('Escape');
expect(await titleBarTextContent.textContent()).toBe('Untitled');
});

View File

@@ -0,0 +1,134 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageOperationButton,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import {
addCustomProperty,
closeTagsEditor,
ensurePagePropertiesVisible,
expectTagsVisible,
filterTags,
removeSelectedTag,
} from '@affine-test/kit/utils/properties';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect, type Page } from '@playwright/test';
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();
};
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await ensurePagePropertiesVisible(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
});
test('New a page and open it ,then open info modal in the title bar', async ({
page,
}) => {
await page.getByTestId('header-info-button').click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
const title = page.getByTestId('info-modal-title');
await expect(title).toHaveText('this is a new page');
});
test('New a page and open it ,then open info modal in the title bar more action button', async ({
page,
}) => {
await clickPageMoreActions(page);
await page.getByTestId('editor-option-menu-info').click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
const title = page.getByTestId('info-modal-title');
await expect(title).toHaveText('this is a new page');
});
test('New a page, then open info modal from all doc', async ({ page }) => {
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page');
expect(cell).not.toBeUndefined();
await getPageOperationButton(page, newPageId).click();
await page.getByRole('menuitem', { name: 'View Info' }).click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
const title = page.getByTestId('info-modal-title');
await expect(title).toHaveText('this is a new page');
});
test('New a page and add to favourites, then open info modal from sidebar', async ({
page,
}) => {
const newPageId = getCurrentDocIdFromUrl(page);
await clickPageMoreActions(page);
await page.getByTestId('editor-option-menu-favorite').click();
await page.getByTestId('all-pages').click();
const favoriteListItemInSidebar = page.locator(
`[data-testid="navigation-panel-favorites"] [data-testid="navigation-panel-doc-${newPageId}"]`
);
expect(await favoriteListItemInSidebar.textContent()).toBe(
'this is a new page'
);
await favoriteListItemInSidebar.hover();
await favoriteListItemInSidebar
.getByTestId('navigation-panel-tree-node-operation-button')
.click();
const infoBtn = page.getByText('View Info');
await infoBtn.click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
const title = page.getByTestId('info-modal-title');
await expect(title).toHaveText('this is a new page');
});
test('allow create tag', async ({ page }) => {
await page.getByTestId('header-info-button').click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
await infoModal.getByTestId('property-tags-value').click();
await searchAndCreateTag(page, 'Test1');
await searchAndCreateTag(page, 'Test2');
await closeTagsEditor(page);
await expectTagsVisible(infoModal, ['Test1', 'Test2']);
await infoModal.getByTestId('property-tags-value').click();
await removeSelectedTag(page, 'Test1');
await closeTagsEditor(page);
await expectTagsVisible(infoModal, ['Test2']);
});
test('add custom property', async ({ page }) => {
await page.getByTestId('header-info-button').click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
await addCustomProperty(page, infoModal, 'text');
await addCustomProperty(page, infoModal, 'number');
await addCustomProperty(page, infoModal, 'date');
await addCustomProperty(page, infoModal, 'checkbox');
await addCustomProperty(page, infoModal, 'createdBy');
await addCustomProperty(page, infoModal, 'updatedBy');
});

View File

@@ -0,0 +1,438 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { pressBackspace, pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
dragTo,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import {
getCurrentCollectionIdFromUrl,
getCurrentDocIdFromUrl,
getDocIdFromUrl,
} from '@affine-test/kit/utils/url';
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
const dragToFavourites = async (
page: Page,
dragItem: Locator,
id: string,
type: 'doc' | 'collection' | 'tag' | 'folder' = 'doc'
) => {
const favourites = page.getByTestId(
'navigation-panel-favorite-category-divider'
);
if (type === 'collection') {
await dragItem.dragTo(favourites);
} else {
await dragTo(page, dragItem, favourites);
}
const item = page
.getByTestId(`navigation-panel-favorites`)
.locator(`[data-testid="navigation-panel-${type}-${id}"]`);
await expect(item).toBeVisible();
return item;
};
const createCollection = async (page: Page, name: string) => {
await page.getByTestId('navigation-panel-bar-add-collection-button').click();
const input = page.getByTestId('prompt-modal-input');
await expect(input).toBeVisible();
await input.fill(name);
await page.getByTestId('prompt-modal-confirm').click();
const newCollectionId = getCurrentCollectionIdFromUrl(page);
const collection = page.getByTestId(
`navigation-panel-collection-${newCollectionId}`
);
await expect(collection).toBeVisible();
return collection;
};
const createPage = async (page: Page, title: string) => {
await clickNewPageButton(page, title);
};
const dragToCollection = async (page: Page, dragItem: Locator) => {
const collection = await createCollection(page, 'test collection');
await clickSideBarAllPageButton(page);
await dragTo(page, dragItem, collection);
await page.waitForTimeout(500);
const collectionPage = collection.locator(
'[data-testid^="navigation-panel-doc-"]'
);
await expect(collectionPage).toBeVisible();
return collectionPage;
};
const dragToTrash = async (page: Page, title: string, dragItem: Locator) => {
// drag to trash
await dragTo(page, dragItem, page.getByTestId('trash-page'));
const confirmTip = page.getByText('Delete doc?');
await expect(confirmTip).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(
page.getByText(title),
'The deleted post is no longer on the All Page list'
).toHaveCount(0);
await page.waitForTimeout(500);
await page.getByTestId('trash-page').click();
await expect(
page.getByText(title),
'The deleted post exists in the Trash list'
).toHaveCount(1);
};
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
});
test('drag a page from "All pages" list to favourites, then drag to trash', async ({
page,
}) => {
const title = 'this is a new page to drag';
await waitForEditorLoad(page);
await createPage(page, title);
const pageId = getCurrentDocIdFromUrl(page);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
const favouritePage = await dragToFavourites(
page,
page.locator(`[data-testid="doc-list-item"]:has-text("${title}")`),
pageId
);
await dragToTrash(page, title, favouritePage);
});
test('drag a page from "All pages" list to collections, then drag to trash', async ({
page,
}) => {
const title = 'this is a new page to drag';
await waitForEditorLoad(page);
await createPage(page, title);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
const collectionPage = await dragToCollection(
page,
page.locator(`[data-testid="doc-list-item"]:has-text("${title}")`)
);
await dragToTrash(page, title, collectionPage);
});
test('drag a page from "All pages" list to trash', async ({ page }) => {
const title = 'this is a new page to drag';
await createPage(page, title);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
await dragToTrash(
page,
title,
page.locator(`[data-testid="doc-list-item"]:has-text("${title}")`)
);
});
test('drag a page from favourites to collection', async ({ page }) => {
const title = 'this is a new page to drag';
await createPage(page, title);
const pageId = getCurrentDocIdFromUrl(page);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
// drag to favourites
const favouritePage = await dragToFavourites(
page,
page.locator(`[data-testid="doc-list-item"]:has-text("${title}")`),
pageId
);
// drag to collections
await dragToCollection(page, favouritePage);
});
test('drag a collection to favourites', async ({ page }) => {
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
const collection = await createCollection(page, 'test collection');
const collectionId = getCurrentCollectionIdFromUrl(page);
await dragToFavourites(
page,
collection.locator('[data-affine-draggable]'),
collectionId,
'collection'
);
});
test('items in favourites can be reordered by dragging', async ({ page }) => {
const title0 = 'this is a new page to drag';
await createPage(page, title0);
await page.getByTestId('pin-button').click();
const title1 = 'this is another new page to drag';
await createPage(page, title1);
await page.getByTestId('pin-button').click();
{
const collection = await createCollection(page, 'test collection');
const collectionId = getCurrentCollectionIdFromUrl(page);
await dragToFavourites(
page,
collection.locator('[data-affine-draggable]'),
collectionId,
'collection'
);
}
// assert the order of the items in favourites
await expect(
page.getByTestId('navigation-panel-favorites').locator('[draggable]')
).toHaveCount(3);
await expect(
page
.getByTestId('navigation-panel-favorites')
.locator('[draggable]')
.first()
).toHaveText('test collection');
await expect(
page.getByTestId('navigation-panel-favorites').locator('[draggable]').last()
).toHaveText(title0);
// drag the first item to the last
const firstItem = page
.getByTestId('navigation-panel-favorites')
.locator('[draggable]')
.first();
const lastItem = page
.getByTestId('navigation-panel-favorites')
.locator('[draggable]')
.last();
await dragTo(page, firstItem, lastItem, 'bottom');
// now check the order again
await expect(
page.getByTestId('navigation-panel-favorites').locator('[draggable]')
).toHaveCount(3);
await expect(
page
.getByTestId('navigation-panel-favorites')
.locator('[draggable]')
.first()
).toHaveText(title1);
await expect(
page.getByTestId('navigation-panel-favorites').locator('[draggable]').last()
).toHaveText('test collection');
});
// some how this test always timeout, so we skip it
test('drag a page link in editor to favourites', async ({ page }) => {
await clickNewPageButton(page);
await page.waitForTimeout(500);
await page.keyboard.press('Enter');
await createLinkedPage(page, 'hi from another page');
const pageReference = page.locator('a').filter({
has: page.locator(
'.affine-reference-title:has-text("hi from another page")'
),
});
const pageLink = await pageReference.evaluate(
el => (el as HTMLAnchorElement).href
);
expect(pageLink).toBeTruthy();
if (!pageLink) {
return;
}
const pageId = getDocIdFromUrl(pageLink);
await dragToFavourites(
page,
page.locator('.affine-reference-title:has-text("hi from another page")'),
pageId
);
});
test('drag a page card block to another page', async ({ page }) => {
await clickNewPageButton(page);
await page.waitForTimeout(500);
await page.keyboard.press('Enter');
await createLinkedPage(page, 'hi from another page');
const pageReference = page.locator('a').filter({
has: page.locator(
'.affine-reference-title:has-text("hi from another page")'
),
});
const pageLink = await pageReference.evaluate(
el => (el as HTMLAnchorElement).href
);
expect(pageLink).toBeTruthy();
if (!pageLink) {
return;
}
const pageId = getDocIdFromUrl(pageLink);
await pageReference.hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
// convert page reference to card block
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await toolbar.getByRole('button', { name: 'Card view' }).click();
// hover the card block to show the drag handle
const box = await page.locator('affine-embed-linked-doc-block').boundingBox();
expect(box).toBeTruthy();
if (!box) {
return;
}
await page.mouse.move(box.x - 5, box.y + box.height / 2);
await dragToFavourites(
page,
page.locator('.affine-drag-handle-container'),
pageId
);
});
test('drag a favourite page into note on page mode', async ({ page }) => {
await clickNewPageButton(page, 'hi from page');
await page.getByTestId('pin-button').click();
const pageId = getCurrentDocIdFromUrl(page);
const item = page
.getByTestId(`navigation-panel-favorites`)
.locator(`[data-testid="navigation-panel-doc-${pageId}"]`);
await expect(item).toBeVisible();
// drag item into blocksuite editor
await dragTo(
page,
item,
page.locator('.affine-paragraph-block-container').first()
);
await expect(page.locator('affine-embed-linked-doc-block')).toContainText(
'hi from page'
);
});
test('drag a favourite page into canvas on edgeless mode', async ({ page }) => {
await clickNewPageButton(page, 'hi from page');
await pressEnter(page);
await type(page, 'Hello world');
await page.getByTestId('pin-button').click();
const pageId = getCurrentDocIdFromUrl(page);
const item = page
.getByTestId(`navigation-panel-favorites`)
.locator(`[data-testid="navigation-panel-doc-${pageId}"]`);
await expect(item).toBeVisible();
await clickNewPageButton(page, 'edgeless page');
await clickEdgelessModeButton(page);
// drag item into blocksuite editor
await dragTo(page, item, page.locator('affine-edgeless-root').first());
const embedDoc = page.locator('affine-embed-edgeless-synced-doc-block');
await expect(embedDoc).toContainText('Hello world');
await embedDoc.click();
const toolbar = locateToolbar(page);
const switchViewButton = toolbar.getByLabel('Switch view');
await switchViewButton.click();
await toolbar.getByLabel('Card view').click(); // this will record the view as preferred view
const cardDoc = page.locator('affine-embed-edgeless-linked-doc-block');
await expect(embedDoc).toBeHidden();
await expect(cardDoc).toBeVisible();
await pressBackspace(page);
await expect(embedDoc).toBeHidden();
await expect(cardDoc).toBeHidden();
await dragTo(page, item, page.locator('affine-edgeless-root').first());
await expect(cardDoc).toBeVisible();
await expect(embedDoc).toBeHidden();
await page.reload();
await cardDoc.click();
await pressBackspace(page);
await dragTo(page, item, page.locator('affine-edgeless-root').first());
await expect(cardDoc, 'prefer view should be persistence').toBeVisible();
await expect(embedDoc).toBeHidden();
});
test('drag a favourite page into note on edgeless mode', async ({ page }) => {
await clickNewPageButton(page, 'hi from page');
await pressEnter(page);
await type(page, 'Hello world');
await page.getByTestId('pin-button').click();
const pageId = getCurrentDocIdFromUrl(page);
const item = page
.getByTestId(`navigation-panel-favorites`)
.locator(`[data-testid="navigation-panel-doc-${pageId}"]`);
await expect(item).toBeVisible();
await clickNewPageButton(page, 'edgeless page');
await clickEdgelessModeButton(page);
const edgelessNote = page.locator('affine-edgeless-note');
await dragTo(page, item, edgelessNote);
const embedDocInNote = edgelessNote.locator('affine-embed-synced-doc-block');
const cardDocInNote = edgelessNote.locator('affine-embed-linked-doc-block');
await expect(embedDocInNote).toBeHidden();
await expect(cardDocInNote).toBeVisible();
await edgelessNote.click({ delay: 100 });
await edgelessNote.click({ delay: 100 });
await cardDocInNote.click();
const toolbar = locateToolbar(page);
const switchViewButton = toolbar.getByLabel('Switch view');
await switchViewButton.click();
await toolbar.getByLabel('Embed view').click(); // this should not record the view as preferred view
await expect(embedDocInNote).toBeVisible();
await expect(cardDocInNote).toBeHidden();
await pressBackspace(page);
await dragTo(page, item, edgelessNote);
await expect(
embedDocInNote,
'prefer view should only be available in canvas'
).toBeHidden();
await expect(cardDocInNote).toBeVisible();
});

View File

@@ -0,0 +1,23 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Duplicate page should work', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await expect(title).toBeVisible();
await title.fill('test');
await clickPageMoreActions(page);
const duplicateButton = page.getByTestId('editor-option-menu-duplicate');
await duplicateButton.click();
const title2 = getBlockSuiteEditorTitle(page);
await expect(title2).toHaveText('test(1)');
});

View File

@@ -0,0 +1,9 @@
import { test } from '@affine-test/kit/playwright';
import { open404Page } from '@affine-test/kit/utils/load-page';
import { expect } from '@playwright/test';
test('visit 404 page', async ({ page }) => {
await open404Page(page);
const notFoundTip = page.getByTestId('not-found');
await expect(notFoundTip).toBeVisible();
});

View File

@@ -0,0 +1,741 @@
/* oxlint-disable unicorn/prefer-dom-node-dataset */
import fs from 'node:fs';
import { test } from '@affine-test/kit/playwright';
import { importImage } from '@affine-test/kit/utils/image';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
async function closeImagePreviewModal(page: Page) {
await page.getByTestId('image-preview-close-button').first().click();
await page.waitForTimeout(500);
}
test('image preview should be shown', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
await closeImagePreviewModal(page);
await expect(locator).not.toBeVisible();
});
test('image go left and right', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
await closeImagePreviewModal(page);
}
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
}
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeHidden();
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(1000);
{
const newBlobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(newBlobId).not.toBe(blobId);
}
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(1000);
{
const newBlobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(newBlobId).toBe(blobId);
}
});
test('image able to zoom in and out with mouse scroll', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-content');
const naturalWidth = await locator.evaluate(
(img: HTMLImageElement) => img.naturalWidth
);
await expect(locator).toBeVisible();
const { width, height } = await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
// zoom in
await page.mouse.move(width / 2, height / 2);
await page.mouse.wheel(0, 100);
await page.mouse.wheel(0, 100);
await page.mouse.wheel(0, 100);
await page.waitForTimeout(1000);
let imageBoundary = await locator.boundingBox();
let imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('0.54');
}
// zoom in
await page.mouse.move(width / 2, height / 2);
await page.mouse.wheel(0, -100);
await page.mouse.wheel(0, -100);
await page.mouse.wheel(0, -100);
await page.waitForTimeout(1000);
imageBoundary = await locator.boundingBox();
imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('0.84');
}
});
test('image able to zoom in and out with button click', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-content');
await expect(locator).toBeVisible();
const naturalWidth = await locator.evaluate(
(img: HTMLImageElement) => img.naturalWidth
);
await page.waitForTimeout(500);
// zoom in
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('zoom-in-button').dblclick();
}
await page.waitForTimeout(1000);
let imageBoundary = await locator.boundingBox();
let imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('1.04');
}
// zooom out
await page.getByTestId('zoom-out-button').dblclick();
imageBoundary = await locator.boundingBox();
imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('0.84');
}
});
test('image should able to go left and right by buttons', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
await closeImagePreviewModal(page);
}
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
}
const locator = page.getByTestId('image-preview-modal');
await page.locator('affine-page-image').first().dblclick();
await expect(locator).toBeVisible();
{
const newBlobId = (await locator
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).not.toBe(blobId);
}
await locator.getByTestId('next-image-button').click();
await page.waitForTimeout(1000);
{
const newBlobId = (await page
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).toBe(blobId);
}
await locator.getByTestId('previous-image-button').click();
await page.waitForTimeout(1000);
{
const newBlobId = (await locator
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).not.toBe(blobId);
}
});
test('image able to fit to screen by button', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-content');
await expect(locator).toBeVisible();
const naturalWidth = await locator.evaluate(
(img: HTMLImageElement) => img.naturalWidth
);
const [viewportWidth, viewportHeight] = await page.evaluate(() => {
return [window.innerWidth, window.innerHeight];
});
// zooom in
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('zoom-in-button').dblclick();
}
await page.waitForTimeout(1000);
let imageBoundary = await locator.boundingBox();
let imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('1.04');
} else {
throw new Error("Image doesn't exist!");
}
//reset zoom
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('fit-to-screen-button').click();
}
imageBoundary = await locator.boundingBox();
imageWidth = imageBoundary?.width;
const imageHeight = imageBoundary?.height;
if (imageWidth && imageHeight) {
expect(imageWidth).toBeLessThan(viewportWidth);
expect(imageHeight).toBeLessThan(viewportHeight);
} else {
throw new Error("Image doesn't exist!");
}
});
test('image able to reset zoom to 100%', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-content');
await expect(locator).toBeVisible();
const naturalWidth = await locator.evaluate(
(img: HTMLImageElement) => img.naturalWidth
);
await page.waitForTimeout(500);
// zoom in
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('zoom-in-button').dblclick();
}
await page.waitForTimeout(1000);
let imageBoundary = await locator.boundingBox();
let imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('1.04');
} else {
throw new Error("Image doesn't exist!");
}
//reset zoom
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('reset-scale-button').click();
}
imageBoundary = await locator.boundingBox();
imageWidth = imageBoundary?.width;
if (imageWidth) {
expect((imageWidth / naturalWidth).toFixed(2)).toBe('1.00');
} else {
throw new Error("Image doesn't exist!");
}
});
test('image able to copy to clipboard', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
await locator.getByTestId('copy-to-clipboard-button').click();
await expect(
page.locator('[data-testid=affine-toast]:has-text("Copied to clipboard.")')
).toBeVisible();
});
test('image preview should be able to copy image to clipboard on copy event', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
await page.dispatchEvent('body', 'copy');
await expect(
page.locator('[data-testid=affine-toast]:has-text("Copied to clipboard.")')
).toBeVisible();
});
test('image able to download', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
const downloadPromise = page.waitForEvent('download');
await locator.getByTestId('download-button').click();
const download = await downloadPromise;
const name = download.suggestedFilename();
await download.saveAs(`download/ + ${name}`);
expect(fs.existsSync(`download/ + ${name}`)).toBeTruthy();
});
test('image should only able to move when image is larger than viewport', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-content');
await expect(locator).toBeVisible();
const { width, height } = await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
let imageBoundary = await locator.boundingBox();
const initialXPos = imageBoundary?.x;
const initialYPos = imageBoundary?.y;
// check will it be able to move when zoomed in
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('zoom-in-button').dblclick();
await locator.getByTestId('zoom-in-button').dblclick();
}
await page.mouse.move(width / 2, height / 2);
await page.mouse.down();
await page.mouse.move(20, 20);
await page.mouse.up();
imageBoundary = await locator.boundingBox();
expect(initialXPos).not.toBe(imageBoundary?.x);
expect(initialYPos).not.toBe(imageBoundary?.y);
// check will it be able to move when zoomed out
{
const locator = page.getByTestId('image-preview-modal');
await locator.getByTestId('fit-to-screen-button').click();
}
await page.mouse.move(width / 2, height / 2);
await page.mouse.down();
await page.mouse.move(20, 20);
await page.mouse.up();
imageBoundary = await locator.boundingBox();
expect(initialXPos).toBe(imageBoundary?.x);
expect(initialYPos).toBe(imageBoundary?.y);
});
test('image should able to delete and when delete, it will move to previous/next image', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
await closeImagePreviewModal(page);
}
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
}
const locator = page.getByTestId('image-preview-modal');
await page.locator('affine-page-image').first().dblclick();
await expect(locator).toBeVisible();
// ensure the new image was imported
await page.waitForTimeout(1000);
{
const newBlobId = (await locator
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).not.toBe(blobId);
}
await page.waitForTimeout(500);
await locator.getByTestId('delete-button').click();
{
const newBlobId = (await locator
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).toBe(blobId);
await closeImagePreviewModal(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
}
await page.locator('affine-page-image').first().dblclick();
await locator.getByTestId('next-image-button').click();
await page.waitForTimeout(1000);
{
const newBlobId = (await page
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).toBe(blobId);
}
await locator.getByTestId('delete-button').click();
{
const newBlobId = (await locator
.getByTestId('image-content')
.getAttribute('data-blob-id')) as string;
expect(newBlobId).not.toBe(blobId);
}
await locator.getByTestId('delete-button').click();
await page.waitForTimeout(500);
{
const locator = await page.getByTestId('image-preview-modal').count();
expect(locator).toBe(0);
}
});
test('tooltips for all buttons should be visible when hovering', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
let blobId: string;
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
await page.waitForTimeout(500);
blobId = (await page
.getByTestId('image-preview-modal')
.locator('img')
.first()
.getAttribute('data-blob-id')) as string;
expect(blobId).toBeTruthy();
}
const locator = page.getByTestId('image-preview-modal');
await page.waitForTimeout(500);
await locator.getByTestId('previous-image-button').isDisabled();
await page.waitForTimeout(1000);
await locator.getByTestId('next-image-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const nextImageTooltip = await element.getByText('Next').count();
expect(nextImageTooltip).toBe(1);
}
await locator.getByTestId('next-image-button').click();
await locator.getByTestId('next-image-button').isDisabled();
await page.waitForTimeout(1000);
await locator.getByTestId('previous-image-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const previousImageTooltip = await element.getByText('Previous').count();
expect(previousImageTooltip).toBe(1);
}
await locator.getByTestId('fit-to-screen-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const fitToScreenToolTip = await element.getByText('Fit to Screen').count();
expect(fitToScreenToolTip).toBe(1);
}
await locator.getByTestId('zoom-out-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const zoomOutToolTip = await element.getByText('Zoom out').count();
expect(zoomOutToolTip).toBe(1);
}
await locator.getByTestId('reset-scale-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const resetScaleTooltip = await element.getByText('Reset Scale').count();
expect(resetScaleTooltip).toBe(1);
}
await locator.getByTestId('zoom-in-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const zoominToolTip = await element.getByText('Zoom in').count();
expect(zoominToolTip).toBe(1);
}
await locator.getByTestId('download-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const downloadTooltip = await element.getByText('Download').count();
expect(downloadTooltip).toBe(1);
}
await locator.getByTestId('copy-to-clipboard-button').hover();
await page.waitForTimeout(1000);
{
const element = page.getByRole('tooltip');
const downloadTooltip = await element
.getByText('Copy to clipboard')
.count();
expect(downloadTooltip).toBe(1);
}
await locator.getByTestId('delete-button').hover();
await page.waitForTimeout(500);
{
const element = page.getByRole('tooltip');
const downloadTooltip = await element.getByText('Delete').count();
expect(downloadTooltip).toBe(1);
}
});
test('keypress esc should close the modal', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
await expect(locator).toBeHidden();
});
test('when mouse moves outside, the modal should be closed', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().dblclick();
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
// animation delay
await page.waitForTimeout(1000);
await page.mouse.click(10, 10);
await page.waitForTimeout(1000);
await expect(locator).not.toBeVisible();
});
test('caption should be visible and different styles were applied if image zoomed larger than viewport', async ({
page,
}) => {
const sampleCaption = 'affine owns me and all';
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
await page.locator('affine-page-image').first().hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await toolbar.getByLabel('Caption').click();
await page.getByPlaceholder('Write a caption').fill(sampleCaption);
await page.locator('affine-page-image').first().dblclick();
const locator = page.getByTestId('image-preview-modal');
await expect(locator).toBeVisible();
await page.waitForTimeout(1000);
let captionLocator = locator.getByTestId('image-caption-zoomedout');
await expect(captionLocator).toBeVisible();
expect(await captionLocator.innerText()).toBe(sampleCaption);
await page.getByTestId('zoom-in-button').click({ clickCount: 4 });
await expect(captionLocator).not.toBeVisible();
captionLocator = locator.getByTestId('image-caption-zoomedin');
await expect(captionLocator).toBeVisible();
expect(await captionLocator.innerText()).toBe(sampleCaption);
});

View File

@@ -0,0 +1,59 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import test, { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
});
test('Open import dialog by click root sidebar import button', async ({
page,
}) => {
await page.getByTestId('slider-bar-import-button').click();
const importDialog = page.getByTestId('import-dialog');
await expect(importDialog).toBeVisible();
await page.getByTestId('modal-close-button').click();
await expect(importDialog).not.toBeVisible();
});
test('Open import dialog by click header menu import button', async ({
page,
}) => {
await page.getByTestId('header-dropDownButton').click();
await page.getByTestId('editor-option-menu-import').click();
const importDialog = page.getByTestId('import-dialog');
await expect(importDialog).toBeVisible();
await page.getByTestId('modal-close-button').click();
await expect(importDialog).not.toBeVisible();
});
test('Open import dialog by @ menu import button', async ({ page }) => {
await page.keyboard.press('Enter');
await page.waitForTimeout(100);
await page.keyboard.type('@', { delay: 50 });
const linkedPagePopover = page.locator('.linked-doc-popover');
await expect(linkedPagePopover).toBeVisible();
const importButton = page.locator(
'.linked-doc-popover icon-button[data-id="import"]'
);
await expect(importButton).toBeVisible();
await importButton.click();
const importDialog = page.getByTestId('import-dialog');
await expect(importDialog).toBeVisible();
await page.getByTestId('modal-close-button').click();
await expect(importDialog).not.toBeVisible();
});

View File

@@ -0,0 +1,126 @@
import { test } from '@affine-test/kit/playwright';
import {
confirmCreateJournal,
openHomePage,
} from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect, type Locator, type Page } from '@playwright/test';
type MaybeDate = string | number | Date;
function isSameDay(d1: MaybeDate, d2: MaybeDate) {
const date1 = new Date(d1);
const date2 = new Date(d2);
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
function getJournalRow(page: Page) {
return page.locator(
'[data-testid="doc-property-row"][data-info-id="journal"]'
);
}
async function isJournalEditor(page: Page, maybeDate?: string | number | Date) {
// journal header
const header = page.getByTestId('header');
const weekPicker = header.getByTestId('journal-week-picker');
await expect(weekPicker).toBeVisible();
// journal title
const journalTitle = page.getByTestId('journal-title');
await expect(journalTitle).toBeVisible();
if (maybeDate) {
const date = (await journalTitle.getByTestId('date').textContent()) ?? '';
expect(isSameDay(date, maybeDate)).toBeTruthy();
}
}
async function openPagePropertiesAndAddJournal(page: Page) {
const collapse = page.getByTestId('page-info-collapse');
const open = await collapse.getAttribute('aria-expanded');
if (open?.toLowerCase() !== 'true') {
await collapse.click();
}
// add if not exists
if ((await getJournalRow(page).count()) === 0) {
const addPropertyButton = page.getByTestId('add-property-button');
if (!(await addPropertyButton.isVisible())) {
await page.getByTestId('property-collapsible-button').click();
}
await addPropertyButton.click();
await page
.locator('[role="menuitem"][data-property-type="journal"]')
.click();
await page.keyboard.press('Escape');
}
// expand if collapsed
else if (!(await getJournalRow(page).isVisible())) {
await page.getByTestId('property-collapsible-button').click();
}
const journalRow = getJournalRow(page);
await expect(journalRow).toBeVisible();
return journalRow;
}
async function toggleJournal(row: Locator, value: boolean) {
const checkbox = row.locator('input[type="checkbox"]');
const state = await checkbox.inputValue();
const checked = state === 'on';
if (checked !== value) {
await checkbox.click();
const newState = await checkbox.inputValue();
const newChecked = newState === 'on';
expect(newChecked).toBe(value);
}
}
async function createPageAndTurnIntoJournal(page: Page) {
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
const journalRow = await openPagePropertiesAndAddJournal(page);
await toggleJournal(journalRow, true);
return journalRow;
}
test('Create a journal from sidebar', async ({ page }) => {
await openHomePage(page);
await page.getByTestId('slider-bar-journals-button').click();
await confirmCreateJournal(page);
await waitForEditorLoad(page);
await isJournalEditor(page);
});
test('Create a page and turn it into a journal', async ({ page }) => {
await openHomePage(page);
await createPageAndTurnIntoJournal(page);
await isJournalEditor(page, new Date());
});
test('Should show duplicated tag when create journal on same day', async ({
page,
}) => {
await openHomePage(page);
await createPageAndTurnIntoJournal(page);
const journalRow2 = await createPageAndTurnIntoJournal(page);
await expect(journalRow2.getByTestId('conflict-tag')).toBeVisible();
});
test('Resolve duplicated journal', async ({ page }) => {
await openHomePage(page);
await createPageAndTurnIntoJournal(page);
const journalRow2 = await createPageAndTurnIntoJournal(page);
await journalRow2.getByTestId('conflict-tag').click();
const journalPanel = page.getByTestId('sidebar-journal-panel');
await expect(journalPanel).toBeVisible();
const conflictList = journalPanel.getByTestId('journal-conflict-list');
await expect(conflictList).toBeVisible();
const conflictItems = conflictList.getByTestId('journal-conflict-item');
const first = conflictItems.first();
await first.getByTestId('journal-conflict-edit').click();
await page.getByTestId('journal-conflict-remove-mark').click();
await expect(journalRow2.getByTestId('conflict-tag')).not.toBeVisible();
await expect(conflictList).not.toBeVisible();
});

View File

@@ -0,0 +1,83 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Collapse Sidebar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
.click();
const sliderBarArea = page.getByTestId('app-sidebar');
await expect(sliderBarArea).not.toBeInViewport();
});
test('Expand Sidebar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
.click();
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).not.toBeInViewport();
await page
.locator('[data-testid=app-sidebar-arrow-button-expand][data-show=true]')
.click();
await expect(sliderBarArea).toBeInViewport();
});
test('Click resizer can close sidebar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).toBeVisible();
await page
.getByTestId('app-sidebar-wrapper')
.getByTestId('resize-handle')
.click();
await expect(sliderBarArea).not.toBeInViewport();
});
test('Drag resizer can resize sidebar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).toBeVisible();
const sliderResizer = page
.getByTestId('app-sidebar-wrapper')
.getByTestId('resize-handle');
await sliderResizer.hover();
await page.mouse.down();
await page.mouse.move(400, 300, {
steps: 10,
});
await page.mouse.up();
const boundingBox = await page.getByTestId('app-sidebar').boundingBox();
expect(Math.floor(boundingBox?.width ?? 0)).toBe(399);
});
test('Sidebar in between sm & md breakpoint', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const sliderBarArea = page.getByTestId('sliderBar-inner');
const sliderBarModalBackground = page.getByTestId('app-sidebar-float-mask');
await expect(sliderBarArea).toBeInViewport();
await expect(sliderBarModalBackground).not.toBeVisible();
await page.setViewportSize({
width: 768,
height: 1024,
});
await expect(sliderBarModalBackground).toBeVisible();
// click modal background can close sidebar
await sliderBarModalBackground.click({
force: true,
position: { x: 600, y: 150 },
});
await expect(sliderBarArea).not.toBeInViewport();
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
import { ProjectRoot, test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test';
test('should create a page with a local first avatar and remove it', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await createLocalWorkspace({ name: 'Test Workspace 1' }, page);
await page.waitForTimeout(1000);
await page.getByTestId('workspace-name').click();
await page
.getByTestId('workspace-card')
.nth(1)
.click({ position: { x: 10, y: 10 } });
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('workspace-setting:preference').click();
await page
.getByTestId('upload-avatar')
.setInputFiles(ProjectRoot.join('tests', 'fixtures', 'blue.png').value);
await page.mouse.click(0, 0);
await page.getByTestId('workspace-name').click();
await page
.getByTestId('workspace-card')
.nth(0)
.getByTestId('workspace-avatar')
.click();
await page.waitForTimeout(2000);
await page.getByTestId('workspace-name').click();
await page
.getByTestId('workspace-card')
.nth(1)
.getByTestId('workspace-avatar')
.click();
const avatarCanvas = await page
.getByTestId('workspace-avatar')
.locator('canvas')
.first()
.elementHandle();
const avatarPixelData = await page.evaluate(
({ avatarCanvas }) => {
return Array.from(
(avatarCanvas as HTMLCanvasElement)
.getContext('2d')!
.getImageData(1, 1, 1, 1).data // get pixel data of the avatar
);
},
{ avatarCanvas }
);
expect(avatarPixelData).toEqual([0, 0, 255, 255]); // blue color
// Click remove button to remove workspace avatar
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('workspace-setting:preference').click();
await page.getByTestId('workspace-setting-avatar').hover();
await page.getByTestId('workspace-setting-remove-avatar-button').click();
await page.mouse.click(0, 0);
await page.waitForTimeout(1000);
await page.getByTestId('workspace-name').click();
await page.getByTestId('workspace-card').nth(1).click();
const removedAvatarImage = await page
.getByTestId('workspace-avatar')
.locator('canvas')
.count();
expect(removedAvatarImage).toBe(0);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,172 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import {
clickSideBarAllPageButton,
clickSideBarCurrentWorkspaceBanner,
} from '@affine-test/kit/utils/sidebar';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
const removeOnboardingPages = async (page: Page) => {
await page.getByTestId('all-pages').click();
await page
.getByTestId('doc-list-item')
.first()
.click({
modifiers: ['Shift'],
});
await page
.getByTestId('doc-list-item')
.last()
.click({
modifiers: ['Shift'],
});
await page.getByTestId('list-toolbar-delete').click();
// confirm delete
await page.getByTestId('confirm-modal-confirm').click();
};
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
});
const createAndPinCollection = async (
page: Page,
options?: {
collectionName?: string;
}
) => {
await page.getByTestId('all-pages').click();
await page.getByTestId('navigation-panel-bar-add-collection-button').click();
const title = page.getByTestId('prompt-modal-input');
await expect(title).toBeVisible();
await title.fill(options?.collectionName ?? 'test collection');
await page.getByTestId('prompt-modal-confirm').click();
await page.waitForTimeout(100);
await page
.locator('[data-testid^="navigation-panel-collection-"]')
.first()
.click();
await page.getByTestId('collection-add-doc-button').click();
await page.getByTestId('confirm-modal-confirm').click();
// fixme: remove this timeout. looks like an issue with useBindWorkbenchToBrowserRouter?
await page.waitForTimeout(500);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page');
await page.getByTestId('all-pages').click();
const cell = page.getByTestId('doc-list-item-title').getByText('test page');
await expect(cell).toBeVisible();
};
test('Show collections items in sidebar', async ({ page }) => {
await removeOnboardingPages(page);
await createAndPinCollection(page);
const collections = page.getByTestId('navigation-panel-collections');
const items = collections.locator(
'[data-testid^="navigation-panel-collection-"]'
);
await expect(items).toHaveCount(1);
const first = items.first();
expect((await first.textContent())!.startsWith('test collection')).toBe(true);
const collectionPage = first
.locator('[data-testid^="navigation-panel-doc-"]')
.nth(0);
expect(await collectionPage.textContent()).toBe('test page');
await collectionPage.hover();
await collectionPage
.getByTestId('navigation-panel-tree-node-operation-button')
.click();
const deletePage = page.getByText('Move to trash');
await deletePage.click();
await page.getByTestId('confirm-modal-confirm').click();
await expect(
first.locator('[data-testid^="navigation-panel-doc-"]')
).toHaveCount(0);
// position is a workaround for the hover issue when empty collection status's height > 26px (will cause scroll)
await first.hover({ position: { x: 10, y: 10 } });
await first
.getByTestId('navigation-panel-tree-node-operation-button')
.click();
const deleteCollection = page.getByText('Delete');
await deleteCollection.click();
await page.waitForTimeout(50);
await expect(items).toHaveCount(0);
await createAndPinCollection(page);
await expect(items).toHaveCount(1);
await clickSideBarAllPageButton(page);
await createLocalWorkspace(
{
name: 'Test 1',
},
page
);
await waitForEditorLoad(page);
await expect(items).toHaveCount(0);
await clickSideBarCurrentWorkspaceBanner(page);
await page.getByTestId('workspace-card').nth(0).click();
});
test('edit collection', async ({ page }) => {
await removeOnboardingPages(page);
await createAndPinCollection(page);
const collections = page.getByTestId('navigation-panel-collections');
const items = collections.locator(
'[data-testid^="navigation-panel-collection-"]'
);
await expect(items).toHaveCount(1);
const first = items.first();
await first.getByTestId('navigation-panel-collapsed-button').first().click();
await first.hover();
await first
.getByTestId('navigation-panel-tree-node-operation-button')
.click();
const editCollection = page.getByText('Rename');
await editCollection.click();
await page.getByTestId('rename-modal-input').fill('123');
await page.keyboard.press('Enter');
await page.waitForTimeout(100);
expect(await first.textContent()).toBe('123');
});
test('add collection from sidebar', async ({ page }) => {
await removeOnboardingPages(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page');
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'test page');
await expect(cell).toBeVisible();
await page
.getByTestId('navigation-panel-collections')
.getByTestId('category-divider-collapse-button')
.click();
const nullCollection = page.getByTestId(
'slider-bar-collection-empty-message'
);
await expect(nullCollection).toBeVisible();
await page.getByTestId('navigation-panel-bar-add-collection-button').click();
const title = page.getByTestId('prompt-modal-input');
await expect(title).toBeVisible();
await title.fill('test collection');
await page.getByTestId('prompt-modal-confirm').click();
await page.waitForTimeout(100);
const collections = page.getByTestId('navigation-panel-collections');
const items = collections.locator(
'[data-testid^="navigation-panel-collection-"]'
);
await expect(items).toHaveCount(1);
await expect(nullCollection).not.toBeVisible();
});

View File

@@ -0,0 +1,146 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageItem,
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('page delete -> refresh page -> it should be disappear', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page delete');
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'this is a new page delete');
await expect(cell).toBeVisible();
await getPageOperationButton(page, newPageId).click();
const deleteBtn = page.getByTestId('doc-list-operation-trash');
await deleteBtn.click();
const confirmTip = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('trash-page').click();
await page.getByTestId('delete-page-button').click();
await page.getByText('Delete permanently?').dblclick();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Deleted docs will appear here.')).toBeVisible();
await page.getByTestId('all-pages').click();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('page delete -> create new page -> refresh page -> new page should be appear -> old page should be disappear', async ({
page,
workspace,
}) => {
test.slow();
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page delete');
const newPageDeleteId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cellDelete = await getPageByTitle(page, 'this is a new page delete');
await expect(cellDelete).toBeVisible();
await getPageOperationButton(page, newPageDeleteId).click();
const deleteBtn = page.getByTestId('doc-list-operation-trash');
await deleteBtn.click();
const confirmTip = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('trash-page').click();
await page.getByTestId('delete-page-button').click();
await page.getByText('Delete permanently?').dblclick();
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Deleted docs will appear here')).toBeVisible();
await page.getByTestId('all-pages').click();
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page1');
await page.waitForTimeout(1000);
const newPageId1 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page2');
await page.waitForTimeout(1000);
const newPageId2 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await getPageItem(page, newPageId1).click();
await page.getByTestId('all-pages').click();
await getPageItem(page, newPageId2).click();
await page.getByTestId('all-pages').click();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('delete multiple pages -> create multiple pages -> refresh', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
// create 1st page
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page1');
const newPageId1 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
// create 2nd page
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page2');
const newPageId2 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
// 1st cell to be deleted
const cellDelete1 = await getPageByTitle(page, 'this is a new page1');
await expect(cellDelete1).toBeVisible();
await getPageOperationButton(page, newPageId1).click();
const deleteBtn1 = page.getByTestId('doc-list-operation-trash');
await deleteBtn1.click();
const confirmTip1 = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip1).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('trash-page').click();
await page.getByTestId('delete-page-button').click();
await page.getByText('Delete permanently?').dblclick();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('all-pages').click();
// 2nd cell to be deleted
const cellDelete2 = await getPageByTitle(page, 'this is a new page2');
await expect(cellDelete2).toBeVisible();
await getPageOperationButton(page, newPageId2).click();
const deleteBtn2 = page.getByTestId('doc-list-operation-trash');
await deleteBtn2.click();
const confirmTip2 = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip2).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('trash-page').click();
await page.getByTestId('delete-page-button').click();
await page.getByText('Delete permanently?').dblclick();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('all-pages').click();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,74 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import {
openSettingModal,
openWorkspaceSettingPanel,
} from '@affine-test/kit/utils/setting';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test';
test('Create new workspace, then delete it', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await createLocalWorkspace({ name: 'Test Workspace' }, page);
await page.waitForSelector('[data-testid="workspace-name"]');
expect(await page.getByTestId('workspace-name').textContent()).toBe(
'Test Workspace'
);
await openSettingModal(page);
await openWorkspaceSettingPanel(page);
await page.getByTestId('delete-workspace-button').click();
await expect(
page.locator('.affine-notification-center').first()
).not.toBeVisible();
const workspaceNameDom = page.getByTestId('workspace-name');
const currentWorkspaceName = (await workspaceNameDom.evaluate(
node => node.textContent
)) as string;
expect(currentWorkspaceName).toBeDefined();
await page
.getByTestId('delete-workspace-input')
.pressSequentially(currentWorkspaceName);
const promise = page
.getByText('Successfully deleted', { exact: true })
.waitFor({ state: 'visible' });
await page.getByTestId('delete-workspace-confirm-button').click();
await promise;
await page.reload();
await page.waitForSelector('[data-testid="workspace-name"]');
await page.waitForTimeout(1000);
expect(await page.getByTestId('workspace-name').textContent()).toBe(
'Demo Workspace'
);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('Delete last workspace', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const workspaceNameDom = page.getByTestId('workspace-name');
const currentWorkspaceName = await workspaceNameDom.evaluate(
node => node.textContent
);
await openSettingModal(page);
await openWorkspaceSettingPanel(page);
await page.getByTestId('delete-workspace-button').click();
await page
.getByTestId('delete-workspace-input')
.pressSequentially(currentWorkspaceName as string);
const deleted = page
.getByText('Successfully deleted', { exact: true })
.waitFor({ state: 'visible' });
await page.getByTestId('delete-workspace-confirm-button').click();
await deleted;
await openHomePage(page);
await expect(page.getByTestId('new-workspace')).toBeVisible();
await createLocalWorkspace({ name: 'Test Workspace' }, page, true);
await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="workspace-name"]');
await expect(page.getByTestId('workspace-name')).toHaveText('Test Workspace');
});

View File

@@ -0,0 +1,78 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.skip('New a page ,then open it and export html', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await page
.getByPlaceholder('Title')
.fill('this is a new page to export html content');
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export html content',
});
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const exportParentBtn = page.getByRole('tooltip', {
name: 'Add to favorites Convert to Edgeless Export Delete',
});
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to HTML' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export html content.html'
);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test.skip('New a page ,then open it and export markdown', async ({
page,
workspace,
}) => {
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await page
.getByPlaceholder('Title')
.fill('this is a new page to export markdown content');
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to export markdown content',
});
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const exportParentBtn = page.getByRole('tooltip', {
name: 'Add to favorites Convert to Edgeless Export Delete',
});
await exportParentBtn.click();
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export to Markdown' }).click(),
]);
expect(download.suggestedFilename()).toBe(
'this is a new page to export markdown content.md'
);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,101 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
getPageByTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('New a page and open it, then favorite it', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByTestId('all-pages').click();
const cell = page
.getByTestId('doc-list-item-title')
.getByText('this is a new page to favorite');
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('Export to html, markdown and png', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
{
await clickPageMoreActions(page);
await page.getByTestId('export-menu').hover();
const downloadPromise = page.waitForEvent('download');
await page.getByTestId('export-to-markdown').click();
await downloadPromise;
}
await page.waitForTimeout(50);
{
await clickPageMoreActions(page);
await page.getByTestId('export-menu').click();
const downloadPromise = page.waitForEvent('download');
await page.getByTestId('export-to-html').click();
await downloadPromise;
}
// await page.waitForTimeout(50);
// {
// await clickPageMoreActions(page);
// await page.getByTestId('export-menu').click();
// const downloadPromise = page.waitForEvent('download');
// await page.getByTestId('export-to-png').click();
// await downloadPromise;
// }
});
test('Cancel favorite', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page to favorite');
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const favorites = page.getByTestId('navigation-panel-favorites');
// expect it in favorite list
await expect(
favorites.getByText('this is a new page to favorite')
).toBeVisible();
// cancel favorite
await page.getByTestId('all-pages').click();
const doc = await getPageByTitle(page, 'this is a new page to favorite');
await doc.getByTestId('doc-list-operation-favorite').click();
// expect it not in favorite list
await expect(
favorites.getByTestId('slider-bar-favorites-empty-message')
).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,163 @@
import { test } from '@affine-test/kit/playwright';
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
createLinkedPage,
getBlockSuiteEditorTitle,
getPageByTitle,
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('Show favorite items in sidebar', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page to favorite');
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const favoriteListItemInSidebar = page.getByTestId(
'navigation-panel-doc-' + newPageId
);
expect(await favoriteListItemInSidebar.textContent()).toBe(
'this is a new page to favorite'
);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('Show favorite reference in sidebar', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
// goes to main content
await page.keyboard.press('Enter', { delay: 50 });
await createLinkedPage(page, 'Another page');
const newPageId = getCurrentDocIdFromUrl(page);
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
const favItemTestId = 'navigation-panel-doc-' + newPageId;
const favoriteListItemInSidebar = page.getByTestId(favItemTestId);
expect(await favoriteListItemInSidebar.textContent()).toBe(
'this is a new page to favorite'
);
const collapseButton = favoriteListItemInSidebar.getByTestId(
'navigation-panel-collapsed-button'
);
await expect(collapseButton).toBeVisible();
await collapseButton.click();
await expect(
favoriteListItemInSidebar.locator(
'[data-testid^="navigation-panel-doc-"]:has-text("Another page")'
)
).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test("Deleted page's reference will not be shown in sidebar", async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
const newPageId = getCurrentDocIdFromUrl(page);
// goes to main content
await page.keyboard.press('Enter', { delay: 50 });
await createLinkedPage(page, 'Another page');
await clickPageMoreActions(page);
const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
await favoriteBtn.click();
// goto "Another page"
await page.locator('.affine-reference-title').click();
await expect(
page.locator('.doc-title-container:has-text("Another page")')
).toBeVisible();
const anotherPageId = getCurrentDocIdFromUrl(page);
const favItemTestId = 'navigation-panel-doc-' + newPageId;
await expect(page.getByTestId(favItemTestId)).toHaveText(
'this is a new page to favorite'
);
await page
.getByTestId(favItemTestId)
.getByTestId('navigation-panel-collapsed-button')
.click();
const favItemAnotherPageTestId = 'navigation-panel-doc-' + anotherPageId;
await expect(
page.getByTestId(favItemTestId).getByTestId(favItemAnotherPageTestId)
).toBeVisible();
// delete the page
await clickPageMoreActions(page);
const deleteBtn = page.getByTestId('editor-option-menu-delete');
await deleteBtn.click();
// confirm delete
await page.locator('button >> text=Delete').click();
await expect(
page.getByTestId(favItemTestId).getByTestId(favItemAnotherPageTestId)
).toBeHidden();
});
test('Add new favorite page via sidebar', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page
.getByTestId('navigation-panel-bar-add-favorite-button')
.first()
.click();
await clickPageModeButton(page);
await waitForEmptyEditor(page);
// enter random page title
await getBlockSuiteEditorTitle(page).fill('this is a new fav page');
// check if the page title is shown in the favorite list
const favItem = page
.getByTestId('navigation-panel-favorites')
.locator('[draggable] >> text=this is a new fav page');
await expect(favItem).toBeVisible();
});

View File

@@ -0,0 +1,39 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('click btn new page', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const originPageId = getCurrentDocIdFromUrl(page);
await clickNewPageButton(page);
const newPageId = getCurrentDocIdFromUrl(page);
expect(newPageId).not.toBe(originPageId);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('click btn bew page and find it in all pages', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page');
await expect(cell).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,151 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('click btn new page and open in tab', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
const newPageUrl = page.url();
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await getPageOperationButton(page, newPageId).click();
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('menuitem', { name: 'Open in new tab' }).click(),
]);
await expect(newTabPage).toHaveURL(newPageUrl, { timeout: 15000 });
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('switch between new page and all page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const title = 'this is a new page';
await clickNewPageButton(page, title);
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, title);
await expect(cell).toBeVisible();
await cell.click();
await expect(getBlockSuiteEditorTitle(page)).toHaveText(title);
await page.getByTestId('all-pages').click();
await expect(cell).toBeVisible();
});
test('ctrl click all page and open in new tab', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
page.getByTestId('all-pages').click({
modifiers: ['ControlOrMeta'],
}),
]);
await expect(newTabPage).toHaveURL(/\/all/, {
timeout: 15000,
});
});
test('mid click all page and open in new tab', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
page.getByTestId('all-pages').click({
button: 'middle',
}),
]);
await expect(newTabPage).toHaveURL(/\/all/, {
timeout: 15000,
});
});
test('ctrl click embedded doc link and open in new tab', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
const newPageUrl = page.url();
await clickNewPageButton(page);
await page.keyboard.press('Enter'); // goto main content
// paste new page url to create linked page
await page.evaluate(
async ([url]) => {
const clipData = {
'text/plain': url,
};
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);
},
[newPageUrl]
);
const referenceNode = page.locator(
'affine-reference:has-text("this is a new page")'
);
// hover on the reference node and change it to embedded card mode
await referenceNode.hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByRole('button', { name: 'Switch view' }).click();
await page.getByRole('button', { name: 'Card view' }).click();
const embededDocBlock = page.locator('affine-embed-linked-doc-block');
await expect(embededDocBlock).toBeVisible();
// open in new tab
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
embededDocBlock.click({
button: 'left',
modifiers: ['ControlOrMeta'],
}),
]);
const newTabPageUrl = new URL(newTabPage.url());
// ignores `search params`
newTabPageUrl.search = '';
expect(newTabPageUrl.toString()).toBe(newPageUrl);
});

View File

@@ -0,0 +1,53 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('New a page , then delete it in all pages, restore it', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'this is a new page to restore');
await expect(cell).toBeVisible();
await getPageOperationButton(page, newPageId).click();
const deleteBtn = page.getByTestId('doc-list-operation-trash');
await deleteBtn.click();
const confirmTip = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByTestId('trash-page').click();
await page.waitForTimeout(50);
const trashPage = page.url();
// restore it
await page.getByTestId('restore-page-button').click();
// stay in trash page
expect(page.url()).toBe(trashPage);
await page.getByTestId('all-pages').click();
const restoreCell = await getPageByTitle(
page,
'this is a new page to restore'
);
await expect(restoreCell).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,60 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('New a page ,then open it and show delete modal', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'this is a new page to delete');
await expect(cell).toBeVisible();
await cell.click();
await clickPageMoreActions(page);
const deleteBtn = page.getByTestId('editor-option-menu-delete');
await deleteBtn.click();
const confirmTip = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('New a page ,then go to all pages and show delete modal', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'this is a new page to delete');
await expect(cell).toBeVisible();
await getPageOperationButton(page, newPageId).click();
const deleteBtn = page.getByTestId('doc-list-operation-trash');
await deleteBtn.click();
const confirmTip = page.getByRole('dialog', { name: 'Delete doc?' });
await expect(confirmTip).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,79 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
getPageByTitle,
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect, type Page } from '@playwright/test';
const movePageToTrash = async (page: Page, docId: string) => {
await getPageOperationButton(page, docId).click();
await page.getByTestId('doc-list-operation-trash').click();
await expect(page.getByRole('dialog', { name: 'Delete doc?' })).toBeVisible();
await page.getByRole('button', { name: 'Delete' }).click();
};
const createAndTrashPage = async (page: Page, title: string) => {
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill(title);
const docId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await movePageToTrash(page, docId);
};
test('New a page , then delete it in all pages, finally find it in trash', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = await getPageByTitle(page, 'this is a new page to delete');
await expect(cell).toBeVisible();
await movePageToTrash(page, newPageId);
await page.getByTestId('trash-page').click();
await expect(page.getByText('this is a new page to delete')).toBeVisible();
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('select all trashed pages from the Trash title', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await createAndTrashPage(page, 'trash select all 1');
await createAndTrashPage(page, 'trash select all 2');
await page.getByTestId('trash-page').click();
const selectAllButton = page.getByTestId('trash-select-all');
await expect(selectAllButton).not.toBeVisible();
await page
.locator('[data-testid="doc-list-item"]')
.first()
.click({ modifiers: ['Shift'] });
await expect(selectAllButton).toHaveText('Select all');
await selectAllButton.click();
await expect(page.getByTestId('floating-toolbar')).toHaveText(
'2 doc(s) selected'
);
await expect(selectAllButton).toHaveText('Clear selection');
await selectAllButton.click();
await expect(page.getByTestId('floating-toolbar')).toHaveText(
'0 doc(s) selected'
);
await expect(selectAllButton).toHaveText('Select all');
});

View File

@@ -0,0 +1,152 @@
import { test } from '@affine-test/kit/playwright';
import { getPagesCount } from '@affine-test/kit/utils/filter';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import {
createLocalWorkspace,
openWorkspaceListModal,
} from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test';
test('just one item in the workspace list at first', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
await expect(page.getByTestId('workspace-card')).toHaveCount(1);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test('create one workspace in the workspace list', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
const newWorkspaceNameStr = 'New Workspace';
await createLocalWorkspace({ name: newWorkspaceNameStr }, page);
// check new workspace name
const newWorkspaceName = page.getByTestId('workspace-name');
await newWorkspaceName.click();
//check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(2);
//check page list length
await page.keyboard.press('Escape');
await clickSideBarAllPageButton(page);
await page.waitForTimeout(2000);
const result = await getPagesCount(page);
expect(result).toBe(2);
await page.reload();
await page.waitForTimeout(4000);
const result1 = await getPagesCount(page);
expect(result1).toBe(2);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});
test.skip('create multi workspace in the workspace list', async ({
page,
workspace,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await createLocalWorkspace({ name: 'New Workspace 2' }, page);
await createLocalWorkspace({ name: 'New Workspace 3' }, page);
// show workspace list
const workspaceName = page.getByTestId('workspace-name');
await workspaceName.click();
await page.waitForTimeout(1000);
{
// check workspace list length
const workspaceCards = page.getByTestId('workspace-card');
await expect(workspaceCards).toHaveCount(3);
}
await page.reload();
await openWorkspaceListModal(page);
await page.getByTestId('workspace-card').nth(1).click();
await page.waitForTimeout(500);
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
await openWorkspaceListModal(page);
await page.waitForTimeout(1000);
const sourceElement = page.getByTestId('workspace-card').nth(2);
const targetElement = page.getByTestId('workspace-card').nth(1);
const sourceBox = await sourceElement.boundingBox();
const targetBox = await targetElement.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error('sourceBox or targetBox is null');
}
await page.mouse.move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2,
{
steps: 5,
}
);
await page.mouse.down();
await page.mouse.move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2,
{
steps: 5,
}
);
await page.mouse.up();
// check workspace list order
await page.waitForFunction(
() => {
const cards = document.querySelectorAll('[data-testid="workspace-card"]');
return (
cards[1].textContent?.includes('New Workspace 3') &&
cards[2].textContent?.includes('New Workspace 2')
);
},
[],
{ timeout: 5000 }
);
await page.reload();
await openWorkspaceListModal(page);
await page.waitForTimeout(1000);
// check workspace list length
{
const workspaceCards = page.getByTestId('workspace-card');
await expect(workspaceCards).toHaveCount(3);
}
await page.getByTestId('draggable-item').nth(2).click();
await page.evaluate(async () => {
await new Promise(resolve => {
window.addEventListener('affine:workspace:change', resolve, {
once: true,
});
});
});
const nextWorkspace = await workspace.current();
expect(currentWorkspace.meta.id).toBe(nextWorkspace.meta.id);
});

View File

@@ -0,0 +1,15 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('preset workspace name', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const workspaceName = page.getByTestId('workspace-name');
await page.waitForTimeout(1000);
expect(await workspaceName.textContent()).toBe('Demo Workspace');
const currentWorkspace = await workspace.current();
expect(currentWorkspace.meta.flavour).toContain('local');
});

View File

@@ -0,0 +1,45 @@
import { test } from '@affine-test/kit/playwright';
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
await withCtrlOrMeta(page, () =>
page.keyboard.press(command === 'goBack' ? '[' : ']', { delay: 300 })
);
};
test('back and forward buttons', async ({ page }) => {
await openHomePage(page);
await expect(page.getByTestId('app-navigation-button-back')).toBeHidden();
await expect(page.getByTestId('app-navigation-button-forward')).toBeHidden();
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.fill('test1');
await clickSideBarAllPageButton(page);
await page.getByTestId('workspace-collections-button').click({ delay: 50 });
await page.waitForURL(url => url.pathname.endsWith('collection'));
await page.getByTestId('workspace-tags-button').click({ delay: 50 });
await page.waitForURL(url => url.pathname.endsWith('tag'));
await page.goBack();
await page.waitForURL(url => url.pathname.endsWith('collection'));
await page.goBack();
await page.waitForURL(url => url.pathname.endsWith('all'));
await historyShortcut(page, 'goBack');
await waitForEditorLoad(page);
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
});

View File

@@ -0,0 +1,42 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test';
test('Open last workspace when back to affine', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await createLocalWorkspace({ name: 'New Workspace 2' }, page);
await waitForEditorLoad(page);
// show workspace list
await page.getByTestId('workspace-name').click();
//check workspace list length
const workspaceCards = await page.$$('data-testid=workspace-card');
expect(workspaceCards.length).toBe(2);
await workspaceCards[1].click();
await openHomePage(page);
const workspaceNameDom = page.getByTestId('workspace-name');
const currentWorkspaceName = await workspaceNameDom.evaluate(
node => node.textContent
);
expect(currentWorkspaceName).toEqual('New Workspace 2');
});
test('Download client tip', async ({ page }) => {
await openHomePage(page);
const localDemoTipsItem = page.locator('[data-testid=local-demo-tips]');
await expect(localDemoTipsItem).toBeVisible();
const closeButton = page.locator(
'[data-testid=local-demo-tips-close-button]'
);
await closeButton.click();
await expect(localDemoTipsItem).not.toBeVisible();
await page.reload();
const currentLocalDemoTipsItemItem = page.locator(
'[data-testid=local-demo-tips]'
);
await expect(currentLocalDemoTipsItemItem).toBeVisible();
});

View File

@@ -0,0 +1,374 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { test } from '@affine-test/kit/playwright';
import {
openHomePage,
openJournalsPage,
} from '@affine-test/kit/utils/load-page';
import {
addDatabase,
addDatabaseRow,
clickNewPageButton,
createLinkedPage,
dragTo,
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import {
addCustomProperty,
changePropertyVisibility,
clickPropertyValue,
closeTagsEditor,
ensurePagePropertiesVisible,
expectTagsVisible,
getPropertyValueLocator,
openTagsEditor,
openWorkspaceProperties,
removeSelectedTag,
searchAndCreateTag,
togglePropertyListVisibility,
} from '@affine-test/kit/utils/properties';
import { expect } from '@playwright/test';
import { addColumn } from './blocksuite/database/utils';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await ensurePagePropertiesVisible(page);
});
test('allow create tag', async ({ page }) => {
await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1');
await searchAndCreateTag(page, 'Test2');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test1', 'Test2']);
await openTagsEditor(page);
await removeSelectedTag(page, 'Test1');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test2']);
});
test('allow using keyboard to navigate tags', async ({ page }) => {
await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1');
await searchAndCreateTag(page, 'Test2');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('Backspace');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test1']);
await openTagsEditor(page);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test1', 'Test2']);
});
test('allow create tag on journals page', async ({ page }) => {
await openJournalsPage(page);
await waitForEditorLoad(page);
await ensurePagePropertiesVisible(page);
await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1');
await searchAndCreateTag(page, 'Test2');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test1', 'Test2']);
await openTagsEditor(page);
await removeSelectedTag(page, 'Test1');
await closeTagsEditor(page);
await expectTagsVisible(page, ['Test2']);
});
test('add custom property', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await addCustomProperty(page, page, 'number');
await addCustomProperty(page, page, 'date');
await addCustomProperty(page, page, 'checkbox');
await addCustomProperty(page, page, 'createdBy');
await addCustomProperty(page, page, 'updatedBy');
});
test('add custom property & edit', async ({ page }) => {
await addCustomProperty(page, page, 'checkbox');
await expect(
getPropertyValueLocator(page, 'checkbox').locator('input')
).not.toBeChecked();
await clickPropertyValue(page, 'checkbox');
await expect(
getPropertyValueLocator(page, 'checkbox').locator('input')
).toBeChecked();
});
test('property table reordering', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await addCustomProperty(page, page, 'number');
await addCustomProperty(page, page, 'date');
await addCustomProperty(page, page, 'checkbox');
await addCustomProperty(page, page, 'createdBy');
await addCustomProperty(page, page, 'updatedBy');
await dragTo(
page,
page.locator('[data-testid="doc-property-name"]:has-text("Text")'),
page.locator(
'[data-testid="doc-property-name"]:has-text("Checkbox") + div'
),
'bottom'
);
// new order should be Doc mode, (Tags), Created at, Updated at, Number, Date, Checkbox, Text
for (const [index, property] of [
'Tags',
'Doc mode',
'Journal',
'Template',
'Created',
'Updated',
'Created by',
'Edgeless theme',
'Page width',
'Number',
'Date',
'Checkbox',
'Text',
'Created by',
'Last edited by',
].entries()) {
await expect(
page
.getByTestId('doc-property-row')
.nth(index)
.getByTestId('doc-property-name')
).toHaveText(property);
}
});
test('page info show more will not should by default when there is no properties', async ({
page,
}) => {
// by default page info show more should not show
await expect(page.getByTestId('page-info-show-more')).not.toBeVisible();
});
test('page info show more will show all properties', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await addCustomProperty(page, page, 'number');
await addCustomProperty(page, page, 'date');
await addCustomProperty(page, page, 'checkbox');
await addCustomProperty(page, page, 'createdBy');
await addCustomProperty(page, page, 'updatedBy');
await changePropertyVisibility(page, 'Text', 'always-hide');
await expect(page.getByTestId('property-collapsible-button')).toBeVisible();
await page.click('[data-testid="property-collapsible-button"]');
for (const [index, property] of [
'Tags',
'Doc mode',
'Journal',
'Template',
'Created',
'Updated',
'Created by',
'Edgeless theme',
'Page width',
'Text',
'Number',
'Date',
'Checkbox',
'Created by',
'Last edited by',
].entries()) {
await expect(
page
.getByTestId('doc-property-row')
.nth(index)
.getByTestId('doc-property-name')
).toHaveText(property);
}
});
test('change page properties visibility', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await addCustomProperty(page, page, 'number');
await addCustomProperty(page, page, 'date');
await addCustomProperty(page, page, 'checkbox');
await togglePropertyListVisibility(page);
// add some number to number property
await clickPropertyValue(page, 'Number');
await page.locator('input[type=number]').fill('123');
await changePropertyVisibility(page, 'Text', 'always-hide');
await changePropertyVisibility(page, 'Number', 'hide-when-empty');
// text property should not be visible
await expect(
page.locator('[data-testid="doc-property-name"]:has-text("Text")')
).not.toBeVisible();
// number property should be visible
await expect(
page.locator('[data-testid="doc-property-name"]:has-text("Number")')
).toBeVisible();
});
test('check if added property is also in workspace settings', async ({
page,
}) => {
await addCustomProperty(page, page, 'text');
await openWorkspaceProperties(page);
const settingModal = page.locator('[data-testid=setting-modal-content]');
const item = settingModal.locator(
'[data-testid=doc-property-manager-item]:has-text("Text")'
);
await item.waitFor({ state: 'attached' });
await expect(item).toBeVisible();
});
test('edit property name', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await page
.locator('[data-testid="doc-property-name"]:has-text("Text")')
.click();
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
'Text'
);
await page.locator('[data-radix-menu-content] input').fill('New Text');
await page.keyboard.press('Enter');
await expect(page.locator('[data-radix-menu-content] input')).toHaveValue(
'New Text'
);
await page.keyboard.press('Escape');
// check if the property name is also updated in workspace settings
await openWorkspaceProperties(page);
const settingModal = page.locator('[data-testid=setting-modal-content]');
const item = settingModal.locator(
'[data-testid=doc-property-manager-item]:has-text("New Text")'
);
await item.waitFor({ state: 'attached' });
await expect(item).toBeVisible();
});
test('delete property via property popup', async ({ page }) => {
await addCustomProperty(page, page, 'text');
await page
.locator('[data-testid="doc-property-name"]:has-text("Text")')
.click();
await expect(page.locator('[data-radix-menu-content]')).toBeVisible();
await page
.locator('[data-radix-menu-content]')
.getByRole('menuitem', {
name: 'Delete property',
})
.click();
// confirm delete dialog should show
await expect(page.getByRole('dialog')).toBeVisible();
await page
.getByRole('button', {
name: 'Confirm',
})
.click();
// check if the property is removed
await expect(
page.locator('[data-testid="http://localhost:8080/"]:has-text("Text")')
).not.toBeVisible();
});
test('workspace properties can be collapsed', async ({ page }) => {
await expect(page.getByTestId('doc-property-row').first()).toBeVisible();
await page.getByRole('button', { name: 'Workspace properties' }).click();
await expect(page.getByTestId('doc-property-row').first()).not.toBeVisible();
await page.getByRole('button', { name: 'Workspace properties' }).click();
await expect(page.getByTestId('doc-property-row').first()).toBeVisible();
});
// todo: add more tests for database backlink info for different cell types
test('can show database backlink info', async ({ page }) => {
const pageTitle = 'some page title';
await clickNewPageButton(page, pageTitle);
await page.keyboard.press('Enter');
const databaseTitle = 'some database title';
await addDatabase(page, databaseTitle);
await addColumn(page, 'select', 2);
await expect(page.locator('affine-database-title')).toContainText(
databaseTitle
);
await expect(
page.locator(`affine-database-title:has-text("${databaseTitle}")`)
).toBeVisible();
await addDatabaseRow(page, 0);
// the new row's title cell should have been focused at the point of adding the row
await createLinkedPage(page, 'linked page');
// change status label
await page.keyboard.press('Escape');
await page.keyboard.press('ArrowRight');
await page.keyboard.press('Enter');
await waitNextFrame(page);
await waitNextFrame(page);
await page.keyboard.type('Done');
await waitNextFrame(page);
await page.keyboard.press('Enter');
// go back to title cell
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('Enter');
// goto the linked page
await page.locator('.affine-reference-title:has-text("linked page")').click();
// ensure the page properties are visible
await ensurePagePropertiesVisible(page);
// database backlink property should be rendered, but collapsed
const linkedDatabaseSection = page
.getByTestId('property-collapsible-section')
.filter({
hasText: 'some database title',
});
await expect(linkedDatabaseSection).toBeVisible();
await expect(
linkedDatabaseSection.getByTestId('property-collapsible-section-content')
).not.toBeVisible();
await expect(
linkedDatabaseSection.locator(
`.affine-reference-title:has-text("${pageTitle}")`
)
).toBeVisible();
// expand the linked database section
await linkedDatabaseSection
.getByTestId('property-collapsible-section-trigger')
.click();
await expect(
linkedDatabaseSection.getByTestId('property-collapsible-section-content')
).toBeVisible();
await expect(
linkedDatabaseSection
.getByTestId('database-backlink-cell')
.getByTestId('inline-tags-list')
.filter({
hasText: 'Done',
})
).toBeVisible();
});

View File

@@ -0,0 +1,252 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
getSelectedXYWH,
getViewportBound,
} from '@affine-test/kit/utils/editor';
import { pressEnter, pressEscape } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
createSyncedPageInEdgeless,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
});
test('can open peek view via link popover', async ({ page }) => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel(/^Open doc with$/).click();
await toolbar
.getByLabel('Open doc menu')
.getByLabel('Open in center peek')
.click();
// verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
await expect(page.getByTestId('peek-view-modal')).toContainText('Test Page');
// can use 'esc' to close peek view
await page.keyboard.press('Escape');
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
});
test('can open peek view via shift+click link', async ({ page }) => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').click({ modifiers: ['Shift'] });
// verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
await expect(page.getByTestId('peek-view-modal')).toContainText('Test Page');
// can use 'esc' to close peek view
await page.keyboard.press('Escape');
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
});
test('can open peek view via db+click link card', async ({ page }) => {
await page.keyboard.press('Enter');
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Card view').click();
await expect(
page.locator('affine-embed-linked-doc-block:has-text("Test Page")')
).toBeVisible();
// double click to open peek view
await page.locator('affine-embed-linked-doc-block').dblclick();
// verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
await expect(page.getByTestId('peek-view-modal')).toContainText('Test Page');
// verify peek view can be closed by clicking close button
await page
.locator('[data-testid="peek-view-control"][data-action-name="close"]')
.click();
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
// double click to open peek view
await page.locator('affine-embed-linked-doc-block').dblclick();
// check if open-in-new button works
await page
.locator('[data-testid="peek-view-control"][data-action-name="open"]')
.click();
// verify page is routed to the linked page
await expect(
page
.getByTestId('main-container')
.locator('doc-title:has-text("Test Page")')
).toBeVisible();
});
test('should not open peek view when content is covered by canvas element', async ({
page,
}) => {
await page.keyboard.press('Enter');
await clickEdgelessModeButton(page);
await createSyncedPageInEdgeless(page, 'Test Page');
const syncedDocBlock = await page.locator(
'affine-embed-edgeless-synced-doc-block'
);
const syncedDocRect = (await syncedDocBlock.boundingBox())!;
await expect(syncedDocBlock).toBeDefined();
const syncedDocCenter = {
x: syncedDocRect.x + syncedDocRect.width / 2,
y: syncedDocRect.y + syncedDocRect.height / 2,
};
// click to make sure peek view is working
await page.mouse.move(syncedDocCenter.x, syncedDocCenter.y, {
steps: 10,
});
await page.mouse.dblclick(syncedDocCenter.x, syncedDocCenter.y);
await page.waitForTimeout(100);
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
// close peek view
await page
.locator('[data-testid="peek-view-control"][data-action-name="close"]')
.click();
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
// create a shape covering the synced doc block
await page.locator('edgeless-toolbar-button.edgeless-shape-button').click();
await page.mouse.move(syncedDocRect.x, syncedDocRect.y);
await page.mouse.down();
await page.mouse.move(
syncedDocRect.x + syncedDocRect.width,
syncedDocRect.y + syncedDocRect.height,
{
steps: 10,
}
);
await page.mouse.up();
await page.waitForTimeout(100);
// click the same position again
await page.mouse.move(syncedDocCenter.x, syncedDocCenter.y, {
steps: 10,
});
await page.mouse.dblclick(syncedDocCenter.x, syncedDocCenter.y);
// should not open peek view because the synced doc block is covered by shape
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
});
test('can open peek view for embedded frames', async ({ page }) => {
const frameInViewport = async () => {
const peekView = page.locator('[data-testid="peek-view-modal"]');
// wait for peek view ani
const frameTitle = peekView.locator('edgeless-editor affine-frame-title');
await frameTitle.waitFor({ state: 'visible' });
await frameTitle.click();
const frameXYWH = await getSelectedXYWH(page, 0, 1);
const viewportBound = await getViewportBound(page, 1);
return (
frameXYWH[0] >= viewportBound[0] &&
frameXYWH[1] >= viewportBound[1] &&
frameXYWH[0] + frameXYWH[2] <= viewportBound[0] + viewportBound[2] &&
frameXYWH[1] + frameXYWH[3] <= viewportBound[1] + viewportBound[3]
);
};
await pressEnter(page);
// create a blank frame using slash command
await type(page, '/frame');
await pressEnter(page);
const surfaceRef = page.locator('affine-surface-ref');
const peekView = page.locator('[data-testid="peek-view-modal"]');
await expect(surfaceRef).toBeVisible();
await surfaceRef.click();
await surfaceRef.hover();
await page
.locator('affine-toolbar-widget editor-menu-button[aria-label="Open"]')
.click();
await page
.locator(
'affine-toolbar-widget editor-menu-action[aria-label="Open in center peek"]'
)
.click();
await expect(peekView).toBeVisible();
expect(await frameInViewport()).toBe(true);
await pressEscape(page);
await expect(peekView).toBeHidden();
// check if can open peek view by shift+click
await surfaceRef.click({ modifiers: ['Shift'] });
await expect(peekView).toBeVisible();
expect(await frameInViewport()).toBe(true);
await pressEscape(page);
await expect(peekView).toBeHidden();
// check if can open peek view by double click
await surfaceRef.dblclick();
await expect(peekView).toBeVisible();
expect(await frameInViewport()).toBe(true);
await pressEscape(page);
await expect(peekView).toBeHidden();
// can close modal when navigate
await openHomePage(page);
await expect(peekView).toBeHidden();
});
test.skip('can open peek view for fav link', async ({ page }) => {
// give current page a title
await page.keyboard.insertText('test page title');
await page.getByTestId('pin-button').click();
await expect(page.locator('[data-testid="pin-button"].active')).toBeVisible();
await page
.getByTestId('favourites')
.locator('[data-favourite-page-item]:has-text("test page title")')
.click({
modifiers: ['Shift'],
});
// verify peek view is opened
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
await expect(page.getByTestId('peek-view-modal')).toContainText(
'test page title'
);
// can close modal when navigate
await openHomePage(page);
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
});

View File

@@ -0,0 +1,591 @@
import { test } from '@affine-test/kit/playwright';
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
import {
copyByKeyboard,
pasteByKeyboard,
selectAllByKeyboard,
withCtrlOrMeta,
writeTextToClipboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
const openQuickSearchByShortcut = async (page: Page, checkVisible = true) => {
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
if (checkVisible) {
await expect(page.getByTestId('cmdk-quick-search')).toBeVisible();
}
};
const insertInputText = async (page: Page, text: string) => {
await page.locator('[cmdk-input]').fill(text);
const actual = await page.locator('[cmdk-input]').inputValue();
expect(actual).toBe(text);
};
const selectItem = async (page: Page, label: string) => {
const selectedEl = page
.locator('[cmdk-item] [data-testid="cmdk-label"]')
.filter({ hasText: label });
await selectedEl.click();
};
const commandsIsVisible = async (page: Page, label: string) => {
const locators = page.locator('[cmdk-item] [data-testid="cmdk-label"]');
const actual = (await locators.allInnerTexts()).find(text => text === label);
return !!actual;
};
async function assertTitle(page: Page, text: string) {
const edgeless = page.locator('affine-edgeless-root');
if (!edgeless) {
const locator = getBlockSuiteEditorTitle(page);
const actual = await locator.inputValue();
expect(actual).toBe(text);
}
}
async function checkElementIsInView(page: Page, locator: Locator) {
// check if the element is in view
const elementRect = await locator.boundingBox();
const viewportHeight = page.viewportSize()?.height;
if (!elementRect || !viewportHeight) {
return false;
}
expect(elementRect.y).toBeLessThan(viewportHeight);
expect(elementRect.y + elementRect.height).toBeGreaterThan(0);
return true;
}
async function waitForScrollToFinish(page: Page) {
await page.evaluate(async () => {
await new Promise(resolve => {
let lastScrollTop: number;
const interval = setInterval(() => {
const { scrollTop } = document.documentElement;
if (scrollTop !== lastScrollTop) {
lastScrollTop = scrollTop;
} else {
clearInterval(interval);
resolve(null);
}
}, 500); // you can adjust the interval time
});
});
}
async function assertResultList(page: Page, texts: string[]) {
await expect(async () => {
const actual = await page
.locator('[cmdk-item] [data-testid=cmdk-label]')
.allInnerTexts();
const actualSplit = actual[0].split('\n');
expect(actualSplit[0]).toEqual(texts[0]);
if (actualSplit[1]) {
expect(actualSplit[1]).toEqual(texts[1]);
}
}).toPass();
}
async function titleIsFocused(page: Page) {
const edgeless = page.locator('affine-edgeless-root');
if (!edgeless) {
const title = getBlockSuiteEditorTitle(page);
await expect(title).toBeVisible();
await expect(title).toBeFocused();
}
}
test('Click slider bar button', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const quickSearchButton = page.locator(
'[data-testid=slider-bar-quick-search-button]'
);
await quickSearchButton.click();
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
});
test('Press the shortcut key cmd+k and close with esc', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
// press esc to close quick search
await page.keyboard.press('Escape');
await expect(quickSearch).toBeVisible({ visible: false });
});
test('Create a new page without keyword', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, '');
});
test('Create a new page with keyword', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
await insertInputText(page, '"test123456"');
const addNewPage = page.locator(
'[cmdk-item] >> text=New ""test123456"" Page'
);
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, '"test123456"');
});
test('Enter a keyword to search for', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
await insertInputText(page, 'test123456');
});
test('Create a new page and search this page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
// input title and create new page
await insertInputText(page, 'test123456');
await page.waitForTimeout(300);
const addNewPage = page.locator('[cmdk-item] >> text=New "test123456" Page');
await addNewPage.click();
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
await openQuickSearchByShortcut(page);
await insertInputText(page, 'test123456');
await page.waitForTimeout(1000);
await assertResultList(page, ['test123456', 'test123456']);
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
await page.reload();
await waitForEditorLoad(page);
await openQuickSearchByShortcut(page);
await insertInputText(page, 'test123456');
await page.waitForTimeout(1000);
await assertResultList(page, ['test123456', 'test123456']);
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
await assertTitle(page, 'test123456');
});
test('Navigate to the 404 page and try to open quick search', async ({
page,
}) => {
await page.goto('http://localhost:8080/404');
const notFoundTip = page.locator('button >> text=Back to My Content');
await expect(notFoundTip).toBeVisible();
await openQuickSearchByShortcut(page, false);
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible({ visible: false });
});
test('Open quick search on local page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const publishedSearchResults = page.locator('[publishedSearchResults]');
await expect(publishedSearchResults).toBeVisible({ visible: false });
});
test('Autofocus input after opening quick search', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const locator = page.locator('[cmdk-input]');
await expect(locator).toBeVisible();
await expect(locator).toBeFocused();
});
test('Autofocus input after select', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await page.waitForTimeout(500); // wait for new page loaded
await openQuickSearchByShortcut(page);
await page.keyboard.press('ArrowUp');
const locator = page.locator('[cmdk-input]');
await expect(locator).toBeVisible();
await expect(locator).toBeFocused();
});
test('Focus title after creating a new page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await titleIsFocused(page);
});
test('can use keyboard down to select goto setting', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openQuickSearchByShortcut(page);
await selectItem(page, 'Go to Settings');
await expect(page.getByTestId('setting-modal')).toBeVisible();
});
test('assert the recent browse pages are on the recent list', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
// create first page
await clickNewPageButton(page);
await waitForEditorLoad(page);
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.waitForTimeout(200);
await title.pressSequentially('sgtokidoki', { delay: 100 });
await expect(title).toHaveText('sgtokidoki');
}
// create second page
await openQuickSearchByShortcut(page);
const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
await waitForEditorLoad(page);
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.waitForTimeout(200);
await title.pressSequentially('theliquidhorse', { delay: 100 });
await expect(title).toHaveText('theliquidhorse');
}
await page.waitForTimeout(200);
// create thrid page
await openQuickSearchByShortcut(page);
await addNewPage.click();
await waitForEditorLoad(page);
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.waitForTimeout(200);
await title.pressSequentially('battlekot', { delay: 100 });
await expect(title).toHaveText('battlekot');
}
await openQuickSearchByShortcut(page);
{
// check does all 3 pages exists on recent page list
const quickSearchItems = page.locator(
'[cmdk-item] [data-testid="cmdk-label"]'
);
await expect(quickSearchItems.nth(0)).toHaveText('battlekot');
await expect(quickSearchItems.nth(1)).toHaveText('theliquidhorse');
await expect(quickSearchItems.nth(2)).toHaveText('sgtokidoki');
}
// create forth page, and check does the recent page list only contains three pages
await page.reload();
await waitForEditorLoad(page);
await openQuickSearchByShortcut(page);
{
const addNewPage = page.getByText('New page', { exact: true });
await addNewPage.click();
}
await waitForEditorLoad(page);
{
const title = getBlockSuiteEditorTitle(page);
await title.click();
await page.waitForTimeout(200);
await title.pressSequentially('affine is the best', { delay: 100 });
await expect(title).toHaveText('affine is the best', { timeout: 500 });
}
await page.waitForTimeout(1000);
await openQuickSearchByShortcut(page);
{
const quickSearchItems = page.locator(
'[cmdk-item] [data-testid="cmdk-label"]'
);
await expect(quickSearchItems.nth(0)).toHaveText('affine is the best');
}
});
test('can use cmdk to export png', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to export');
await openQuickSearchByShortcut(page);
const [download] = await Promise.all([
page.waitForEvent('download'),
selectItem(page, 'Export to PNG'),
]);
expect(download.suggestedFilename()).toBe('this is a new page to export.png');
});
test('can use cmdk to delete page and restore it', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
await openQuickSearchByShortcut(page);
await selectItem(page, 'Move to trash');
await page.getByTestId('confirm-modal-confirm').click();
const restoreButton = page.getByTestId('page-restore-button');
await expect(restoreButton).toBeVisible();
await page.waitForTimeout(100);
await openQuickSearchByShortcut(page);
expect(await commandsIsVisible(page, 'Move to trash')).toBe(false);
expect(await commandsIsVisible(page, 'Restore from trash')).toBe(true);
await selectItem(page, 'Restore from trash');
await expect(restoreButton).not.toBeVisible();
});
test('can use cmdk to search page content and scroll to it, then the block will be selected', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill(
'this is a new page to search for content'
);
for (let i = 0; i < 30; i++) {
await page.keyboard.press('Enter', { delay: 10 });
}
await page.keyboard.insertText('123456');
const textBlock = page
.locator('[data-affine-editor-container]')
.getByText('123456');
await expect(textBlock).toBeVisible();
await clickSideBarAllPageButton(page);
await openQuickSearchByShortcut(page);
await insertInputText(page, '123456');
await page.waitForTimeout(1000);
await assertResultList(page, [
'this is a new page to search for content',
'123456',
]);
await page.locator('[cmdk-item] [data-testid=cmdk-label]').first().click();
await waitForScrollToFinish(page);
const isVisitable = await checkElementIsInView(
page,
page.locator('[data-affine-editor-container]').getByText('123456')
);
expect(isVisitable).toBe(true);
const selectionElement = page.locator(
'affine-scroll-anchoring-widget div.highlight'
);
await expect(selectionElement).toBeVisible();
});
test('Create a new page with special characters in the title and search for this page', async ({
page,
}) => {
const specialTitle = '"test123456"';
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill(specialTitle);
await page.keyboard.press('Enter');
await expect(getBlockSuiteEditorTitle(page)).toContainText(specialTitle);
await openQuickSearchByShortcut(page);
await insertInputText(page, specialTitle);
await expect
.poll(async () => {
const labels = await page
.locator('[cmdk-item] [data-testid=cmdk-label]')
.allInnerTexts();
return labels.some(label => label.split('\n').includes(specialTitle));
})
.toBe(true);
await page.keyboard.press('Enter');
await page.waitForTimeout(1000);
await assertTitle(page, specialTitle);
});
test('disable quick search when the link-popup is visitable', async ({
page,
}) => {
const specialTitle = '"test"';
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await openQuickSearchByShortcut(page);
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
await withCtrlOrMeta(page, () => page.keyboard.press('k'));
await expect(quickSearch).toBeHidden();
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill(specialTitle);
await page.keyboard.press('Enter', { delay: 10 });
await page.keyboard.insertText('1234567890');
await page.getByText('1234567890').dblclick();
await withCtrlOrMeta(page, () => page.keyboard.press('k'));
const linkPopup = page.locator('.affine-link-popover');
await expect(linkPopup).toBeVisible();
const currentQuickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(currentQuickSearch).not.toBeVisible();
});
test('can use @ to open quick search to search for doc and insert into canvas', async ({
page,
}) => {
await openHomePage(page);
await waitForEditorLoad(page);
const url = page.url();
const docTitle = await page.getByTestId('title-edit-button').innerText();
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
await page.locator('affine-edgeless-root').press('@');
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
// search by using url
await insertInputText(page, url);
// expect the default page to be selected
await expect(
page.locator('[cmdk-group-items] [cmdk-item][data-selected="true"]')
).toContainText(docTitle);
// press enter to insert the page to canvas
await page.keyboard.press('Enter');
await expect(
page.locator('affine-embed-edgeless-synced-doc-block')
).toBeVisible();
await expect(
page.getByTestId('edgeless-embed-synced-doc-title')
).toContainText(docTitle);
// focus on the note block
await page.waitForTimeout(500);
await page
.locator('affine-embed-edgeless-synced-doc-block')
.click({ force: true });
await page.waitForTimeout(500);
// double clock to show peek view
await page
.locator('affine-embed-edgeless-synced-doc-block')
.dblclick({ force: true });
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
});
test('can paste a doc link to create link reference', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const url = page.url();
await clickNewPageButton(page);
// goto main content
await page.keyboard.press('Enter');
// paste the url
await writeTextToClipboard(page, url);
await expect(
page.locator('affine-reference:has-text("Getting Started")')
).toBeVisible();
// can ctrl-z to revert to normal link
await page.keyboard.press('ControlOrMeta+z');
await expect(page.locator(`affine-link:has-text("${url}")`)).toBeVisible();
});
test('can use slash menu to insert an external link', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
// goto main content
await page.keyboard.press('Enter');
// open slash menu
await page.keyboard.type('/link', {
delay: 50,
});
await page.keyboard.press('Enter');
await expect(page.getByTestId('cmdk-quick-search')).toBeVisible();
const link = 'affine.pro';
await page.locator('[cmdk-input]').fill(link);
const insertLinkBtn = page.locator(
'[cmdk-item] [data-value="external-link:affine.pro"]'
);
await expect(insertLinkBtn).toBeVisible();
await insertLinkBtn.click();
await expect(page.locator('affine-bookmark')).toBeVisible();
await expect(page.locator('.affine-bookmark-content-url')).toContainText(
link
);
});
test('Paste content with keyboard', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, 'Test');
// goto main content
await page.keyboard.press('Enter');
// input hello world to editor
await page.keyboard.type('hello world', {
delay: 50,
});
await selectAllByKeyboard(page);
await copyByKeyboard(page);
const quickSearchButton = page.locator(
'[data-testid=slider-bar-quick-search-button]'
);
await quickSearchButton.click();
const quickSearch = page.locator('[data-testid=cmdk-quick-search]');
await expect(quickSearch).toBeVisible();
await pasteByKeyboard(page);
await expect(page.locator('[cmdk-input]')).toHaveValue('hello world');
});

View File

@@ -0,0 +1,27 @@
import { test } from '@affine-test/kit/playwright';
import { coreUrl, openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('goto not found page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const currentUrl = new URL(page.url());
const invalidUrl = (currentUrl.pathname =
currentUrl.pathname.concat('invalid'));
await page.goto(invalidUrl.toString());
await expect(page.getByTestId('not-found')).toBeVisible({
timeout: 10000,
});
});
test('goto not found workspace', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
// if doesn't wait for timeout, data won't be saved into indexedDB
await page.waitForTimeout(1000);
await page.goto(new URL('/workspace/invalid/all', coreUrl).toString());
await expect(page.getByTestId('not-found')).toBeVisible({
timeout: 10000,
});
});

View File

@@ -0,0 +1,105 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import {
confirmExperimentalPrompt,
openAboutPanel,
openAppearancePanel,
openEditorSetting,
openExperimentalFeaturesPanel,
openSettingModal,
openShortcutsPanel,
} from '@affine-test/kit/utils/setting';
import { expect } from '@playwright/test';
test('Open settings modal', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
const modal = page.getByTestId('setting-modal');
await expect(modal).toBeVisible();
});
test('change language using keyboard', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
const locator = page.getByTestId('language-menu-button');
const oldName = await locator.textContent();
await locator.click();
await page.waitForTimeout(200);
await page.keyboard.press('ArrowDown', {
delay: 50,
});
// incase the current language is the top one
await page.keyboard.press('ArrowDown', {
delay: 50,
});
await page.keyboard.press('Enter', {
delay: 50,
});
{
const newName = await locator.textContent();
expect(oldName).not.toBe(newName);
}
});
test('Change theme', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
await openAppearancePanel(page);
const root = page.locator('html');
await page.getByTestId('light-theme-trigger').click();
const lightMode = await root.evaluate(element => element.dataset.theme);
expect(lightMode).toBe('light');
await page.getByTestId('dark-theme-trigger').click();
const darkMode = await root.evaluate(element => element.dataset.theme);
expect(darkMode).toBe('dark');
});
test('Change layout width', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openEditorSetting(page);
await page.getByTestId('full-width-trigger').click();
const editorWrapper = page.locator('.editor-wrapper');
const className = await editorWrapper.getAttribute('class');
expect(className).toContain('full-screen');
});
test('Open shortcuts panel', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
await openShortcutsPanel(page);
const title = page.getByTestId('keyboard-shortcuts-title');
await expect(title).toBeVisible();
});
test('Open about panel', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
await openAboutPanel(page);
const title = page.getByTestId('about-title');
await expect(title).toBeVisible();
});
test('Open experimental features panel', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await openSettingModal(page);
await openExperimentalFeaturesPanel(page);
const prompt = page.getByTestId('experimental-prompt');
await expect(prompt).toBeVisible();
await confirmExperimentalPrompt(page);
const settings = page.getByTestId('experimental-settings');
await expect(settings).toBeVisible();
});

View File

@@ -0,0 +1,278 @@
import { test } from '@affine-test/kit/playwright';
import {
confirmCreateJournal,
openHomePage,
} from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect, type Locator, type Page } from '@playwright/test';
function getTemplateRow(page: Page) {
return page.locator(
'[data-testid="doc-property-row"][data-info-id="template"]'
);
}
async function toggleTemplate(row: Locator, value: boolean) {
const checkbox = row.locator('input[type="checkbox"]');
const state = await checkbox.inputValue();
const checked = state === 'on';
if (checked !== value) {
await checkbox.click();
const newState = await checkbox.inputValue();
const newChecked = newState === 'on';
expect(newChecked).toBe(value);
}
}
const createDocAndMarkAsTemplate = async (
page: Page,
title?: string,
onCreated?: () => Promise<void>
) => {
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
if (title) {
await page.keyboard.type(title);
}
const collapse = page.getByTestId('page-info-collapse');
const open = await collapse.getAttribute('aria-expanded');
if (open?.toLowerCase() !== 'true') {
await collapse.click();
}
// add if not exists
if ((await getTemplateRow(page).count()) === 0) {
const addPropertyButton = page.getByTestId('add-property-button');
if (!(await addPropertyButton.isVisible())) {
await page.getByTestId('property-collapsible-button').click();
}
await addPropertyButton.click();
await page
.locator('[role="menuitem"][data-property-type="journal"]')
.click();
await page.keyboard.press('Escape');
}
// expand if collapsed
else if (!(await getTemplateRow(page).isVisible())) {
await page.getByTestId('property-collapsible-button').click();
}
const templateRow = getTemplateRow(page);
await expect(templateRow).toBeVisible();
await toggleTemplate(templateRow, true);
// focus editor
await page.locator('affine-note').first().click();
await onCreated?.();
};
const getDocId = async (page: Page) => {
const docId = await page.evaluate(() => {
const url = window.location.href;
const id = url.split('/').pop()?.split('?')[0];
return id;
});
return docId;
};
const enableAskMeEveryTime = async (page: Page) => {
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('editor-panel-trigger').click();
await page.getByTestId('new-doc-default-mode-trigger').click();
await page.getByTestId('ask-every-time-trigger').click();
await page.keyboard.press('Escape');
};
test('create a doc and mark it as template', async ({ page }) => {
await openHomePage(page);
await createDocAndMarkAsTemplate(page, 'Test Template', async () => {
await page.keyboard.type('# Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a template doc');
});
});
test('create a doc, and initialize it from template', async ({ page }) => {
await openHomePage(page);
await createDocAndMarkAsTemplate(page, 'Test Template', async () => {
await page.keyboard.type('# Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a template doc');
});
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await page.getByTestId('template-docs-badge').click();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.getByText('This is a template doc')).toBeVisible();
// the starter bar should be hidden
await expect(page.getByTestId('template-docs-badge')).not.toBeVisible();
});
test('enable ask me every time for new doc', async ({ page }) => {
const withAskButton = page.getByTestId('sidebar-new-page-with-ask-button');
const withoutAskButton = page.getByTestId('sidebar-new-page-button');
await openHomePage(page);
await expect(withAskButton).not.toBeVisible();
await expect(withoutAskButton).toBeVisible();
await enableAskMeEveryTime(page);
await expect(withAskButton).toBeVisible();
await expect(withoutAskButton).not.toBeVisible();
});
test('set default template for page', async ({ page }) => {
await openHomePage(page);
await createDocAndMarkAsTemplate(page, 'Test Template', async () => {
await page.keyboard.type('# Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a template doc');
});
const templateDocId = await getDocId(page);
// create a new page, should not inherit template
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await expect(page.getByText('This is a template doc')).not.toBeVisible();
// enable page template and set a template
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:preference').click();
const pageTemplateSwitch = page.getByTestId('page-template-switch');
await pageTemplateSwitch.click();
const pageTemplateSelector = page.getByTestId('page-template-selector');
await expect(pageTemplateSelector).toBeVisible();
await pageTemplateSelector.click();
await page.getByTestId(`template-doc-item-${templateDocId}`).click();
// close setting, create a new page from sidebar
await page.getByTestId('modal-close-button').click();
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await expect(page.getByText('This is a template doc')).toBeVisible();
// remove template
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:preference').click();
await page.getByTestId('page-template-selector').click();
await page.getByTestId('template-doc-item-remove').click();
// close setting, create a new page from sidebar
await page.getByTestId('modal-close-button').click();
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await expect(page.getByText('This is a template doc')).not.toBeVisible();
});
test('set default template for journal', async ({ page }) => {
await openHomePage(page);
await createDocAndMarkAsTemplate(page, 'Page Template', async () => {
await page.keyboard.type('# Page Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a page template doc');
});
const pageTemplateDocId = await getDocId(page);
await createDocAndMarkAsTemplate(page, 'Journal Template', async () => {
await page.keyboard.type('# Journal Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a journal template doc');
});
const journalTemplateDocId = await getDocId(page);
// by default create a journal, should not use template
await page.getByTestId('slider-bar-journals-button').click();
await confirmCreateJournal(page);
await waitForEditorLoad(page);
await expect(
page.getByText('This is a journal template doc')
).not.toBeVisible();
await expect(page.getByText('This is a page template doc')).not.toBeVisible();
// enable page template, new journal should use page template
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:preference').click();
await page.getByTestId('page-template-switch').click();
await page.getByTestId('page-template-selector').click();
await page.getByTestId(`template-doc-item-${pageTemplateDocId}`).click();
await page.getByTestId('modal-close-button').click();
// create a new journal
const prevWeekButton = page.getByTestId('week-picker-prev');
await prevWeekButton.click();
await page.getByTestId('week-picker-day').first().click();
await confirmCreateJournal(page);
await waitForEditorLoad(page);
await expect(page.getByText('This is a page template doc')).toBeVisible();
// set journal template, new journal should use journal template
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:preference').click();
await page.getByTestId('journal-template-selector').click();
await page.getByTestId(`template-doc-item-${journalTemplateDocId}`).click();
await page.getByTestId('modal-close-button').click();
// create a new journal
await prevWeekButton.click();
await page.getByTestId('week-picker-day').first().click();
await confirmCreateJournal(page);
await waitForEditorLoad(page);
await expect(page.getByText('This is a journal template doc')).toBeVisible();
});
test('open template doc from sidebar template entrance', async ({ page }) => {
await openHomePage(page);
await createDocAndMarkAsTemplate(page, 'Test Template', async () => {
await page.keyboard.type('# Template');
await page.keyboard.press('Enter');
await page.keyboard.type('This is a template doc');
});
const templateDocId = await getDocId(page);
await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await expect(page.getByText('This is a template doc')).not.toBeVisible();
await page.getByTestId('sidebar-template-doc-entrance').click();
await page.getByTestId(`template-doc-item-${templateDocId}`).click();
await waitForEditorLoad(page);
await expect(page.getByText('This is a template doc')).toBeVisible();
});
test('create template doc from sidebar template entrance', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page.getByTestId('sidebar-template-doc-entrance').click();
await page.getByTestId('template-doc-item-create').click();
await page.locator('affine-note').first().click();
await page.keyboard.press('Backspace');
await page.keyboard.type('Template');
const templateDocId = await getDocId(page);
await page.getByTestId('sidebar-template-doc-entrance').click();
await expect(
page.getByTestId(`template-doc-item-${templateDocId}`)
).toBeVisible();
});
test('should show starter-bar when doc is empty', async ({ page }) => {
await openHomePage(page);
await page.getByTestId('sidebar-new-page-button').click();
await page.keyboard.press('ArrowDown');
const starterBar = page.getByTestId('starter-bar');
await expect(starterBar).toBeVisible();
await page.keyboard.type('1', { delay: 100 });
await expect(starterBar).not.toBeVisible();
await page.keyboard.type('1', { delay: 100 });
await expect(starterBar).not.toBeVisible();
await page.keyboard.press('Backspace');
await expect(starterBar).not.toBeVisible();
await page.keyboard.press('Backspace');
await expect(starterBar).toBeVisible();
await page.keyboard.type('1', { delay: 100 });
await expect(starterBar).not.toBeVisible();
});

View File

@@ -0,0 +1,31 @@
import { resolve } from 'node:path';
import { test, testResultDir } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.use({
colorScheme: 'light',
});
// default could be anything, according to the system
test('default white', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const root = page.locator('html');
const themeMode = await root.evaluate(element => element.dataset.theme);
expect(themeMode).toBe('light');
await page.screenshot({
path: resolve(testResultDir, 'affine-light-theme.png'),
});
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('appearance-panel-trigger').click();
await page.waitForTimeout(50);
await page.getByTestId('dark-theme-trigger').click();
const darkMode = await root.evaluate(element => element.dataset.theme);
expect(darkMode).toBe('dark');
await page.screenshot({
path: resolve(testResultDir, 'affine-dark-theme.png'),
});
});

View File

@@ -0,0 +1,28 @@
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import test, { expect } from '@playwright/test';
test('should be able to undo on empty page', async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).isVisible();
await waitForEmptyEditor(page);
await clickEdgelessModeButton(page);
await page.keyboard.press('ControlOrMeta+Z');
// test editor still work
await page.locator('.edgeless-note-page-content').click({ force: true });
await page.locator('.edgeless-note-page-content').click({ force: true });
await page.waitForTimeout(100);
await page
.locator('.edgeless-note-page-content')
.pressSequentially('test text');
await expect(page.locator('.edgeless-note-page-content')).toContainText(
'test text'
);
});

View File

@@ -0,0 +1,13 @@
{
"name": "@affine-test/affine-local",
"private": true,
"type": "module",
"scripts": {
"e2e": "yarn playwright test"
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@playwright/test": "=1.58.2"
},
"version": "0.27.5"
}

View File

@@ -0,0 +1,62 @@
import { testResultDir } from '@affine-test/kit/playwright';
import type {
PlaywrightTestConfig,
PlaywrightWorkerOptions,
} from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './e2e',
fullyParallel: true,
timeout: process.env.CI ? 50_000 : 30_000,
outputDir: testResultDir,
use: {
baseURL: 'http://localhost:8080/',
browserName:
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
'chromium',
permissions: ['clipboard-read', 'clipboard-write'],
viewport: { width: 1440, height: 800 },
actionTimeout: 5 * 1000,
locale: 'en-US',
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
// You can open traces locally(`npx playwright show-trace trace.zip`)
// or in your browser on [Playwright Trace Viewer](https://trace.playwright.dev/).
trace: 'retain-on-failure',
// Record video only when retrying a test for the first time.
video: 'retain-on-failure',
},
forbidOnly: !!process.env.CI,
workers: 4,
retries: 1,
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
// default 'list' when running locally
// See https://playwright.dev/docs/test-reporters#github-actions-annotations
reporter: process.env.CI ? 'github' : 'list',
webServer: [
{
command: 'yarn run -T affine dev -p @affine/web',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
env: {
COVERAGE: process.env.COVERAGE || 'false',
},
url: 'http://localhost:8080',
},
],
};
if (process.env.CI) {
config.retries = 3;
config.workers = '50%';
}
export default config;

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.web.json",
"compilerOptions": {
"rootDir": "./e2e",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./e2e"],
"references": [{ "path": "../kit" }]
}