Files
STCourses/packages/feature-studio/test/convert-ui.test.tsx
T
Lucas WintherandClaude Opus 5.5 f02e30450c fix(studio): review round 2 - a conversion that fails after its commit recovers without a restart
- 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]>
2026-10-30 21:00:45 +01:00

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");
});