- The convert job reloads the course on success and on failure, so learners get the intact import (rolled back) or the published studio course, never a course whose folder moved away. - POST /convert on a course with an incomplete conversion (.converting-<id>, staging or retired folders) queues a job that recovers and finishes it instead of answering 409; the studio page shows "Conversion incomplete" with Retry for the owner. - The 409 for pending import jobs names their kinds and ids; the dead withLock in git.ts is gone; the dot-folder discovery rule is documented in docs/operations.md and spec section 5. - Route tests (SQLite and Postgres) inject a failed cross-filesystem copy, a checksum mismatch and a pre-commit failure. Co-Authored-By: Claude Opus 5.5 <[email protected]>
52 lines
3.0 KiB
TypeScript
52 lines
3.0 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { renderToString } from "react-dom/server";
|
|
import { ConvertPanelView, IncompleteConversionView, ReadOnlyView } from "../src/ui/ConvertPanel";
|
|
|
|
const noop = () => {};
|
|
|
|
test("the owner sees the action and what it does", () => {
|
|
const html = renderToString(<ConvertPanelView phase={{ kind: "idle" }} onConvert={noop} />);
|
|
expect(html).toContain("Make editable in the studio");
|
|
expect(html).toContain("same id");
|
|
expect(html).toContain("progress and notes");
|
|
expect(html).toContain("outside git");
|
|
expect(html).toContain("<button");
|
|
});
|
|
|
|
test("progress and failure states", () => {
|
|
const running = renderToString(<ConvertPanelView phase={{ kind: "running", progress: 0.4, message: "Checksumming media/a.mp4" }} onConvert={noop} />);
|
|
expect(running).toContain('role="progressbar"');
|
|
expect(running).toContain('aria-valuenow="40"');
|
|
expect(running).toContain("Checksumming media/a.mp4");
|
|
expect(running).toContain("disabled");
|
|
const failed = renderToString(<ConvertPanelView phase={{ kind: "failed", error: "the imported manifest is invalid" }} onConvert={noop} />);
|
|
expect(failed).toContain('role="alert"');
|
|
expect(failed).toContain("the imported manifest is invalid");
|
|
expect(failed).toContain("Try again");
|
|
});
|
|
|
|
test("read-only view: imported for the owner offers the action; for others explains who can", () => {
|
|
const owner = renderToString(<ReadOnlyView courseId="imp" info={{ title: "Imp", message: "Imported course", root: "/d/courses/imp", imported: true, convertible: true }} onConverted={noop} />);
|
|
expect(owner).toContain("Make editable in the studio");
|
|
expect(owner).not.toContain("/d/courses/imp");
|
|
const author = renderToString(<ReadOnlyView courseId="imp" info={{ title: "Imp", message: "Imported course", root: "/d/courses/imp", imported: true, convertible: false }} onConverted={noop} />);
|
|
expect(author).not.toContain("<button");
|
|
expect(author).toContain("Only the owner");
|
|
const outside = renderToString(<ReadOnlyView courseId="o" info={{ title: "O", message: "This course is managed outside STCourses", root: "/repos/o", imported: false, convertible: false }} onConverted={noop} />);
|
|
expect(outside).toContain("/repos/o");
|
|
expect(outside).not.toContain("<button");
|
|
});
|
|
|
|
test("an incomplete conversion: the owner can retry, others are told who can", () => {
|
|
const owner = renderToString(<IncompleteConversionView phase={{ kind: "idle" }} canRetry onRetry={noop} />);
|
|
expect(owner).toContain("Conversion incomplete");
|
|
expect(owner).toContain(">Retry</button>");
|
|
const author = renderToString(<IncompleteConversionView phase={{ kind: "idle" }} canRetry={false} onRetry={noop} />);
|
|
expect(author).toContain("Conversion incomplete");
|
|
expect(author).not.toContain("<button");
|
|
expect(author).toContain("Ask the owner");
|
|
const failed = renderToString(<IncompleteConversionView phase={{ kind: "failed", error: "disk full" }} canRetry onRetry={noop} />);
|
|
expect(failed).toContain('role="alert"');
|
|
expect(failed).toContain("disk full");
|
|
});
|