mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(electron): app tabs dnd (#7684)
<div class='graphite__hidden'>
<div>🎥 Video uploaded on Graphite:</div>
<a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/cd84e155-9f2e-4d12-a933-8673eb6bc6cb.mp4">
<img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/cd84e155-9f2e-4d12-a933-8673eb6bc6cb.mp4">
</a>
</div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/cd84e155-9f2e-4d12-a933-8673eb6bc6cb.mp4">Kapture 2024-07-31 at 19.39.30.mp4</video>
fix AF-1149
fix PD-1513
fix PD-1515
This commit is contained in:
@@ -2,18 +2,65 @@ import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
createLinkedPage,
|
||||
dragTo,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
async function expectActiveTab(page: Page, index: number, activeViewIndex = 0) {
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('workbench-tab')
|
||||
.nth(index)
|
||||
.getByTestId('split-view-label')
|
||||
.nth(activeViewIndex)
|
||||
).toHaveAttribute('data-active', 'true');
|
||||
}
|
||||
|
||||
async function expectTabTitle(
|
||||
page: Page,
|
||||
index: number,
|
||||
title: string | string[]
|
||||
) {
|
||||
if (typeof title === 'string') {
|
||||
await expect(page.getByTestId('workbench-tab').nth(index)).toContainText(
|
||||
title
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < title.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('workbench-tab')
|
||||
.nth(index)
|
||||
.getByTestId('split-view-label')
|
||||
.nth(i)
|
||||
).toContainText(title[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function expectTabCount(page: Page, count: number) {
|
||||
await expect(page.getByTestId('workbench-tab')).toHaveCount(count);
|
||||
}
|
||||
|
||||
async function closeTab(page: Page, index: number) {
|
||||
await page.getByTestId('workbench-tab').nth(index).hover();
|
||||
|
||||
await page
|
||||
.getByTestId('workbench-tab')
|
||||
.nth(index)
|
||||
.getByTestId('close-tab-button')
|
||||
.click();
|
||||
}
|
||||
|
||||
test('create new tab', async ({ views }) => {
|
||||
let page = await views.getActive();
|
||||
|
||||
await page.getByTestId('add-tab-view-button').click();
|
||||
await expect(page.getByTestId('workbench-tab')).toHaveCount(2);
|
||||
await expectTabCount(page, 2);
|
||||
// new tab title should be All docs
|
||||
await expect(page.getByTestId('workbench-tab').nth(1)).toContainText(
|
||||
'All docs'
|
||||
);
|
||||
await expectTabTitle(page, 1, 'All docs');
|
||||
await expectActiveTab(page, 1);
|
||||
page = await views.getActive();
|
||||
// page content should be at all docs page
|
||||
await expect(page.getByTestId('virtualized-page-list')).toContainText(
|
||||
@@ -24,58 +71,21 @@ test('create new tab', async ({ views }) => {
|
||||
test('can switch & close tab by clicking', async ({ page }) => {
|
||||
await page.getByTestId('add-tab-view-button').click();
|
||||
|
||||
await expect(page.getByTestId('workbench-tab').nth(1)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 1);
|
||||
|
||||
// switch to the previous tab by clicking on it
|
||||
await page.getByTestId('workbench-tab').nth(0).click();
|
||||
await expect(page.getByTestId('workbench-tab').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 0);
|
||||
|
||||
// switch to the next tab by clicking on it
|
||||
await page.getByTestId('workbench-tab').nth(1).click();
|
||||
await expect(page.getByTestId('workbench-tab').nth(1)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 1);
|
||||
|
||||
// close the current tab
|
||||
await page
|
||||
.getByTestId('workbench-tab')
|
||||
.nth(1)
|
||||
.getByTestId('close-tab-button')
|
||||
.click();
|
||||
await closeTab(page, 1);
|
||||
|
||||
// the first tab should be active
|
||||
await expect(page.getByTestId('workbench-tab').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
|
||||
test('can switch tab by CTRL+number', async ({ page }) => {
|
||||
test.fixme(); // the shortcut can be only captured by the main process
|
||||
await page.keyboard.down('ControlOrMeta+T');
|
||||
await expect(page.getByTestId('workbench-tab')).toHaveCount(2);
|
||||
await expect(page.getByTestId('workbench-tab').nth(1)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
// switch to the previous tab by pressing CTRL+1
|
||||
await page.locator('body').press('ControlOrMeta+1');
|
||||
await expect(page.getByTestId('workbench-tab').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
// switch to the next tab by pressing CTRL+2
|
||||
await page.locator('body').press('ControlOrMeta+2');
|
||||
await expect(page.getByTestId('workbench-tab').nth(1)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 0);
|
||||
});
|
||||
|
||||
test('Collapse Sidebar', async ({ page }) => {
|
||||
@@ -100,17 +110,15 @@ test('Expand Sidebar', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('tab title will change when navigating', async ({ page }) => {
|
||||
await expect(page.getByTestId('workbench-tab')).toContainText(
|
||||
'Write, Draw, Plan all at Once'
|
||||
);
|
||||
await expectTabTitle(page, 0, 'Write, Draw, Plan all at Once');
|
||||
|
||||
// create new page
|
||||
await clickNewPageButton(page);
|
||||
await expect(page.getByTestId('workbench-tab')).toContainText('Untitled');
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
|
||||
// go to all page
|
||||
await page.getByTestId('all-pages').click();
|
||||
await expect(page.getByTestId('workbench-tab')).toContainText('All docs');
|
||||
await expectTabTitle(page, 0, 'All docs');
|
||||
|
||||
// go to today's journal
|
||||
await page.getByTestId('slider-bar-journals-button').click();
|
||||
@@ -120,7 +128,7 @@ test('tab title will change when navigating', async ({ page }) => {
|
||||
.textContent();
|
||||
|
||||
if (dateString) {
|
||||
await expect(page.getByTestId('workbench-tab')).toContainText(dateString);
|
||||
await expectTabTitle(page, 0, dateString);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,6 +157,23 @@ async function enableSplitView(page: Page) {
|
||||
await page.reload();
|
||||
}
|
||||
|
||||
test('open new tab via cmd+click page link', async ({ page }) => {
|
||||
await enableSplitView(page);
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
await createLinkedPage(page, 'hi from another page');
|
||||
await page
|
||||
.locator('.affine-reference-title:has-text("hi from another page")')
|
||||
.click({
|
||||
modifiers: ['ControlOrMeta'],
|
||||
});
|
||||
await expectTabCount(page, 2);
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
await expectTabTitle(page, 1, 'hi from another page');
|
||||
await expectActiveTab(page, 0);
|
||||
});
|
||||
|
||||
test('open split view', async ({ page }) => {
|
||||
await enableSplitView(page);
|
||||
await clickNewPageButton(page);
|
||||
@@ -164,27 +189,62 @@ test('open split view', async ({ page }) => {
|
||||
|
||||
// check tab title
|
||||
await expect(page.getByTestId('split-view-label')).toHaveCount(2);
|
||||
await expect(page.getByTestId('split-view-label').nth(0)).toContainText(
|
||||
'Untitled'
|
||||
);
|
||||
await expect(page.getByTestId('split-view-label').nth(1)).toContainText(
|
||||
'hi from another page'
|
||||
);
|
||||
await expectTabTitle(page, 0, ['Untitled', 'hi from another page']);
|
||||
|
||||
// the second split view should be active
|
||||
await expect(page.getByTestId('split-view-label').nth(1)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 0, 1);
|
||||
|
||||
// by clicking the first split view label, the first split view should be active
|
||||
await page.getByTestId('split-view-label').nth(0).click();
|
||||
await expect(page.getByTestId('split-view-label').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
await expectActiveTab(page, 0, 0);
|
||||
await expect(page.getByTestId('split-view-indicator').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
});
|
||||
|
||||
test('drag a page from "All pages" list to tabs header', async ({ page }) => {
|
||||
const title = 'this is a new page to drag';
|
||||
await clickNewPageButton(page, title);
|
||||
await clickSideBarAllPageButton(page);
|
||||
|
||||
await dragTo(
|
||||
page,
|
||||
page.locator(`[data-testid="page-list-item"]:has-text("${title}")`),
|
||||
page.getByTestId('add-tab-view-button')
|
||||
);
|
||||
|
||||
await expectTabCount(page, 2);
|
||||
await expectTabTitle(page, 1, title);
|
||||
await expectActiveTab(page, 1);
|
||||
});
|
||||
|
||||
test('reorder tabs', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
const titles = ['aaa', 'bbb'];
|
||||
await createLinkedPage(page, titles[0]);
|
||||
await createLinkedPage(page, titles[1]);
|
||||
await page.locator(`.affine-reference-title:has-text("${titles[0]}")`).click({
|
||||
modifiers: ['ControlOrMeta', 'Alt'],
|
||||
});
|
||||
await page.locator(`.affine-reference-title:has-text("${titles[1]}")`).click({
|
||||
modifiers: ['ControlOrMeta', 'Alt'],
|
||||
});
|
||||
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
await expectTabTitle(page, 1, titles[0]);
|
||||
await expectTabTitle(page, 2, titles[1]);
|
||||
|
||||
await dragTo(
|
||||
page,
|
||||
page.getByTestId('workbench-tab').nth(0),
|
||||
page.getByTestId('workbench-tab').nth(1),
|
||||
'right'
|
||||
);
|
||||
|
||||
await expectTabTitle(page, 0, titles[0]);
|
||||
await expectTabTitle(page, 1, 'Untitled');
|
||||
await expectTabTitle(page, 2, titles[1]);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function waitForAllPagesLoad(page: Page) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function clickNewPageButton(page: Page) {
|
||||
//FiXME: when the page is in edgeless mode, clickNewPageButton will create a new edgeless page
|
||||
export async function clickNewPageButton(page: Page, title?: string) {
|
||||
// FiXME: when the page is in edgeless mode, clickNewPageButton will create a new edgeless page
|
||||
const edgelessPage = page.locator('edgeless-editor');
|
||||
if (await edgelessPage.isVisible()) {
|
||||
await page.getByTestId('switch-page-mode-button').click({
|
||||
@@ -27,6 +27,9 @@ export async function clickNewPageButton(page: Page) {
|
||||
delay: 100,
|
||||
});
|
||||
await waitForEmptyEditor(page);
|
||||
if (title) {
|
||||
await getBlockSuiteEditorTitle(page).fill(title);
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForEmptyEditor(page: Page) {
|
||||
@@ -75,7 +78,13 @@ export const dragTo = async (
|
||||
page: Page,
|
||||
locator: Locator,
|
||||
target: Locator,
|
||||
location: 'top-left' | 'top' | 'bottom' | 'center' = 'center'
|
||||
location:
|
||||
| 'top-left'
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'center'
|
||||
| 'left'
|
||||
| 'right' = 'center'
|
||||
) => {
|
||||
await locator.hover();
|
||||
await page.mouse.down();
|
||||
@@ -85,19 +94,29 @@ export const dragTo = async (
|
||||
if (!targetElement) {
|
||||
throw new Error('target element not found');
|
||||
}
|
||||
const position =
|
||||
location === 'center'
|
||||
? {
|
||||
const position = (() => {
|
||||
switch (location) {
|
||||
case 'center':
|
||||
return {
|
||||
x: targetElement.width / 2,
|
||||
y: targetElement.height / 2,
|
||||
}
|
||||
: location === 'top-left'
|
||||
? { x: 1, y: 1 }
|
||||
: location === 'top'
|
||||
? { x: targetElement.width / 2, y: 1 }
|
||||
: location === 'bottom'
|
||||
? { x: targetElement.width / 2, y: targetElement.height - 1 }
|
||||
: { x: 1, y: 1 };
|
||||
};
|
||||
case 'top':
|
||||
return { x: targetElement.width / 2, y: 1 };
|
||||
case 'bottom':
|
||||
return { x: targetElement.width / 2, y: targetElement.height - 1 };
|
||||
|
||||
case 'left':
|
||||
return { x: 1, y: targetElement.height / 2 };
|
||||
|
||||
case 'right':
|
||||
return { x: targetElement.width - 1, y: targetElement.height / 2 };
|
||||
|
||||
case 'top-left':
|
||||
default:
|
||||
return { x: 1, y: 1 };
|
||||
}
|
||||
})();
|
||||
await target.hover({
|
||||
position: position,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user