feat: cover global trash workflow in desktop
This commit is contained in:
@@ -218,7 +218,7 @@ test.describe('G: Files Plugin', () => {
|
||||
await expect(page.locator('[data-file-name="project-only.txt"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('files explorer restores an item from trash metadata', async ({ page }) => {
|
||||
test('files explorer keeps removed items out of live files without Trash metadata', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await openFilesTool(page);
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
|
||||
@@ -232,14 +232,8 @@ test.describe('G: Files Plugin', () => {
|
||||
await page.locator('[data-files-action="trash"]').click();
|
||||
await page.locator('.files-modal-btn.confirm').click();
|
||||
await expect(page.locator('[data-file-name="RestoreMe.txt"]')).toHaveCount(0);
|
||||
|
||||
await page.locator('[data-files-action="trash-view"]').click();
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Trash metadata');
|
||||
await expect(page.locator('[data-files-trash-id]').filter({ hasText: 'Project/RestoreMe.txt' })).toBeVisible();
|
||||
|
||||
await page.locator('[data-files-restore-trash]').first().click();
|
||||
await expect(page.locator('.files-breadcrumb')).toContainText('Project');
|
||||
await expect(page.locator('[data-file-name="RestoreMe.txt"]')).toBeVisible();
|
||||
await expect(page.locator('[data-files-action="trash-view"]')).toHaveCount(0);
|
||||
await expect(page.locator('[data-files-restore-trash]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('files explorer uses labeled controls and no row New Here action', async ({ page }) => {
|
||||
@@ -258,7 +252,6 @@ test.describe('G: Files Plugin', () => {
|
||||
open: 'open',
|
||||
rename: 'rename',
|
||||
trash: 'trash',
|
||||
'trash-view': 'trashView',
|
||||
cut: 'cut',
|
||||
copy: 'copy',
|
||||
paste: 'paste',
|
||||
|
||||
@@ -78,13 +78,13 @@ test.describe('E: Plugin Manager layout', () => {
|
||||
|
||||
const health = page.locator('[data-plugin-manager-summary="health"]');
|
||||
await expect(health).toBeVisible();
|
||||
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('9');
|
||||
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('10');
|
||||
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
|
||||
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
|
||||
|
||||
const risk = page.locator('[data-plugin-manager-summary="risk"]');
|
||||
await expect(risk).toBeVisible();
|
||||
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('5');
|
||||
await expect(risk.locator('[data-plugin-risk-summary="elevated-permissions"]')).toContainText('6');
|
||||
await expect(risk).toContainText('elevated permissions');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||
|
||||
async function openTrash(page) {
|
||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Trash' }).click();
|
||||
await expect(page.locator('.trash-root')).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
async function createTrashEntry(page, path, content, deletedAt) {
|
||||
return page.evaluate(async ({ path, content, deletedAt }) => {
|
||||
const api = window.createPluginAPI('verstak.files');
|
||||
await api.files.writeText(path, content, { createIfMissing: true });
|
||||
const entry = await api.files.trash(path);
|
||||
window.__wailsMock.setTrashDeletedAt(entry.trashId, deletedAt);
|
||||
api.dispose();
|
||||
return entry;
|
||||
}, { path, content, deletedAt });
|
||||
}
|
||||
|
||||
test.describe('L: Global Trash 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('moves a Files item into global Trash without exposing Trash metadata in Files', async ({ page }) => {
|
||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
||||
await page.getByRole('tab', { name: 'Files' }).click();
|
||||
await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.locator('[data-files-action="new-text"]').click();
|
||||
await page.locator('[data-files-create-input]').fill('GlobalTrash.txt');
|
||||
await page.locator('[data-files-create-confirm]').click();
|
||||
await expect(page.locator('[data-file-name="GlobalTrash.txt"]')).toBeVisible();
|
||||
await page.locator('[data-file-name="GlobalTrash.txt"]').click();
|
||||
await page.locator('[data-files-action="trash"]').click();
|
||||
await page.locator('.files-modal-btn.confirm').click();
|
||||
await expect(page.locator('[data-file-name="GlobalTrash.txt"]')).toHaveCount(0);
|
||||
await expect(page.locator('[data-files-action="trash-view"]')).toHaveCount(0);
|
||||
|
||||
await openTrash(page);
|
||||
const row = page.locator('[data-trash-row]').filter({ hasText: 'GlobalTrash.txt' });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row).toContainText('Project');
|
||||
await expect(row).toContainText('Project/GlobalTrash.txt');
|
||||
});
|
||||
|
||||
test('filters and sorts global Trash and restores without overwriting a conflict', async ({ page }) => {
|
||||
const older = await createTrashEntry(page, 'Project/Older.txt', 'older original', '2026-06-20T08:00:00.000Z');
|
||||
const newest = await createTrashEntry(page, 'Test/Newest.txt', 'newest original', '2026-06-29T08:00:00.000Z');
|
||||
const restore = await createTrashEntry(page, 'Project/Restore.txt', 'restore original', '2026-06-25T08:00:00.000Z');
|
||||
const conflict = await page.evaluate(async () => {
|
||||
const api = window.createPluginAPI('verstak.files');
|
||||
await api.files.writeText('Project/Conflict.txt', 'trashed content', { createIfMissing: true });
|
||||
const entry = await api.files.trash('Project/Conflict.txt');
|
||||
await api.files.writeText('Project/Conflict.txt', 'existing content', { createIfMissing: true });
|
||||
window.__wailsMock.setTrashDeletedAt(entry.trashId, '2026-06-24T08:00:00.000Z');
|
||||
api.dispose();
|
||||
return entry;
|
||||
});
|
||||
|
||||
await openTrash(page);
|
||||
await page.locator('[data-trash-filter-workspace]').selectOption('Project');
|
||||
await expect(page.locator(`[data-trash-row="${older.trashId}"]`)).toBeVisible();
|
||||
await expect(page.locator(`[data-trash-row="${newest.trashId}"]`)).toHaveCount(0);
|
||||
|
||||
await page.locator('[data-trash-filter-workspace]').selectOption('');
|
||||
await page.locator('[data-trash-filter-search]').fill('Newest');
|
||||
await expect(page.locator(`[data-trash-row="${newest.trashId}"]`)).toBeVisible();
|
||||
await expect(page.locator(`[data-trash-row="${older.trashId}"]`)).toHaveCount(0);
|
||||
|
||||
await page.locator('[data-trash-filter-search]').fill('');
|
||||
await page.locator('[data-trash-sort]').selectOption('date-asc');
|
||||
await expect(page.locator('[data-trash-row]').first()).toHaveAttribute('data-trash-row', older.trashId);
|
||||
|
||||
await page.locator(`[data-trash-restore="${restore.trashId}"]`).click();
|
||||
await expect(page.locator(`[data-trash-row="${restore.trashId}"]`)).toHaveCount(0);
|
||||
await expect.poll(() => page.evaluate(async () => {
|
||||
const api = window.createPluginAPI('verstak.files');
|
||||
const content = await api.files.readText('Project/Restore.txt');
|
||||
api.dispose();
|
||||
return content;
|
||||
})).toBe('restore original');
|
||||
|
||||
await page.locator(`[data-trash-restore="${conflict.trashId}"]`).click();
|
||||
await expect(page.locator('[data-trash-status]')).toContainText('Restore blocked');
|
||||
await expect(page.locator(`[data-trash-row="${conflict.trashId}"]`)).toBeVisible();
|
||||
await expect.poll(() => page.evaluate(async () => {
|
||||
const api = window.createPluginAPI('verstak.files');
|
||||
const content = await api.files.readText('Project/Conflict.txt');
|
||||
api.dispose();
|
||||
return content;
|
||||
})).toBe('existing content');
|
||||
});
|
||||
|
||||
test('requires confirmation before permanently deleting a Trash item', async ({ page }) => {
|
||||
const entry = await createTrashEntry(page, 'Test/Permanent.txt', 'remove forever', '2026-06-26T08:00:00.000Z');
|
||||
await openTrash(page);
|
||||
|
||||
await page.locator(`[data-trash-delete="${entry.trashId}"]`).click();
|
||||
await expect(page.locator(`[data-trash-confirm="${entry.trashId}"]`)).toBeVisible();
|
||||
await expect(page.locator(`[data-trash-row="${entry.trashId}"]`)).toBeVisible();
|
||||
|
||||
await page.locator(`[data-trash-confirm-delete="${entry.trashId}"]`).click();
|
||||
await expect(page.locator(`[data-trash-row="${entry.trashId}"]`)).toHaveCount(0);
|
||||
await expect.poll(() => page.evaluate(async (trashId) => {
|
||||
const api = window.createPluginAPI('verstak.files');
|
||||
const entries = await api.files.listTrash();
|
||||
api.dispose();
|
||||
return entries.some((item) => item.trashId === trashId);
|
||||
}, entry.trashId)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user