rbxts-transformer-jest
v0.0.8
Published
A Jest transformer for Roblox TypeScript (rbxts) projects.
Maintainers
Readme
rbxts-transformer-jest
TypeScript custom transformer for roblox-ts that hoists jest.mock() and
jest.unmock() calls above imports at compile time.
The Problem
Jest requires jest.mock() calls to execute before the modules they target
are imported. You could manually place mocks above imports, but that's tedious
and easy to forget. Without Babel in the roblox-ts pipeline, there's no built-in
way to automate it.
This transformer reorders statements at the AST level so mocks run before imports.
Prerequisites
Requires @rbxts/jest and
@rbxts/jest-globals.
Install
pnpm i -D rbxts-transformer-jestSetup
Add to your tsconfig.json:
{
"compilerOptions": {
"plugins": [
{
"transform": "rbxts-transformer-jest",
},
],
},
}Before / After
Input:
import { jest } from "@rbxts/jest-globals";
import { MyService } from "./my-service";
const mockHandler = jest.fn();
jest.mock("./my-service", () => ({ handler: mockHandler }));Output (reordered at compile time):
import { jest } from "@rbxts/jest-globals";
const mockHandler = jest.fn();
jest.mock("./my-service", () => ({ handler: mockHandler }));
import { MyService } from "./my-service";The @rbxts/jest-globals import always stays first. Mock-prefix variables
referenced in factories get hoisted alongside the mock call.
String Require Support
The transformer can resolve package specifiers like "@rbxts/services" into
Roblox instance paths at compile time, so you can write:
jest.mock<typeof import("@rbxts/services")>("@rbxts/services", () => {
return { Workspace: {} as Workspace };
});This compiles to the equivalent of:
jest.mock(game:GetService("ReplicatedStorage"):FindFirstChild("rbxts_include"):FindFirstChild("node_modules"):FindFirstChild("@rbxts"):FindFirstChild("services"), function() ... end)Note: This string-specifier support applies to every method whose first argument the transformer resolves:
jest.mock(),jest.unmock(),jest.doMock(),jest.dontMock(), andjest.requireActual(). If your@rbxts/jesttypes only acceptModuleScriptfor any of these (they don't account for string specifiers being rewritten at compile time), you must augment@rbxts/jest-globalswith astringoverload for that method — e.g.declare module "@rbxts/jest-globals" { namespace jest { function doMock<T = unknown>(moduleScript: string, factory?: () => T): typeof jest; } }— orpnpm typecheckwill reject the string argument.
Per-test Mocking (doMock / dontMock)
jest.mock() / jest.unmock() are hoisted to file scope, so they apply to the
whole test file. For per-test or per-describe mocking, use the
imperative jest.doMock() / jest.dontMock(). These are not hoisted (they
run where you write them) and their factories are not validated, so the
factory may reference imported helpers:
import { beforeEach, describe, expect, it, jest } from "@rbxts/jest-globals";
import { createServicesMock } from "./test/mock-services";
describe("server", () => {
beforeEach(() => {
jest.resetModules();
jest.doMock("@rbxts/services", () => {
return createServicesMock({ RunService: { IsServer: () => true } });
});
});
it("sees IsServer() === true via dynamic import", async () => {
const { RunService } = await import("@rbxts/services");
expect(RunService.IsServer()).toBe(true);
});
});The transformer resolves the module-string first argument of doMock,
dontMock, and requireActual to a Roblox instance path, exactly as it does
for hoisted mock / unmock. Chained doMock / dontMock calls are
supported. (As with mock / unmock, the string overload must exist in your
@rbxts/jest types — see the note above.)
