core: Milestone 7b — Files explorer and Default Editor improvements
- Files plugin: richer explorer with breadcrumbs, selection, toolbar actions, rename/trash, filter, sorting, hidden/reserved entries filtered - Default Editor: line numbers, Ctrl+S, markdown toolbar, Edit/Preview/Split, markdown preview, Reload/Revert - E2E tests: 39 passed for files + editor - Workspace model: correction, naming alignment, compatibility wrappers - Updated docs: NOTES_FILES_PLUGIN_PLAN.md, PLUGIN_RUNTIME.md
This commit is contained in:
@@ -36,6 +36,67 @@ test.describe('F: Default Editor Plugin', () => {
|
||||
await expect(textarea).toHaveValue('Buy groceries\nWrite tests');
|
||||
});
|
||||
|
||||
test('editor supports markdown toolbar split save reopen and revert', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const err = await window.go.api.App.WriteVaultTextFile(
|
||||
'verstak.platform-test',
|
||||
'Project/Notes/editing.md',
|
||||
'# Editing\n\nplain text',
|
||||
{ createIfMissing: true, overwrite: true }
|
||||
);
|
||||
if (err) throw new Error(err);
|
||||
const [result, openErr] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Project/Notes/editing.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'files', isInsideNotesFolder: true, notesMode: true },
|
||||
});
|
||||
if (openErr) throw new Error(openErr);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
const editor = page.locator('[data-editor-mode="notes-markdown"]');
|
||||
await expect(editor).toBeVisible({ timeout: 10000 });
|
||||
await expect(editor.locator('[data-notes-badge]')).toBeVisible();
|
||||
|
||||
await editor.locator('[data-editor-mode-button="edit"]').click();
|
||||
const textarea = editor.locator('[data-editor-textarea]');
|
||||
await expect(textarea).toBeVisible();
|
||||
await textarea.fill('plain text');
|
||||
await textarea.selectText();
|
||||
await editor.locator('[data-md-action="bold"]').click();
|
||||
await expect(textarea).toHaveValue('**plain text**');
|
||||
|
||||
await editor.locator('[data-md-action="heading"]').click();
|
||||
await expect(textarea).toHaveValue('# **plain text**');
|
||||
await expect(editor.locator('[data-save-state]')).toContainText('Modified');
|
||||
|
||||
await editor.locator('[data-editor-mode-button="split"]').click();
|
||||
await expect(editor.locator('[data-editor-textarea]')).toBeVisible();
|
||||
await expect(editor.locator('[data-preview]')).toBeVisible();
|
||||
await expect(editor.locator('[data-preview]')).toContainText('plain text');
|
||||
|
||||
await textarea.press(process.platform === 'darwin' ? 'Meta+S' : 'Control+S');
|
||||
await expect(editor.locator('[data-save-state]')).toContainText('Saved');
|
||||
|
||||
await textarea.fill('discard me');
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await editor.locator('[data-editor-action="reload"]').click();
|
||||
await expect(textarea).toHaveValue('# **plain text**');
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const [result, openErr] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Project/Notes/editing.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'files', isInsideNotesFolder: true, notesMode: true },
|
||||
});
|
||||
if (openErr) throw new Error(openErr);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
await expect(page.locator('[data-editor-mode="notes-markdown"] [data-preview]')).toContainText('plain text', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('open .md file outside Notes routes to highest-priority provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
|
||||
@@ -28,16 +28,65 @@ test.describe('G: Files Plugin', () => {
|
||||
});
|
||||
|
||||
test('workspace Files view is scoped to selected workspace folder', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Alpha Case' }).click();
|
||||
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: 'alpha-only.txt' })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.files-item-name').filter({ hasText: 'beta-only.txt' })).toHaveCount(0);
|
||||
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: 'Beta Case' }).click();
|
||||
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
||||
|
||||
await expect(page.locator('.files-item-name').filter({ hasText: 'beta-only.txt' })).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.files-item-name').filter({ hasText: 'alpha-only.txt' })).toHaveCount(0);
|
||||
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('[data-file-name="Daily"]')).toBeVisible({ timeout: 10000 });
|
||||
await page.locator('[data-file-name="Daily"]').dblclick();
|
||||
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('open .txt via workbench from files context shows default-editor', async ({ page }) => {
|
||||
@@ -104,7 +153,7 @@ test.describe('G: Files Plugin', () => {
|
||||
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: 'test.txt', extension: '.txt',
|
||||
kind: 'vault-file', path: 'Docs/todo.txt', extension: '.txt',
|
||||
context: { sourcePluginId: 'verstak.files', sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
|
||||
@@ -74,12 +74,33 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
test('workspace selection keeps exactly one active node', async ({ page }) => {
|
||||
const selected = page.locator('.wt-node.selected .wt-label');
|
||||
await expect(selected).toHaveCount(1);
|
||||
await expect(selected).toHaveText('Alpha Case');
|
||||
await expect(selected).toHaveText('Project');
|
||||
|
||||
await page.locator('.wt-label').filter({ hasText: 'Beta Case' }).click();
|
||||
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
||||
|
||||
await expect(selected).toHaveCount(1);
|
||||
await expect(selected).toHaveText('Beta Case');
|
||||
await expect(selected).toHaveText('Test');
|
||||
});
|
||||
|
||||
test('workspace sidebar creates renames and trashes top-level workspaces', async ({ page }) => {
|
||||
await page.locator('button[title="New workspace"]').click();
|
||||
await page.locator('.wt-create input').fill('ClientA');
|
||||
await page.locator('.wt-btn-primary', { hasText: 'Create' }).click();
|
||||
|
||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toBeVisible();
|
||||
|
||||
const client = page.locator('.wt-node').filter({ hasText: 'ClientA' });
|
||||
await client.locator('button[title="Rename workspace"]').click();
|
||||
await page.locator('.wt-rename').fill('ClientB');
|
||||
await page.locator('button[title="Save rename"]').click();
|
||||
|
||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toBeVisible();
|
||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toHaveCount(0);
|
||||
|
||||
const renamed = page.locator('.wt-node').filter({ hasText: 'ClientB' });
|
||||
await renamed.locator('button[title="Trash workspace"]').click();
|
||||
|
||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('shell icons render through bundled Lucide SVG components', async ({ page }) => {
|
||||
@@ -87,7 +108,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
await expect(logo).toBeVisible();
|
||||
await expect(logo).toHaveClass(/lucide/);
|
||||
|
||||
await page.locator('.wt-label').filter({ hasText: 'Alpha Case' }).click();
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
const workspaceIcon = page.locator('.wt-node-icon').first();
|
||||
await expect(workspaceIcon).toBeVisible();
|
||||
await expect(workspaceIcon).toHaveClass(/lucide/);
|
||||
|
||||
Reference in New Issue
Block a user