Files
C.E.L_Slide_test2/Front/client/tests/imp90_bottom_actions.test.ts
kyeongmin 943957562f
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
feat(#90): IMP-56 u20 BottomActions wiring to /api/connect + /api/export (replace placeholder toasts + standalone HTML download + cel mirror connect; pure builders exported for vitest)
Stage 2 final unit for Step 22 (user edit + export). u20 wires the previously
placeholder bottom-action footer to the u18 /api/connect and u19 /api/export
middlewares living in Front/vite.config.ts:

- BottomActions.tsx
  • drops the dead `serializeSlidePlan` import (TS2305 blocker since u14;
    project-wide `tsc --noEmit` now exits 0)
  • exports three pure builders for vitest (no jsdom / RTL devDep needed):
      buildConnectRequest(run_id, slug) -> POST /api/connect {run_id, slug}
      buildExportRequest(run_id)        -> POST /api/export  {run_id}
      buildDownloadFilename(run_id)     -> "<run_id>.html"
  • handleExport: POST -> blob -> a[download] click chain; toast on
    success / failure / network error.
  • handleConnect: derives slug via deriveUserOverridesKey(uploadedFile.name)
    and PUTs to u18 cel mirror; reports assets_copied count.
  • both buttons disable when runMeta is null so the UI cannot fire
    requests with an undefined run_id.

- Home.tsx
  • mounts <BottomActions/> in the footer with
    {slidePlan, runMeta, uploadedFile, isLoading, onGenerate}.
  • removes 2 of 3 placeholder `toast.info('… 준비 중입니다.')` buttons
    (LeftMdxPanel MDX-edit placeholder remains — out of u20 scope).
  • adds handleTextEdit (u15 wire to text_overrides axis) and
    handleStructureEdit (u15 wire to structure_overrides axis) to satisfy
    the SlideCanvas props introduced earlier in the u-series.

- imp90_bottom_actions.test.ts (new)
  • 11 vitest specs locking the builder URL + JSON shape against u18/u19
    middleware contracts. Verified 11/11 pass.

Stage 4 verification (all PASS):
  • u20 vitest: 11/11
  • u18/u19 endpoint vitest: 31/31
  • npx tsc --noEmit: exit 0 (carry-forward TS2305 resolved)
  • backend pytest (u1~u9 + u17 print mode, 9 files): 185/185

Out of scope:
  • LeftMdxPanel.tsx:333 MDX-edit placeholder toast (separate unit)
  • #1 / #72 / #74 / #79 / #80 / #81 / #93 closed dependencies (no re-impl)
  • AI-generated HTML structure (Phase Z regression guard)
  • frame swap via structure_overrides (locked to slot_order + hidden_slots)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:31:38 +09:00

91 lines
3.8 KiB
TypeScript

// IMP-56 (#90) u20 — vitest coverage for the pure request builders exported
// by `BottomActions`. The React component itself is not rendered (jsdom /
// @testing-library NOT in Front devDependencies — verified against the prior
// u14 `imp90_structure_overlay.test.tsx` pattern); we test the deterministic
// pieces that drive the network payload sent to the u18 / u19 middlewares.
//
// Upstream / downstream contracts (verified by prior units):
// - u18 /api/connect : body shape = { run_id, slug } (Front/vite.config.ts
// handleConnectMirror — `imp90_connect_endpoint.test.ts`).
// - u19 /api/export : body shape = { run_id }; response = raw text/html
// with `Content-Disposition: attachment; filename="<run_id>.html"`
// (Front/vite.config.ts handleExportStandalone —
// `imp90_export_endpoint.test.ts`).
//
// u20 scope: builders only. Any drift in URL or JSON shape fails here before
// the request leaves the client. Toast / fetch / blob plumbing is not tested
// (it would require jsdom + a fetch mock; the existing server-side tests
// already pin the wire contract).
import { describe, it, expect } from "vitest";
import {
buildConnectRequest,
buildExportRequest,
buildDownloadFilename,
} from "../src/components/BottomActions";
describe("buildConnectRequest", () => {
it("targets /api/connect", () => {
const { url } = buildConnectRequest("run_42", "mdx_03");
expect(url).toBe("/api/connect");
});
it("emits { run_id, slug } JSON body — matches u18 middleware shape", () => {
const { body } = buildConnectRequest("run_42", "mdx_03");
expect(JSON.parse(body)).toEqual({ run_id: "run_42", slug: "mdx_03" });
});
it("preserves zero-length and unicode run_id verbatim (server validates)", () => {
const { body } = buildConnectRequest("", "x");
expect(JSON.parse(body)).toEqual({ run_id: "", slug: "x" });
const { body: uni } = buildConnectRequest("런", "슬러그");
expect(JSON.parse(uni)).toEqual({ run_id: "런", slug: "슬러그" });
});
it("does not leak extra keys (frame swap / overrides etc.)", () => {
const { body } = buildConnectRequest("r", "s");
expect(Object.keys(JSON.parse(body)).sort()).toEqual(["run_id", "slug"]);
});
});
describe("buildExportRequest", () => {
it("targets /api/export", () => {
const { url } = buildExportRequest("run_42");
expect(url).toBe("/api/export");
});
it("emits { run_id } JSON body — matches u19 middleware shape", () => {
const { body } = buildExportRequest("run_42");
expect(JSON.parse(body)).toEqual({ run_id: "run_42" });
});
it("does not leak extra keys (slug / format etc.)", () => {
const { body } = buildExportRequest("r");
expect(Object.keys(JSON.parse(body))).toEqual(["run_id"]);
});
it("preserves zero-length and unicode run_id verbatim (server validates)", () => {
expect(JSON.parse(buildExportRequest("").body)).toEqual({ run_id: "" });
expect(JSON.parse(buildExportRequest("런").body)).toEqual({ run_id: "런" });
});
});
describe("buildDownloadFilename", () => {
it("returns <run_id>.html for the a[download] click chain", () => {
expect(buildDownloadFilename("run_42")).toBe("run_42.html");
});
it("appends exactly one .html suffix even when run_id already ends in .html", () => {
// The server-side `Content-Disposition` already carries the same
// filename; we mirror it verbatim so browser default behavior wins.
// We intentionally do NOT strip a trailing `.html` — run_id is the
// backend's `Path(args.mdx_path).stem`-style key, which never contains
// a dot suffix (validated by `isValidUserOverridesKey` at u18/u19).
expect(buildDownloadFilename("foo.html")).toBe("foo.html.html");
});
it("returns just .html for empty run_id (server rejects upstream)", () => {
expect(buildDownloadFilename("")).toBe(".html");
});
});