verstak-desktop/frontend/e2e/trash-plugin.spec.js

122 lines
6.0 KiB
JavaScript

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);
});
});