@isentinel/jest-roblox 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.mjs +6 -25
- package/dist/index.mjs +1 -1
- package/dist/{run-Cl5gYSQr.mjs → run-CyHhajiY.mjs} +184 -25
- package/dist/sea-entry.cjs +188 -48
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -444,6 +444,7 @@ root, and `workspace.outputFile: true` writes a per-package result file per
|
|
|
444
444
|
| `--outputFile <path>` | Write results to a file |
|
|
445
445
|
| `--gameOutput <path>` | Write game print/warn/error to a file |
|
|
446
446
|
| `--coverage` | Collect coverage |
|
|
447
|
+
| `--no-coverage` | Disable coverage for this run, even when enabled in config |
|
|
447
448
|
| `--coverageDirectory <path>` | Where to put coverage reports |
|
|
448
449
|
| `--coverageReporters <r...>` | Which report formats to use |
|
|
449
450
|
| `--collectCoverageFrom <glob>` | Globs for files to include in coverage (repeatable) |
|
package/dist/cli.mjs
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as VALID_BACKENDS, G as version, M as mergeCliWithConfig, N as loadConfig, O as formatBanner, U as isValidBackend, W as ConfigError, d as walkErrorChain, h as outputSingleResult, k as LuauScriptError, m as outputMultiResult, t as runJestRoblox, u as formatMissingScopes, x as parseGameOutput } from "./run-CyHhajiY.mjs";
|
|
2
2
|
import { OpenCloudError } from "@bedrock-rbx/ocale";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
5
5
|
import color from "tinyrainbow";
|
|
6
|
-
//#region src/utils/error-chain.ts
|
|
7
|
-
const MAX_DEPTH = 5;
|
|
8
|
-
function walkErrorChain(err) {
|
|
9
|
-
const entries = [];
|
|
10
|
-
let current = err;
|
|
11
|
-
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
12
|
-
entries.push({
|
|
13
|
-
name: current.constructor.name,
|
|
14
|
-
code: readStringProperty(current, "code"),
|
|
15
|
-
errno: readStringProperty(current, "errno"),
|
|
16
|
-
message: current.message,
|
|
17
|
-
syscall: readStringProperty(current, "syscall")
|
|
18
|
-
});
|
|
19
|
-
current = current.cause;
|
|
20
|
-
}
|
|
21
|
-
return entries;
|
|
22
|
-
}
|
|
23
|
-
function readStringProperty(err, key) {
|
|
24
|
-
const value = Reflect.get(err, key);
|
|
25
|
-
if (value === void 0 || value === null) return;
|
|
26
|
-
return String(value);
|
|
27
|
-
}
|
|
28
|
-
//#endregion
|
|
29
6
|
//#region src/cli.ts
|
|
30
7
|
const VERSION = version;
|
|
31
8
|
const HELP_TEXT = `
|
|
@@ -47,6 +24,7 @@ Options:
|
|
|
47
24
|
--no-color Disable colored output
|
|
48
25
|
-u, --updateSnapshot Update snapshot files
|
|
49
26
|
--coverage Enable coverage collection
|
|
27
|
+
--no-coverage Disable coverage for this run (overrides config)
|
|
50
28
|
--collectCoverageFrom <glob> Globs for files to include in coverage (repeatable)
|
|
51
29
|
--coverageDirectory <path> Directory for coverage output (default: coverage)
|
|
52
30
|
--coverageReporters <r...> Coverage reporters (default: text, lcov)
|
|
@@ -90,6 +68,7 @@ Examples:
|
|
|
90
68
|
jest-roblox -t "should spawn" Run tests matching pattern
|
|
91
69
|
jest-roblox --formatters json Output JSON to file
|
|
92
70
|
jest-roblox --coverage Run tests with coverage instrumentation
|
|
71
|
+
jest-roblox --no-coverage Skip coverage instrumentation for this run
|
|
93
72
|
`;
|
|
94
73
|
function parseArgs(args) {
|
|
95
74
|
const { positionals, values } = parseArgs$1({
|
|
@@ -122,6 +101,7 @@ function parseArgs(args) {
|
|
|
122
101
|
type: "boolean"
|
|
123
102
|
},
|
|
124
103
|
"no-color": { type: "boolean" },
|
|
104
|
+
"no-coverage": { type: "boolean" },
|
|
125
105
|
"no-coverage-cache": { type: "boolean" },
|
|
126
106
|
"no-show-luau": { type: "boolean" },
|
|
127
107
|
"outputFile": { type: "string" },
|
|
@@ -176,7 +156,7 @@ function parseArgs(args) {
|
|
|
176
156
|
affectedSince: values["affected-since"],
|
|
177
157
|
apiKey: values.apiKey,
|
|
178
158
|
backend: validateBackend(values.backend),
|
|
179
|
-
collectCoverage: values.coverage,
|
|
159
|
+
collectCoverage: values["no-coverage"] === true ? false : values.coverage,
|
|
180
160
|
collectCoverageFrom: values.collectCoverageFrom,
|
|
181
161
|
color: values["no-color"] === true ? false : values.color,
|
|
182
162
|
config: values.config,
|
|
@@ -288,6 +268,7 @@ function formatBackendErrorBanner(err) {
|
|
|
288
268
|
const extras = formatChainExtras(entry);
|
|
289
269
|
const label = color.dim(`[${index.toString()}]`);
|
|
290
270
|
body.push(` ${label} ${entry.name}: ${entry.message}${extras}`);
|
|
271
|
+
if (entry.requiredScopes !== void 0) body.push(` ${color.yellow(formatMissingScopes(entry.requiredScopes))}`);
|
|
291
272
|
}
|
|
292
273
|
return formatBanner({
|
|
293
274
|
body,
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as extractJsonFromOutput, C as formatJson, D as formatTestSummary, E as formatResult, F as DEFAULT_CONFIG, H as defineProject, I as GLOBAL_TEST_KEYS, L as JEST_ARGV_EXCLUDED_KEYS, N as loadConfig, P as resolveConfig, R as ROOT_CLI_KEYS, S as writeGameOutput, T as formatFailure, V as defineConfig, _ as formatJobSummary, a as visitStatement, b as formatGameOutputNotice, c as OpenCloudBackend, f as buildJestArgv, g as formatAnnotations, i as visitExpression, j as parseJestOutput, l as createOpenCloudBackend, n as runTypecheck, o as StudioBackend, p as generateTestScript, r as visitBlock, s as createStudioBackend, t as runJestRoblox, v as formatExecuteOutput, w as writeJsonFile, x as parseGameOutput, y as runProjects, z as SHARED_TEST_KEYS } from "./run-CyHhajiY.mjs";
|
|
2
2
|
export { DEFAULT_CONFIG, GLOBAL_TEST_KEYS, JEST_ARGV_EXCLUDED_KEYS, OpenCloudBackend, ROOT_CLI_KEYS, SHARED_TEST_KEYS, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, defineProject, extractJsonFromOutput, formatAnnotations, formatExecuteOutput, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runJestRoblox, runProjects, runTypecheck, visitBlock, visitExpression, visitStatement, writeGameOutput, writeJsonFile };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { PermissionError, PollTimeoutError } from "@bedrock-rbx/ocale";
|
|
2
|
+
import { PermissionError, PollTimeoutError, TRANSIENT_TRANSPORT_CODES } from "@bedrock-rbx/ocale";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import color from "tinyrainbow";
|
|
@@ -33,7 +33,7 @@ import { Buffer } from "node:buffer";
|
|
|
33
33
|
import { parseJSONC, parseYAML } from "confbox";
|
|
34
34
|
import { Visitor, parseSync } from "oxc-parser";
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.3.
|
|
36
|
+
var version = "0.3.2";
|
|
37
37
|
//#endregion
|
|
38
38
|
//#region src/config/errors.ts
|
|
39
39
|
var ConfigError = class extends Error {
|
|
@@ -933,12 +933,35 @@ function mapBranchArmLocations(traceMap, locations, sourceMapDirectory) {
|
|
|
933
933
|
tsPath
|
|
934
934
|
};
|
|
935
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Detects a phantom branch arm produced by a source-less synthetic statement
|
|
938
|
+
* `if` (e.g. a roblox-ts Array polyfill like `.filter`/`.includes`). The
|
|
939
|
+
* synthetic `if` has no source map entry, so trace-mapping's greatest-lower-
|
|
940
|
+
* bound bias snaps both arms onto the nearest preceding segment — the then-
|
|
941
|
+
* arm's own start — yielding a zero-width arm that coincides with another
|
|
942
|
+
* arm's start and can never be covered.
|
|
943
|
+
*
|
|
944
|
+
* A genuine statement `if` is safe: roblox-ts always renders it multi-line, so
|
|
945
|
+
* the then-body (generated line `if+1`) and the implicit-else arm (generated
|
|
946
|
+
* line `if`) carry distinct source-map segments and never collapse. This is
|
|
947
|
+
* gated to `type === "if"` by the caller: a single-line `expr-if` (ternary)
|
|
948
|
+
* legitimately collapses to one column-0 segment and must NOT be dropped.
|
|
949
|
+
*/
|
|
950
|
+
function hasCollapsedPhantomArm(locations) {
|
|
951
|
+
return locations.some((arm, index) => {
|
|
952
|
+
if (!(arm.start.line === arm.end.line && arm.start.column === arm.end.column)) return false;
|
|
953
|
+
return locations.some((other, otherIndex) => {
|
|
954
|
+
return otherIndex !== index && other.start.line === arm.start.line && other.start.column === arm.start.column;
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
936
958
|
function mapFileBranches(resources, fileCoverage, pendingBranches) {
|
|
937
959
|
if (resources.coverageMap.branchMap === void 0) return;
|
|
938
960
|
for (const [branchId, entry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
939
961
|
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
940
962
|
const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
|
|
941
963
|
if (result === void 0) continue;
|
|
964
|
+
if (entry.type === "if" && hasCollapsedPhantomArm(result.locations)) continue;
|
|
942
965
|
let fileBranches = pendingBranches.get(result.tsPath);
|
|
943
966
|
if (fileBranches === void 0) {
|
|
944
967
|
fileBranches = [];
|
|
@@ -2081,6 +2104,7 @@ function findMapping(filePath, mappings, key = "outDir") {
|
|
|
2081
2104
|
}
|
|
2082
2105
|
function replacePrefix(filePath, from, to) {
|
|
2083
2106
|
if (filePath === from) return to;
|
|
2107
|
+
if (from === ".") return `${to}/${filePath.startsWith("./") ? filePath.slice(2) : filePath}`;
|
|
2084
2108
|
if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
|
|
2085
2109
|
return filePath;
|
|
2086
2110
|
}
|
|
@@ -2717,7 +2741,7 @@ function formatTestSummary(result, timing, styles, options) {
|
|
|
2717
2741
|
}
|
|
2718
2742
|
function formatResult(result, timing, options) {
|
|
2719
2743
|
const styles = createStyles(options.color, options.slowTestThreshold);
|
|
2720
|
-
const lines = [
|
|
2744
|
+
const lines = [""];
|
|
2721
2745
|
for (const file of result.testResults) {
|
|
2722
2746
|
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
2723
2747
|
lines.push(formatFileSummary(file, options, styles));
|
|
@@ -2891,7 +2915,7 @@ function formatMultiProjectResult(projects, timing, options) {
|
|
|
2891
2915
|
result,
|
|
2892
2916
|
styles
|
|
2893
2917
|
}));
|
|
2894
|
-
const lines = [
|
|
2918
|
+
const lines = ["", sections.join("\n\n")];
|
|
2895
2919
|
const mergedResult = mergeJestResults(projects.map((project) => project.result));
|
|
2896
2920
|
lines.push("", formatTestSummary(mergedResult, timing, styles, {
|
|
2897
2921
|
snapshotWriteFailures: options.snapshotWriteFailures,
|
|
@@ -3698,6 +3722,15 @@ function hasFormatter(formatters, name) {
|
|
|
3698
3722
|
function usesAgentFormatter(formatters, verbose = false) {
|
|
3699
3723
|
return hasFormatter(formatters, "agent") && !verbose;
|
|
3700
3724
|
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Whether human-facing progress output (the run header, the "Running X of Y"
|
|
3727
|
+
* notice, the workspace streaming lines) should be written: not silent and not
|
|
3728
|
+
* a machine-readable formatter (json, or non-verbose agent). The single source
|
|
3729
|
+
* of truth so these sinks can't drift apart.
|
|
3730
|
+
*/
|
|
3731
|
+
function isDefaultHumanFormatter(options) {
|
|
3732
|
+
return options.silent !== true && !usesAgentFormatter(options.formatters, options.verbose) && !hasFormatter(options.formatters, "json");
|
|
3733
|
+
}
|
|
3701
3734
|
//#endregion
|
|
3702
3735
|
//#region src/snapshot/path-resolver.ts
|
|
3703
3736
|
function createSnapshotPathResolver(config) {
|
|
@@ -4896,7 +4929,10 @@ var OcaleRunner = class {
|
|
|
4896
4929
|
script,
|
|
4897
4930
|
timeoutSeconds,
|
|
4898
4931
|
universeId: this.credentials.universeId
|
|
4899
|
-
}, {
|
|
4932
|
+
}, {
|
|
4933
|
+
retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
|
|
4934
|
+
timeoutMs: timeout
|
|
4935
|
+
});
|
|
4900
4936
|
if (!result.success) {
|
|
4901
4937
|
if (result.err instanceof PollTimeoutError) throw new Error("Execution timed out", { cause: result.err });
|
|
4902
4938
|
throw new Error(result.err.message, { cause: result.err });
|
|
@@ -4918,7 +4954,7 @@ var OcaleRunner = class {
|
|
|
4918
4954
|
placeId: this.credentials.placeId,
|
|
4919
4955
|
universeId: this.credentials.universeId
|
|
4920
4956
|
};
|
|
4921
|
-
const result = await this.places.save(parameters);
|
|
4957
|
+
const result = await this.places.save(parameters, { retryableTransportCodes: TRANSIENT_TRANSPORT_CODES });
|
|
4922
4958
|
if (!result.success) throw new Error(`Failed to upload place: ${result.err.message}`, { cause: result.err });
|
|
4923
4959
|
return {
|
|
4924
4960
|
uploadMs: Date.now() - uploadStart,
|
|
@@ -5036,6 +5072,35 @@ function generateTestScript(options) {
|
|
|
5036
5072
|
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
|
|
5037
5073
|
}
|
|
5038
5074
|
//#endregion
|
|
5075
|
+
//#region src/utils/error-chain.ts
|
|
5076
|
+
const MAX_DEPTH = 5;
|
|
5077
|
+
function walkErrorChain(err) {
|
|
5078
|
+
const entries = [];
|
|
5079
|
+
let current = err;
|
|
5080
|
+
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
5081
|
+
entries.push({
|
|
5082
|
+
name: current.constructor.name,
|
|
5083
|
+
code: readStringProperty(current, "code"),
|
|
5084
|
+
errno: readStringProperty(current, "errno"),
|
|
5085
|
+
message: current.message,
|
|
5086
|
+
requiredScopes: current instanceof PermissionError ? current.requiredScopes : void 0,
|
|
5087
|
+
syscall: readStringProperty(current, "syscall")
|
|
5088
|
+
});
|
|
5089
|
+
current = current.cause;
|
|
5090
|
+
}
|
|
5091
|
+
return entries;
|
|
5092
|
+
}
|
|
5093
|
+
function formatMissingScopes(scopes) {
|
|
5094
|
+
if (scopes.length === 0) return "API key has insufficient scopes. Add via Creator Dashboard.";
|
|
5095
|
+
const joined = scopes.join(", ");
|
|
5096
|
+
return `API key missing scope${scopes.length === 1 ? "" : "s"} ${joined}. Add via Creator Dashboard.`;
|
|
5097
|
+
}
|
|
5098
|
+
function readStringProperty(err, key) {
|
|
5099
|
+
const value = Reflect.get(err, key);
|
|
5100
|
+
if (value === void 0 || value === null) return;
|
|
5101
|
+
return String(value);
|
|
5102
|
+
}
|
|
5103
|
+
//#endregion
|
|
5039
5104
|
//#region src/backends/open-cloud.ts
|
|
5040
5105
|
const PARALLEL_AUTO_CAP = 3;
|
|
5041
5106
|
const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
|
|
@@ -5171,10 +5236,7 @@ function createOpenCloudBackend(credentials) {
|
|
|
5171
5236
|
}
|
|
5172
5237
|
function describeError(err) {
|
|
5173
5238
|
const cause = err instanceof Error ? err.cause : void 0;
|
|
5174
|
-
if (cause instanceof PermissionError)
|
|
5175
|
-
const scopes = cause.requiredScopes.join(", ");
|
|
5176
|
-
return `API key missing scope${cause.requiredScopes.length === 1 ? "" : "s"} ${scopes}. Add via Creator Dashboard.`;
|
|
5177
|
-
}
|
|
5239
|
+
if (cause instanceof PermissionError) return formatMissingScopes(cause.requiredScopes);
|
|
5178
5240
|
return err instanceof Error ? err.message : String(err);
|
|
5179
5241
|
}
|
|
5180
5242
|
function warnStreamingDisabled(err, state) {
|
|
@@ -5947,6 +6009,27 @@ function narrowConfigByFiles(config, files) {
|
|
|
5947
6009
|
testPathPattern: `(${branches.join("|")})`
|
|
5948
6010
|
};
|
|
5949
6011
|
}
|
|
6012
|
+
/**
|
|
6013
|
+
* Forward an Instance-namespace `testPathPattern` to the Luau runner.
|
|
6014
|
+
*
|
|
6015
|
+
* Node-side discovery is the source of truth: the FS-namespace filter
|
|
6016
|
+
* (positional args or `--testPathPattern`) has already resolved to a concrete
|
|
6017
|
+
* file set against real paths. Drop the raw FS-shaped pattern and re-narrow by
|
|
6018
|
+
* the discovered files so Jest-on-Roblox matches the same files — its paths are
|
|
6019
|
+
* Roblox Instance names (e.g. `ServerScriptService/...`) with no `src/` prefix,
|
|
6020
|
+
* so a raw FS pattern like `src/server/foo.spec` matches zero files there.
|
|
6021
|
+
*
|
|
6022
|
+
* `filterActive` gates the rewrite: a bare run (no positionals, no
|
|
6023
|
+
* `testPathPattern`) leaves the config untouched so the Luau side runs every
|
|
6024
|
+
* `testMatch` file rather than a giant basename alternation.
|
|
6025
|
+
*/
|
|
6026
|
+
function narrowForLuauRun(config, runtimeFiles, filterActive) {
|
|
6027
|
+
if (!filterActive) return config;
|
|
6028
|
+
return narrowConfigByFiles({
|
|
6029
|
+
...config,
|
|
6030
|
+
testPathPattern: void 0
|
|
6031
|
+
}, runtimeFiles);
|
|
6032
|
+
}
|
|
5950
6033
|
function toBasenamePattern(file) {
|
|
5951
6034
|
const posix = file.replaceAll("\\", "/");
|
|
5952
6035
|
const lastSlash = posix.lastIndexOf("/");
|
|
@@ -7972,6 +8055,27 @@ function resolveSetupFilePaths(config) {
|
|
|
7972
8055
|
resolveAllSetupFilePaths([config]);
|
|
7973
8056
|
}
|
|
7974
8057
|
//#endregion
|
|
8058
|
+
//#region src/run/run-header.ts
|
|
8059
|
+
/**
|
|
8060
|
+
* Print the ` RUN vX.Y <rootDir>` header to stdout at the moment a run begins
|
|
8061
|
+
* (right before the backend uploads), so the CLI doesn't look stalled while it
|
|
8062
|
+
* waits for remote results. The end-of-run formatters no longer emit it.
|
|
8063
|
+
*
|
|
8064
|
+
* Self-gates to the default human formatter: nothing is written under
|
|
8065
|
+
* `--silent`, `--formatters json`, or `--formatters agent` (without
|
|
8066
|
+
* `--verbose`), which produce machine-readable output that must stay clean.
|
|
8067
|
+
*/
|
|
8068
|
+
function emitRunHeader(input) {
|
|
8069
|
+
if (!isDefaultHumanFormatter(input)) return;
|
|
8070
|
+
process.stdout.write(formatRunHeader({
|
|
8071
|
+
collectCoverage: input.collectCoverage,
|
|
8072
|
+
color: input.color,
|
|
8073
|
+
rootDir: input.rootDir,
|
|
8074
|
+
verbose: input.verbose ?? false,
|
|
8075
|
+
version: input.version
|
|
8076
|
+
}));
|
|
8077
|
+
}
|
|
8078
|
+
//#endregion
|
|
7975
8079
|
//#region src/run/multi.ts
|
|
7976
8080
|
const DEFAULT_ROJO_PROJECT = "default.project.json";
|
|
7977
8081
|
const VERSION$2 = version;
|
|
@@ -8015,6 +8119,15 @@ async function runMultiProject(options) {
|
|
|
8015
8119
|
rootConfig
|
|
8016
8120
|
});
|
|
8017
8121
|
});
|
|
8122
|
+
if (pendingJobs.length > 0) emitRunHeader({
|
|
8123
|
+
collectCoverage: rootConfig.collectCoverage,
|
|
8124
|
+
color: rootConfig.color,
|
|
8125
|
+
formatters: rootConfig.formatters,
|
|
8126
|
+
rootDir: rootConfig.rootDir,
|
|
8127
|
+
silent: rootConfig.silent,
|
|
8128
|
+
verbose: rootConfig.verbose,
|
|
8129
|
+
version: VERSION$2
|
|
8130
|
+
});
|
|
8018
8131
|
const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
|
|
8019
8132
|
const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
|
|
8020
8133
|
const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
|
|
@@ -8086,10 +8199,11 @@ function collectPendingJobs(arguments_) {
|
|
|
8086
8199
|
testMatch: project.include
|
|
8087
8200
|
};
|
|
8088
8201
|
const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, projectCliFiles).files, rootConfig);
|
|
8089
|
-
const
|
|
8202
|
+
const filterActive = (projectCliFiles?.length ?? 0) > 0 || discoveryConfig.testPathPattern !== void 0;
|
|
8203
|
+
const projConfig = narrowForLuauRun({
|
|
8090
8204
|
...discoveryConfig,
|
|
8091
8205
|
testMatch: project.testMatch
|
|
8092
|
-
},
|
|
8206
|
+
}, runtimeFiles, filterActive);
|
|
8093
8207
|
allTypeTestFiles.push(...typeTestFiles);
|
|
8094
8208
|
if (runtimeFiles.length === 0) continue;
|
|
8095
8209
|
const runtimeInjectionPaths = [];
|
|
@@ -8212,15 +8326,15 @@ const VERSION$1 = version;
|
|
|
8212
8326
|
async function runSingleProject(options) {
|
|
8213
8327
|
const { cli } = options;
|
|
8214
8328
|
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
8215
|
-
const
|
|
8216
|
-
return narrowConfigByFiles(options.config, cli.files ?? []);
|
|
8217
|
-
});
|
|
8329
|
+
const baseConfig = { ...options.config };
|
|
8218
8330
|
timing.profile("resolveSetupFilePaths", () => {
|
|
8219
|
-
resolveSetupFilePaths(
|
|
8331
|
+
resolveSetupFilePaths(baseConfig);
|
|
8332
|
+
});
|
|
8333
|
+
const discovery = timing.profile("discoverTestFiles", () => {
|
|
8334
|
+
return discoverTestFiles(baseConfig, cli.files);
|
|
8220
8335
|
});
|
|
8221
|
-
const discovery = timing.profile("discoverTestFiles", () => discoverTestFiles(config, cli.files));
|
|
8222
8336
|
if (discovery.files.length === 0) {
|
|
8223
|
-
if (
|
|
8337
|
+
if (baseConfig.passWithNoTests) return {
|
|
8224
8338
|
mode: "single",
|
|
8225
8339
|
preCoverageMs: 0
|
|
8226
8340
|
};
|
|
@@ -8232,7 +8346,11 @@ async function runSingleProject(options) {
|
|
|
8232
8346
|
};
|
|
8233
8347
|
}
|
|
8234
8348
|
const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
|
|
8235
|
-
return classifyTestFiles(discovery.files,
|
|
8349
|
+
return classifyTestFiles(discovery.files, baseConfig);
|
|
8350
|
+
});
|
|
8351
|
+
const filterActive = (cli.files?.length ?? 0) > 0 || baseConfig.testPathPattern !== void 0;
|
|
8352
|
+
const config = timing.profile("narrowForLuauRun", () => {
|
|
8353
|
+
return narrowForLuauRun(baseConfig, runtimeFiles, filterActive);
|
|
8236
8354
|
});
|
|
8237
8355
|
if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
|
|
8238
8356
|
if (config.passWithNoTests) return {
|
|
@@ -8280,7 +8398,17 @@ async function runSingleProject(options) {
|
|
|
8280
8398
|
}
|
|
8281
8399
|
async function executeRuntimeTests(options) {
|
|
8282
8400
|
const { cli, config, testFiles, timing, totalFiles } = options;
|
|
8283
|
-
|
|
8401
|
+
const useDefaultFormatter = isDefaultHumanFormatter(config);
|
|
8402
|
+
emitRunHeader({
|
|
8403
|
+
collectCoverage: config.collectCoverage,
|
|
8404
|
+
color: config.color,
|
|
8405
|
+
formatters: config.formatters,
|
|
8406
|
+
rootDir: config.rootDir,
|
|
8407
|
+
silent: config.silent,
|
|
8408
|
+
verbose: config.verbose,
|
|
8409
|
+
version: VERSION$1
|
|
8410
|
+
});
|
|
8411
|
+
if (useDefaultFormatter && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
8284
8412
|
const backend = await timing.profileAsync("resolveBackend", async () => {
|
|
8285
8413
|
return resolveBackend(cli, config);
|
|
8286
8414
|
});
|
|
@@ -9213,13 +9341,33 @@ async function prepareWorkspaceDispatch(input) {
|
|
|
9213
9341
|
}
|
|
9214
9342
|
return { scriptOverride: generateMaterializerScript(inputs) };
|
|
9215
9343
|
}
|
|
9344
|
+
/**
|
|
9345
|
+
* Resolve a `--testPathPattern` against this package's files Node-side, then
|
|
9346
|
+
* forward an Instance-namespace basename pattern (see {@link narrowForLuauRun}).
|
|
9347
|
+
*
|
|
9348
|
+
* A pattern that matches no file in this package simply targets a different
|
|
9349
|
+
* package: keep the (zero-matching) raw pattern so Jest-on-Roblox runs nothing,
|
|
9350
|
+
* and set `passWithNoTests` so it doesn't `exit(1)`. The raw pattern is
|
|
9351
|
+
* load-bearing here — clearing it would drop the filter entirely and make the
|
|
9352
|
+
* Luau side fall back to `testMatch`, running the whole package.
|
|
9353
|
+
*/
|
|
9354
|
+
function narrowPackageTestPathPattern(packageConfig) {
|
|
9355
|
+
if (packageConfig.testPathPattern === void 0) return packageConfig;
|
|
9356
|
+
const { files } = discoverTestFiles(packageConfig);
|
|
9357
|
+
const { runtimeFiles } = classifyTestFiles(files, packageConfig);
|
|
9358
|
+
if (runtimeFiles.length === 0) return {
|
|
9359
|
+
...packageConfig,
|
|
9360
|
+
passWithNoTests: true
|
|
9361
|
+
};
|
|
9362
|
+
return narrowForLuauRun(packageConfig, runtimeFiles, true);
|
|
9363
|
+
}
|
|
9216
9364
|
async function loadPackages(input) {
|
|
9217
9365
|
const { cli, packageInfos, timing } = input;
|
|
9218
9366
|
const loaded = [];
|
|
9219
9367
|
for (const info of packageInfos) {
|
|
9220
|
-
const packageConfig = mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
9368
|
+
const packageConfig = narrowPackageTestPathPattern(mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
9221
9369
|
return loadConfig$1(void 0, info.packageDirectory);
|
|
9222
|
-
}));
|
|
9370
|
+
})));
|
|
9223
9371
|
const rojoProject = packageConfig.rojoProject ?? ROJO_PROJECT_DEFAULT;
|
|
9224
9372
|
const hasExplicitIgnore = packageConfig.coveragePathIgnorePatterns !== DEFAULT_CONFIG.coveragePathIgnorePatterns;
|
|
9225
9373
|
const hasExplicitCoverageCache = packageConfig.coverageCache !== DEFAULT_CONFIG.coverageCache;
|
|
@@ -9811,6 +9959,14 @@ async function runWorkspaceMode(cli, workspace, timing) {
|
|
|
9811
9959
|
}
|
|
9812
9960
|
let runtimeResults;
|
|
9813
9961
|
try {
|
|
9962
|
+
emitRunHeader({
|
|
9963
|
+
color: runOptions.color,
|
|
9964
|
+
formatters: runOptions.formatters,
|
|
9965
|
+
rootDir: workspaceRoot,
|
|
9966
|
+
silent: runOptions.silent,
|
|
9967
|
+
verbose: cli.verbose,
|
|
9968
|
+
version: VERSION
|
|
9969
|
+
});
|
|
9814
9970
|
const onStreamingResult = resolveStreamingProgressSink(runOptions, cli);
|
|
9815
9971
|
runtimeResults = await runWorkspace({
|
|
9816
9972
|
backend,
|
|
@@ -9908,8 +10064,11 @@ function composeWorkspaceDisplayName(package_, project) {
|
|
|
9908
10064
|
* either break the structured output or be silenced anyway.
|
|
9909
10065
|
*/
|
|
9910
10066
|
function resolveStreamingProgressSink(runOptions, cli) {
|
|
9911
|
-
if (
|
|
9912
|
-
|
|
10067
|
+
if (!isDefaultHumanFormatter({
|
|
10068
|
+
formatters: runOptions.formatters,
|
|
10069
|
+
silent: runOptions.silent,
|
|
10070
|
+
verbose: cli.verbose
|
|
10071
|
+
})) return;
|
|
9913
10072
|
return (entry) => {
|
|
9914
10073
|
const line = formatStreamingProgressLine(entry, { color: runOptions.color });
|
|
9915
10074
|
process.stdout.write(`${line}\n`);
|
|
@@ -9942,4 +10101,4 @@ async function runJestRoblox(cli, config) {
|
|
|
9942
10101
|
}
|
|
9943
10102
|
}
|
|
9944
10103
|
//#endregion
|
|
9945
|
-
export {
|
|
10104
|
+
export { extractJsonFromOutput as A, VALID_BACKENDS as B, formatJson as C, formatTestSummary as D, formatResult as E, DEFAULT_CONFIG as F, version as G, defineProject as H, GLOBAL_TEST_KEYS as I, JEST_ARGV_EXCLUDED_KEYS as L, mergeCliWithConfig as M, loadConfig$1 as N, formatBanner as O, resolveConfig as P, ROOT_CLI_KEYS as R, writeGameOutput as S, formatFailure as T, isValidBackend as U, defineConfig as V, ConfigError as W, formatJobSummary as _, visitStatement as a, formatGameOutputNotice as b, OpenCloudBackend as c, walkErrorChain as d, buildJestArgv as f, formatAnnotations as g, outputSingleResult as h, visitExpression as i, parseJestOutput as j, LuauScriptError as k, createOpenCloudBackend as l, outputMultiResult as m, runTypecheck as n, StudioBackend as o, generateTestScript as p, visitBlock as r, createStudioBackend as s, runJestRoblox as t, formatMissingScopes as u, formatExecuteOutput as v, writeJsonFile$1 as w, parseGameOutput as x, runProjects as y, SHARED_TEST_KEYS as z };
|
package/dist/sea-entry.cjs
CHANGED
|
@@ -686,7 +686,7 @@ function C$4({ force: e } = {}) {
|
|
|
686
686
|
var y$3 = C$4();
|
|
687
687
|
//#endregion
|
|
688
688
|
//#region package.json
|
|
689
|
-
var version = "0.3.
|
|
689
|
+
var version = "0.3.2";
|
|
690
690
|
//#endregion
|
|
691
691
|
//#region src/config/errors.ts
|
|
692
692
|
var ConfigError = class extends Error {
|
|
@@ -20150,12 +20150,35 @@ function mapBranchArmLocations(traceMap, locations, sourceMapDirectory) {
|
|
|
20150
20150
|
tsPath
|
|
20151
20151
|
};
|
|
20152
20152
|
}
|
|
20153
|
+
/**
|
|
20154
|
+
* Detects a phantom branch arm produced by a source-less synthetic statement
|
|
20155
|
+
* `if` (e.g. a roblox-ts Array polyfill like `.filter`/`.includes`). The
|
|
20156
|
+
* synthetic `if` has no source map entry, so trace-mapping's greatest-lower-
|
|
20157
|
+
* bound bias snaps both arms onto the nearest preceding segment — the then-
|
|
20158
|
+
* arm's own start — yielding a zero-width arm that coincides with another
|
|
20159
|
+
* arm's start and can never be covered.
|
|
20160
|
+
*
|
|
20161
|
+
* A genuine statement `if` is safe: roblox-ts always renders it multi-line, so
|
|
20162
|
+
* the then-body (generated line `if+1`) and the implicit-else arm (generated
|
|
20163
|
+
* line `if`) carry distinct source-map segments and never collapse. This is
|
|
20164
|
+
* gated to `type === "if"` by the caller: a single-line `expr-if` (ternary)
|
|
20165
|
+
* legitimately collapses to one column-0 segment and must NOT be dropped.
|
|
20166
|
+
*/
|
|
20167
|
+
function hasCollapsedPhantomArm(locations) {
|
|
20168
|
+
return locations.some((arm, index) => {
|
|
20169
|
+
if (!(arm.start.line === arm.end.line && arm.start.column === arm.end.column)) return false;
|
|
20170
|
+
return locations.some((other, otherIndex) => {
|
|
20171
|
+
return otherIndex !== index && other.start.line === arm.start.line && other.start.column === arm.start.column;
|
|
20172
|
+
});
|
|
20173
|
+
});
|
|
20174
|
+
}
|
|
20153
20175
|
function mapFileBranches(resources, fileCoverage, pendingBranches) {
|
|
20154
20176
|
if (resources.coverageMap.branchMap === void 0) return;
|
|
20155
20177
|
for (const [branchId, entry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
20156
20178
|
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
20157
20179
|
const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
|
|
20158
20180
|
if (result === void 0) continue;
|
|
20181
|
+
if (entry.type === "if" && hasCollapsedPhantomArm(result.locations)) continue;
|
|
20159
20182
|
let fileBranches = pendingBranches.get(result.tsPath);
|
|
20160
20183
|
if (fileBranches === void 0) {
|
|
20161
20184
|
fileBranches = [];
|
|
@@ -25919,6 +25942,7 @@ function findMapping(filePath, mappings, key = "outDir") {
|
|
|
25919
25942
|
}
|
|
25920
25943
|
function replacePrefix(filePath, from, to) {
|
|
25921
25944
|
if (filePath === from) return to;
|
|
25945
|
+
if (from === ".") return `${to}/${filePath.startsWith("./") ? filePath.slice(2) : filePath}`;
|
|
25922
25946
|
if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
|
|
25923
25947
|
return filePath;
|
|
25924
25948
|
}
|
|
@@ -26758,7 +26782,7 @@ var core_default = (/* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
|
|
|
26758
26782
|
}
|
|
26759
26783
|
});
|
|
26760
26784
|
};
|
|
26761
|
-
var MODES =
|
|
26785
|
+
var MODES = /*#__PURE__*/ Object.freeze({
|
|
26762
26786
|
__proto__: null,
|
|
26763
26787
|
APOS_STRING_MODE,
|
|
26764
26788
|
BACKSLASH_ESCAPE,
|
|
@@ -29055,7 +29079,7 @@ function formatTestSummary(result, timing, styles, options) {
|
|
|
29055
29079
|
}
|
|
29056
29080
|
function formatResult(result, timing, options) {
|
|
29057
29081
|
const styles = createStyles(options.color, options.slowTestThreshold);
|
|
29058
|
-
const lines = [
|
|
29082
|
+
const lines = [""];
|
|
29059
29083
|
for (const file of result.testResults) {
|
|
29060
29084
|
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
29061
29085
|
lines.push(formatFileSummary(file, options, styles));
|
|
@@ -29229,7 +29253,7 @@ function formatMultiProjectResult(projects, timing, options) {
|
|
|
29229
29253
|
result,
|
|
29230
29254
|
styles
|
|
29231
29255
|
}));
|
|
29232
|
-
const lines = [
|
|
29256
|
+
const lines = ["", sections.join("\n\n")];
|
|
29233
29257
|
const mergedResult = mergeJestResults(projects.map((project) => project.result));
|
|
29234
29258
|
lines.push("", formatTestSummary(mergedResult, timing, styles, {
|
|
29235
29259
|
snapshotWriteFailures: options.snapshotWriteFailures,
|
|
@@ -30036,6 +30060,15 @@ function hasFormatter(formatters, name) {
|
|
|
30036
30060
|
function usesAgentFormatter(formatters, verbose = false) {
|
|
30037
30061
|
return hasFormatter(formatters, "agent") && !verbose;
|
|
30038
30062
|
}
|
|
30063
|
+
/**
|
|
30064
|
+
* Whether human-facing progress output (the run header, the "Running X of Y"
|
|
30065
|
+
* notice, the workspace streaming lines) should be written: not silent and not
|
|
30066
|
+
* a machine-readable formatter (json, or non-verbose agent). The single source
|
|
30067
|
+
* of truth so these sinks can't drift apart.
|
|
30068
|
+
*/
|
|
30069
|
+
function isDefaultHumanFormatter(options) {
|
|
30070
|
+
return options.silent !== true && !usesAgentFormatter(options.formatters, options.verbose) && !hasFormatter(options.formatters, "json");
|
|
30071
|
+
}
|
|
30039
30072
|
//#endregion
|
|
30040
30073
|
//#region src/snapshot/path-resolver.ts
|
|
30041
30074
|
function createSnapshotPathResolver(config) {
|
|
@@ -37469,7 +37502,10 @@ var OcaleRunner = class {
|
|
|
37469
37502
|
script,
|
|
37470
37503
|
timeoutSeconds,
|
|
37471
37504
|
universeId: this.credentials.universeId
|
|
37472
|
-
}, {
|
|
37505
|
+
}, {
|
|
37506
|
+
retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
|
|
37507
|
+
timeoutMs: timeout
|
|
37508
|
+
});
|
|
37473
37509
|
if (!result.success) {
|
|
37474
37510
|
if (result.err instanceof PollTimeoutError) throw new Error("Execution timed out", { cause: result.err });
|
|
37475
37511
|
throw new Error(result.err.message, { cause: result.err });
|
|
@@ -37491,7 +37527,7 @@ var OcaleRunner = class {
|
|
|
37491
37527
|
placeId: this.credentials.placeId,
|
|
37492
37528
|
universeId: this.credentials.universeId
|
|
37493
37529
|
};
|
|
37494
|
-
const result = await this.places.save(parameters);
|
|
37530
|
+
const result = await this.places.save(parameters, { retryableTransportCodes: TRANSIENT_TRANSPORT_CODES });
|
|
37495
37531
|
if (!result.success) throw new Error(`Failed to upload place: ${result.err.message}`, { cause: result.err });
|
|
37496
37532
|
return {
|
|
37497
37533
|
uploadMs: Date.now() - uploadStart,
|
|
@@ -37609,6 +37645,35 @@ function generateTestScript(options) {
|
|
|
37609
37645
|
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
|
|
37610
37646
|
}
|
|
37611
37647
|
//#endregion
|
|
37648
|
+
//#region src/utils/error-chain.ts
|
|
37649
|
+
const MAX_DEPTH = 5;
|
|
37650
|
+
function walkErrorChain(err) {
|
|
37651
|
+
const entries = [];
|
|
37652
|
+
let current = err;
|
|
37653
|
+
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
37654
|
+
entries.push({
|
|
37655
|
+
name: current.constructor.name,
|
|
37656
|
+
code: readStringProperty(current, "code"),
|
|
37657
|
+
errno: readStringProperty(current, "errno"),
|
|
37658
|
+
message: current.message,
|
|
37659
|
+
requiredScopes: current instanceof PermissionError ? current.requiredScopes : void 0,
|
|
37660
|
+
syscall: readStringProperty(current, "syscall")
|
|
37661
|
+
});
|
|
37662
|
+
current = current.cause;
|
|
37663
|
+
}
|
|
37664
|
+
return entries;
|
|
37665
|
+
}
|
|
37666
|
+
function formatMissingScopes(scopes) {
|
|
37667
|
+
if (scopes.length === 0) return "API key has insufficient scopes. Add via Creator Dashboard.";
|
|
37668
|
+
const joined = scopes.join(", ");
|
|
37669
|
+
return `API key missing scope${scopes.length === 1 ? "" : "s"} ${joined}. Add via Creator Dashboard.`;
|
|
37670
|
+
}
|
|
37671
|
+
function readStringProperty(err, key) {
|
|
37672
|
+
const value = Reflect.get(err, key);
|
|
37673
|
+
if (value === void 0 || value === null) return;
|
|
37674
|
+
return String(value);
|
|
37675
|
+
}
|
|
37676
|
+
//#endregion
|
|
37612
37677
|
//#region src/backends/open-cloud.ts
|
|
37613
37678
|
const PARALLEL_AUTO_CAP = 3;
|
|
37614
37679
|
const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
|
|
@@ -37744,10 +37809,7 @@ function createOpenCloudBackend(credentials) {
|
|
|
37744
37809
|
}
|
|
37745
37810
|
function describeError(err) {
|
|
37746
37811
|
const cause = err instanceof Error ? err.cause : void 0;
|
|
37747
|
-
if (cause instanceof PermissionError)
|
|
37748
|
-
const scopes = cause.requiredScopes.join(", ");
|
|
37749
|
-
return `API key missing scope${cause.requiredScopes.length === 1 ? "" : "s"} ${scopes}. Add via Creator Dashboard.`;
|
|
37750
|
-
}
|
|
37812
|
+
if (cause instanceof PermissionError) return formatMissingScopes(cause.requiredScopes);
|
|
37751
37813
|
return err instanceof Error ? err.message : String(err);
|
|
37752
37814
|
}
|
|
37753
37815
|
function warnStreamingDisabled(err, state) {
|
|
@@ -38520,6 +38582,27 @@ function narrowConfigByFiles(config, files) {
|
|
|
38520
38582
|
testPathPattern: `(${branches.join("|")})`
|
|
38521
38583
|
};
|
|
38522
38584
|
}
|
|
38585
|
+
/**
|
|
38586
|
+
* Forward an Instance-namespace `testPathPattern` to the Luau runner.
|
|
38587
|
+
*
|
|
38588
|
+
* Node-side discovery is the source of truth: the FS-namespace filter
|
|
38589
|
+
* (positional args or `--testPathPattern`) has already resolved to a concrete
|
|
38590
|
+
* file set against real paths. Drop the raw FS-shaped pattern and re-narrow by
|
|
38591
|
+
* the discovered files so Jest-on-Roblox matches the same files — its paths are
|
|
38592
|
+
* Roblox Instance names (e.g. `ServerScriptService/...`) with no `src/` prefix,
|
|
38593
|
+
* so a raw FS pattern like `src/server/foo.spec` matches zero files there.
|
|
38594
|
+
*
|
|
38595
|
+
* `filterActive` gates the rewrite: a bare run (no positionals, no
|
|
38596
|
+
* `testPathPattern`) leaves the config untouched so the Luau side runs every
|
|
38597
|
+
* `testMatch` file rather than a giant basename alternation.
|
|
38598
|
+
*/
|
|
38599
|
+
function narrowForLuauRun(config, runtimeFiles, filterActive) {
|
|
38600
|
+
if (!filterActive) return config;
|
|
38601
|
+
return narrowConfigByFiles({
|
|
38602
|
+
...config,
|
|
38603
|
+
testPathPattern: void 0
|
|
38604
|
+
}, runtimeFiles);
|
|
38605
|
+
}
|
|
38523
38606
|
function toBasenamePattern(file) {
|
|
38524
38607
|
const posix = file.replaceAll("\\", "/");
|
|
38525
38608
|
const lastSlash = posix.lastIndexOf("/");
|
|
@@ -40549,6 +40632,27 @@ function resolveSetupFilePaths(config) {
|
|
|
40549
40632
|
resolveAllSetupFilePaths([config]);
|
|
40550
40633
|
}
|
|
40551
40634
|
//#endregion
|
|
40635
|
+
//#region src/run/run-header.ts
|
|
40636
|
+
/**
|
|
40637
|
+
* Print the ` RUN vX.Y <rootDir>` header to stdout at the moment a run begins
|
|
40638
|
+
* (right before the backend uploads), so the CLI doesn't look stalled while it
|
|
40639
|
+
* waits for remote results. The end-of-run formatters no longer emit it.
|
|
40640
|
+
*
|
|
40641
|
+
* Self-gates to the default human formatter: nothing is written under
|
|
40642
|
+
* `--silent`, `--formatters json`, or `--formatters agent` (without
|
|
40643
|
+
* `--verbose`), which produce machine-readable output that must stay clean.
|
|
40644
|
+
*/
|
|
40645
|
+
function emitRunHeader(input) {
|
|
40646
|
+
if (!isDefaultHumanFormatter(input)) return;
|
|
40647
|
+
node_process.default.stdout.write(formatRunHeader({
|
|
40648
|
+
collectCoverage: input.collectCoverage,
|
|
40649
|
+
color: input.color,
|
|
40650
|
+
rootDir: input.rootDir,
|
|
40651
|
+
verbose: input.verbose ?? false,
|
|
40652
|
+
version: input.version
|
|
40653
|
+
}));
|
|
40654
|
+
}
|
|
40655
|
+
//#endregion
|
|
40552
40656
|
//#region src/run/multi.ts
|
|
40553
40657
|
const DEFAULT_ROJO_PROJECT = "default.project.json";
|
|
40554
40658
|
const VERSION$3 = version;
|
|
@@ -40592,6 +40696,15 @@ async function runMultiProject(options) {
|
|
|
40592
40696
|
rootConfig
|
|
40593
40697
|
});
|
|
40594
40698
|
});
|
|
40699
|
+
if (pendingJobs.length > 0) emitRunHeader({
|
|
40700
|
+
collectCoverage: rootConfig.collectCoverage,
|
|
40701
|
+
color: rootConfig.color,
|
|
40702
|
+
formatters: rootConfig.formatters,
|
|
40703
|
+
rootDir: rootConfig.rootDir,
|
|
40704
|
+
silent: rootConfig.silent,
|
|
40705
|
+
verbose: rootConfig.verbose,
|
|
40706
|
+
version: VERSION$3
|
|
40707
|
+
});
|
|
40595
40708
|
const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
|
|
40596
40709
|
const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
|
|
40597
40710
|
const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
|
|
@@ -40663,10 +40776,11 @@ function collectPendingJobs(arguments_) {
|
|
|
40663
40776
|
testMatch: project.include
|
|
40664
40777
|
};
|
|
40665
40778
|
const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, projectCliFiles).files, rootConfig);
|
|
40666
|
-
const
|
|
40779
|
+
const filterActive = (projectCliFiles?.length ?? 0) > 0 || discoveryConfig.testPathPattern !== void 0;
|
|
40780
|
+
const projConfig = narrowForLuauRun({
|
|
40667
40781
|
...discoveryConfig,
|
|
40668
40782
|
testMatch: project.testMatch
|
|
40669
|
-
},
|
|
40783
|
+
}, runtimeFiles, filterActive);
|
|
40670
40784
|
allTypeTestFiles.push(...typeTestFiles);
|
|
40671
40785
|
if (runtimeFiles.length === 0) continue;
|
|
40672
40786
|
const runtimeInjectionPaths = [];
|
|
@@ -40789,15 +40903,15 @@ const VERSION$2 = version;
|
|
|
40789
40903
|
async function runSingleProject(options) {
|
|
40790
40904
|
const { cli } = options;
|
|
40791
40905
|
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
40792
|
-
const
|
|
40793
|
-
return narrowConfigByFiles(options.config, cli.files ?? []);
|
|
40794
|
-
});
|
|
40906
|
+
const baseConfig = { ...options.config };
|
|
40795
40907
|
timing.profile("resolveSetupFilePaths", () => {
|
|
40796
|
-
resolveSetupFilePaths(
|
|
40908
|
+
resolveSetupFilePaths(baseConfig);
|
|
40909
|
+
});
|
|
40910
|
+
const discovery = timing.profile("discoverTestFiles", () => {
|
|
40911
|
+
return discoverTestFiles(baseConfig, cli.files);
|
|
40797
40912
|
});
|
|
40798
|
-
const discovery = timing.profile("discoverTestFiles", () => discoverTestFiles(config, cli.files));
|
|
40799
40913
|
if (discovery.files.length === 0) {
|
|
40800
|
-
if (
|
|
40914
|
+
if (baseConfig.passWithNoTests) return {
|
|
40801
40915
|
mode: "single",
|
|
40802
40916
|
preCoverageMs: 0
|
|
40803
40917
|
};
|
|
@@ -40809,7 +40923,11 @@ async function runSingleProject(options) {
|
|
|
40809
40923
|
};
|
|
40810
40924
|
}
|
|
40811
40925
|
const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
|
|
40812
|
-
return classifyTestFiles(discovery.files,
|
|
40926
|
+
return classifyTestFiles(discovery.files, baseConfig);
|
|
40927
|
+
});
|
|
40928
|
+
const filterActive = (cli.files?.length ?? 0) > 0 || baseConfig.testPathPattern !== void 0;
|
|
40929
|
+
const config = timing.profile("narrowForLuauRun", () => {
|
|
40930
|
+
return narrowForLuauRun(baseConfig, runtimeFiles, filterActive);
|
|
40813
40931
|
});
|
|
40814
40932
|
if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
|
|
40815
40933
|
if (config.passWithNoTests) return {
|
|
@@ -40857,7 +40975,17 @@ async function runSingleProject(options) {
|
|
|
40857
40975
|
}
|
|
40858
40976
|
async function executeRuntimeTests(options) {
|
|
40859
40977
|
const { cli, config, testFiles, timing, totalFiles } = options;
|
|
40860
|
-
|
|
40978
|
+
const useDefaultFormatter = isDefaultHumanFormatter(config);
|
|
40979
|
+
emitRunHeader({
|
|
40980
|
+
collectCoverage: config.collectCoverage,
|
|
40981
|
+
color: config.color,
|
|
40982
|
+
formatters: config.formatters,
|
|
40983
|
+
rootDir: config.rootDir,
|
|
40984
|
+
silent: config.silent,
|
|
40985
|
+
verbose: config.verbose,
|
|
40986
|
+
version: VERSION$2
|
|
40987
|
+
});
|
|
40988
|
+
if (useDefaultFormatter && testFiles.length !== totalFiles) node_process.default.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
40861
40989
|
const backend = await timing.profileAsync("resolveBackend", async () => {
|
|
40862
40990
|
return resolveBackend(cli, config);
|
|
40863
40991
|
});
|
|
@@ -41790,13 +41918,33 @@ async function prepareWorkspaceDispatch(input) {
|
|
|
41790
41918
|
}
|
|
41791
41919
|
return { scriptOverride: generateMaterializerScript(inputs) };
|
|
41792
41920
|
}
|
|
41921
|
+
/**
|
|
41922
|
+
* Resolve a `--testPathPattern` against this package's files Node-side, then
|
|
41923
|
+
* forward an Instance-namespace basename pattern (see {@link narrowForLuauRun}).
|
|
41924
|
+
*
|
|
41925
|
+
* A pattern that matches no file in this package simply targets a different
|
|
41926
|
+
* package: keep the (zero-matching) raw pattern so Jest-on-Roblox runs nothing,
|
|
41927
|
+
* and set `passWithNoTests` so it doesn't `exit(1)`. The raw pattern is
|
|
41928
|
+
* load-bearing here — clearing it would drop the filter entirely and make the
|
|
41929
|
+
* Luau side fall back to `testMatch`, running the whole package.
|
|
41930
|
+
*/
|
|
41931
|
+
function narrowPackageTestPathPattern(packageConfig) {
|
|
41932
|
+
if (packageConfig.testPathPattern === void 0) return packageConfig;
|
|
41933
|
+
const { files } = discoverTestFiles(packageConfig);
|
|
41934
|
+
const { runtimeFiles } = classifyTestFiles(files, packageConfig);
|
|
41935
|
+
if (runtimeFiles.length === 0) return {
|
|
41936
|
+
...packageConfig,
|
|
41937
|
+
passWithNoTests: true
|
|
41938
|
+
};
|
|
41939
|
+
return narrowForLuauRun(packageConfig, runtimeFiles, true);
|
|
41940
|
+
}
|
|
41793
41941
|
async function loadPackages(input) {
|
|
41794
41942
|
const { cli, packageInfos, timing } = input;
|
|
41795
41943
|
const loaded = [];
|
|
41796
41944
|
for (const info of packageInfos) {
|
|
41797
|
-
const packageConfig = mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
41945
|
+
const packageConfig = narrowPackageTestPathPattern(mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
|
|
41798
41946
|
return loadConfig(void 0, info.packageDirectory);
|
|
41799
|
-
}));
|
|
41947
|
+
})));
|
|
41800
41948
|
const rojoProject = packageConfig.rojoProject ?? ROJO_PROJECT_DEFAULT;
|
|
41801
41949
|
const hasExplicitIgnore = packageConfig.coveragePathIgnorePatterns !== DEFAULT_CONFIG.coveragePathIgnorePatterns;
|
|
41802
41950
|
const hasExplicitCoverageCache = packageConfig.coverageCache !== DEFAULT_CONFIG.coverageCache;
|
|
@@ -42389,6 +42537,14 @@ async function runWorkspaceMode(cli, workspace, timing) {
|
|
|
42389
42537
|
}
|
|
42390
42538
|
let runtimeResults;
|
|
42391
42539
|
try {
|
|
42540
|
+
emitRunHeader({
|
|
42541
|
+
color: runOptions.color,
|
|
42542
|
+
formatters: runOptions.formatters,
|
|
42543
|
+
rootDir: workspaceRoot,
|
|
42544
|
+
silent: runOptions.silent,
|
|
42545
|
+
verbose: cli.verbose,
|
|
42546
|
+
version: VERSION$1
|
|
42547
|
+
});
|
|
42392
42548
|
const onStreamingResult = resolveStreamingProgressSink(runOptions, cli);
|
|
42393
42549
|
runtimeResults = await runWorkspace({
|
|
42394
42550
|
backend,
|
|
@@ -42486,8 +42642,11 @@ function composeWorkspaceDisplayName(package_, project) {
|
|
|
42486
42642
|
* either break the structured output or be silenced anyway.
|
|
42487
42643
|
*/
|
|
42488
42644
|
function resolveStreamingProgressSink(runOptions, cli) {
|
|
42489
|
-
if (
|
|
42490
|
-
|
|
42645
|
+
if (!isDefaultHumanFormatter({
|
|
42646
|
+
formatters: runOptions.formatters,
|
|
42647
|
+
silent: runOptions.silent,
|
|
42648
|
+
verbose: cli.verbose
|
|
42649
|
+
})) return;
|
|
42491
42650
|
return (entry) => {
|
|
42492
42651
|
const line = formatStreamingProgressLine(entry, { color: runOptions.color });
|
|
42493
42652
|
node_process.default.stdout.write(`${line}\n`);
|
|
@@ -42520,29 +42679,6 @@ async function runJestRoblox(cli, config) {
|
|
|
42520
42679
|
}
|
|
42521
42680
|
}
|
|
42522
42681
|
//#endregion
|
|
42523
|
-
//#region src/utils/error-chain.ts
|
|
42524
|
-
const MAX_DEPTH = 5;
|
|
42525
|
-
function walkErrorChain(err) {
|
|
42526
|
-
const entries = [];
|
|
42527
|
-
let current = err;
|
|
42528
|
-
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
42529
|
-
entries.push({
|
|
42530
|
-
name: current.constructor.name,
|
|
42531
|
-
code: readStringProperty(current, "code"),
|
|
42532
|
-
errno: readStringProperty(current, "errno"),
|
|
42533
|
-
message: current.message,
|
|
42534
|
-
syscall: readStringProperty(current, "syscall")
|
|
42535
|
-
});
|
|
42536
|
-
current = current.cause;
|
|
42537
|
-
}
|
|
42538
|
-
return entries;
|
|
42539
|
-
}
|
|
42540
|
-
function readStringProperty(err, key) {
|
|
42541
|
-
const value = Reflect.get(err, key);
|
|
42542
|
-
if (value === void 0 || value === null) return;
|
|
42543
|
-
return String(value);
|
|
42544
|
-
}
|
|
42545
|
-
//#endregion
|
|
42546
42682
|
//#region src/cli.ts
|
|
42547
42683
|
const VERSION = version;
|
|
42548
42684
|
const HELP_TEXT = `
|
|
@@ -42564,6 +42700,7 @@ Options:
|
|
|
42564
42700
|
--no-color Disable colored output
|
|
42565
42701
|
-u, --updateSnapshot Update snapshot files
|
|
42566
42702
|
--coverage Enable coverage collection
|
|
42703
|
+
--no-coverage Disable coverage for this run (overrides config)
|
|
42567
42704
|
--collectCoverageFrom <glob> Globs for files to include in coverage (repeatable)
|
|
42568
42705
|
--coverageDirectory <path> Directory for coverage output (default: coverage)
|
|
42569
42706
|
--coverageReporters <r...> Coverage reporters (default: text, lcov)
|
|
@@ -42607,6 +42744,7 @@ Examples:
|
|
|
42607
42744
|
jest-roblox -t "should spawn" Run tests matching pattern
|
|
42608
42745
|
jest-roblox --formatters json Output JSON to file
|
|
42609
42746
|
jest-roblox --coverage Run tests with coverage instrumentation
|
|
42747
|
+
jest-roblox --no-coverage Skip coverage instrumentation for this run
|
|
42610
42748
|
`;
|
|
42611
42749
|
function parseArgs(args) {
|
|
42612
42750
|
const { positionals, values } = (0, node_util.parseArgs)({
|
|
@@ -42639,6 +42777,7 @@ function parseArgs(args) {
|
|
|
42639
42777
|
type: "boolean"
|
|
42640
42778
|
},
|
|
42641
42779
|
"no-color": { type: "boolean" },
|
|
42780
|
+
"no-coverage": { type: "boolean" },
|
|
42642
42781
|
"no-coverage-cache": { type: "boolean" },
|
|
42643
42782
|
"no-show-luau": { type: "boolean" },
|
|
42644
42783
|
"outputFile": { type: "string" },
|
|
@@ -42693,7 +42832,7 @@ function parseArgs(args) {
|
|
|
42693
42832
|
affectedSince: values["affected-since"],
|
|
42694
42833
|
apiKey: values.apiKey,
|
|
42695
42834
|
backend: validateBackend(values.backend),
|
|
42696
|
-
collectCoverage: values.coverage,
|
|
42835
|
+
collectCoverage: values["no-coverage"] === true ? false : values.coverage,
|
|
42697
42836
|
collectCoverageFrom: values.collectCoverageFrom,
|
|
42698
42837
|
color: values["no-color"] === true ? false : values.color,
|
|
42699
42838
|
config: values.config,
|
|
@@ -42805,6 +42944,7 @@ function formatBackendErrorBanner(err) {
|
|
|
42805
42944
|
const extras = formatChainExtras(entry);
|
|
42806
42945
|
const label = y$3.dim(`[${index.toString()}]`);
|
|
42807
42946
|
body.push(` ${label} ${entry.name}: ${entry.message}${extras}`);
|
|
42947
|
+
if (entry.requiredScopes !== void 0) body.push(` ${y$3.yellow(formatMissingScopes(entry.requiredScopes))}`);
|
|
42808
42948
|
}
|
|
42809
42949
|
return formatBanner({
|
|
42810
42950
|
body,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isentinel/jest-roblox",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jest",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@isentinel/eslint-config": "5.0.0-beta.11",
|
|
62
|
-
"@isentinel/roblox-ts": "4.0.
|
|
62
|
+
"@isentinel/roblox-ts": "4.0.6",
|
|
63
63
|
"@isentinel/tsconfig": "1.2.0",
|
|
64
64
|
"@isentinel/weld": "0.2.0",
|
|
65
65
|
"@oxc-project/types": "0.123.0",
|