feat(ses-ava): Support the full ava API
Fixes
Ref
MODIFY
packages/promise-kit/test/test-promise-kit.js
packages/promise-kit/test/test-promise-kit.js
@@ -23,7 +23,6 @@ if (typeof globalThis.gc !== 'function') {
}
/** @type {typeof rawTest} */
-// @ts-expect-error cast
const test = wrapTest(rawTest);
test('makePromiseKit', async t => {

MODIFY
packages/ses-ava/NEWS.md
packages/ses-ava/NEWS.md
User-visible changes in SES-Ava:
+# Next release
+* Support the full ava API ([#1235](https://github.com/endojs/endo/issues/1235))
+
# 0.2.0 (2021-06-01)
* *BREAKING*: Removes CommonJS and UMD downgrade compatibility.

MODIFY
packages/ses-ava/exported.js
packages/ses-ava/exported.js
-import './src/types.js';

MODIFY
packages/ses-ava/src/ses-ava-test.js
packages/ses-ava/src/ses-ava-test.js
import 'ses';
-import './types.js';
+const { defineProperty, freeze } = Object;
const { apply } = Reflect;
/**
- * Just forwards to global `console.error`.
- *
- * @type {Logger}
+ * @typedef {(...args: unknown[]) => void} Logger
*/
+
const defaultLogger = (...args) => {
console.error(...args);
};
@@ -23,9 +22,28 @@ const isPromise = maybePromise =>
Promise.resolve(maybePromise) === maybePromise;
/**
- * @type {LogCallError}
+ * Calls `func(...args)` passing back approximately its outcome, but first
+ * logging any erroneous outcome to the `logger`.
+ *
+ * * If `func(...args)` returns a non-promise, silently return it.
+ * * If `func(...args)` throws, log what was thrown and then rethrow it.
+ * * If `func(...args)` returns a promise, immediately return a new
+ * unresolved promise.
+ * * If the first promise fulfills, silently fulfill the returned promise
+ * even if the fulfillment was an error.
+ * * If the first promise rejects, log the rejection reason and then
+ * reject the returned promise with the same reason.
+ *
+ * The delayed rejection of the returned promise is an observable difference
+ * from directly calling `func(...args)` but will be equivalent enough for most
+ * purposes.
+ *
+ * @param {(...unknown) => unknown} func
+ * @param {unknown[]} args
+ * @param {string} name
+ * @param {Logger} logger
*/
-const logErrorFirst = (func, args, name, logger = defaultLogger) => {
+const logErrorFirst = (func, args, name, logger) => {
let result;
try {
result = apply(func, undefined, args);
@@ -46,7 +64,7 @@ const logErrorFirst = (func, args, name, logger = defaultLogger) => {
}
};
-const testerMethodsWhitelist = [
+const overrideList = [
'after',
'afterEach',
'before',
@@ -54,37 +72,64 @@ const testerMethodsWhitelist = [
'failing',
'serial',
'only',
- 'skip',
];
/**
- * @param {TesterFunc} testerFunc
- * @param {Logger} [logger]
- * @returns {TesterFunc} Not yet frozen!
+ * @template {import('ava').TestFn} T
+ * @param {T} testerFunc
+ * @param {Logger} logger
+ * @returns {T} Not yet frozen!
*/
-const wrapTester = (testerFunc, logger = defaultLogger) => {
- /** @type {TesterFunc} */
- const testerWrapper = (title, implFunc, ...otherArgs) => {
- /** @type {ImplFunc} */
- const testFuncWrapper = t => {
+const augmentLogging = (testerFunc, logger) => {
+ const testerFuncName = `ava ${testerFunc.name || 'test'}`;
+ const augmented = (...args) => {
+ // Align with ava argument parsing.
+ // https://github.com/avajs/ava/blob/c74934853db1d387c46ed1f953970c777feed6a0/lib/parse-test-args.js
+ const maybeTitle = typeof args[0] === 'string' ? [args.shift()] : [];
+ const implFuncOrObj = args.shift();
+ const wrapImplFunc = fn => {
+ const wrappedFunc = t => {
harden(t);
- return logErrorFirst(implFunc, [t, ...otherArgs], 'ava test', logger);
+ return logErrorFirst(fn, [t, ...args], testerFuncName, logger);
};
- if (implFunc && implFunc.title) {
- testFuncWrapper.title = implFunc.title;
+ if (fn.title) {
+ wrappedFunc.title = fn.title;
}
- return testerFunc(title, testFuncWrapper, ...otherArgs);
+ return wrappedFunc;
};
- return testerWrapper;
+ let implArg;
+ if (typeof implFuncOrObj === 'function') {
+ // Handle common cases like `test(title, t => { ... }, ...)`.
+ implArg = wrapImplFunc(implFuncOrObj);
+ } else if (typeof implFuncOrObj === 'object' && implFuncOrObj) {
+ // Handle cases like `test(title, test.macro(...), ...)`.
+ // Note that this will need updating if a future version of ava adds an alternative to `exec`.
+ implArg = freeze({
+ ...implFuncOrObj,
+ exec: wrapImplFunc(implFuncOrObj.exec),
+ });
+ } else {
+ // Let ava handle this bad input.
+ implArg = implFuncOrObj;
+ }
+ // @ts-expect-error these spreads are acceptable
+ return testerFunc(...maybeTitle, implArg, ...args);
+ };
+ // re-use other properties (e.g. `.always`)
+ // https://github.com/endojs/endo/issues/647#issuecomment-809010961
+ Object.assign(augmented, testerFunc);
+ // @ts-expect-error cast
+ return /** @type {import('ava').TestFn} */ augmented;
};
/**
* The ava `test` function takes a callback argument of the form
- * `t => {...}`. If the outcome of this function indicates an error, either
+ * `t => {...}` or `async t => {...}`.
+ * If the outcome of this function indicates an error, either
* by throwing or by eventually rejecting a returned promise, ava does its
- * own peculiar console-like display of this error and its stacktrace.
- * However, it does not use the ses `console` and so bypasses all the fancy
- * diagnostics provided by the ses `console`.
+ * own console-like display of this error and its stacktrace.
+ * However, it does not use the SES `console` and so misses out on features
+ * such as unredaction.
*
* To use this package, a test file replaces the line
* ```js
@@ -100,27 +145,31 @@ const wrapTester = (testerFunc, logger = defaultLogger) => {
* Then the calls to `test` in the rest of the test file will act like they
* used to, except that, if a test fails because the test function (the
* callback argument to `test`) throws or returns a promise
- * that eventually rejects, the error is first sent to the `console` before
- * propagating into `rawTest`.
+ * that eventually rejects, the error is first sent to the logger
+ * (which defaults to using the SES-aware `console.error`)
+ * before propagating into `rawTest`.
*
- * @param {TesterInterface} avaTest
+ * @template {import('ava').TestFn} T ava `test`
+ * @param {T} avaTest
* @param {Logger} [logger]
- * @returns {TesterInterface}
+ * @returns {T}
*/
const wrapTest = (avaTest, logger = defaultLogger) => {
- const testerWrapper = /** @type {TesterInterface} */ (
- wrapTester(avaTest, logger)
- );
- for (const methodName of testerMethodsWhitelist) {
- if (methodName in avaTest) {
- /** @type {TesterFunc} */
- const testerMethod = (title, implFunc, ...otherArgs) =>
- avaTest[methodName](title, implFunc, ...otherArgs);
- testerWrapper[methodName] = wrapTester(testerMethod);
- }
+ const sesAvaTest = augmentLogging(avaTest, logger);
+ for (const methodName of overrideList) {
+ defineProperty(sesAvaTest, methodName, {
+ value: augmentLogging(avaTest[methodName], logger),
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ });
}
- harden(testerWrapper);
- return testerWrapper;
+ harden(sesAvaTest);
+ return sesAvaTest;
};
-// harden(wrapTest);
+// Successful instantiation of this module must be possible before `lockdown`
+// allows `harden(wrapTest)` to function, but `freeze` is a suitable replacement
+// because all objects reachable from the result are intrinsics hardened by
+// lockdown.
+freeze(wrapTest);
export { wrapTest };

DELETE
packages/ses-ava/src/types.js
-/**
- * @typedef {(...args: unknown[]) => void} Logger
- */
-
-/**
- * @callback LogCallError
- *
- * Calls `func(...args)` passing back approximately its outcome, but first
- * logging any erroneous outcome to the `logger`, which defaults to
- * `console.log`.
- *
- * * If `func(...args)` returns a non-promise, silently return it.
- * * If `func(...args)` throws, log what was thrown and then rethrow it.
- * * If `func(...args)` returns a promise, immediately return a new
- * unresolved promise.
- * * If the first promise fulfills, silently fulfill the returned promise
- * even if the fulfillment was an error.
- * * If the first promise rejects, log the rejection reason and then
- * reject the returned promise with the same reason.
- *
- * The delayed rejection of the returned promise is an observable difference
- * from directly calling `func(...args)` but will be equivalent enough for most
- * purposes.
- *
- * TODO This function is useful independent of ava, so consider moving it
- * somewhere and exporting it for general reuse.
- *
- * @param {(...unknown) => unknown} func
- * @param {unknown[]} args
- * @param {string} name
- * @param {Logger=} logger
- */
-
-/**
- * Simplified form of ava's types.
- * TODO perhaps just import ava's type declarations instead
- * TODO reconcile also with types and API defined in avaAssertXS.js
- *
- * @typedef {Object} Assertions
- * @property {(actual: unknown, message?: string) => void} assert
- * // TODO is, deepEqual, truthy, falsy, etc...
- */
-
-/**
- * @callback BaseImplFunc
- * This is the function that invariably starts `t => {`.
- * Ava's types call this `Implementation`, but that's just too confusing.
- *
- * @param {Assertions} t
- * @returns {unknown}
- *
- * @typedef {BaseImplFunc | Object} ImplFunc
- * @property {(...unknown) => string} [title]
- *
- * @callback TesterFunc
- * @param {string} title
- * @param {ImplFunc} implFunc
- * @returns {void}
- *
- * @typedef {Object} TesterProperties
- * @property {TesterFunc} after
- * @property {TesterFunc} afterEach
- * @property {TesterFunc} before
- * @property {TesterFunc} beforeEach
- * @property {TesterFunc} failing
- * @property {TesterFunc} serial
- * @property {TesterFunc} only
- * @property {TesterFunc} skip
- *
- * @typedef {TesterFunc & TesterProperties} TesterInterface
- */

feat(ses-ava): Support the full ava API (#1465)

Fixes #1235
Ref https://github.com/Agoric/agoric-sdk/pull/5774