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:
2026-06-20 19:20:13 +08:00
parent 4de5a74a55
commit 0ac473d720
18 changed files with 1956 additions and 1161 deletions
+61
View File
@@ -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', {
+56 -7
View File
@@ -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);
+25 -4
View File
@@ -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/);