Ctrl K
ComunidadGuías…

test/report.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import JSZip from "jszip";
import { buildDonor } from "./donor-fixture.js";
import {
  REPORT_SECTIONS,
  buildBody,
  presentSections,
  validateReport,
  resolveDonor,
  donorStatus,
  renderReport,
  assembleDocx,
  tocTable,
  locatePages,
} from "../src/report.js";
import { reportPreview } from "../src/preview.js";

const IDENTITY = {
  academic: { exam_type: "Innovative Examination (IE)", subject: "Operating Systems", year: "2025-2026" },
  guide: { name: "Ms. L. Suganya", designation: "Assistant Professor" },
  team: {
    label: "Group no. 2b [18-24]",
    members: [{ name: "Deepesh Sonar", roll: "21" }, { name: "Vedant Sud", roll: "23" }],
  },
};

function report(over = {}) {
  return {
    title: "A comparative study",
    content: {
      Abstract: { paragraphs: ["The abstract body."] },
      Introduction: { paragraphs: ["First paragraph.", "Second paragraph."] },
      "Theoretical Background": {
        paragraphs: ["The theory."],
        table: { caption: "Table 1: Comparison.", header: ["Parameter", "NVIDIA", "AMD"], rows: [["Model", "SIMT", "SIMD"]] },
      },
      References: { entries: ["1. First source.", "2. Second source."] },
    },
    ...over,
  };
}

const HEADING = (body) =>
  [...body.matchAll(/>(\d+)\. (Abstract|Acknowledgement|Introduction|Theoretical Background|Application|Future Scope|Conclusion|References)</g)]
    .map((m) => `${m[1]}. ${m[2]}`);

test("validateReport accepts a full report and rejects malformed ones", async () => {
  assert.deepEqual(await validateReport(report()), { ok: true, errors: [] });

  const noTitle = report();
  delete noTitle.title;
  assert.equal((await validateReport(noTitle)).ok, false);

  // A section outside the default eight is accepted — the structure belongs to
  // whichever donor template is installed, and a topic may earn one of its own.
  const custom = report();
  custom.content["Grid Integration Case Study"] = { paragraphs: ["A real section."] };
  custom.order = [...presentSections(report()), "Grid Integration Case Study"];
  assert.deepEqual(await validateReport(custom), { ok: true, errors: [] });

  // Accepted by NAME, still held to the section shape by CONTENT.
  const malformedCustom = report();
  malformedCustom.content["Grid Integration Case Study"] = { paragraphs: "not an array" };
  assert.equal((await validateReport(malformedCustom)).ok, false);

  const tooLong = report();
  tooLong.content.Introduction.paragraphs = ["x".repeat(5000)];
  assert.equal((await validateReport(tooLong)).ok, false);
});

test("presentSections keeps the fixed order and drops empty sections", () => {
  const r = report({
    content: {
      Conclusion: { paragraphs: ["End."] },
      Abstract: { paragraphs: ["Start."] },
      Introduction: { paragraphs: [] },
      Acknowledgement: {},
      References: { entries: ["1. Src."] },
    },
  });
  assert.deepEqual(presentSections(r), ["Abstract", "Conclusion", "References"]);
});

test("the body emits sections in the fixed order regardless of file order", () => {
  const r = report({
    content: {
      References: { entries: ["1. Src."] },
      Abstract: { paragraphs: ["A."] },
      Conclusion: { paragraphs: ["C."] },
      Acknowledgement: { paragraphs: ["G."] },
      Introduction: { paragraphs: ["I."] },
      "Theoretical Background": { paragraphs: ["T."] },
      Application: { paragraphs: ["App."] },
      "Future Scope": { paragraphs: ["F."] },
    },
  });
  const body = buildBody(r, IDENTITY, presentSections(r), {}, { includeToc: false });
  const headings = HEADING(body);
  assert.deepEqual(headings, REPORT_SECTIONS.map((s, i) => `${i + 1}. ${s}`));
});

test("empty sections are skipped gracefully, never a bare heading", () => {
  const r = report({ content: { Abstract: { paragraphs: ["A."] }, Acknowledgement: {} } });
  const body = buildBody(r, IDENTITY, presentSections(r), {}, { includeToc: false });
  assert.deepEqual(HEADING(body), ["1. Abstract"]);
});

test("content injection: paragraphs, references, caption and table all land", () => {
  const r = report();
  const body = buildBody(r, IDENTITY, presentSections(r), {}, { includeToc: false });
  assert.ok(body.includes("The abstract body."));
  assert.ok(body.includes("1. Abstract"));
  assert.ok(body.includes("First paragraph."));
  assert.ok(body.includes("Second paragraph."));
  assert.ok(body.includes("1. First source."));
  assert.ok(body.includes("Table 1: Comparison."));
  assert.ok(body.includes("Parameter"));
  assert.ok(body.includes("SIMT"));
  // The donor's own body text must never leak into a report.
  assert.ok(!body.includes("Second paragraph that must not survive"));
});

test("the TOC table lists present sections with the located page numbers", () => {
  const table = tocTable(["Abstract", "References"], { Abstract: 4, References: 7 });
  assert.ok(table.includes("TABLE OF CONTENT") === false); // heading is separate
  assert.ok(table.includes("ABSTRACT"));
  assert.ok(table.includes("REFERENCES"));
  assert.ok(table.includes("SR. NO."));
  assert.ok(table.includes(">4<"));
  assert.ok(table.includes(">7<"));
});

