Tests Custom Reports

Writing the Function

The function must be a single synchronous function named anonymous that returns a plain object; helpers go inside it, and object spread, optional chaining (?.) and nullish coalescing (??) are rejected by the parser.

Published 2026/10/15

The function turns the Report Input into the values your template prints. It is optional, but recommended for every report beyond the simplest.

The contract

function anonymous(data) {
  // ...compute...
  return { /* a plain object */ };
}
RuleWhy
Exactly one top-level function, named anonymous, taking one parameter.The validator rejects anything else — including const anonymous = (data) => ….
Helper functions go inside anonymous.A second top-level statement is rejected.
Not async; no await or Promises.The function runs synchronously.
Return a plain object — not an array, string or null.The template's variables are the keys of that object.
Return only data: objects, arrays, strings, numbers, booleans, null.The output is copied out of the sandbox. A function or class instance inside it fails with could not be cloned.
No network, require, import, fetch or timers.They don't exist in the sandbox.
Finish within 5 seconds and 128 MB.Longer runs are stopped with Script execution timed out.

Supported syntax

JavaScript up to ES2017 is supported: const/let, arrow functions, template literals, destructuring, for…of, Object.keys/values/entries, array spread [...list]. Three newer forms are rejected with “Unexpected token”:

Not supportedWrite instead
Object spread { ...obj, x: 1 }Object.assign({}, obj, { x: 1 })
Optional chaining a?.ba && a.b
Nullish coalescing a ?? ba !== undefined && a !== null ? a : b

Design guidelines

  1. Put the logic in the function, keep the template simple. Compute percentages, bands, sorted lists and chosen texts in JavaScript; the template only loops and prints. The Output: Data panel lets you check every value before any HTML is involved.
  2. Return a view model, not the raw input. Name values for the page — overall.percent, dimensions[].band.label — so the template never digs through result.dimensions["Aptitude/Verbal"].stats.point.
  3. Guard everything that may be missing: result, ticket, normy, testReporting, dates.completed. Return a ready: false flag and render a friendly message for it.
  4. Never divide by zero — a section or dimension can have a totalPoint of 0.
  5. Keep all display texts in one lookup object (band descriptions, level texts). There is one place to edit, and the longest text is easy to find when you check the PDF layout.
  6. Make the order explicit. Sort lists yourself instead of relying on object key order.

Complete example

Overall score and band, the pass/fail verdict from the test's own Pass / Fail settings, a table of top-level dimensions with their benchmark percentile, and section scores. It handles unscored sessions and missing benchmark or reporting data. The matching template is on Writing the Template.

function anonymous(data) {
  // ---- helpers (must live INSIDE the function) ----
  function pct(point, total) {
    if (!total) return 0;
    return Math.round((point / total) * 100);
  }
  function bandOf(p) {
    if (p >= 80) return { key: "high", label: "Strong", color: "#1b7f4b" };
    if (p >= 60) return { key: "mid", label: "Proficient", color: "#b7791f" };
    return { key: "low", label: "Developing", color: "#c53030" };
  }

  const result = data.result;
  if (!result) {
    // session not completed / not scored yet
    return { ready: false };
  }

  // ---- overall ----
  const percent = pct(result.stats.point, result.stats.totalPoint);

  // pass/fail from the test's own Pass / Fail settings (may be missing in some contexts)
  let verdict = null;
  const pf = data.testReporting && data.testReporting.passFail;
  if (pf && pf.active) {
    const min = pf.pass.min;
    const threshold = min.score * 100; // stored as a 0-1 fraction
    const passed = min.inclusive ? percent >= threshold : percent > threshold;
    verdict = { passed: passed, label: passed ? pf.pass.content.label : pf.fail.content.label };
  }

  // ---- dimensions: only top-level dimensions ("Aptitude", not "Aptitude/Verbal") ----
  const normDims = data.normy && data.normy.normativeResult ? data.normy.normativeResult.dimensions : {};
  const dimensions = Object.keys(result.dimensions)
    .filter(function (name) { return name.indexOf("/") === -1; })
    .sort()
    .map(function (name) {
      const s = result.dimensions[name].stats;
      const p = pct(s.point, s.totalPoint);
      const norm = normDims[name];
      return {
        name: name,
        point: s.point,
        totalPoint: s.totalPoint,
        percent: p,
        band: bandOf(p),
        percentile: norm ? norm.percentile : null
      };
    });

  // ---- sections ----
  const sections = result.sections.map(function (sec, i) {
    return {
      title: sec.title || "Section " + (i + 1),
      percent: pct(sec.stats.point, sec.stats.totalPoint)
    };
  });

  return {
    ready: true,
    candidate: {
      name: data.ticket && data.ticket.info ? data.ticket.info.fullname : "",
      email: data.ticket && data.ticket.info ? data.ticket.info.email : ""
    },
    testTitle: data.info.testTitle,
    completedAt: data.dates.completed ? data.dates.completed.text : "",
    overall: { point: result.stats.point, totalPoint: result.stats.totalPoint, percent: percent, band: bandOf(percent) },
    verdict: verdict,
    dimensions: dimensions,
    sections: sections
  };
}

Its output, for a candidate with 34 of 50 points and a 60% pass mark, starts like this — the exact shape the template works with:

{
  "ready": true,
  "candidate": { "name": "Jane Doe", "email": "jane@example.com" },
  "testTitle": "Sales Aptitude Test",
  "overall": { "point": 34, "totalPoint": 50, "percent": 68,
               "band": { "key": "mid", "label": "Proficient", "color": "#b7791f" } },
  "verdict": { "passed": true, "label": "Pass" },
  "dimensions": [ { "name": "Aptitude", "percent": 73, "percentile": null, … }, … ],
  "sections": [ { "title": "Reasoning", "percent": 73 }, … ]
}