Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions e2e/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ requester side.
| Opting into the OneDrive shell and switching back.

| `e2e/tests/onedrive-navigation.spec.ts`
| The nav rail switches between all five views.
| The nav rail switches between all six views (including the Recycle bin).

| `e2e/tests/onedrive-topbar.spec.ts`
| Avatar menu, settings menu language switch, log out.
Expand All @@ -157,8 +157,17 @@ selecting fills the detail panel, create menu makes a folder, search
filters the list.

| `e2e/tests/onedrive-selection.spec.ts`
| Selection action strip, copy link with toast and clipboard, delete
with confirm and background re-read, share dialog.
| Selection action strip, copy link with toast and clipboard, "Move to
bin" (soft delete) with confirm and background re-read, "Delete
permanently" bypassing the Recycle bin, share dialog.

| `e2e/tests/trash.spec.ts`
| The Recycle bin, picking up from a file already moved to `trash/`:
restore returns it to its exact original location with its original
ACL (a Control-mode grant `readAclAgents` would not round-trip),
permanently deleting from the bin removes the trash catalog row and
container, and an item past its retention window is purged the moment
the Recycle bin is opened.

| `e2e/tests/notification-bell.spec.ts`
| Inbox-driven NotificationBell: badge appears on unseen requests,
Expand Down
4 changes: 2 additions & 2 deletions e2e/helpers/onedrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { expect, type Page } from "@playwright/test";
import { APP_EVENTS, STORAGE_KEYS, UI_TIMEOUTS } from "../config";
import { shot } from "./screenshots";

/** One of the five OneDriveLayout views. Mirrors `ViewId` in useViewParam. */
export type ViewId = "recent" | "my-files" | "shared" | "requests" | "people";
/** One of the six OneDriveLayout views. Mirrors `ViewId` in useViewParam. */
export type ViewId = "recent" | "my-files" | "shared" | "requests" | "people" | "trash";

/**
* Switches an already-authenticated page into the OneDrive shell and
Expand Down
23 changes: 4 additions & 19 deletions e2e/helpers/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,6 @@ const EMPTY_CATALOG_TURTLE = `@prefix dcat: <http://www.w3.org/ns/dcat#> .
<> a dcat:Catalog .
`;

const SD_NAMESPACE = "https://w3id.org/solid-drive#";

/**
* Maps a schema.org dataset class (used in the catalog's dcterms:conformsTo)
* to the corresponding solid-drive file type used in the per-file index.ttl.
*/
function inferSolidDriveType(schemaClassUri: string): string {
if (schemaClassUri.endsWith("ImageObject")) return `${SD_NAMESPACE}ImageFile`;
if (schemaClassUri.endsWith("VideoObject")) return `${SD_NAMESPACE}VideoFile`;
if (schemaClassUri.endsWith("AudioObject")) return `${SD_NAMESPACE}AudioFile`;
if (schemaClassUri.endsWith("SpreadsheetDigitalDocument")) return `${SD_NAMESPACE}SpreadsheetDocument`;
if (schemaClassUri.endsWith("TextDigitalDocument")) return `${SD_NAMESPACE}TextDocument`;
return "http://schema.org/DigitalDocument";
}