test("locatePages finds the page each heading landed on", () => {
  const text = "cover stuff\n\f\n\fTABLE OF CONTENT\n\f1. Abstract\nbody\n\fbody two\n2. Introduction\n\f3. References";
  const pages = locatePages(text, ["Abstract", "Introduction", "References", "Conclusion"]);
  assert.equal(pages.Abstract, 4);
  assert.equal(pages.Introduction, 5);
  assert.equal(pages.References, 6);
  assert.equal(pages.Conclusion, null); // heading never found in the text
});

test("a missing donor fails with a clear message, not a half-working render", async () => {
  await assert.rejects(resolveDonor(path.join("/nonexistent", "donor.docx")), /donor not found/);

  const empty = await mkdtemp(path.join(tmpdir(), "forge-ref-"));
  await assert.rejects(resolveDonor(null, empty), /report donor \.docx not found/);

  const multi = await mkdtemp(path.join(tmpdir(), "forge-ref-"));
  await writeFile(path.join(multi, "a.docx"), "x");
  await writeFile(path.join(multi, "b.docx"), "x");
  await assert.rejects(resolveDonor(null, multi), /multiple donors/);
});

test("renderReport preserves the donor chrome and strips its body", async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), "forge-report-"));
  t.after(() => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true })));

  const donorPath = path.join(dir, "donor.docx");
  await writeFile(donorPath, await buildDonor());
  const reportFile = path.join(dir, "report.yaml");
  await writeFile(reportFile, `title: "A comparative study"\ncontent:\n  Abstract:\n    paragraphs:\n      - "Injected abstract."\n`);

  const r = await renderReport({ reportFile, donor: donorPath, toc: false });
  assert.equal(r.outFile, path.join(dir, "out", "report.docx"));
  assert.deepEqual(r.sections, ["Abstract"]);

  const zip = await JSZip.loadAsync(await readFile(r.outFile));
  const header = await zip.file("word/header1.xml").async("string");
  const footer = await zip.file("word/footer1.xml").async("string");
  const styles = await zip.file("word/styles.xml").async("string");
  const doc = await zip.file("word/document.xml").async("string");

  // Chrome parts survive byte-for-byte.
  assert.ok(header.includes("WordPictureWatermark1"));
  assert.ok(footer.includes("PAGE"));
  assert.ok(styles.includes('w:styleId="a0"'));

  // Media survives.
  assert.ok(zip.file("word/media/image1.png"));

  // Body stripped and injected; section properties (which carry the chrome
  // references and page geometry) preserved verbatim.
  assert.ok(!doc.includes("DONOR BODY TEXT TO BE STRIPPED"));
  assert.ok(doc.includes("Injected abstract."));
  assert.ok(doc.includes("<w:sectPr>"));
  assert.ok(doc.includes('<w:headerReference w:type="default" r:id="rId3"/>'));
});

test("assembleDocx errors clearly when the donor has no sectPr", async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), "forge-report-"));
  t.after(() => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true })));
  const donorPath = path.join(dir, "donor.docx");
  const zip = new JSZip();
  zip.file("word/document.xml", '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p/></w:body></w:document>');
  await writeFile(donorPath, await zip.generateAsync({ type: "nodebuffer" }));

  const r = report();
  await assert.rejects(
    assembleDocx(donorPath, r, IDENTITY, presentSections(r)),
    /no body-level <w:sectPr>/,
  );
});

test("reportPreview rasterises the rendered docx to one PNG per page", async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), "forge-report-prev-"));
  t.after(() => import("node:fs/promises").then(({ rm }) => rm(dir, { recursive: true, force: true })));

  const donorPath = path.join(dir, "donor.docx");
  await writeFile(donorPath, await buildDonor());
  const reportFile = path.join(dir, "report.yaml");
  await writeFile(reportFile, `title: "A comparative study"\ncontent:\n  Abstract:\n    paragraphs:\n      - "Injected abstract."\n      - "A second paragraph."\n`);

  const r = await renderReport({ reportFile, donor: donorPath, toc: false });
  const p = await reportPreview(r.outFile);

  assert.ok(p.pages.length >= 1, "at least one page rasterised");
  assert.equal(p.pages.length, p.thumbs.length);

  // The PNG is a real raster of the document, not a placeholder.
  const meta = await import("sharp").then((s) => s.default(p.pages[0]).metadata());
  assert.equal(meta.format, "png");
  assert.ok(meta.width > 400, "page is wide enough to be a real render");

  // The preview lives in out/report-preview so it never collides with the
  // deck's slide previews.
  assert.ok(p.pages[0].includes(path.join("out", "report-preview")));
  assert.ok(p.pdf.endsWith(".pdf"));
});

/* ------------------------------------------------------------ donor state */

test("donorStatus names the three states a box can be in", async () => {
  const dir = await mkdtemp(path.join(tmpdir(), "forge-donor-"));
  try {
    // A directory that does not exist at all — the fresh-container case.
    const absent = await donorStatus(path.join(dir, "nothing-here"));
    assert.equal(absent.ok, false);
    assert.equal(absent.reason, "missing");

    await writeFile(path.join(dir, "notes.txt"), "not a template");
    const empty = await donorStatus(dir);
    assert.equal(empty.ok, false);
    assert.equal(empty.reason, "missing", "a non-.docx file is not a donor");

    await writeFile(path.join(dir, "Template.docx"), "PK");
    const one = await donorStatus(dir);
    assert.equal(one.ok, true);
    assert.deepEqual(one.donors, ["Template.docx"]);

    // resolveDonor refuses to guess between several, so the status has to say
    // so rather than reporting a healthy box that fails only at render time.
    await writeFile(path.join(dir, "Other.docx"), "PK");
    const many = await donorStatus(dir);
    assert.equal(many.ok, false);
    assert.equal(many.reason, "ambiguous");
    assert.equal(many.donors.length, 2);
  } finally {
    await rm(dir, { recursive: true, force: true });
  }
});