mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Accept mock.fn() without an original
node:test changes, matching Node 24.22:
- mock.fn() and t.mock.fn() without an original return undefined and
record the arguments of each call, whatever the callers pass. The
original and the initial implementation are a function that returns
undefined, so restore() brings that function back.
- mock.fn<F>() with an explicit function type records F's parameters
when F's return type is void or includes undefined. A return type
without undefined, such as mock.fn<() => number>(), throws SC2020.
mock.fn()'s default type, (...args: any[]) => undefined, maps to the
checked-dynamic rest ABI used for program-declared (...args: unknown[])
functions: the calls log records the argument array, and the mock's
wrapper spreads it into the implementation.
A function whose fixed parameters are all unknown or any, or that has
none, now converts to a checked-dynamic rest signature without fixed
parameters, such as (...args: unknown[]) => void: each call passes the
arguments in order, undefined for the ones the caller left out. This is
how () => {} reaches mockImplementation and mockImplementationOnce, and
it applies to any such slot in TypeScript programs, which threw SC1090
before.
The mock-fn-default fixture and corpus program 5816 compare with Node,
plain and with SCRIPTC_SAN=1.
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
|
||||
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original returns `undefined` and records the arguments of each call, as in Node; with a function type whose return type does not include `undefined`, such as `mock.fn<() => number>()`, it throws `SC2020`. `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
|
||||
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. A function value with fixed parameters cannot be stored where such a variadic function type is expected.
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members. Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments. A function value whose fixed parameters are all `unknown` or `any`, or that has none, can be stored where such a variadic function type without fixed parameters is expected: each call passes the arguments in order, `undefined` for the ones the caller left out. Other function values with fixed parameters cannot be stored there.
|
||||
|
||||
@@ -66502,6 +66502,7 @@
|
||||
"source": "compiler-feature:test.mock.fn",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -91073,6 +91073,7 @@
|
||||
"tests/fixtures/node-test/cases/ctx-plan.ts",
|
||||
"tests/fixtures/node-test/cases/get-test-context-cancelled.ts",
|
||||
"tests/fixtures/node-test/cases/get-test-context.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-method.ts",
|
||||
"tests/fixtures/node-test/cases/mock-timers-date.ts",
|
||||
@@ -92484,6 +92485,7 @@
|
||||
"evidence": "compiler-feature:test.mock.fn",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-fn-default.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
|
||||
|
||||
@@ -127,15 +127,71 @@ export function cleanFuncAdaptable(
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Adapt a fixed-parameter function to a checked-dynamic rest signature
|
||||
* without fixed parameters (`() => {}` into mock.fn()'s `(...args: any[])
|
||||
* => undefined`): the adapter receives the hidden argument array and
|
||||
* passes its element i as parameter i, undefined when the caller passed
|
||||
* fewer arguments, as JS does. Null unless every parameter is
|
||||
* checked-dynamic, which holds a missing argument as undefined, and the
|
||||
* result is discarded or converts. */
|
||||
function funcIntoDynRestAdapter(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (fromT.rest === true || fromT.params.some((p) => p.kind !== "dyn")) return null;
|
||||
if (toT.ret.kind !== "void" && !lowerer.coercibleValue(fromT.ret, toT.ret)) return null;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `%fn.adapt.${lowerer.coercions.retags.size}`;
|
||||
lowerer.coercions.retags.set(key, name);
|
||||
lowerer.freshClosureAdapters.add(name);
|
||||
const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }];
|
||||
const call: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: fromT.params.map((_, i): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: { kind: "varRef", localId: "rest.0", type: DYN, loc },
|
||||
key: { kind: "strLit", value: String(i), type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
})),
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
};
|
||||
let body: IrStmt[];
|
||||
if (toT.ret.kind === "void") {
|
||||
body = [
|
||||
{ kind: "exprStmt", expr: call, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
} else {
|
||||
const result = lowerer.coerceToExpected(call, toT.ret);
|
||||
if (!typeEquals(result.type, toT.ret))
|
||||
throw new InternalCompilerError("lowerer bug: probed fn-adapter return stopped coercing");
|
||||
body = [{ kind: "return", value: result, loc }];
|
||||
}
|
||||
lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc));
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Adapt a function value to a destination signature, forwarding and
|
||||
* converting its arguments and result. Unsupported parameter/result pairs
|
||||
* produce checked traps when called; rest signatures remain refused. */
|
||||
* produce checked traps when called; rest signatures remain refused,
|
||||
* except a fixed-parameter function into a checked-dynamic rest signature
|
||||
* without fixed parameters (funcIntoDynRestAdapter). */
|
||||
export function funcCoerceAdapter(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (toT.rest === true && toT.restAbi === undefined && toT.params.length === 0)
|
||||
return funcIntoDynRestAdapter(lowerer, fromT, toT, loc);
|
||||
if (fromT.rest === true || toT.rest === true) return null;
|
||||
if (fromT.params.length > toT.params.length) return null;
|
||||
// Piece dispositions beyond coercibleValue, all CHECKER-APPROVED
|
||||
|
||||
@@ -154,7 +154,12 @@ function mockWrapperFactory(
|
||||
const { fn, call, calls, once, onceEntry, result, args } = mockContextFields(lowerer, ctxType)!;
|
||||
const impl = `${name}.impl`;
|
||||
const ctx = () => varRef("ctx.0", ctxType, loc);
|
||||
// A checked-dynamic rest function (mock.fn()'s default type) takes its
|
||||
// arguments as one trailing dyn array, recorded as the call's arguments
|
||||
// and spread into the implementation.
|
||||
const dynRest = fn.rest === true && fn.restAbi === undefined;
|
||||
const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
if (dynRest) params.push({ localId: "rest.0", name: "args", type: DYN });
|
||||
const paramRefs = () => params.map((p) => varRef(p.localId, p.type, loc));
|
||||
const argumentsValue = (): IrExpr =>
|
||||
fn.rest
|
||||
@@ -219,13 +224,23 @@ function mockWrapperFactory(
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const invoke: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fn, loc),
|
||||
args: paramRefs(),
|
||||
type: fn.ret,
|
||||
loc,
|
||||
};
|
||||
const invoke: IrExpr = dynRest
|
||||
? {
|
||||
kind: "dynCall",
|
||||
callee: { kind: "dynFrom", value: varRef("f.0", fn, loc), type: DYN, loc },
|
||||
calleeName: "implementation",
|
||||
args: paramRefs(),
|
||||
spreads: [{ arg: 0, what: "args" }],
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fn, loc),
|
||||
args: paramRefs(),
|
||||
type: fn.ret,
|
||||
loc,
|
||||
};
|
||||
const tryBody: IrStmt[] = voidReturn
|
||||
? [
|
||||
{ kind: "exprStmt", expr: invoke, loc },
|
||||
@@ -629,7 +644,37 @@ function requireUndefinedResultArm(
|
||||
}
|
||||
}
|
||||
|
||||
/** mock.fn(original, implementation?) and t.mock.fn(...). */
|
||||
/** `%mock.noop.N`: mock.fn()'s original, Node's `function () {}` — it
|
||||
* returns undefined. Null when the function type's return type cannot hold
|
||||
* undefined. */
|
||||
function mockNoopOriginal(
|
||||
lowerer: Lowerer,
|
||||
ctxType: IrType,
|
||||
fn: FuncType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const voidReturn = fn.ret.kind === "void" || fn.ret.kind === "undefinedT";
|
||||
const undefinedValue = voidReturn ? null : lowerer.wrappedUndefined(fn.ret, loc);
|
||||
if (!voidReturn && !undefinedValue) return null;
|
||||
const name = liftOnce(lowerer, ctxType, "noop", (name) => {
|
||||
const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
if (fn.rest === true && fn.restAbi === undefined)
|
||||
params.push({ localId: "rest.0", name: "args", type: DYN });
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: fn.ret,
|
||||
locals: params.map((p) => local(p.localId, p.name, p.type)),
|
||||
body: [{ kind: "return", value: undefinedValue, loc }],
|
||||
loc,
|
||||
});
|
||||
});
|
||||
return { kind: "closure", fnName: name, captures: [], type: fn, loc };
|
||||
}
|
||||
|
||||
/** mock.fn(original?, implementation?) and t.mock.fn(...). Without an
|
||||
* original, the mock's original and implementation are a function that
|
||||
* returns undefined, as in Node. */
|
||||
export function lowerMockFn(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -658,11 +703,19 @@ export function lowerMockFn(
|
||||
const { fn } = fields;
|
||||
requireUndefinedResultArm(lowerer, fields, call, what);
|
||||
const args = call.arguments.nodes;
|
||||
if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
if (args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`${what} with ${args.length} arguments`,
|
||||
call,
|
||||
"the supported forms are mock.fn(original) and mock.fn(original, implementation); mock.fn() and mock options have no lowering",
|
||||
"the supported forms are mock.fn(), mock.fn(original) and mock.fn(original, implementation); mock options have no lowering",
|
||||
);
|
||||
}
|
||||
const noop = args.length === 0 ? mockNoopOriginal(lowerer, ctxType!, fn, loc) : null;
|
||||
if (args.length === 0 && !noop) {
|
||||
lowerer.noLowering(
|
||||
`${what}() of a function returning '${lowerer.fmt(fn.ret)}'`,
|
||||
call,
|
||||
"mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined",
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
@@ -670,7 +723,7 @@ export function lowerMockFn(
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: originalLocal.id,
|
||||
init: lowerer.lowerExprExpecting(args[0]!, fn),
|
||||
init: noop ?? lowerer.lowerExprExpecting(args[0]!, fn),
|
||||
loc,
|
||||
});
|
||||
const implementation =
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
* object; empty for mock.fn
|
||||
*
|
||||
* A MockFunctionCall<F> is { arguments, error, result, this }: the
|
||||
* arguments as a tuple of F's parameters, the result as F's return type or
|
||||
* undefined (a checked-dynamic value for functions returning void or
|
||||
* undefined, since a record field cannot have a unit type), and error and
|
||||
* this as checked-dynamic values. */
|
||||
* arguments as a tuple of F's parameters (a checked-dynamic array for
|
||||
* mock.fn()'s `(...args: any[]) => undefined`), the result as F's return
|
||||
* type or undefined (a checked-dynamic value for functions returning void
|
||||
* or undefined, since a record field cannot have a unit type), and error
|
||||
* and this as checked-dynamic values. */
|
||||
import { DYN, F64, NULL_T, UNDEFINED_T, VOID, typeKey, type IrType } from "../ir/ir.js";
|
||||
import type { TypeMapperCtx } from "./type-mapper.js";
|
||||
|
||||
@@ -34,11 +35,17 @@ function orUndefined(t: IrType, reg: MockShapeRegistries): IrType {
|
||||
}
|
||||
|
||||
/** The arguments type of a call record: a tuple of the parameters, or the
|
||||
* rest array itself for a function whose only parameter is a rest array.
|
||||
* Null for other rest shapes. A function without parameters records the
|
||||
* rest array itself for a function whose only parameter is a rest array,
|
||||
* or a checked-dynamic array for a function returning nothing whose only
|
||||
* parameter is a checked-dynamic rest (mock.fn()'s default, `(...args:
|
||||
* any[]) => undefined`). Null for other rest shapes. A function without parameters records the
|
||||
* empty tuple, which maps to an array of the unit-only union, as
|
||||
* type-mapper.ts maps `[]`. */
|
||||
function mockArgumentsType(fn: IrType & { kind: "func" }, reg: MockShapeRegistries): IrType | null {
|
||||
if (fn.rest && fn.restAbi === undefined) {
|
||||
const returnsNothing = fn.ret.kind === "void" || fn.ret.kind === "undefinedT";
|
||||
return fn.params.length === 0 && returnsNothing ? DYN : null;
|
||||
}
|
||||
if (fn.rest) {
|
||||
if (fn.params.length !== 1 || fn.params[0]!.kind !== "array") return null;
|
||||
return fn.params[0]!;
|
||||
|
||||
@@ -4384,10 +4384,13 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
// params). Declaration files keep their existing paths: library
|
||||
// callback slots such as String#replace's `(substring, ...args: any[])`
|
||||
// have intrinsic lowerings that inspect the unmapped type.
|
||||
// node:test's default mock function type, `(...args: any[]) =>
|
||||
// undefined` (the type parameter default of mock.fn), takes the same
|
||||
// ABI: mock.fn() without an original records whatever its callers pass.
|
||||
if (
|
||||
rest &&
|
||||
decl !== undefined &&
|
||||
!decl.getSourceFile().isDeclarationFile &&
|
||||
(!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
@@ -6364,6 +6367,23 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** A parameter of a function type that is the default of a type parameter
|
||||
* declared in node:test's standard-library declarations (MockTracker.fn's
|
||||
* `F extends Function = (...args: any[]) => undefined`). */
|
||||
function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx): boolean {
|
||||
const fnType = decl.parent;
|
||||
return (
|
||||
ts.isParameter(decl) &&
|
||||
fnType !== undefined &&
|
||||
ts.isFunctionTypeNode(fnType) &&
|
||||
fnType.parent !== undefined &&
|
||||
ts.isTypeParameterDeclaration(fnType.parent) &&
|
||||
fnType.parent.defaultType === fnType &&
|
||||
ctx.isStdlibFile(decl.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(decl, "test")
|
||||
);
|
||||
}
|
||||
|
||||
/** `any[]` (or `readonly any[]`): an array type whose element is `any`. */
|
||||
export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
if (!checker.isArrayType(t)) return false;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5816-fixed-into-unknown-rest.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// A function with only `unknown` parameters, or none, flows into a
|
||||
// `(...args: unknown[]) => R` slot: each call passes the callers'
|
||||
// arguments in order, undefined for a parameter the caller left out, and
|
||||
// drops the extra ones, as JS calls do. Sync and async functions, stored
|
||||
// in variables and arrays and passed as arguments.
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
type AsyncHandler = (...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
const none: Handler = () => console.log("none");
|
||||
const one: Handler = (a: unknown) => console.log("one", a);
|
||||
const two: Handler = (a: unknown, b: unknown) => console.log("two", a, b);
|
||||
|
||||
none();
|
||||
none(1, 2, 3);
|
||||
one();
|
||||
one("x");
|
||||
one("x", "y");
|
||||
two(1);
|
||||
two(1, [2], { three: 3 });
|
||||
|
||||
const handlers: Handler[] = [none, one, two];
|
||||
for (const h of handlers) h("all", true);
|
||||
|
||||
function dispatch(h: Handler, ...args: unknown[]): void {
|
||||
h(...args);
|
||||
}
|
||||
dispatch(two, "spread", null);
|
||||
dispatch(one);
|
||||
|
||||
const first: AsyncHandler = async (value: unknown) => value;
|
||||
const pair: AsyncHandler = async (a: unknown, b: unknown) => [a, b];
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(await first(10, 20));
|
||||
console.log(await first());
|
||||
console.log(await pair("p"));
|
||||
console.log(await pair("p", "q", "r"));
|
||||
}
|
||||
main().then(() => console.log("done"));
|
||||
@@ -30,8 +30,3 @@ console.log(typeof widened);
|
||||
// `instanceof Promise` on a value that is never a promise.
|
||||
const record = { v: 1 };
|
||||
console.log(record instanceof Promise);
|
||||
|
||||
// A variadic `unknown[]` slot takes closures with the same variadic ABI.
|
||||
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
const fixed: HandlerFn = async (first: unknown) => first;
|
||||
fixed(1, 2).then((v) => console.log(v));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// node:test mocks lower for mock.fn(original[, implementation]),
|
||||
// node:test mocks lower for mock.fn([original[, implementation]]),
|
||||
// mock.method(object, methodName[, implementation]) on records, their
|
||||
// t.mock twins, and the trackers' restoreAll and reset. The rest of the
|
||||
// mock surface is refused, each with a named diagnostic: mock.fn() without
|
||||
// an original; mock.method on a class instance or a builtin object, with
|
||||
// an original for a function type whose return type does not include
|
||||
// undefined; mock.method on a class instance or a builtin object, with
|
||||
// an options object, or with an implementation of another type when the
|
||||
// result is used; and functions whose union return type has no undefined
|
||||
// arm (the calls log records undefined for calls that throw).
|
||||
@@ -11,8 +12,11 @@ import { mock, test } from "node:test";
|
||||
const add = mock.fn((a: number, b: number) => a + b); // lowers
|
||||
add(1, 2);
|
||||
|
||||
const empty = mock.fn(); // no original function
|
||||
empty();
|
||||
const empty = mock.fn(); // lowers
|
||||
empty(1, "two");
|
||||
|
||||
const counted = mock.fn<() => number>(); // no original, returns number
|
||||
counted();
|
||||
|
||||
const obj = { value: () => 1 };
|
||||
mock.method(obj, "value"); // lowers
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// mock.fn() and t.mock.fn() without an original: the mock returns
|
||||
// undefined and records the arguments of each call, whatever its callers
|
||||
// pass. mockImplementation and mockImplementationOnce replace its
|
||||
// behavior, and restore() brings back the function that returns undefined.
|
||||
// mock.fn<F>() records F's parameters, for a function type whose return
|
||||
// type is void or includes undefined.
|
||||
import assert from "node:assert";
|
||||
import { mock, test } from "node:test";
|
||||
|
||||
test("records calls with any arguments", () => {
|
||||
const fn = mock.fn();
|
||||
fn(1, "two");
|
||||
fn();
|
||||
fn(true, null, [3]);
|
||||
assert.strictEqual(fn.mock.callCount(), 3);
|
||||
const [first, second, third] = fn.mock.calls;
|
||||
assert.deepStrictEqual(first!.arguments, [1, "two"]);
|
||||
assert.strictEqual(second!.arguments.length, 0);
|
||||
assert.deepStrictEqual(third!.arguments, [true, null, [3]]);
|
||||
assert.ok(first!.result === undefined);
|
||||
assert.ok(first!.error === undefined);
|
||||
assert.ok(first!.this === undefined);
|
||||
});
|
||||
|
||||
test("changes implementations", () => {
|
||||
const seen: string[] = [];
|
||||
const fn = mock.fn();
|
||||
fn.mock.mockImplementation((...args: unknown[]) => {
|
||||
seen.push(`impl ${args.length}`);
|
||||
return undefined;
|
||||
});
|
||||
fn.mock.mockImplementationOnce(() => {
|
||||
seen.push("once");
|
||||
return undefined;
|
||||
});
|
||||
fn("a");
|
||||
fn("a", "b");
|
||||
fn.mock.restore();
|
||||
fn("c");
|
||||
assert.deepStrictEqual(seen, ["once", "impl 2"]);
|
||||
assert.strictEqual(fn.mock.callCount(), 3);
|
||||
fn.mock.resetCalls();
|
||||
assert.strictEqual(fn.mock.callCount(), 0);
|
||||
});
|
||||
|
||||
test("records a throwing implementation", () => {
|
||||
const fn = mock.fn();
|
||||
fn.mock.mockImplementation(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
assert.throws(() => fn(7), { message: "boom" });
|
||||
const call = fn.mock.calls[0]!;
|
||||
assert.deepStrictEqual(call.arguments, [7]);
|
||||
assert.ok(call.error instanceof Error);
|
||||
});
|
||||
|
||||
test("t.mock.fn() and typed mock.fn<F>()", (t) => {
|
||||
const fn = t.mock.fn();
|
||||
fn("x");
|
||||
assert.strictEqual(fn.mock.callCount(), 1);
|
||||
const typed = t.mock.fn<(a: number, b: string) => void>();
|
||||
typed(1, "x");
|
||||
typed(2, "y");
|
||||
assert.strictEqual(typed.mock.callCount(), 2);
|
||||
assert.strictEqual(typed.mock.calls[1]!.arguments[0], 2);
|
||||
assert.strictEqual(typed.mock.calls[1]!.arguments[1], "y");
|
||||
const maybe = t.mock.fn<() => number | undefined>();
|
||||
assert.ok(maybe() === undefined);
|
||||
maybe.mock.mockImplementation(() => 4);
|
||||
assert.ok(maybe() === 4);
|
||||
assert.ok(maybe.mock.calls[1]!.result === 4);
|
||||
});
|
||||
@@ -55,11 +55,4 @@ maybe-promise-fences.ts:32:13 - error SC1090: 'instanceof Promise' on '{ v: numb
|
||||
31 | const record = { v: 1 };
|
||||
32 | console.log(record instanceof Promise);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
33 |
|
||||
|
||||
maybe-promise-fences.ts:36:26 - error SC1090: '(unknown) => Promise<unknown>' values where '() => Promise<unknown>' is expected is not supported yet
|
||||
|
||||
35 | type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
||||
36 | const fixed: HandlerFn = async (first: unknown) => first;
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
37 | fixed(1, 2).then((v) => console.log(v));
|
||||
33 |
|
||||
@@ -1,62 +1,62 @@
|
||||
mock-fences.ts:14:15 - error SC2020: 'mock.fn of this function type' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:18:17 - error SC2020: 'mock.fn() of a function returning 'number'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
13 |
|
||||
14 | const empty = mock.fn(); // no original function
|
||||
| ^~~~~~~~~
|
||||
15 | empty();
|
||||
17 |
|
||||
18 | const counted = mock.fn<() => number>(); // no original, returns number
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~
|
||||
19 | counted();
|
||||
|
||||
hint: mock functions lower for functions with fixed parameters, or a single rest array parameter, and an implementation of the same type
|
||||
hint: mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined
|
||||
|
||||
mock-fences.ts:27:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:31:13 - error SC2020: 'mock.method on 'Counter'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
26 | }
|
||||
27 | mock.method(new Counter(), "count"); // class instance
|
||||
30 | }
|
||||
31 | mock.method(new Counter(), "count"); // class instance
|
||||
| ^~~~~~~~~~~~~
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
|
||||
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
|
||||
|
||||
mock-fences.ts:28:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:32:13 - error SC2020: 'mock.method on this object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
27 | mock.method(new Counter(), "count"); // class instance
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
31 | mock.method(new Counter(), "count"); // class instance
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
| ^~~~~~~
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
|
||||
hint: mock.method lowers for objects whose type maps to a record (object literals and interface-typed objects) and a function-valued field named by a string literal
|
||||
|
||||
mock-fences.ts:29:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:33:27 - error SC2020: 'mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
28 | mock.method(console, "log"); // builtin object
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
32 | mock.method(console, "log"); // builtin object
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
| ^~~~~~~~~~~~
|
||||
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
|
||||
hint: mock options (times, getter and setter) have no lowering
|
||||
|
||||
mock-fences.ts:30:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:34:41 - error SC2020: 'mock.method with an implementation of another type' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
29 | mock.method(obj, "value", { times: 1 }); // options
|
||||
30 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
33 | mock.method(obj, "value", { times: 1 }); // options
|
||||
34 | const other = mock.method(obj, "value", () => "one"); // implementation of another type
|
||||
| ^~~~~~~~~~~
|
||||
31 | other();
|
||||
35 | other();
|
||||
|
||||
hint: give the implementation the method's parameter and return types, or discard the result
|
||||
|
||||
mock-fences.ts:35:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:39:31 - error SC2020: 't.mock.method with an options object' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
34 | t.mock.method(obj, "value"); // lowers
|
||||
35 | t.mock.method(obj, "value", { times: 2 }); // options
|
||||
38 | t.mock.method(obj, "value"); // lowers
|
||||
39 | t.mock.method(obj, "value", { times: 2 }); // options
|
||||
| ^~~~~~~~~~~~
|
||||
36 | });
|
||||
40 | });
|
||||
|
||||
hint: mock options (times, getter and setter) have no lowering
|
||||
|
||||
mock-fences.ts:38:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
|
||||
mock-fences.ts:42:14 - error SC2020: 'mock.fn of a function returning 'number | string'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
37 |
|
||||
38 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
|
||||
41 |
|
||||
42 | const pick = mock.fn((flag: boolean): number | string => (flag ? 1 : "one")); // union without undefined
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
39 | pick(true);
|
||||
43 | pick(true);
|
||||
|
||||
hint: a union return type needs an undefined arm here (the calls log records undefined for calls that throw)
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3028, "san": 5470 }
|
||||
Reference in New Issue
Block a user