async function listContainerChildren(authedFetch: typeof fetch, containerUri: string): Promise<string[]> {
const response = await authedFetch(containerUri, { headers: { Accept: TURTLE } });
if (response.status === 404) return [];
Expand Down Expand Up @@ -208,13 +193,13 @@ export async function seedFile(args: SeedFileArgs): Promise<SeededFile> {

// Mirror production layout: each file gets a per-file container with an
// index.ttl carrying the dataset metadata using the CatalogEntrySh shape
// (schema:name etc.). FileCard reads these fields via LDO.
const sdType = inferSolidDriveType(classUri);
// (schema:name etc.). FileCard reads these fields via LDO. The `a` value
// reuses the same schema.org class as the catalog's dcterms:conformsTo —
// the CatalogEntrySh shape accepts exactly this set of classes.
const indexTtl = `@prefix schema: <http://schema.org/> .
@prefix sd: <https://w3id.org/solid-drive#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<> a <${sdType}> ;
<> a <${classUri}> ;
schema:name "${title.replace(/"/g, '\\"')}" ;
schema:encodingFormat "${mediaType}" ;
schema:contentSize "${body.byteLength}" ;
Expand Down
23 changes: 12 additions & 11 deletions e2e/tests/onedrive-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { TEST_TIMEOUTS } from "../config";
* survival lives in the useViewParam unit tests.
*/

// The five rail views: the label on the NavRail button, the value the
// The six rail views: the label on the NavRail button, the value the
// view writes into the ?view= param, and a per-view identity assertion.
// My Files and Requests render the standard page-header with an
// My Files, Requests, and Trash render the standard page-header with an
// `.odl-page-title` heading. Recent, Shared, and People each render
// their own toolbar inline, so the page-header is suppressed and the
// title lives in a view-specific element.
Expand All @@ -25,14 +25,15 @@ const VIEWS: ReadonlyArray<{
identityLocator: string;
identityText: string;
}> = [
{ label: "Home", view: "recent", identityLocator: ".odl-recent__heading", identityText: "Recent" },
{ label: "My Files", view: "my-files", identityLocator: ".odl-page-title", identityText: "My Files" },
{ label: "Shared", view: "shared", identityLocator: "shared-toolbar-tabs", identityText: "With you" },
{ label: "Requests", view: "requests", identityLocator: ".odl-page-title", identityText: "Requests" },
{ label: "People", view: "people", identityLocator: ".odl-people-list__heading", identityText: "People" },
{ label: "Home", view: "recent", identityLocator: ".odl-recent__heading", identityText: "Recent" },
{ label: "My Files", view: "my-files", identityLocator: ".odl-page-title", identityText: "My Files" },
{ label: "Shared", view: "shared", identityLocator: "shared-toolbar-tabs", identityText: "With you" },
{ label: "Requests", view: "requests", identityLocator: ".odl-page-title", identityText: "Requests" },
{ label: "People", view: "people", identityLocator: ".odl-people-list__heading", identityText: "People" },
{ label: "Recycle bin", view: "trash", identityLocator: ".odl-page-title", identityText: "Recycle bin" },
];

test("the NavRail switches between all five views", async ({ browser, parni }) => {
test("the NavRail switches between all six views", async ({ browser, parni }) => {
test.setTimeout(TEST_TIMEOUTS.short);

const { page, close } = await freshLogin(browser, parni);
Expand All @@ -50,9 +51,9 @@ test("the NavRail switches between all five views", async ({ browser, parni }) =
await expect(page.locator(identityLocator).first()).toContainText(identityText);
}

// People does not touch the URL itself, so the NavRail's ?view= write
// is still there to assert on.
await expect(page).toHaveURL(/[?&]view=people/);
// Recycle bin (the last view visited) does not touch the URL itself,
// so the NavRail's ?view= write is still there to assert on.
await expect(page).toHaveURL(/[?&]view=trash/);

await close();
});
Expand Down
32 changes: 24 additions & 8 deletions e2e/tests/onedrive-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect, freshLogin } from "../helpers/fixtures";
import { seedFile } from "../helpers/seed";
import { openMyFiles } from "../helpers/onedrive";
import { openMyFiles, navigateToView } from "../helpers/onedrive";
import { TEST_TIMEOUTS, UI_TIMEOUTS } from "../config";

/**
Expand Down Expand Up @@ -47,7 +47,10 @@ test("selecting a file reveals the contextual action strip", async ({ browser, p
const strip = page.locator("selection-actions");
await expect(strip).toBeVisible();
await expect(page.locator(".odl-page-title")).toHaveCount(0);
for (const action of ["Share", "Copy link", "Delete", "Download"]) {
// Delete is "Move to bin" for a file selection — soft delete. Folders
// would show "Delete" instead, which hard-deletes directly since
// folders aren't catalog-backed and can't be tombstoned.
for (const action of ["Share", "Copy link", "Move to bin", "Download"]) {
await expect(strip.getByRole("button", { name: action, exact: true })).toBeVisible();
}
await expect(page.getByRole("button", { name: "Clear selection" })).toBeVisible();
Expand Down Expand Up @@ -94,7 +97,7 @@ test("Copy link writes the resource URI to the clipboard and confirms with a toa
await close();
});

test("Delete asks for confirmation and removes the row on confirm", async ({ browser, peach }) => {
test("Move to bin asks for confirmation, removes the row, and the file lands in the Recycle bin", async ({ browser, peach }) => {
test.setTimeout(TEST_TIMEOUTS.medium);

await seedFile({
Expand All @@ -113,24 +116,37 @@ test("Delete asks for confirmation and removes the row on confirm", async ({ bro
await expect(fileRow).toBeVisible({ timeout: UI_TIMEOUTS.medium });
await fileRow.click();

// Delete opens the confirmation dialog. Cancelling leaves the row.
await page.locator("selection-actions").getByRole("button", { name: "Delete", exact: true }).click();
// Move to bin opens the confirmation dialog. Cancelling leaves the row.
await page.locator("selection-actions").getByRole("button", { name: "Move to bin", exact: true }).click();
const dialog = page.locator("confirm-dialog");
await expect(dialog).toBeVisible();
await expect(dialog.locator(".confirm-dialog__message")).toContainText("Holiday Snapshot");
await dialog.getByRole("button", { name: "Cancel" }).click();
await expect(dialog).toHaveCount(0);
await expect(fileRow).toBeVisible();

// Confirming deletes the per-file container. The row disappears, the
// selection clears, and the header drops back to the title.
await page.locator("selection-actions").getByRole("button", { name: "Delete", exact: true }).click();
// Confirming soft-deletes: the file moves to trash/ and the original is
// removed. The row disappears, the selection clears, and the header
// drops back to the title.
await page.locator("selection-actions").getByRole("button", { name: "Move to bin", exact: true }).click();
await page.locator("confirm-dialog").getByRole("button", { name: "Confirm" }).click();

await expect(fileRow).toHaveCount(0, { timeout: UI_TIMEOUTS.medium });
await expect(page.locator("selection-actions")).toHaveCount(0);
await expect(page.locator(".odl-page-title")).toHaveText("My Files");

// The trash container is a real, plainly-named folder on the Pod, but it
// does not clutter the regular My Files listing — it's reachable only
// through the Recycle bin nav item.
await expect(page.locator(".odl-files-row--folder").filter({ hasText: /^trash$/ })).toHaveCount(0);

// The file now shows up in the Recycle bin — see trash.spec.ts for the
// full restore/purge/expiry coverage.
await navigateToView(page, "Recycle bin");
await expect(page.locator("trash-row").filter({ hasText: "Holiday Snapshot" })).toBeVisible({
timeout: UI_TIMEOUTS.medium,
});

await close();
});

Expand Down
Loading