Improve workspace UX flows
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('Activity workflow', () => {
|
||||
let consoleCollector;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
consoleCollector = setupConsoleCollector(page);
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
consoleCollector.assertNoErrors();
|
||||
});
|
||||
|
||||
test('workspace activity explains empty event flow', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
|
||||
const activity = page.locator('.activity-root');
|
||||
await expect(activity).toBeVisible({ timeout: 10000 });
|
||||
await expect(activity.locator('.activity-title')).toContainText('Activity');
|
||||
await expect(activity.locator('.activity-count')).toHaveText('0 events');
|
||||
await expect(activity.locator('.activity-empty')).toContainText('No activity events yet');
|
||||
await expect(activity.locator('.activity-empty')).toContainText('File changes, browser captures, and conversions will appear here');
|
||||
await expect(activity.locator('[data-activity-action="clear"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('workspace activity renders stored events and worklog suggestions', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
||||
'events:workspace:Project': [
|
||||
{
|
||||
activityId: 'activity-e2e-capture',
|
||||
occurredAt: '2026-06-30T08:00:00.000Z',
|
||||
type: 'browser.capture.selection',
|
||||
title: 'Research Capture',
|
||||
summary: 'Selected text from the article',
|
||||
sourcePluginId: 'verstak.browser-inbox',
|
||||
workspaceRootPath: 'Project',
|
||||
},
|
||||
{
|
||||
activityId: 'activity-e2e-note',
|
||||
occurredAt: '2026-06-30T08:25:00.000Z',
|
||||
type: 'note.saved',
|
||||
title: 'Saved note',
|
||||
summary: 'Project/Notes/Research Capture.md',
|
||||
sourcePluginId: 'verstak.files',
|
||||
workspaceRootPath: 'Project',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
|
||||
const activity = page.locator('.activity-root');
|
||||
await expect(activity.locator('.activity-count')).toHaveText('2 events');
|
||||
await expect(activity.locator('[data-activity-id="activity-e2e-capture"]')).toContainText('Research Capture');
|
||||
await expect(activity.locator('[data-activity-id="activity-e2e-note"]')).toContainText('Saved note');
|
||||
await expect(activity.locator('[data-activity-section="worklog-suggestions"]')).toContainText('Worklog suggestions');
|
||||
await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('Project work on 2026-06-30');
|
||||
await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('50 min');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('Browser Inbox workflow', () => {
|
||||
let consoleCollector;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
consoleCollector = setupConsoleCollector(page);
|
||||
await resetMockState(page);
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
consoleCollector.assertNoErrors();
|
||||
});
|
||||
|
||||
test('workspace inbox explains empty capture flow and primary actions', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Browser Inbox' }).click();
|
||||
|
||||
const inbox = page.locator('.browser-inbox-root');
|
||||
await expect(inbox).toBeVisible({ timeout: 10000 });
|
||||
await expect(inbox.locator('.browser-inbox-title')).toContainText('Browser Inbox');
|
||||
await expect(inbox.locator('.browser-inbox-count')).toHaveText('0 items');
|
||||
await expect(inbox.locator('.browser-inbox-empty')).toContainText('No browser captures yet');
|
||||
await expect(inbox.locator('.browser-inbox-empty')).toContainText('send a page, selection, link, or file from the browser extension');
|
||||
await expect(inbox.locator('[data-browser-inbox-action="clear"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('workspace inbox renders stored captures with conversion actions', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
||||
'captures:workspace:Project': [{
|
||||
captureId: 'capture-e2e-1',
|
||||
capturedAt: '2026-06-30T08:00:00.000Z',
|
||||
kind: 'file',
|
||||
url: 'https://example.com/report',
|
||||
title: 'Research Report',
|
||||
domain: 'example.com',
|
||||
text: 'Selected page text',
|
||||
fileName: 'report.txt',
|
||||
fileText: 'Report file contents',
|
||||
workspaceRootPath: 'Project',
|
||||
browserName: 'Firefox',
|
||||
}],
|
||||
});
|
||||
});
|
||||
await page.getByRole('tab', { name: 'Browser Inbox' }).click();
|
||||
|
||||
const inbox = page.locator('.browser-inbox-root');
|
||||
await expect(inbox.locator('.browser-inbox-count')).toHaveText('1 item');
|
||||
await expect(inbox.locator('[data-browser-capture-id="capture-e2e-1"]')).toContainText('Research Report');
|
||||
await expect(inbox.locator('.browser-inbox-detail-title')).toHaveText('Research Report');
|
||||
await expect(inbox.locator('.browser-inbox-meta')).toContainText('example.com');
|
||||
await expect(inbox.locator('.browser-inbox-text').first()).toContainText('Selected page text');
|
||||
await expect(inbox.locator('[data-browser-inbox-action="create-note"]')).toBeVisible();
|
||||
await expect(inbox.locator('[data-browser-inbox-action="create-link"]')).toBeVisible();
|
||||
await expect(inbox.locator('[data-browser-inbox-action="create-file"]')).toBeVisible();
|
||||
await expect(inbox.locator('[data-browser-inbox-action="remove"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -45,4 +45,83 @@ test.describe('Command Palette', () => {
|
||||
await expect(page.locator('.command-palette')).not.toBeVisible();
|
||||
await expect(page.locator('.workspace-host')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows user workflow commands before diagnostics and opens workspace tools', async ({ page }) => {
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
||||
|
||||
const palette = page.locator('.command-palette');
|
||||
await expect(palette).toBeVisible();
|
||||
const items = palette.locator('.command-palette-item');
|
||||
await expect(items.nth(0)).toHaveAttribute('data-command-id', 'verstak.shell.open-today');
|
||||
await expect(items.nth(1)).toHaveAttribute('data-command-id', 'verstak.shell.open-files');
|
||||
await expect(items.nth(2)).toHaveAttribute('data-command-id', 'verstak.shell.open-activity');
|
||||
await expect(items.nth(3)).toHaveAttribute('data-command-id', 'verstak.shell.open-browser-inbox');
|
||||
|
||||
await palette.locator('[data-command-palette-input]').fill('activity');
|
||||
await expect(palette.locator('[data-command-id="verstak.shell.open-activity"]')).toBeVisible();
|
||||
await expect(palette.locator('[data-command-id="verstak.platform-test.run-tests"]')).not.toBeVisible();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(palette).not.toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Activity' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('.activity-root')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('starts file creation workflows from user commands', async ({ page }) => {
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
||||
|
||||
let palette = page.locator('.command-palette');
|
||||
await palette.locator('[data-command-palette-input]').fill('markdown');
|
||||
await expect(palette.locator('[data-command-id="verstak.shell.create-markdown"]')).toBeVisible();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(palette).not.toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('[data-files-create-input]')).toBeVisible();
|
||||
|
||||
await page.locator('[data-files-create-input]').fill('Palette Note.md');
|
||||
await page.locator('[data-files-create-confirm]').click();
|
||||
await expect(page.locator('[data-file-name="Palette Note.md"]')).toBeVisible();
|
||||
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
||||
palette = page.locator('.command-palette');
|
||||
await palette.locator('[data-command-palette-input]').fill('text file');
|
||||
await expect(palette.locator('[data-command-id="verstak.shell.create-text"]')).toBeVisible();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page.locator('[data-files-create-input]')).toBeVisible();
|
||||
await page.locator('[data-files-create-input]').fill('Palette Text.txt');
|
||||
await page.locator('[data-files-create-confirm]').click();
|
||||
await expect(page.locator('[data-file-name="Palette Text.txt"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('runs sync workflow commands', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
const err = await window.go.api.App.PluginSyncConfigure('verstak.sync', 'https://sync.example.test', 'alice', 'secret');
|
||||
if (err) throw new Error(err);
|
||||
});
|
||||
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
||||
|
||||
let palette = page.locator('.command-palette');
|
||||
await palette.locator('[data-command-palette-input]').fill('sync now');
|
||||
await expect(palette.locator('[data-command-id="verstak.shell.sync-now"]')).toBeVisible();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(palette).not.toBeVisible();
|
||||
await expect(page.locator('[data-command-palette-status="success"]')).toContainText('Sync Now');
|
||||
await expect(page.locator('[data-command-palette-status="success"]')).toContainText('handled');
|
||||
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
||||
palette = page.locator('.command-palette');
|
||||
await palette.locator('[data-command-palette-input]').fill('sync settings');
|
||||
await expect(palette.locator('[data-command-id="verstak.shell.open-sync-settings"]')).toBeVisible();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page.locator('.plugin-manager')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.modal[aria-label="Plugin Settings"]')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.modal-header h3')).toContainText('Sync');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||
|
||||
async function openFilesTool(page) {
|
||||
await page.getByRole('tab', { name: 'Files' }).click();
|
||||
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe('G: Files Plugin', () => {
|
||||
let consoleCollector;
|
||||
|
||||
@@ -29,6 +34,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
test('workspace Files view is scoped to selected workspace folder', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
|
||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator('.files-item-name').filter({ hasText: 'project-only.txt' })).toBeVisible({ timeout: 10000 });
|
||||
@@ -42,6 +48,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
test('files explorer supports create navigate rename filter sort open and trash', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await page.locator('[data-files-action="new-folder"]').click();
|
||||
@@ -90,6 +97,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
test('files explorer uses labeled controls and no row New Here action', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
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']) {
|
||||
@@ -108,6 +116,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
test('files explorer supports empty-space context paste after cutting a folder', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await page.locator('[data-files-action="new-folder"]').click();
|
||||
@@ -132,6 +141,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
test('files explorer supports multiselect and internal drag/drop move', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await page.locator('[data-files-action="new-folder"]').click();
|
||||
@@ -166,6 +176,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
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 openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await page.locator('[data-file-name="Notes"]').dblclick();
|
||||
@@ -191,6 +202,7 @@ test.describe('G: Files Plugin', () => {
|
||||
|
||||
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 openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
|
||||
await page.locator('[data-file-name="Notes"]').dblclick();
|
||||
|
||||
@@ -84,15 +84,21 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
await expect(selected).toHaveText('Test');
|
||||
});
|
||||
|
||||
test('workspace tools render as tabs with Files as one tab', async ({ page }) => {
|
||||
test('workspace tools render Today first with Files as one tab', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
|
||||
const tabs = page.locator('.workspace-tabs');
|
||||
await expect(tabs).toBeVisible({ timeout: 10000 });
|
||||
const todayTab = tabs.locator('[role="tab"]').filter({ hasText: 'Today' });
|
||||
const filesTab = tabs.locator('[role="tab"]').filter({ hasText: 'Files' });
|
||||
await expect(todayTab).toBeVisible();
|
||||
await expect(todayTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(filesTab).toBeVisible();
|
||||
await expect(filesTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(filesTab).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(page.locator('.workspace-tool')).toHaveCount(0);
|
||||
await expect(page.locator('.today-root')).toBeVisible();
|
||||
|
||||
await filesTab.click();
|
||||
await expect(page.locator('.files-root')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ test.describe('UX quick wins', () => {
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await page.getByRole('tab', { name: 'Files' }).click();
|
||||
|
||||
const files = page.locator('.files-root');
|
||||
await expect(files).toBeVisible({ timeout: 10000 });
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
test.describe('UX Today workspace 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('workspace opens with Today before plugin tools', async ({ page }) => {
|
||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const tabs = page.getByRole('tab');
|
||||
await expect(tabs.nth(0)).toHaveText('Today');
|
||||
await expect(tabs.nth(1)).toHaveText('Files');
|
||||
await expect(page.getByRole('tab', { name: 'Today' })).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const today = page.locator('.today-root');
|
||||
await expect(today).toBeVisible();
|
||||
await expect(today.locator('[data-today-section="captured"]')).toContainText('Captured');
|
||||
await expect(today.locator('[data-today-section="activity"]')).toContainText('Recent Activity');
|
||||
await expect(today.locator('[data-today-section="worklog"]')).toContainText('Worklog Suggestions');
|
||||
await expect(today.locator('[data-today-section="quick-actions"]')).toContainText('Quick Actions');
|
||||
await expect(today).toContainText('No browser captures yet');
|
||||
await expect(today).toContainText('No activity events yet');
|
||||
});
|
||||
|
||||
test('Today quick action opens Browser Inbox workspace tool', async ({ page }) => {
|
||||
await page.locator('[data-today-action="browser-inbox"]').click();
|
||||
|
||||
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user