scmJS docs

Tests

A script can test itself. A test is ordinary TypeScript that runs here, in the simulator, after every change that compiles. It never runs in the game and adds nothing to the map. If you have used Vitest or Jest, the names are the same ones — test, describe, beforeEach, test.only, test.skip, test.each, expect — imported from "trigscript".

import { test, expect } from "trigscript";

// A Marine on the beacon calls the next wave, each bigger than the last.
program(() => {
  let wave = 0;
  while (true) {
    if (countUnits(P1, units.TerranMarine, locations.Beacon) > 0) {
      wave += 1;
      createUnit(P8, units.ZergZergling, wave * 4, locations.Spawn);
      print(`Wave ${wave}`);
      killUnitAt(P1, units.TerranMarine, "All", locations.Beacon);
    }
    sleep(frames(1));
  }
}, { name: "waves" });

test("the second wave is bigger", (sim) => {
  sim.place(P1, units.TerranMarine, locations.Beacon);
  sim.until(() => sim.program("waves").wave === 1);
  sim.place(P1, units.TerranMarine, locations.Beacon);
  sim.until(() => sim.program("waves").wave === 2);
  expect(sim.count(P8, units.ZergZergling, locations.Spawn)).toBe(12);
  expect(sim).toHavePrinted("Wave 2");
});

Every test gets sim, a world of its own: the map's placed units, locations and players, the script's programs at their first frame and its triggers beside them, and the same random() every run. What a test does with it:

sim.place(player, type, location, count?), sim.kill(unit), sim.remove(unit), sim.give(unit, to), sim.move(unit, to) Change the world. place hands the units back; kill is what a fight is in a test.
sim.frames(n), sim.seconds(n), sim.until(() => …) Let the game run. until fails the test if it is still not true after 2400 frames, so no test hangs.
sim.press("F2"), sim.click(), sim.type("-give 100"), sim.moveMouse(x, y) What a player does, found by the next frame.
sim.count(player, type, location?), sim.units(filter?), sim.resources(player), sim.deaths(player, type), sim.switch(n) Read the world back.
sim.program("waves").wave A program's variables by their names in the code: numbers, texts, arrays, a record as an object. The program is named by its options, program(() => { … }, { name: "waves" }); of a program that runs for several players, sim.program("lives", P2).
sim.printed(), expect(sim).toHavePrinted("Wave 2") What was shown to the players.

A test fails when an expect does not hold, when it throws, and when a program does what is always a mistake and the game would pass over in silence, such as reading past the end of an array. A test that means to see that says expect(sim).toHaveFaulted().

Tests live beside what they test, as above, or in files whose name ends in .test.ts, in any folder. A test file can import the script's own functions and test them as plain TypeScript. It is never part of the map: main.ts cannot import one, and a trigger() or a program() inside one is an error.

A failing test: the mark in the margin, what was expected at the end of the line, and the Testing view

In the editor, every test( has a mark in the margin — passed, failed, not run — and a click on it runs that test. A failure is said where it happened, expected 7, got 6 at the end of the line. The Testing view lists the tests by folder, file and describe, runs all of them, one of them or the ones that failed, and can show only the failing; the status bar keeps the count, and Test Results under the code has the chosen test's message, what it printed and what happened frame by frame. Tests run again by themselves after each change that compiles. A failing test is a warning, not an error: the map still saves and builds. Settings ▸ Tests has a tick, kept in the map, that makes a failing test refuse the build instead.