feat(web): panneau Fichiers — navigation projet + téléchargements fichier/dossier ZIP — #211
This commit is contained in:
157
frontend/src/features/web/ProjectFilesPanel.test.tsx
Normal file
157
frontend/src/features/web/ProjectFilesPanel.test.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { createMockGateways } from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import type { ProjectFileListing } from "@/domain";
|
||||
import { ProjectFilesPanel } from "./ProjectFilesPanel";
|
||||
import { WebWorkspace } from "./WebWorkspace";
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function listing(path: string, entries: ProjectFileListing["entries"]): ProjectFileListing {
|
||||
return { path, entries };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
window.innerWidth = 1024;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
|
||||
function renderWithGateways(gateways: Gateways, element: ReactElement) {
|
||||
return render(<DIProvider gateways={gateways}>{element}</DIProvider>);
|
||||
}
|
||||
|
||||
describe("ProjectFilesPanel", () => {
|
||||
it("navigates progressively and keeps download actions explicit", async () => {
|
||||
const gateways = createMockGateways();
|
||||
const listProjectFiles = vi.fn(async (_projectId: string, path: string) => {
|
||||
if (path === "src") return listing("src", []);
|
||||
return listing("", [
|
||||
{
|
||||
name: "src",
|
||||
path: "src",
|
||||
kind: "directory",
|
||||
modifiedAtMs: 1_786_780_000_000,
|
||||
},
|
||||
{
|
||||
name: "README.md",
|
||||
path: "README.md",
|
||||
kind: "file",
|
||||
sizeBytes: 2048,
|
||||
modifiedAtMs: 1_786_780_010_000,
|
||||
},
|
||||
]);
|
||||
});
|
||||
gateways.project.listProjectFiles = listProjectFiles;
|
||||
gateways.project.downloadProjectFile = vi.fn(async () => {});
|
||||
gateways.project.downloadProjectDirectory = vi.fn(async () => {});
|
||||
|
||||
renderWithGateways(gateways, <ProjectFilesPanel projectId="p1" />);
|
||||
|
||||
expect(await screen.findByText("README.md")).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Ouvrir" }));
|
||||
|
||||
await waitFor(() => expect(listProjectFiles).toHaveBeenLastCalledWith("p1", "src"));
|
||||
expect(await screen.findByText("Ce dossier est vide.")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "src" })).toBeTruthy();
|
||||
expect(gateways.project.downloadProjectDirectory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("downloads files and folders through separate row actions with a preparation state", async () => {
|
||||
const gateways = createMockGateways();
|
||||
const fileDownload = deferred<void>();
|
||||
gateways.project.listProjectFiles = vi.fn(async () =>
|
||||
listing("", [
|
||||
{ name: "docs", path: "docs", kind: "directory" },
|
||||
{ name: "README.md", path: "README.md", kind: "file", sizeBytes: 1024 },
|
||||
]),
|
||||
);
|
||||
gateways.project.downloadProjectFile = vi.fn(() => fileDownload.promise);
|
||||
gateways.project.downloadProjectDirectory = vi.fn(async () => {});
|
||||
|
||||
renderWithGateways(gateways, <ProjectFilesPanel projectId="p1" />);
|
||||
|
||||
const list = await screen.findByRole("list", { name: "Fichiers du projet" });
|
||||
const readmeRow = within(list).getByText("README.md").closest("li");
|
||||
const docsRow = within(list).getByText("docs").closest("li");
|
||||
expect(readmeRow).toBeTruthy();
|
||||
expect(docsRow).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(readmeRow!).getByRole("button", { name: "Télécharger" }));
|
||||
expect(await screen.findByText("Préparation du téléchargement…")).toBeTruthy();
|
||||
expect(gateways.project.downloadProjectFile).toHaveBeenCalledWith("p1", "README.md");
|
||||
expect(gateways.project.downloadProjectDirectory).not.toHaveBeenCalled();
|
||||
|
||||
fileDownload.resolve();
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText("Préparation du téléchargement…")).toBeNull(),
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
within(docsRow!).getByRole("button", { name: "Télécharger le dossier" }),
|
||||
);
|
||||
expect(gateways.project.downloadProjectDirectory).toHaveBeenCalledWith("p1", "docs");
|
||||
});
|
||||
|
||||
it("surfaces list errors", async () => {
|
||||
const gateways = createMockGateways();
|
||||
gateways.project.listProjectFiles = vi.fn(async () => {
|
||||
throw { code: "INVALID", message: "path outside project" };
|
||||
});
|
||||
|
||||
renderWithGateways(gateways, <ProjectFilesPanel projectId="p1" />);
|
||||
|
||||
expect((await screen.findByRole("alert")).textContent).toContain("path outside project");
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebWorkspace files integration", () => {
|
||||
async function setupWorkspace() {
|
||||
const gateways = createMockGateways();
|
||||
await gateways.project.createProject("IdeA", "/srv/idea");
|
||||
gateways.project.listProjectFiles = vi.fn(async () =>
|
||||
listing("", [{ name: "README.md", path: "README.md", kind: "file", sizeBytes: 512 }]),
|
||||
);
|
||||
gateways.project.downloadProjectFile = vi.fn(async () => {});
|
||||
gateways.project.downloadProjectDirectory = vi.fn(async () => {});
|
||||
return gateways;
|
||||
}
|
||||
|
||||
it("opens Fichiers from the desktop Panneaux menu", async () => {
|
||||
const gateways = await setupWorkspace();
|
||||
renderWithGateways(gateways, <WebWorkspace />);
|
||||
|
||||
fireEvent.click(await screen.findByText("IdeA"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Panneaux" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Fichiers" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Flottant" }));
|
||||
|
||||
expect(await screen.findByTestId("project-files-panel")).toBeTruthy();
|
||||
expect(screen.getByText("README.md")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("exposes Fichiers as a first-level mobile destination", async () => {
|
||||
window.innerWidth = 390;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
const gateways = await setupWorkspace();
|
||||
renderWithGateways(gateways, <WebWorkspace />);
|
||||
|
||||
fireEvent.click(await screen.findByText("IdeA"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Fichiers" }));
|
||||
|
||||
expect(screen.getByTestId("web-workspace").getAttribute("data-shell")).toBe(
|
||||
"mobile-task-flow",
|
||||
);
|
||||
expect(await screen.findByTestId("project-files-panel")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user