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
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:
17
tests/affine-local/e2e/ai-land.spec.ts
Normal file
17
tests/affine-local/e2e/ai-land.spec.ts
Normal 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();
|
||||
});
|
||||
225
tests/affine-local/e2e/all-page.spec.ts
Normal file
225
tests/affine-local/e2e/all-page.spec.ts
Normal 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();
|
||||
});
|
||||
462
tests/affine-local/e2e/attachment-preview.spec.ts
Normal file
462
tests/affine-local/e2e/attachment-preview.spec.ts
Normal 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();
|
||||
});
|
||||
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal file
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal 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();
|
||||
});
|
||||
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal file
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal 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);
|
||||
});
|
||||
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal 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();
|
||||
});
|
||||
115
tests/affine-local/e2e/blocksuite/code/crud.spec.ts
Normal file
115
tests/affine-local/e2e/blocksuite/code/crud.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
192
tests/affine-local/e2e/blocksuite/code/line-numbers.spec.ts
Normal file
192
tests/affine-local/e2e/blocksuite/code/line-numbers.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
46
tests/affine-local/e2e/blocksuite/code/utils.ts
Normal file
46
tests/affine-local/e2e/blocksuite/code/utils.ts
Normal 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 };
|
||||
};
|
||||
111
tests/affine-local/e2e/blocksuite/color-picker.spec.ts
Normal file
111
tests/affine-local/e2e/blocksuite/color-picker.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
69
tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts
Normal file
69
tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
99
tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts
Normal file
99
tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
122
tests/affine-local/e2e/blocksuite/database/utils.ts
Normal file
122
tests/affine-local/e2e/blocksuite/database/utils.ts
Normal 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);
|
||||
};
|
||||
100
tests/affine-local/e2e/blocksuite/edgeless/auto-complete.spec.ts
Normal file
100
tests/affine-local/e2e/blocksuite/edgeless/auto-complete.spec.ts
Normal 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();
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
65
tests/affine-local/e2e/blocksuite/edgeless/connector.spec.ts
Normal file
65
tests/affine-local/e2e/blocksuite/edgeless/connector.spec.ts
Normal 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');
|
||||
});
|
||||
60
tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts
Normal file
60
tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts
Normal 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);
|
||||
});
|
||||
64
tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts
Normal file
64
tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts
Normal 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);
|
||||
});
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
535
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal file
535
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal 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?');
|
||||
});
|
||||
110
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal file
110
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal 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');
|
||||
});
|
||||
102
tests/affine-local/e2e/blocksuite/edgeless/text.spec.ts
Normal file
102
tests/affine-local/e2e/blocksuite/edgeless/text.spec.ts
Normal 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');
|
||||
});
|
||||
99
tests/affine-local/e2e/blocksuite/editor.spec.ts
Normal file
99
tests/affine-local/e2e/blocksuite/editor.spec.ts
Normal 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);
|
||||
});
|
||||
263
tests/affine-local/e2e/blocksuite/embed/embed-iframe.spec.ts
Normal file
263
tests/affine-local/e2e/blocksuite/embed/embed-iframe.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
170
tests/affine-local/e2e/blocksuite/embed/synced.spec.ts
Normal file
170
tests/affine-local/e2e/blocksuite/embed/synced.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
119
tests/affine-local/e2e/blocksuite/list.spec.ts
Normal file
119
tests/affine-local/e2e/blocksuite/list.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
565
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal file
565
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
279
tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts
Normal file
279
tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal file
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal 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;
|
||||
}
|
||||
460
tests/affine-local/e2e/blocksuite/paragraph.spec.ts
Normal file
460
tests/affine-local/e2e/blocksuite/paragraph.spec.ts
Normal 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);
|
||||
});
|
||||
126
tests/affine-local/e2e/blocksuite/table/collab.spec.ts
Normal file
126
tests/affine-local/e2e/blocksuite/table/collab.spec.ts
Normal 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');
|
||||
});
|
||||
200
tests/affine-local/e2e/blocksuite/table/edge-insertion.spec.ts
Normal file
200
tests/affine-local/e2e/blocksuite/table/edge-insertion.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
198
tests/affine-local/e2e/blocksuite/table/insertion.spec.ts
Normal file
198
tests/affine-local/e2e/blocksuite/table/insertion.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
458
tests/affine-local/e2e/blocksuite/toolbar.spec.ts
Normal file
458
tests/affine-local/e2e/blocksuite/toolbar.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
144
tests/affine-local/e2e/change-page-mode.spec.ts
Normal file
144
tests/affine-local/e2e/change-page-mode.spec.ts
Normal 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');
|
||||
});
|
||||
134
tests/affine-local/e2e/doc-info-modal.spec.ts
Normal file
134
tests/affine-local/e2e/doc-info-modal.spec.ts
Normal 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');
|
||||
});
|
||||
438
tests/affine-local/e2e/drag-page.spec.ts
Normal file
438
tests/affine-local/e2e/drag-page.spec.ts
Normal 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();
|
||||
});
|
||||
23
tests/affine-local/e2e/duplicate-page.spec.ts
Normal file
23
tests/affine-local/e2e/duplicate-page.spec.ts
Normal 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)');
|
||||
});
|
||||
9
tests/affine-local/e2e/exception-page.spec.ts
Normal file
9
tests/affine-local/e2e/exception-page.spec.ts
Normal 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();
|
||||
});
|
||||
741
tests/affine-local/e2e/image-preview.spec.ts
Normal file
741
tests/affine-local/e2e/image-preview.spec.ts
Normal 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);
|
||||
});
|
||||
59
tests/affine-local/e2e/import-dialog.spec.ts
Normal file
59
tests/affine-local/e2e/import-dialog.spec.ts
Normal 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();
|
||||
});
|
||||
126
tests/affine-local/e2e/journal.spec.ts
Normal file
126
tests/affine-local/e2e/journal.spec.ts
Normal 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();
|
||||
});
|
||||
83
tests/affine-local/e2e/layout.spec.ts
Normal file
83
tests/affine-local/e2e/layout.spec.ts
Normal 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();
|
||||
});
|
||||
1246
tests/affine-local/e2e/links.spec.ts
Normal file
1246
tests/affine-local/e2e/links.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
78
tests/affine-local/e2e/local-first-avatar.spec.ts
Normal file
78
tests/affine-local/e2e/local-first-avatar.spec.ts
Normal 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');
|
||||
});
|
||||
172
tests/affine-local/e2e/local-first-collections-items.spec.ts
Normal file
172
tests/affine-local/e2e/local-first-collections-items.spec.ts
Normal 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();
|
||||
});
|
||||
146
tests/affine-local/e2e/local-first-delete-page.spec.ts
Normal file
146
tests/affine-local/e2e/local-first-delete-page.spec.ts
Normal 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');
|
||||
});
|
||||
74
tests/affine-local/e2e/local-first-delete-workspace.spec.ts
Normal file
74
tests/affine-local/e2e/local-first-delete-workspace.spec.ts
Normal 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');
|
||||
});
|
||||
78
tests/affine-local/e2e/local-first-export-page.spec.ts
Normal file
78
tests/affine-local/e2e/local-first-export-page.spec.ts
Normal 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');
|
||||
});
|
||||
101
tests/affine-local/e2e/local-first-favorite-page.spec.ts
Normal file
101
tests/affine-local/e2e/local-first-favorite-page.spec.ts
Normal 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');
|
||||
});
|
||||
163
tests/affine-local/e2e/local-first-favorites-items.spec.ts
Normal file
163
tests/affine-local/e2e/local-first-favorites-items.spec.ts
Normal 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();
|
||||
});
|
||||
39
tests/affine-local/e2e/local-first-new-page.spec.ts
Normal file
39
tests/affine-local/e2e/local-first-new-page.spec.ts
Normal 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');
|
||||
});
|
||||
151
tests/affine-local/e2e/local-first-openpage-newtab.spec.ts
Normal file
151
tests/affine-local/e2e/local-first-openpage-newtab.spec.ts
Normal 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);
|
||||
});
|
||||
53
tests/affine-local/e2e/local-first-restore-page.spec.ts
Normal file
53
tests/affine-local/e2e/local-first-restore-page.spec.ts
Normal 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');
|
||||
});
|
||||
60
tests/affine-local/e2e/local-first-show-delete-modal.spec.ts
Normal file
60
tests/affine-local/e2e/local-first-show-delete-modal.spec.ts
Normal 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');
|
||||
});
|
||||
79
tests/affine-local/e2e/local-first-trash-page.spec.ts
Normal file
79
tests/affine-local/e2e/local-first-trash-page.spec.ts
Normal 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');
|
||||
});
|
||||
152
tests/affine-local/e2e/local-first-workspace-list.spec.ts
Normal file
152
tests/affine-local/e2e/local-first-workspace-list.spec.ts
Normal 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);
|
||||
});
|
||||
15
tests/affine-local/e2e/local-first-workspace.spec.ts
Normal file
15
tests/affine-local/e2e/local-first-workspace.spec.ts
Normal 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');
|
||||
});
|
||||
45
tests/affine-local/e2e/navigation.spec.ts
Normal file
45
tests/affine-local/e2e/navigation.spec.ts
Normal 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');
|
||||
});
|
||||
42
tests/affine-local/e2e/open-affine.spec.ts
Normal file
42
tests/affine-local/e2e/open-affine.spec.ts
Normal 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();
|
||||
});
|
||||
374
tests/affine-local/e2e/page-properties.spec.ts
Normal file
374
tests/affine-local/e2e/page-properties.spec.ts
Normal 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();
|
||||
});
|
||||
252
tests/affine-local/e2e/peek-view.spec.ts
Normal file
252
tests/affine-local/e2e/peek-view.spec.ts
Normal 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();
|
||||
});
|
||||
591
tests/affine-local/e2e/quick-search.spec.ts
Normal file
591
tests/affine-local/e2e/quick-search.spec.ts
Normal 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');
|
||||
});
|
||||
27
tests/affine-local/e2e/router.spec.ts
Normal file
27
tests/affine-local/e2e/router.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
105
tests/affine-local/e2e/settings.spec.ts
Normal file
105
tests/affine-local/e2e/settings.spec.ts
Normal 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();
|
||||
});
|
||||
278
tests/affine-local/e2e/template.spec.ts
Normal file
278
tests/affine-local/e2e/template.spec.ts
Normal 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();
|
||||
});
|
||||
31
tests/affine-local/e2e/theme.spec.ts
Normal file
31
tests/affine-local/e2e/theme.spec.ts
Normal 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'),
|
||||
});
|
||||
});
|
||||
28
tests/affine-local/e2e/undo-empty.spec.ts
Normal file
28
tests/affine-local/e2e/undo-empty.spec.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user