305 lines
15 KiB
JavaScript
305 lines
15 KiB
JavaScript
import { test, expect } from '@playwright/test';
|
|
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
|
|
|
test.describe('G: Files Plugin', () => {
|
|
let consoleCollector;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
consoleCollector = setupConsoleCollector(page);
|
|
await resetMockState(page);
|
|
await page.goto('/');
|
|
await waitForAppReady(page);
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
consoleCollector.assertNoErrors();
|
|
});
|
|
|
|
test('files plugin appears in Plugin Manager as loaded', async ({ page }) => {
|
|
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
|
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.files' });
|
|
await expect(card).toBeVisible({ timeout: 10000 });
|
|
await expect(card.locator('.status-badge')).toHaveText('loaded');
|
|
});
|
|
|
|
test('files plugin does not add global sidebar item', async ({ page }) => {
|
|
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Files' });
|
|
await expect(sidebarItem).toHaveCount(0);
|
|
});
|
|
|
|
test('workspace Files view is scoped to selected workspace folder', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
|
|
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-item-name').filter({ hasText: 'project-only.txt' })).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-item-name').filter({ hasText: 'test-only.txt' })).toHaveCount(0);
|
|
|
|
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
|
|
|
await expect(page.locator('.files-item-name').filter({ hasText: 'test-only.txt' })).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-item-name').filter({ hasText: 'project-only.txt' })).toHaveCount(0);
|
|
});
|
|
|
|
test('files explorer supports create navigate rename filter sort open and trash', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
await page.locator('[data-files-action="new-folder"]').click();
|
|
await page.locator('[data-files-create-input]').fill('Daily');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
await expect(page.locator('[data-file-name="Daily"]')).toBeVisible();
|
|
|
|
await page.locator('[data-file-name="Daily"]').dblclick();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Daily');
|
|
|
|
await page.locator('[data-files-action="new-markdown"]').click();
|
|
await page.locator('[data-files-create-input]').fill('Log.md');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
await expect(page.locator('[data-file-name="Log.md"]')).toBeVisible();
|
|
|
|
await page.locator('[data-file-name="Log.md"]').click();
|
|
await page.locator('[data-files-action="rename"]').click();
|
|
await page.locator('[data-files-rename-input]').fill('Journal.md');
|
|
await page.locator('[data-files-rename-confirm]').click();
|
|
await expect(page.locator('[data-file-name="Journal.md"]')).toBeVisible();
|
|
await expect(page.locator('[data-file-name="Log.md"]')).toHaveCount(0);
|
|
|
|
await page.locator('[data-files-filter]').fill('journ');
|
|
await expect(page.locator('[data-file-name="Journal.md"]')).toBeVisible();
|
|
await expect(page.locator('[data-file-name="project-only.txt"]')).toHaveCount(0);
|
|
await page.locator('[data-files-filter]').fill('');
|
|
|
|
await page.locator('[data-files-sort]').selectOption('modified-desc');
|
|
await expect(page.locator('[data-file-name="Journal.md"]')).toBeVisible();
|
|
|
|
await page.locator('[data-file-name="Journal.md"]').dblclick();
|
|
await expect(page.locator('[data-editor-mode="generic-markdown"]')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('[data-resource-path="Project/Daily/Journal.md"]')).toBeVisible();
|
|
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Daily', { timeout: 10000 });
|
|
await expect(page.locator('[data-file-name="Journal.md"]')).toBeVisible({ timeout: 10000 });
|
|
await page.locator('[data-file-name="Journal.md"]').click();
|
|
page.once('dialog', (dialog) => dialog.accept());
|
|
await page.locator('[data-files-action="trash"]').click();
|
|
await expect(page.locator('[data-file-name="Journal.md"]')).toHaveCount(0);
|
|
|
|
await page.locator('[data-files-action="up"]').click();
|
|
await expect(page.locator('.files-breadcrumb')).not.toContainText('Daily');
|
|
});
|
|
|
|
test('files explorer uses icon controls and no row New Here action', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
for (const action of ['back', 'forward', 'up', 'refresh', 'new-folder', 'new-markdown', 'new-text', 'open', 'rename', 'trash', 'cut', 'copy', 'paste']) {
|
|
const button = page.locator(`[data-files-action="${action}"]`);
|
|
await expect(button).toHaveAttribute('title', /.+/);
|
|
await expect(button.locator('svg')).toBeVisible();
|
|
await expect(button).not.toHaveText(/\S/);
|
|
}
|
|
|
|
await expect(page.locator('.files-row-btn').filter({ hasText: 'New here' })).toHaveCount(0);
|
|
const firstRowButton = page.locator('[data-file-name="Notes"] .files-row-btn').first();
|
|
await expect(firstRowButton).toBeVisible();
|
|
await expect(firstRowButton).not.toHaveText(/\S/);
|
|
expect(await firstRowButton.evaluate((node) => node.innerHTML)).toContain('<svg');
|
|
});
|
|
|
|
test('files explorer supports empty-space context paste after cutting a folder', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
await page.locator('[data-files-action="new-folder"]').click();
|
|
await page.locator('[data-files-create-input]').fill('CutMe');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
await page.locator('[data-files-action="new-folder"]').click();
|
|
await page.locator('[data-files-create-input]').fill('Target');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
|
|
await page.locator('[data-file-name="CutMe"]').click({ button: 'right' });
|
|
await page.locator('[data-files-menu-action="cut"]').click();
|
|
|
|
await page.locator('[data-file-name="Target"]').dblclick();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Target');
|
|
await page.locator('[data-files-list]').click({ button: 'right', position: { x: 24, y: 110 } });
|
|
await page.locator('[data-files-menu-action="paste"]').click();
|
|
|
|
await expect(page.locator('[data-file-name="CutMe"]')).toBeVisible();
|
|
await page.locator('[data-files-action="up"]').click();
|
|
await expect(page.locator('[data-file-name="CutMe"]')).toHaveCount(0);
|
|
});
|
|
|
|
test('files explorer supports multiselect and internal drag/drop move', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
await page.locator('[data-files-action="new-folder"]').click();
|
|
await page.locator('[data-files-create-input]').fill('DropTarget');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
await page.locator('[data-files-action="new-markdown"]').click();
|
|
await page.locator('[data-files-create-input]').fill('DragOne.md');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
await page.locator('[data-files-action="new-text"]').click();
|
|
await page.locator('[data-files-create-input]').fill('DragTwo.txt');
|
|
await page.locator('[data-files-create-confirm]').click();
|
|
|
|
await page.locator('[data-file-name="DragOne.md"]').click();
|
|
await page.locator('[data-file-name="DragTwo.txt"]').click({ modifiers: [process.platform === 'darwin' ? 'Meta' : 'Control'] });
|
|
await expect(page.locator('.files-item.selected')).toHaveCount(2);
|
|
|
|
await page.evaluate(() => {
|
|
const source = document.querySelector('[data-file-name="DragOne.md"]');
|
|
const target = document.querySelector('[data-file-name="DropTarget"]');
|
|
const dt = new DataTransfer();
|
|
source.dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: dt }));
|
|
target.dispatchEvent(new DragEvent('dragover', { bubbles: true, dataTransfer: dt }));
|
|
target.dispatchEvent(new DragEvent('drop', { bubbles: true, dataTransfer: dt }));
|
|
});
|
|
|
|
await expect(page.locator('[data-file-name="DragOne.md"]')).toHaveCount(0);
|
|
await expect(page.locator('[data-file-name="DragTwo.txt"]')).toHaveCount(0);
|
|
await page.locator('[data-file-name="DropTarget"]').dblclick();
|
|
await expect(page.locator('[data-file-name="DragOne.md"]')).toBeVisible();
|
|
await expect(page.locator('[data-file-name="DragTwo.txt"]')).toBeVisible();
|
|
});
|
|
|
|
test('files history persists in workspace context and handles mouse back forward buttons', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
await page.locator('[data-file-name="Notes"]').dblclick();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
|
|
await page.locator('.files-root').focus();
|
|
await page.keyboard.press('Alt+ArrowLeft');
|
|
await expect(page.locator('.files-breadcrumb')).not.toContainText('Notes');
|
|
|
|
await page.keyboard.press('Alt+ArrowRight');
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
|
|
await page.dispatchEvent('.files-root', 'mouseup', { button: 8, buttons: 128, bubbles: true, cancelable: true });
|
|
await expect(page.locator('.files-breadcrumb')).not.toContainText('Notes');
|
|
|
|
await page.dispatchEvent('.files-root', 'mouseup', { button: 9, buttons: 256, bubbles: true, cancelable: true });
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
|
|
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
});
|
|
|
|
test('workbench close and mouse back return from editor to the previous Files folder', async ({ page }) => {
|
|
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
|
|
|
await page.locator('[data-file-name="Notes"]').dblclick();
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
|
|
await page.locator('[data-file-name="Overview.md"]').dblclick();
|
|
await expect(page.locator('[data-editor-mode="notes-markdown"]')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.dispatchEvent('body', 'mousedown', { button: 3, bubbles: true, cancelable: true });
|
|
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
await expect(page.locator('[data-file-name="Overview.md"]')).toBeVisible();
|
|
|
|
await page.locator('[data-file-name="Overview.md"]').dblclick();
|
|
await expect(page.locator('[data-editor-mode="notes-markdown"]')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.waitForTimeout(150);
|
|
await page.evaluate(() => {
|
|
document.body.dispatchEvent(new PointerEvent('pointerdown', {
|
|
button: 3,
|
|
buttons: 8,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
pointerType: 'mouse'
|
|
}));
|
|
});
|
|
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
|
|
await page.locator('[data-file-name="Overview.md"]').dblclick();
|
|
await expect(page.locator('[data-editor-mode="notes-markdown"]')).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.locator('.workbench-header .close-btn[aria-label="Close"]').click();
|
|
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.files-breadcrumb')).toContainText('Notes');
|
|
});
|
|
|
|
test('open .txt via workbench from files context shows default-editor', async ({ page }) => {
|
|
await page.evaluate(async () => {
|
|
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
|
kind: 'vault-file',
|
|
path: 'Docs/todo.txt',
|
|
extension: '.txt',
|
|
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
|
});
|
|
if (err) throw new Error(err);
|
|
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
});
|
|
|
|
const editor = page.locator('[data-editor-mode="text"]');
|
|
await expect(editor).toBeVisible({ timeout: 10000 });
|
|
await expect(editor).toHaveAttribute('data-resource-path', 'Docs/todo.txt');
|
|
|
|
const textarea = page.locator('.de-textarea');
|
|
await expect(textarea).toBeVisible({ timeout: 10000 });
|
|
const textareaBox = await textarea.boundingBox();
|
|
expect(textareaBox.height).toBeGreaterThan(300);
|
|
});
|
|
|
|
test('open .md via workbench from files context shows generic-markdown', async ({ page }) => {
|
|
await page.evaluate(async () => {
|
|
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
|
kind: 'vault-file',
|
|
path: 'Docs/readme.md',
|
|
extension: '.md',
|
|
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
|
});
|
|
if (err) throw new Error(err);
|
|
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
});
|
|
|
|
const workbench = page.locator('.workbench-host');
|
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
|
await expect(workbench.locator('.workbench-title')).toHaveText('Docs/readme.md');
|
|
|
|
const preview = page.locator('.de-preview');
|
|
await expect(preview).toBeVisible({ timeout: 10000 });
|
|
const previewBox = await preview.boundingBox();
|
|
expect(previewBox.height).toBeGreaterThan(300);
|
|
});
|
|
|
|
test('open notes markdown via workbench from files context shows notes-markdown', async ({ page }) => {
|
|
await page.evaluate(async () => {
|
|
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
|
kind: 'vault-file',
|
|
path: 'Notes/Overview.md',
|
|
extension: '.md',
|
|
context: { sourcePluginId: 'verstak.files', sourceView: 'files', isInsideNotesFolder: true, notesMode: true },
|
|
});
|
|
if (err) throw new Error(err);
|
|
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
});
|
|
|
|
const workbench = page.locator('.workbench-host');
|
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
|
await expect(workbench.locator('.workbench-title')).toHaveText('Notes/Overview.md');
|
|
});
|
|
|
|
test('files plugin card shows openProviders in contributions', async ({ page }) => {
|
|
await page.evaluate(async () => {
|
|
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.files', {
|
|
kind: 'vault-file', path: 'Docs/todo.txt', extension: '.txt',
|
|
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
|
});
|
|
if (err) throw new Error(err);
|
|
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
});
|
|
const editor = page.locator('[data-editor-mode="text"]');
|
|
await expect(editor).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|