mirror of
https://github.com/suitenumerique/drive.git
synced 2026-09-11 20:28:00 +02:00
✅(frontend) add custom columns E2E tests
Test column visibility toggling, sort interactions, and preference persistence across navigation. Add shared test utilities for column assertions.
This commit is contained in:
@@ -13,6 +13,7 @@ and this project adheres to
|
||||
- ✨(frontend) add PDF viewer with thumbnail sidebar, zoom and page navigation
|
||||
- ✨(frontend) integrate PDF viewer into file preview modal
|
||||
- 📝(doc) add local network setup documentation
|
||||
- ✨(global) add custom columns feature with configurable grid columns
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
module.exports = "test-file-stub";
|
||||
module.exports = { src: "test-file-stub", height: 1, width: 1 };
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = function prettyBytes(number) {
|
||||
return String(number);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ const config: Config = {
|
||||
// Handle static assets FIRST (before path aliases)
|
||||
"\\.(css|less|scss|sass|svg|png|jpg|jpeg|gif)$":
|
||||
"<rootDir>/__mocks__/fileMock.js",
|
||||
"^pretty-bytes$": "<rootDir>/__mocks__/pretty-bytes.js",
|
||||
// Then handle path aliases
|
||||
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
@@ -21,7 +22,7 @@ const config: Config = {
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: {
|
||||
jsx: "react",
|
||||
jsx: "react-jsx",
|
||||
moduleResolution: "node",
|
||||
},
|
||||
},
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
declare module "*.svg" {
|
||||
const content: string;
|
||||
const content: { src: string; height: number; width: number };
|
||||
export default content;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { clearDb, login } from "./utils-common";
|
||||
import { clickToMyFiles, clickToRecent } from "./utils-navigate";
|
||||
import {
|
||||
createFolderInCurrentFolder,
|
||||
createFileFromTemplate,
|
||||
importFile,
|
||||
} from "./utils-item";
|
||||
import {
|
||||
changeColumnType,
|
||||
clickColumnSortButton,
|
||||
clickNameSortButton,
|
||||
expectColumnHeaderLabel,
|
||||
expectRowNamesInOrder,
|
||||
getCellText,
|
||||
getColumnHeader,
|
||||
} from "./utils/custom-columns-utils";
|
||||
|
||||
const PDF_FILE_PATH = path.join(__dirname, "/assets/pv_cm.pdf");
|
||||
const DOCX_FILE_PATH = path.join(__dirname, "/assets/empty_doc.docx");
|
||||
|
||||
test.describe("Custom columns", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await clearDb();
|
||||
await login(page, "drive@example.com");
|
||||
await page.goto("/");
|
||||
await clickToMyFiles(page);
|
||||
});
|
||||
|
||||
// ── Group 1: Default columns ──────────────────────────────────
|
||||
|
||||
test("Default columns are Last modified and Created by", async ({
|
||||
page,
|
||||
}) => {
|
||||
await createFolderInCurrentFolder(page, "TestFolder");
|
||||
|
||||
await expectColumnHeaderLabel(page, 1, "Last modified");
|
||||
await expectColumnHeaderLabel(page, 2, "Created by");
|
||||
|
||||
// The Last modified cell should show a relative time
|
||||
const cellText = await getCellText(page, "TestFolder", 1);
|
||||
expect(cellText).toMatch(/seconds? ago|minute/);
|
||||
});
|
||||
|
||||
// ── Group 2: Change column types ──────────────────────────────
|
||||
|
||||
test("Change column 1 type via dropdown", async ({ page }) => {
|
||||
await importFile(page, PDF_FILE_PATH);
|
||||
// Wait for the uploaded file to appear (no optimistic update, needs API round-trip)
|
||||
const fileRow = page.getByRole("row").filter({ hasText: "pv_cm" }).first();
|
||||
await expect(fileRow).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await changeColumnType(page, 1, "File size");
|
||||
await expectColumnHeaderLabel(page, 1, "File size");
|
||||
|
||||
// File should show a size value (not "-")
|
||||
const cellText = await getCellText(page, "pv_cm", 1);
|
||||
expect(cellText).not.toBe("-");
|
||||
expect(cellText).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Change column 2 type via dropdown", async ({ page }) => {
|
||||
await createFolderInCurrentFolder(page, "MyFolder");
|
||||
|
||||
await changeColumnType(page, 2, "File type");
|
||||
await expectColumnHeaderLabel(page, 2, "File type");
|
||||
|
||||
const cellText = await getCellText(page, "MyFolder", 2);
|
||||
expect(cellText).toBe("Folder");
|
||||
});
|
||||
|
||||
test("Change both columns", async ({ page }) => {
|
||||
await createFolderInCurrentFolder(page, "MyFolder");
|
||||
|
||||
await changeColumnType(page, 1, "Created");
|
||||
await changeColumnType(page, 2, "File size");
|
||||
|
||||
await expectColumnHeaderLabel(page, 1, "Created");
|
||||
await expectColumnHeaderLabel(page, 2, "File size");
|
||||
|
||||
// Folder has no file size
|
||||
const sizeText = await getCellText(page, "MyFolder", 2);
|
||||
expect(sizeText).toBe("-");
|
||||
|
||||
// Created should show a relative time
|
||||
const createdText = await getCellText(page, "MyFolder", 1);
|
||||
expect(createdText).toMatch(/seconds? ago|minute/);
|
||||
});
|
||||
|
||||
// ── Group 3: Persistence ──────────────────────────────────────
|
||||
|
||||
test("Column preferences persist after page reload", async ({ page }) => {
|
||||
await createFolderInCurrentFolder(page, "MyFolder");
|
||||
|
||||
await changeColumnType(page, 1, "File type");
|
||||
await expectColumnHeaderLabel(page, 1, "File type");
|
||||
|
||||
await page.reload();
|
||||
await clickToMyFiles(page);
|
||||
|
||||
await expectColumnHeaderLabel(page, 1, "File type");
|
||||
});
|
||||
|
||||
test("Column preferences are shared across views", async ({ page }) => {
|
||||
// Create a file (not a folder) because Recents uses files_only mode
|
||||
await createFileFromTemplate(page, "TestDoc");
|
||||
|
||||
await changeColumnType(page, 1, "File size");
|
||||
await expectColumnHeaderLabel(page, 1, "File size");
|
||||
|
||||
await clickToRecent(page);
|
||||
// Wait for the file to appear in Recents
|
||||
const fileRow = page
|
||||
.getByRole("row")
|
||||
.filter({ hasText: "TestDoc" })
|
||||
.first();
|
||||
await expect(fileRow).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await expectColumnHeaderLabel(page, 1, "File size");
|
||||
});
|
||||
|
||||
// ── Group 4: Sorting ──────────────────────────────────────────
|
||||
|
||||
test("Sort by Name column cycles through asc/desc/default", async ({
|
||||
page,
|
||||
}) => {
|
||||
await createFolderInCurrentFolder(page, "AAA");
|
||||
await createFolderInCurrentFolder(page, "CCC");
|
||||
await createFolderInCurrentFolder(page, "BBB");
|
||||
|
||||
// Default order: folders first, sorted by title = AAA, BBB, CCC
|
||||
await expectRowNamesInOrder(page, ["AAA", "BBB", "CCC"]);
|
||||
|
||||
// Click sort → ascending
|
||||
await clickNameSortButton(page);
|
||||
await expectRowNamesInOrder(page, ["AAA", "BBB", "CCC"]);
|
||||
|
||||
// Click sort → descending
|
||||
await clickNameSortButton(page);
|
||||
await expectRowNamesInOrder(page, ["CCC", "BBB", "AAA"]);
|
||||
|
||||
// Click sort → reset to default
|
||||
await clickNameSortButton(page);
|
||||
await expectRowNamesInOrder(page, ["AAA", "BBB", "CCC"]);
|
||||
});
|
||||
|
||||
test("Sort by a customizable column (Last modified)", async ({ page }) => {
|
||||
await createFolderInCurrentFolder(page, "First");
|
||||
// Delay to ensure distinct updated_at timestamps
|
||||
await page.waitForTimeout(2000);
|
||||
await createFolderInCurrentFolder(page, "Second");
|
||||
|
||||
// Click sort on col1 (Last modified) → ascending (oldest first)
|
||||
await clickColumnSortButton(page, 1);
|
||||
await expectRowNamesInOrder(page, ["First", "Second"]);
|
||||
|
||||
// Click again → descending (newest first)
|
||||
await clickColumnSortButton(page, 1);
|
||||
await expectRowNamesInOrder(page, ["Second", "First"]);
|
||||
});
|
||||
|
||||
test("Sort by File size after changing column type", async ({ page }) => {
|
||||
// Import two files of different sizes to avoid folders_first interference
|
||||
await importFile(page, PDF_FILE_PATH);
|
||||
await page.getByRole("row").filter({ hasText: "pv_cm" }).first().waitFor({
|
||||
state: "visible",
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await importFile(page, DOCX_FILE_PATH);
|
||||
await page
|
||||
.getByRole("row")
|
||||
.filter({ hasText: "empty_doc" })
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
await changeColumnType(page, 1, "File size");
|
||||
await expectColumnHeaderLabel(page, 1, "File size");
|
||||
|
||||
// Click sort on col1 → ascending (smallest first)
|
||||
await clickColumnSortButton(page, 1);
|
||||
await expectRowNamesInOrder(page, ["empty_doc", "pv_cm"]);
|
||||
|
||||
// Click again → descending (largest first)
|
||||
await clickColumnSortButton(page, 1);
|
||||
await expectRowNamesInOrder(page, ["pv_cm", "empty_doc"]);
|
||||
});
|
||||
|
||||
// ── Group 5: Cell content ─────────────────────────────────────
|
||||
|
||||
test("Cells display correct content for each column type", async ({
|
||||
page,
|
||||
}) => {
|
||||
await createFolderInCurrentFolder(page, "TestFolder");
|
||||
|
||||
// Switch col1 to File size → folders show "-"
|
||||
await changeColumnType(page, 1, "File size");
|
||||
const sizeText = await getCellText(page, "TestFolder", 1);
|
||||
expect(sizeText).toBe("-");
|
||||
|
||||
// Switch col2 to File type → folders show "Folder"
|
||||
await changeColumnType(page, 2, "File type");
|
||||
const typeText = await getCellText(page, "TestFolder", 2);
|
||||
expect(typeText).toBe("Folder");
|
||||
|
||||
// Switch col1 to Created → shows relative time
|
||||
await changeColumnType(page, 1, "Created");
|
||||
const createdText = await getCellText(page, "TestFolder", 1);
|
||||
expect(createdText).toMatch(/seconds? ago|minute/);
|
||||
});
|
||||
|
||||
// ── Group 6: Default marker in dropdown ───────────────────────
|
||||
|
||||
test("Default column option is marked in dropdown", async ({ page }) => {
|
||||
await createFolderInCurrentFolder(page, "TestFolder");
|
||||
|
||||
// Open col1 dropdown — default for col1 is "Last modified"
|
||||
const header1 = getColumnHeader(page, 1);
|
||||
const dropdownButton1 = header1
|
||||
.locator(".explorer__grid__header button")
|
||||
.first();
|
||||
await dropdownButton1.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Last modified (default)" }),
|
||||
).toBeVisible();
|
||||
|
||||
// Close dropdown
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Open col2 dropdown — default for col2 is "Created by"
|
||||
const header2 = getColumnHeader(page, 2);
|
||||
const dropdownButton2 = header2
|
||||
.locator(".explorer__grid__header button")
|
||||
.first();
|
||||
await dropdownButton2.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Created by (default)" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import test, { expect } from "@playwright/test";
|
||||
import { clearDb, login } from "./utils-common";
|
||||
import path from "path";
|
||||
import { clickToMyFiles } from "./utils-navigate";
|
||||
import { getRowItem } from "./utils-embedded-grid";
|
||||
import { uploadFile } from "./utils/upload-utils";
|
||||
|
||||
test("Display HEIC not supported message when opening a HEIC file", async ({
|
||||
@@ -21,14 +22,10 @@ test("Display HEIC not supported message when opening a HEIC file", async ({
|
||||
|
||||
// Wait for the file to be uploaded and visible in the list
|
||||
await expect(page.getByText("Drop your files here")).not.toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "test-image.heic" })).toBeVisible(
|
||||
{
|
||||
timeout: 10000,
|
||||
},
|
||||
);
|
||||
|
||||
// Click on the HEIC file to open the preview
|
||||
await page.getByRole("cell", { name: "test-image.heic" }).dblclick();
|
||||
const row = await getRowItem(page, "test-image");
|
||||
await row.dblclick();
|
||||
|
||||
// Check that the file preview is visible
|
||||
const filePreview = page.getByTestId("file-preview");
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Get the <th> element for a customizable column slot.
|
||||
*/
|
||||
export const getColumnHeader = (page: Page, slot: 1 | 2): Locator => {
|
||||
return page.locator(`th.explorer__grid__th--info-col-${slot}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the type of a customizable column via its dropdown menu.
|
||||
*/
|
||||
export const changeColumnType = async (
|
||||
page: Page,
|
||||
slot: 1 | 2,
|
||||
columnLabel: string,
|
||||
) => {
|
||||
const header = getColumnHeader(page, slot);
|
||||
// Click the dropdown button inside the header (first button = dropdown trigger)
|
||||
const dropdownButton = header
|
||||
.locator(".explorer__grid__header button")
|
||||
.first();
|
||||
await dropdownButton.click();
|
||||
|
||||
// Select the desired column type — use exact to avoid "Created" matching "Created by"
|
||||
const menuItem = page.getByRole("menuitem", {
|
||||
name: columnLabel,
|
||||
exact: true,
|
||||
});
|
||||
await expect(menuItem).toBeVisible();
|
||||
await menuItem.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that a column header displays the expected label.
|
||||
*/
|
||||
export const expectColumnHeaderLabel = async (
|
||||
page: Page,
|
||||
slot: 1 | 2,
|
||||
expectedLabel: string,
|
||||
) => {
|
||||
const header = getColumnHeader(page, slot);
|
||||
const button = header.locator(".explorer__grid__header button").first();
|
||||
await expect(button).toContainText(expectedLabel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Click a sort button via native JS click (bypasses tooltip overlay).
|
||||
* Waits for the aria-label to change, confirming the click was processed.
|
||||
*/
|
||||
const clickSortAndWaitForStateChange = async (
|
||||
sortButton: Locator,
|
||||
nextLabel: string,
|
||||
) => {
|
||||
// Use evaluate to trigger a native click — tooltips can intercept Playwright clicks
|
||||
await sortButton.evaluate((el) => (el as HTMLButtonElement).click());
|
||||
// Wait for the button aria-label to change (state updated)
|
||||
await expect(sortButton).toHaveAttribute("aria-label", nextLabel, {
|
||||
timeout: 10000,
|
||||
});
|
||||
};
|
||||
|
||||
const SORT_TRANSITIONS: Record<string, string> = {
|
||||
"Sort ascending": "Sort descending",
|
||||
"Sort descending": "Reset sorting",
|
||||
"Reset sorting": "Sort ascending",
|
||||
};
|
||||
|
||||
/**
|
||||
* Click the sort button for the "Name" column header.
|
||||
*/
|
||||
export const clickNameSortButton = async (page: Page) => {
|
||||
const nameHeader = page.locator("th").nth(1); // second th = Name column
|
||||
const sortButton = nameHeader.locator(
|
||||
".explorer__grid__header button[aria-label]",
|
||||
);
|
||||
const ariaLabel = await sortButton.getAttribute("aria-label");
|
||||
const nextLabel = SORT_TRANSITIONS[ariaLabel ?? ""];
|
||||
if (nextLabel) {
|
||||
await clickSortAndWaitForStateChange(sortButton, nextLabel);
|
||||
} else {
|
||||
await sortButton.evaluate((el) => (el as HTMLButtonElement).click());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Click the sort button for a customizable column (slot 1 or 2).
|
||||
*/
|
||||
export const clickColumnSortButton = async (page: Page, slot: 1 | 2) => {
|
||||
const header = getColumnHeader(page, slot);
|
||||
// Second button in the header = sort button (first = dropdown trigger)
|
||||
const sortButton = header.locator(".explorer__grid__header button").nth(1);
|
||||
const ariaLabel = await sortButton.getAttribute("aria-label");
|
||||
const nextLabel = SORT_TRANSITIONS[ariaLabel ?? ""];
|
||||
if (nextLabel) {
|
||||
await clickSortAndWaitForStateChange(sortButton, nextLabel);
|
||||
} else {
|
||||
await sortButton.evaluate((el) => (el as HTMLButtonElement).click());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all visible row names in order from the grid.
|
||||
*/
|
||||
export const getRowNamesInOrder = async (page: Page): Promise<string[]> => {
|
||||
const nameElements = page.locator(
|
||||
"tbody tr.selectable .explorer__grid__item__name",
|
||||
);
|
||||
const count = await nameElements.count();
|
||||
if (count === 0) return [];
|
||||
const names: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const text = (await nameElements.nth(i).textContent()) ?? "";
|
||||
names.push(text.trim());
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for row names to match the expected order (retries automatically).
|
||||
*/
|
||||
export const expectRowNamesInOrder = async (
|
||||
page: Page,
|
||||
expectedNames: string[],
|
||||
) => {
|
||||
await expect
|
||||
.poll(async () => getRowNamesInOrder(page), { timeout: 10000 })
|
||||
.toEqual(expectedNames);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text content of a cell in a specific row at a given column slot.
|
||||
* Slot 1 = first info column (3rd td), slot 2 = second info column (4th td).
|
||||
*/
|
||||
export const getCellText = async (
|
||||
page: Page,
|
||||
rowName: string,
|
||||
slot: 1 | 2,
|
||||
): Promise<string> => {
|
||||
const row = page
|
||||
.getByRole("row", { name: rowName })
|
||||
.filter({ hasText: rowName })
|
||||
.first();
|
||||
await expect(row).toBeVisible();
|
||||
// td indices: 0=mobile, 1=title, 2=info-col-1, 3=info-col-2, 4=actions
|
||||
const cellIndex = slot === 1 ? 2 : 3;
|
||||
const cell = row.locator("td").nth(cellIndex);
|
||||
return ((await cell.textContent()) ?? "").trim();
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import path from "path";
|
||||
import test, { expect } from "@playwright/test";
|
||||
import { clearDb, login } from "./utils-common";
|
||||
import { clickToMyFiles } from "./utils-navigate";
|
||||
import { getRowItem } from "./utils-embedded-grid";
|
||||
import { uploadFile } from "./utils/upload-utils";
|
||||
import { grantClipboardPermissions } from "./utils/various-utils";
|
||||
|
||||
@@ -19,12 +20,10 @@ test("Copy and paste works in wopi editor", async ({ page, context, browserName
|
||||
|
||||
// Wait for the file to be uploaded and visible in the list
|
||||
await expect(page.getByText("Drop your files here")).not.toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "empty_doc.docx" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Click on the HEIC file to open the preview
|
||||
await page.getByRole("cell", { name: "empty_doc.docx" }).dblclick();
|
||||
// Click on the file to open the preview
|
||||
const row = await getRowItem(page, "empty_doc");
|
||||
await row.dblclick();
|
||||
|
||||
// Check that the file preview is visible
|
||||
const filePreview = page.getByTestId("file-preview");
|
||||
|
||||
Reference in New Issue
Block a user