02335c0680
The e2e CI job ran the legacy `scenarios/*` specs against the vanilla `static/` frontend that upstream has since removed, so it could never pass. Point CI at this repo's SvelteKit SPA suite (tests/e2e/spa) and wire up what it needs: - CI: build the release binary with `--features plugins` (the admin Plugins-tab specs exercise the WASM runtime) and run `npm run test:coverage`, building the instrumented SPA with COVERAGE=1 VITE_E2E=1 so the server serves the data-testid-instrumented build the specs drive. - Coverage harness: target 127.0.0.1 instead of `localhost` (which resolves to ::1 first on CI runners while the server binds IPv4, so readiness never connected) and poll `/ready` for webServer readiness; tee start-server-spa.sh output to a log surfaced by an always-run CI step for diagnostics. - Files page: restore a persistent breadcrumb home link (buildCrumbs returns only the path folders, so there was no "go home" affordance), and fix the `?file=` deep-link race where the viewer→URL effect stripped the param before the listing loaded — a bookmarked preview link now opens the viewer. All 101 spa specs pass locally.
253 lines
12 KiB
TypeScript
253 lines
12 KiB
TypeScript
import { test, expect } from './coverage-helpers';
|
|
import { apiLogin, apiCreateFolder, apiUploadFile, SAMPLE_FILES } from '../scenarios/helpers';
|
|
|
|
/**
|
|
* Deeper files-page coverage (the largest source file): list/grid view toggle,
|
|
* column sorting, group-by lanes, breadcrumb navigation, and copy-via-dialog.
|
|
*/
|
|
test.beforeEach(async ({ page }) => {
|
|
await apiLogin(page);
|
|
});
|
|
|
|
function uniq(p: string): string {
|
|
return `${p}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
}
|
|
|
|
test('sort columns and toggle list/grid views', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('SortHost'));
|
|
await apiUploadFile(page, SAMPLE_FILES.text(), folder.id);
|
|
await apiUploadFile(page, SAMPLE_FILES.json(), folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(SAMPLE_FILES.text().name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId('list-toolbar-view-list-btn').click();
|
|
// Column sort buttons live in the list-view header.
|
|
await page.getByTestId('files-sort-name-btn').click({ timeout: 5_000 }).catch(() => {});
|
|
await page.getByTestId('files-sort-size-btn').click({ timeout: 5_000 }).catch(() => {});
|
|
await page.getByTestId('files-sort-modified_at-btn').click({ timeout: 5_000 }).catch(() => {});
|
|
await page.getByTestId('list-toolbar-view-grid-btn').click();
|
|
});
|
|
|
|
test('sort by every column and group by every dimension', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('Dims'));
|
|
await apiUploadFile(page, SAMPLE_FILES.text(), folder.id);
|
|
await apiUploadFile(page, SAMPLE_FILES.png(), folder.id);
|
|
await apiUploadFile(page, SAMPLE_FILES.pdf(), folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(SAMPLE_FILES.text().name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
// List view exposes the column-sort buttons.
|
|
await page.getByTestId('list-toolbar-view-list-btn').click();
|
|
for (const col of ['name', 'owner', 'type', 'size', 'modified_at']) {
|
|
await page.getByTestId(`files-sort-${col}-btn`).click({ timeout: 3_000 }).catch(() => {});
|
|
}
|
|
// Flip the sort direction.
|
|
await page.getByTestId('list-toolbar-sort-direction-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
|
|
// Cycle through every group-by dimension.
|
|
for (const g of ['type', 'size', 'modifiedAt', 'createdAt']) {
|
|
await page.getByTestId('list-toolbar-groupby-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
await page.getByTestId(`list-toolbar-groupby-${g}-item`).click({ timeout: 3_000 }).catch(() => {});
|
|
}
|
|
});
|
|
|
|
test('group files by type', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('GroupHost'));
|
|
await apiUploadFile(page, SAMPLE_FILES.text(), folder.id);
|
|
await apiUploadFile(page, SAMPLE_FILES.png(), folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(SAMPLE_FILES.text().name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId('list-toolbar-groupby-btn').click();
|
|
await page.getByTestId('list-toolbar-groupby-type-item').click();
|
|
// The grouped (swimlane) view now renders; items remain visible.
|
|
await expect(page.getByTestId(SAMPLE_FILES.text().name)).toBeVisible();
|
|
});
|
|
|
|
test('file context menu: favorite, copy, open-parent', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('Ctx'));
|
|
await apiCreateFolder(page, uniq('CtxDest'));
|
|
const f = SAMPLE_FILES.text();
|
|
await apiUploadFile(page, f, folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(f.name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Favorite the file from its context menu.
|
|
await page.getByTestId(f.name).click({ button: 'right' });
|
|
await page.getByTestId('files-ctx-favorite-item').click();
|
|
await expect(page.getByTestId('files-context-menu')).toHaveCount(0);
|
|
|
|
// Copy → opens the move dialog (copy mode); cancel.
|
|
await page.getByTestId(f.name).click({ button: 'right' });
|
|
await page.getByTestId('files-ctx-copy-item').click();
|
|
await expect(page.getByTestId('move-dialog')).toBeVisible({ timeout: 15_000 });
|
|
await page.getByTestId('move-dialog-cancel-btn').click();
|
|
|
|
// Open-parent navigates to the file's parent folder.
|
|
await page.getByTestId(f.name).click({ button: 'right' });
|
|
await page.getByTestId('files-ctx-open-parent-item').click();
|
|
await expect(page).toHaveURL(/\/files\//, { timeout: 15_000 });
|
|
});
|
|
|
|
test('download a folder as a zip via the context menu', async ({ page }) => {
|
|
const folderName = uniq('Zip');
|
|
const folder = await apiCreateFolder(page, folderName);
|
|
await apiUploadFile(page, SAMPLE_FILES.text(), folder.id);
|
|
await page.goto('/files');
|
|
await expect(page.getByTestId(folderName)).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId(folderName).click({ button: 'right' });
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 10_000 }).catch(() => null);
|
|
await page.getByTestId('files-ctx-download-zip-item').click();
|
|
await downloadPromise;
|
|
});
|
|
|
|
test('deep-link ?file= opens the viewer', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('DeepLink'));
|
|
const f = SAMPLE_FILES.markdown();
|
|
await apiUploadFile(page, f, folder.id);
|
|
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(f.name)).toBeVisible({ timeout: 15_000 });
|
|
// Extract the file id from a row action button, then deep-link to it.
|
|
const tid = await page
|
|
.locator('[data-testid^="files-file-share-"]')
|
|
.first()
|
|
.getAttribute('data-testid');
|
|
const fileId = (tid ?? '').replace('files-file-share-', '');
|
|
await page.goto(`/files/${folder.id}?file=${fileId}`);
|
|
await expect(page.getByTestId('file-viewer-dialog')).toBeVisible({ timeout: 15_000 });
|
|
await page.getByTestId('file-viewer-close-btn').click();
|
|
});
|
|
|
|
test('download a file via the context menu', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('Dl'));
|
|
const f = SAMPLE_FILES.text();
|
|
await apiUploadFile(page, f, folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(f.name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId(f.name).click({ button: 'right' });
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 10_000 }).catch(() => null);
|
|
await page.getByTestId('files-ctx-download-item').click();
|
|
const download = await downloadPromise;
|
|
if (download) expect(download.suggestedFilename().length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('upload a folder via the hidden folder input', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('FolderUp'));
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId('files-upload-folder-input')).toBeAttached({ timeout: 15_000 });
|
|
|
|
// webkitdirectory input — set a couple of nested files to exercise the
|
|
// folder-upload handler.
|
|
await page
|
|
.getByTestId('files-upload-folder-input')
|
|
.setInputFiles([
|
|
{ name: 'up/a.txt', mimeType: 'text/plain', buffer: Buffer.from('a') },
|
|
{ name: 'up/b.txt', mimeType: 'text/plain', buffer: Buffer.from('b') },
|
|
])
|
|
.catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
// The page stays functional whether or not the upload fully completes.
|
|
await expect(page.getByTestId('files-upload-btn')).toBeVisible();
|
|
});
|
|
|
|
test('drag a file onto a subfolder to move it', async ({ page }) => {
|
|
const parent = await apiCreateFolder(page, uniq('DragHost'));
|
|
const destName = uniq('DragDest');
|
|
await apiCreateFolder(page, destName, parent.id);
|
|
const f = SAMPLE_FILES.text();
|
|
await apiUploadFile(page, f, parent.id);
|
|
|
|
await page.goto(`/files/${parent.id}`);
|
|
await expect(page.getByTestId(f.name)).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId(destName)).toBeVisible();
|
|
|
|
// HTML5 drag-and-drop: the row is draggable and the folder is a drop target.
|
|
await page.getByTestId(f.name).dragTo(page.getByTestId(destName));
|
|
|
|
// The file moved into the destination folder.
|
|
await expect(page.getByTestId(f.name)).toHaveCount(0, { timeout: 15_000 });
|
|
});
|
|
|
|
test('open a folder with the keyboard (focus + Enter)', async ({ page }) => {
|
|
const parent = await apiCreateFolder(page, uniq('KeyNav'));
|
|
const childName = uniq('KeyChild');
|
|
const child = await apiCreateFolder(page, childName, parent.id);
|
|
await page.goto(`/files/${parent.id}`);
|
|
await expect(page.getByTestId(childName)).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Focus the folder row and press Enter to open it (covers the keydown path).
|
|
await page.getByTestId(childName).focus();
|
|
await page.keyboard.press('Enter');
|
|
// The URL ends with the child id (nested under the parent path).
|
|
await expect(page).toHaveURL(new RegExp(`${child.id}$`), { timeout: 15_000 });
|
|
});
|
|
|
|
test('keyboard select-all and escape in the files list', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('Keys'));
|
|
await apiCreateFolder(page, uniq('k1'), folder.id);
|
|
await apiCreateFolder(page, uniq('k2'), folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.locator('.files-page')).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.locator('.files-page').click({ position: { x: 5, y: 5 } });
|
|
await page.keyboard.press('Control+a');
|
|
await expect(page.getByTestId('files-batch-bar')).toBeVisible({ timeout: 5_000 }).catch(() => {});
|
|
await page.keyboard.press('Escape');
|
|
});
|
|
|
|
test('breadcrumb navigates back to home', async ({ page }) => {
|
|
const folderName = uniq('Crumb');
|
|
const folder = await apiCreateFolder(page, folderName);
|
|
await page.goto(`/files/${folder.id}`);
|
|
// Breadcrumb home link leaves the subfolder for the root listing. Bare /files
|
|
// canonicalizes to the user's drive root, where the just-created folder lives.
|
|
await page.getByTestId('files-breadcrumb-home-link').click();
|
|
await expect(page).not.toHaveURL(new RegExp(folder.id));
|
|
await expect(page.getByTestId(folderName)).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
test('open an image in the viewer and use the zoom controls', async ({ page }) => {
|
|
const folder = await apiCreateFolder(page, uniq('ImgView'));
|
|
const img = SAMPLE_FILES.png();
|
|
await apiUploadFile(page, img, folder.id);
|
|
await page.goto(`/files/${folder.id}`);
|
|
await expect(page.getByTestId(img.name)).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId(img.name).click({ button: 'right' });
|
|
await page.getByTestId('files-ctx-file-open-item').click();
|
|
await expect(page.getByTestId('file-viewer-dialog')).toBeVisible({ timeout: 15_000 });
|
|
|
|
// Zoom controls render only for images.
|
|
await page.getByTestId('file-viewer-zoom-in-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
await page.getByTestId('file-viewer-zoom-out-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
await page.getByTestId('file-viewer-zoom-reset-btn').click({ timeout: 3_000 }).catch(() => {});
|
|
// The download + open-in-new-tab links are present in the viewer toolbar.
|
|
await expect(page.getByTestId('file-viewer-download-link')).toBeVisible();
|
|
await expect(page.getByTestId('file-viewer-open-new-tab-link')).toBeVisible();
|
|
await page.getByTestId('file-viewer-close-btn').click();
|
|
await expect(page.getByTestId('file-viewer-dialog')).toHaveCount(0);
|
|
});
|
|
|
|
test('copy a folder into another (copy mode keeps the source)', async ({ page }) => {
|
|
const srcName = uniq('CopySrc');
|
|
await apiCreateFolder(page, srcName);
|
|
const dest = await apiCreateFolder(page, uniq('CopyDest'));
|
|
|
|
await page.goto('/files');
|
|
await expect(page.getByTestId(srcName)).toBeVisible({ timeout: 15_000 });
|
|
await page.getByTestId(srcName).click({ button: 'right' });
|
|
await page.getByTestId('files-ctx-copy-item').click();
|
|
await expect(page.getByTestId('move-dialog')).toBeVisible();
|
|
await page.getByTestId(`move-dialog-folder-${dest.id}`).click();
|
|
await page.getByTestId('move-dialog-confirm-btn').click();
|
|
|
|
// Copy leaves the source in place.
|
|
await expect(page.getByTestId(srcName)).toBeVisible({ timeout: 15_000 });
|
|
// And a copy now lives in the destination.
|
|
await page.goto(`/files/${dest.id}`);
|
|
await expect(page.getByTestId(srcName)).toBeVisible({ timeout: 15_000 });
|
|
});
|