feat: workspace routing, GlobalSearch, and shell refinements
- Add workspace-aware startup routing (openDefaultWorkspaceRoute) - Add GlobalSearch component and workspace-empty state - Refactor Sidebar, StatusBar, WorkspaceTree, VaultSelection - Update PluginCard/PluginManager UI - Extend e2e coverage (ux-p0, ux-followup, helpers) - Add ListWorkspaces/SetCurrentWorkspace backend bindings
This commit is contained in:
@@ -43,6 +43,6 @@ test.describe('Command Palette', () => {
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await expect(page.locator('.command-palette')).not.toBeVisible();
|
||||
await expect(page.locator('.plugin-manager')).toBeVisible();
|
||||
await expect(page.locator('.workspace-host')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('F: Default Editor Plugin', () => {
|
||||
let consoleCollector;
|
||||
@@ -134,14 +134,14 @@ test.describe('F: Default Editor Plugin', () => {
|
||||
});
|
||||
|
||||
test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
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();
|
||||
await openPluginManager(page);
|
||||
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 });
|
||||
@@ -161,7 +161,7 @@ test.describe('F: Default Editor Plugin', () => {
|
||||
});
|
||||
|
||||
test('default-editor plugin card shows openProviders contribution count', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
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');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('G: Files Plugin', () => {
|
||||
let consoleCollector;
|
||||
@@ -16,7 +16,7 @@ test.describe('G: Files Plugin', () => {
|
||||
});
|
||||
|
||||
test('files plugin appears in Plugin Manager as loaded', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
const card = page.locator('.plugin-card').filter({ hasText: 'verstak.files' });
|
||||
await expect(card).toBeVisible({ timeout: 10000 });
|
||||
await expect(card.locator('.status-badge')).toHaveText('loaded');
|
||||
@@ -88,7 +88,7 @@ test.describe('G: Files Plugin', () => {
|
||||
await expect(page.locator('.files-breadcrumb')).not.toContainText('Daily');
|
||||
});
|
||||
|
||||
test('files explorer uses icon controls and no row New Here action', async ({ page }) => {
|
||||
test('files explorer uses labeled 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 });
|
||||
|
||||
@@ -96,13 +96,13 @@ test.describe('G: Files Plugin', () => {
|
||||
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(button).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/);
|
||||
await expect(firstRowButton).toHaveText(/\S/);
|
||||
expect(await firstRowButton.evaluate((node) => node.innerHTML)).toContain('<svg');
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ export async function waitForAppReady(page) {
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
/** Open the secondary Plugin Manager route from the status-bar settings menu. */
|
||||
export async function openPluginManager(page) {
|
||||
await page.locator('[data-settings-menu-button]').click();
|
||||
await page.locator('[data-settings-action="plugin-manager"]').click();
|
||||
await page.waitForSelector('.plugin-manager', { state: 'visible', timeout: 10000 });
|
||||
}
|
||||
|
||||
/** Collect all console errors since last reset */
|
||||
export function setupConsoleCollector(page) {
|
||||
const errors = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('D: Plugin API bridge', () => {
|
||||
let consoleCollector;
|
||||
@@ -26,7 +26,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||
await page.locator('.pt-save-setting').click();
|
||||
await expect(saved).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 });
|
||||
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0);
|
||||
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0);
|
||||
await page.locator('button.reload-btn').click();
|
||||
@@ -155,7 +155,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(1);
|
||||
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(1);
|
||||
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
|
||||
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0);
|
||||
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0);
|
||||
@@ -165,7 +165,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||
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('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
|
||||
await pluginCard.locator('button.btn-disable').click();
|
||||
await expect(pluginCard.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
|
||||
@@ -175,7 +175,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||
});
|
||||
|
||||
test('platform-test settings panel loads bundle content returned as raw string', async ({ page }) => {
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
|
||||
await openPluginManager(page);
|
||||
|
||||
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
|
||||
await pluginCard.locator('button.btn-settings').click();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 8. Verify plugin sidebar item returns
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('A: Plugin Manager Disable/Enable refresh', () => {
|
||||
let consoleCollector;
|
||||
@@ -22,6 +22,7 @@ test.describe('A: Plugin Manager Disable/Enable refresh', () => {
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
await openPluginManager(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('E: Plugin Manager layout', () => {
|
||||
let consoleCollector;
|
||||
@@ -16,6 +16,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
});
|
||||
|
||||
test('plugin list scrolls through the global main scroll surface and stays responsive', async ({ page }) => {
|
||||
await openPluginManager(page);
|
||||
const basePluginCount = await page.locator('.plugin-card').count();
|
||||
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(18));
|
||||
await page.locator('button.reload-btn').click();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 3. Verify UI reflects the updated state
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, setPluginStatus } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, setPluginStatus, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('C: Reload updates UI state', () => {
|
||||
let consoleCollector;
|
||||
@@ -17,6 +17,7 @@ test.describe('C: Reload updates UI state', () => {
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
await openPluginManager(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('Status Bar host', () => {
|
||||
let consoleCollector;
|
||||
@@ -39,6 +39,7 @@ test.describe('Status Bar host', () => {
|
||||
test('refreshes statusBarItems after disabling plugin', async ({ page }) => {
|
||||
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
|
||||
await expect(page.locator('[data-status-item-id="verstak.platform-test.status"]')).toBeVisible();
|
||||
await openPluginManager(page);
|
||||
|
||||
await pluginCard.locator('button.btn-disable').click();
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('UX follow-up fixes', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test('global search stays available after opening tool sidebar views', async ({ page }) => {
|
||||
const search = page.locator('[data-global-search-input]');
|
||||
await expect(search).toBeVisible();
|
||||
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Activity' }).click();
|
||||
await expect(page.locator('.activity-root')).toBeVisible({ timeout: 10000 });
|
||||
await expect(search).toBeVisible();
|
||||
|
||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Browser Inbox' }).click();
|
||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||
await expect(search).toBeVisible();
|
||||
});
|
||||
|
||||
test('global search types ahead across workspaces and file contents with keyboard layout fallback', async ({ page }) => {
|
||||
const search = page.locator('[data-global-search-input]');
|
||||
|
||||
await search.fill('Зкщоусе');
|
||||
await expect(page.locator('[data-global-search-results]')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await search.fill('project file');
|
||||
await expect(page.locator('[data-global-search-results]')).toContainText('project-only.txt', { timeout: 10000 });
|
||||
});
|
||||
|
||||
test('plugin settings modal gives complex panels enough space', async ({ page }) => {
|
||||
await openPluginManager(page);
|
||||
await page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).getByRole('button', { name: 'Settings' }).click();
|
||||
|
||||
const modal = page.locator('.modal[aria-label="Plugin Settings"]');
|
||||
await expect(modal).toBeVisible({ timeout: 10000 });
|
||||
const box = await modal.boundingBox();
|
||||
expect(box.width).toBeGreaterThanOrEqual(760);
|
||||
expect(box.height).toBeGreaterThanOrEqual(560);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
test.describe('UX P0 shell flow', () => {
|
||||
let consoleCollector;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
consoleCollector = setupConsoleCollector(page);
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
consoleCollector.assertNoErrors();
|
||||
});
|
||||
|
||||
test('starts in the first workspace instead of Plugin Manager', async ({ page }) => {
|
||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.plugin-manager')).toHaveCount(0);
|
||||
await expect(page.locator('.wt-node.selected .wt-label')).toHaveText('Project');
|
||||
await expect(page.locator('.workspace-title')).toHaveText('Project');
|
||||
});
|
||||
|
||||
test('workspace selection and main content stay in sync across plugin manager round trip', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
||||
|
||||
await expect(page.locator('.workspace-title')).toHaveText('Test', { timeout: 10000 });
|
||||
await expect(page.locator('.wt-node.selected .wt-label')).toHaveText('Test');
|
||||
await expect(page.locator('.plugin-manager')).toHaveCount(0);
|
||||
|
||||
await openPluginManager(page);
|
||||
await expect(page.locator('.plugin-manager')).toBeVisible();
|
||||
await expect(page.locator('.wt-node.selected .wt-label')).toHaveCount(0);
|
||||
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await expect(page.locator('.workspace-title')).toHaveText('Project', { timeout: 10000 });
|
||||
await expect(page.locator('.plugin-manager')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('status bar plugin contribution failures do not render large error panels', async ({ page }) => {
|
||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText('Plugin View Error')).toHaveCount(0);
|
||||
await expect(page.locator('.status-bar [data-status-item-id]')).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('Plugin Manager remains reachable from the settings menu', async ({ page }) => {
|
||||
await openPluginManager(page);
|
||||
|
||||
await expect(page.locator('.plugin-manager')).toBeVisible();
|
||||
await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UX quick wins', () => {
|
||||
test('Files screen uses readable dates and understandable action controls', async ({ page }) => {
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
|
||||
const files = page.locator('.files-root');
|
||||
await expect(files).toBeVisible({ timeout: 10000 });
|
||||
await expect(files.getByText(/T\d{2}:\d{2}:\d{2}/)).toHaveCount(0);
|
||||
|
||||
const actions = ['New folder', 'New markdown file', 'New text file'];
|
||||
for (const action of actions) {
|
||||
const button = page.locator(`[data-files-action]`).filter({ hasText: action }).first();
|
||||
await expect(button).toBeVisible();
|
||||
await expect(button).toHaveAttribute('title', action);
|
||||
}
|
||||
});
|
||||
|
||||
test('Vault Selection is localized and has a clear primary action', async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
await page.addInitScript(() => {
|
||||
window.go = { api: { App: {
|
||||
GetAppSettings: async () => ({ currentVaultPath: '', recentVaults: ['/tmp/verstak-recent-vault'] }),
|
||||
GetVaultStatus: async () => ({ status: 'closed', path: '', vaultId: '' }),
|
||||
SelectDirectory: async () => '',
|
||||
SelectVaultForOpen: async () => '',
|
||||
CreateVault: async () => null,
|
||||
OpenVault: async () => null,
|
||||
SetCurrentVault: async () => '',
|
||||
WriteFrontendLog: async () => {},
|
||||
} } };
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('.vault-selection', { timeout: 10000 });
|
||||
|
||||
await expect(page.getByText('Выберите vault для начала работы')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Создать vault' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Открыть существующий' })).toBeVisible();
|
||||
await page.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user