diff --git a/frontend/packages/swarm-ui/test/tryout-types.test.mjs b/frontend/packages/swarm-ui/test/tryout-types.test.mjs new file mode 100644 index 00000000..95fa18af --- /dev/null +++ b/frontend/packages/swarm-ui/test/tryout-types.test.mjs @@ -0,0 +1,132 @@ +// The review try-out form's pure logic: the repo and row checks, the gate on +// opening a PR from the rows, and the request body each row becomes. Run by +// `npm test`, which Node runs with its built-in test runner and type +// stripping, so the `.ts` module is imported as it is. +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + draftFromRule, + draftProblem, + duplicateName, + hasTypedText, + prNumber, + repoProblem, + rowsProblem, + thresholdValue, + toBody, +} from "../src/pages/review-tryout/TryOutTypes.ts"; + +// A row ready to send, with `over` replacing any of its fields. +function row(over = {}) { + return { + key: 0, + name: "no-ads", + description: "No ads.", + polarity: "negative", + threshold: "80", + ...over, + }; +} + +test("a repo is owner/name with forge-safe characters in each part", () => { + assert.equal(repoProblem("hyperhive/hyperhive"), null); + assert.equal(repoProblem(" o/r.name_x-1 "), null); + for (const bad of ["", "o", "o/", "/r", "o/r/x", "o/..", "./r", "o/r s"]) { + assert.match(repoProblem(bad), /owner\/name/, bad); + } +}); + +test("a row needs a name, then a description, then a whole 0-100 threshold", () => { + assert.equal(draftProblem(row()), null); + assert.match(draftProblem(row({ name: " " })), /a name/); + assert.match( + draftProblem(row({ name: " ", description: "" })), + /a name/, + "the name is checked first", + ); + assert.match( + draftProblem(row({ description: " " })), + /no-ads needs a description/, + ); + assert.match( + draftProblem(row({ description: "", threshold: "x" })), + /description/, + "the description is checked before the threshold", + ); + for (const bad of ["", "101", "-1", "1.5", "x"]) { + assert.match(draftProblem(row({ threshold: bad })), /threshold/, bad); + } + assert.equal(draftProblem(row({ threshold: " 100 " })), null); + assert.equal(draftProblem(row({ threshold: "0" })), null); +}); + +test("two rows with one trimmed name are refused", () => { + assert.equal(duplicateName([row(), row({ name: "other" })]), null); + assert.match( + duplicateName([row(), row({ name: " no-ads " })]), + /two classifications are named no-ads/, + ); +}); + +test("opening a PR is gated by the repo, then each row, then a shared name", () => { + const good = [row(), row({ name: "other" })]; + assert.equal(rowsProblem("o/r", good), null); + + // Each check on its own. + assert.match(rowsProblem("", good), /owner\/name/); + assert.match(rowsProblem("o/r", [row(), row({ name: "" })]), /a name/); + assert.match(rowsProblem("o/r", [row(), row()]), /two classifications/); + + // The order: a bad repo wins over a bad row, and a bad row over a shared + // name. + assert.match(rowsProblem("o", [row({ name: "" })]), /owner\/name/); + assert.match( + rowsProblem("o/r", [row(), row({ threshold: "x" }), row()]), + /threshold/, + ); + // Rows are checked in their order. + assert.match( + rowsProblem("o/r", [row({ description: "" }), row({ name: "" })]), + /no-ads needs a description/, + ); +}); + +test("opening a PR needs no PR number, which a run does", () => { + assert.equal(prNumber(""), null); + assert.equal(rowsProblem("o/r", [row()]), null); + assert.equal(prNumber(" 7 "), 7); + assert.equal(prNumber("0"), null); + assert.equal(prNumber("7a"), null); +}); + +test("a row becomes the request's classification", () => { + assert.deepEqual( + toBody( + row({ name: " no-ads ", description: " No ads. ", threshold: " 60 " }), + ), + { + name: "no-ads", + description: " No ads. ", + polarity: "negative", + threshold: 60, + }, + ); + assert.equal(thresholdValue("101"), null); +}); + +test("a loaded rule becomes a row that sends back the same rule", () => { + const rule = { + name: "no-ads", + description: "No ads.", + polarity: "negative", + threshold: 80, + }; + const d = draftFromRule(rule, 3); + assert.deepEqual(d, { key: 3, ...rule, threshold: "80" }); + assert.deepEqual(toBody(d), rule); +}); + +test("only a typed name or description counts as the user's work", () => { + assert.equal(hasTypedText([row({ name: " ", description: "" })]), false); + assert.equal(hasTypedText([row({ name: "", description: "x" })]), true); +});