feat: milestones 6b-fix through 6e — default-editor, files plugin, workspace host, workspaceItems contribution
- Fix PluginCard openProviders display - Add default-editor plugin (text/markdown/notes-context) - Add files plugin with workspaceItems placement - Add workspaceItems contribution point (Go + API + mock + SDK) - Add WorkspaceHost component for workspace area - WorkspaceTree dispatches selection event - Fix default-editor layout to fill container - Fix PluginCard unsafe .length access - Add E2E tests: 34/34 pass - Add bundle execution check to official-plugins check.sh - Update docs: PLUGIN_RUNTIME, DEV_PLUGINS, MILESTONE_6B/6C/6D plans
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('F: Default Editor 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('open .txt file shows plain text editor mode', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { 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');
|
||||
await expect(editor).toHaveAttribute('data-request-mode', 'view');
|
||||
const textarea = editor.locator('[data-editor-textarea]');
|
||||
await expect(textarea).toBeVisible();
|
||||
await expect(textarea).toHaveValue('Buy groceries\nWrite tests');
|
||||
});
|
||||
|
||||
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', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/readme.md',
|
||||
extension: '.md',
|
||||
context: { 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 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Docs/readme.md');
|
||||
});
|
||||
|
||||
test('open .md with notes context routes to highest-priority provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Notes/Overview.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'notes', 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 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Notes/Overview.md');
|
||||
});
|
||||
|
||||
test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.status-badge')).toHaveText('loaded');
|
||||
});
|
||||
|
||||
test('disable default-editor plugin removes its providers', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await card.locator('button.btn-disable').click();
|
||||
await expect(card.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-workbench-status="no-provider"]')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('default-editor plugin card shows openProviders contribution count', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.meta-row').filter({ hasText: 'Contributions:' })).toContainText('3 openProviders');
|
||||
});
|
||||
|
||||
test('default-editor does not add sidebar item', async ({ page }) => {
|
||||
const sidebarItems = page.locator('.sidebar .plugin-item');
|
||||
const count = await sidebarItems.count();
|
||||
const hasDefaultEditor = await page.locator('.sidebar .plugin-item').filter({ hasText: /default.editor/i }).count();
|
||||
expect(hasDefaultEditor).toBe(0);
|
||||
});
|
||||
|
||||
test('platform-test workbench buttons open files via default-editor', async ({ page }) => {
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
|
||||
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/todo.txt',
|
||||
extension: '.txt',
|
||||
context: { 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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
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('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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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: 'test.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 });
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||
await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).locator('.status-badge')).toHaveText('loaded', { timeout: 10000 });
|
||||
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
|
||||
await expect(page.locator('.pt-saved-setting')).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 });
|
||||
await expect(page.locator('.pt-badge')).toHaveAttribute('data-command-status', 'handled');
|
||||
await expect(page.locator('.pt-badge')).toContainText('capability available');
|
||||
@@ -44,28 +45,38 @@ test.describe('D: Plugin API bridge', () => {
|
||||
await expect(page.locator('.pt-files-error-result')).toHaveAttribute('data-files-error-status', 'expected');
|
||||
await expect(page.locator('.pt-files-error-result')).toContainText('Files error path: rejected reserved-path');
|
||||
|
||||
await page.locator('.pt-open-workbench-notes').click();
|
||||
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-workbench-status', 'ok');
|
||||
await expect(page.locator('.pt-workbench-result')).toContainText('Workbench: opened Notes/Overview.md with verstak.platform-test.markdown-diagnostic');
|
||||
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-resource-context', 'notes-markdown');
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Notes/Overview.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'notes', 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('platform-test diagnostic provider routes text, markdown, and notes markdown contexts', async ({ page }) => {
|
||||
async function openFromDiagnostics(buttonClass, path, context) {
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
|
||||
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
|
||||
await page.locator(buttonClass).click();
|
||||
const result = page.locator('.pt-workbench-result');
|
||||
await expect(result).toHaveAttribute('data-workbench-status', 'ok', { timeout: 10000 });
|
||||
await expect(result).toHaveAttribute('data-resource-path', path);
|
||||
await expect(result).toHaveAttribute('data-resource-mode', 'edit');
|
||||
await expect(result).toHaveAttribute('data-resource-context', context);
|
||||
await expect(result).toContainText('verstak.platform-test.markdown-diagnostic');
|
||||
}
|
||||
test('workbench routes markdown files to default-editor provider', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||
kind: 'vault-file',
|
||||
path: 'Docs/readme.md',
|
||||
extension: '.md',
|
||||
context: { sourceView: 'files' },
|
||||
});
|
||||
if (err) throw new Error(err);
|
||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||
});
|
||||
|
||||
await openFromDiagnostics('.pt-open-workbench-text', 'Docs/todo.txt', 'generic-text');
|
||||
await openFromDiagnostics('.pt-open-workbench-markdown', 'Docs/readme.md', 'generic-markdown');
|
||||
await openFromDiagnostics('.pt-open-workbench-notes', 'Notes/Overview.md', 'notes-markdown');
|
||||
const workbench = page.locator('.workbench-host');
|
||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||
const title = workbench.locator('.workbench-title');
|
||||
await expect(title).toHaveText('Docs/readme.md');
|
||||
});
|
||||
|
||||
test('workbench shows no-provider fallback when no provider matches', async ({ page }) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
test('plugin list scrolls through the global main scroll surface and stays responsive', async ({ page }) => {
|
||||
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(18));
|
||||
await page.locator('button.reload-btn').click();
|
||||
await expect(page.locator('.plugin-card')).toHaveCount(19, { timeout: 10000 });
|
||||
await expect(page.locator('.plugin-card')).toHaveCount(21, { timeout: 10000 });
|
||||
|
||||
const manager = page.locator('.plugin-manager');
|
||||
const scrollSurface = page.locator('.content.scroll-surface');
|
||||
|
||||
Reference in New Issue
Block a user