diff --git a/mod.ts b/mod.ts index 94e8e89..917b795 100644 --- a/mod.ts +++ b/mod.ts @@ -1,53 +1,10 @@ import { CurrentRuntime, Runtime } from "@cross/runtime"; -/** - * Simple step function without context or callback - */ -export type SimpleStepFunction = () => void | Promise; - -/** - * Context step function - function with context parameter for nested steps - */ -export type ContextStepFunction = (context: TestContext) => void | Promise; - -/** - * Step subject - the function executed within a step with context and callback support - */ -export type StepSubject = (context: TestContext, done: (value?: unknown) => void) => void | Promise; - -/** - * Step options - */ -export interface StepOptions { - waitForCallback?: boolean; // Whether to wait for the done-callback to be called -} +// Re-export types and utilities from shared module for public API +export type { ContextStepFunction, SimpleStepFunction, StepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "./shims/shared.ts"; -/** - * Step function for nested tests - supports simple functions, context functions, and callback functions - */ -export type StepFunction = { - (name: string, fn: SimpleStepFunction): Promise; - (name: string, fn: ContextStepFunction): Promise; - (name: string, fn: StepSubject, options: StepOptions): Promise; -}; - -/** - * Test context with step support - */ -export interface TestContext { - /** - * Run a sub-test as a step of the parent test - * @param name - The name of the step - * @param fn - The function to run for this step - * @param options - Optional configuration for the step - */ - step: StepFunction; -} - -/** - * Test subject - */ -export type TestSubject = (context: TestContext, done: (value?: unknown) => void) => void | Promise; +// Internal utilities are not re-exported to keep the public API clean +// They are used internally by the shims /** * Runtime independent test function @@ -56,14 +13,7 @@ export interface WrappedTest { (name: string, testFn: TestSubject, options?: WrappedTestOptions): Promise; } -/** - * Runtime independent test options - */ -export interface WrappedTestOptions { - timeout?: number; // Timeout duration in milliseconds (optional) - skip?: boolean; // Whether to skip the test (optional) - waitForCallback?: boolean; // Whether to wait for the done-callback to be called -} +import type { TestSubject, WrappedTestOptions } from "./shims/shared.ts"; let wrappedTestToUse: WrappedTest; if (CurrentRuntime == Runtime.Deno) { diff --git a/shims/bun.ts b/shims/bun.ts index 581d87a..c0a1c7f 100644 --- a/shims/bun.ts +++ b/shims/bun.ts @@ -1,108 +1,40 @@ import { test } from "bun:test"; -import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "../mod.ts"; +import { executeStepFn, getFunctionType } from "./shared.ts"; +import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "./shared.ts"; -export async function wrappedTest( - name: string, - testFn: TestSubject, - options: WrappedTestOptions, -): Promise { +export async function wrappedTest(name: string, testFn: TestSubject, options: WrappedTestOptions): Promise { return await test(name, async () => { - // Create wrapped context with step method - const wrappedContext: TestContext = { - // deno-lint-ignore no-explicit-any - step: async (_stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { - // Bun doesn't support nested tests like Deno, so we run steps inline - // We could log the step name for debugging if needed - - // Check function arity to determine how to handle it: - // - length 0: Simple function with no parameters - // - length 1: Function with context parameter for nesting - // - length 2: Function with context and done callback - const isSimpleFunction = stepFn.length === 0; - const isContextFunction = stepFn.length === 1 && !stepOptions?.waitForCallback; - const isCallbackFunction = stepOptions?.waitForCallback === true; - - if (isSimpleFunction && !isCallbackFunction) { - // Simple function without context or callback - await (stepFn as SimpleStepFunction)(); - } else if (isContextFunction) { - // Function with context parameter - create proper nested context - const nestedWrappedContext: TestContext = createNestedContext(); - await (stepFn as (context: TestContext) => void | Promise)(nestedWrappedContext); - } else { - // Callback-based function - const nestedWrappedContext: TestContext = createNestedContext(); - let stepFnPromise = undefined; - const stepCallbackPromise = new Promise((resolve, reject) => { - stepFnPromise = (stepFn as StepSubject)(nestedWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (stepOptions?.waitForCallback) await stepCallbackPromise; - await stepFnPromise; - } - }, - }; - - // Helper function to create nested context with proper step support function createNestedContext(): TestContext { return { // deno-lint-ignore no-explicit-any - step: async (_nestedStepName: string, nestedStepFn: SimpleStepFunction | ContextStepFunction | StepSubject, nestedStepOptions?: StepOptions): Promise => { - const isNestedSimple = nestedStepFn.length === 0; - const isNestedContext = nestedStepFn.length === 1 && !nestedStepOptions?.waitForCallback; - const isNestedCallback = nestedStepOptions?.waitForCallback === true; - - if (isNestedSimple && !isNestedCallback) { - await (nestedStepFn as SimpleStepFunction)(); - } else if (isNestedContext) { - // Recursive: create another level of nesting - const deeperWrappedContext = createNestedContext(); - await (nestedStepFn as (context: TestContext) => void | Promise)(deeperWrappedContext); - } else { - // Callback-based nested step - const deeperWrappedContext = createNestedContext(); - let nestedStepFnPromise = undefined; - const nestedCallbackPromise = new Promise((resolve, reject) => { - nestedStepFnPromise = (nestedStepFn as StepSubject)(deeperWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (nestedStepOptions?.waitForCallback) await nestedCallbackPromise; - await nestedStepFnPromise; - } + step: async (_stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { + const fnType = getFunctionType(stepFn, stepOptions); + await executeStepFn(stepFn, fnType, createNestedContext, stepOptions?.waitForCallback); }, }; } - // Adapt the context here - let testFnPromise = undefined; - const callbackPromise = new Promise((resolve, reject) => { + const wrappedContext = createNestedContext(); + + let testFnPromise: void | Promise | undefined; + const callbackPromise = new Promise((resolve, reject) => { testFnPromise = testFn(wrappedContext, (e) => { if (e) reject(e); - else resolve(0); + else resolve(); }); }); - let timeoutId: number = -1; // Store the timeout ID + let timeoutId: number = -1; try { if (options.timeout) { - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error("Test timed out")); - }, options.timeout); + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error("Test timed out")), options.timeout); }); await Promise.race([options.waitForCallback ? callbackPromise : testFnPromise, timeoutPromise]); } else { - // No timeout, just await testFn - await options.waitForCallback ? callbackPromise : testFnPromise; + await (options.waitForCallback ? callbackPromise : testFnPromise); } - } catch (error) { - throw error; } finally { - if (timeoutId) clearTimeout(timeoutId); - // Make sure testFnPromise has completed + if (timeoutId !== -1) clearTimeout(timeoutId); await testFnPromise; if (options.waitForCallback) await callbackPromise; } diff --git a/shims/deno.ts b/shims/deno.ts index 8828f4e..07b83c4 100644 --- a/shims/deno.ts +++ b/shims/deno.ts @@ -1,4 +1,5 @@ -import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "../mod.ts"; // Assuming cross runtime types are here +import { executeStepFn, getFunctionType } from "./shared.ts"; +import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "./shared.ts"; export function wrappedTest(name: string, testFn: TestSubject, options: WrappedTestOptions): Promise { // @ts-ignore The Deno namespace isn't available in Node or Bun @@ -6,128 +7,45 @@ export function wrappedTest(name: string, testFn: TestSubject, options: WrappedT name, ignore: options?.skip || false, async fn(context) { - // Create wrapped context with step method - const wrappedContext: TestContext = { - // deno-lint-ignore no-explicit-any - step: async (stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { - // Check function arity to determine how to handle it: - // - length 0: Simple function with no parameters - // - length 1: Function with context parameter for nesting - // - length 2: Function with context and done callback - const isSimpleFunction = stepFn.length === 0; - const isContextFunction = stepFn.length === 1 && !stepOptions?.waitForCallback; - const isCallbackFunction = stepOptions?.waitForCallback === true; - - // @ts-ignore context.step exists in Deno - await context.step(stepName, async (stepContext) => { - if (isSimpleFunction && !isCallbackFunction) { - // Simple function without context or callback - await (stepFn as SimpleStepFunction)(); - } else if (isContextFunction) { - // Function with context parameter - create proper nested context - const nestedWrappedContext: TestContext = createNestedContext(stepContext); - await (stepFn as (context: TestContext) => void | Promise)(nestedWrappedContext); - } else { - // Callback-based function - const nestedWrappedContext: TestContext = createNestedContext(stepContext); - let stepFnPromise = undefined; - const stepCallbackPromise = new Promise((resolve, reject) => { - stepFnPromise = (stepFn as StepSubject)(nestedWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (stepOptions?.waitForCallback) await stepCallbackPromise; - await stepFnPromise; - } - }); - }, - }; - - // Helper function to create nested context with proper step support // deno-lint-ignore no-explicit-any function createNestedContext(denoContext: any): TestContext { return { // deno-lint-ignore no-explicit-any - step: async (nestedStepName: string, nestedStepFn: SimpleStepFunction | ContextStepFunction | StepSubject, nestedStepOptions?: StepOptions): Promise => { - const isNestedSimple = nestedStepFn.length === 0; - const isNestedContext = nestedStepFn.length === 1 && !nestedStepOptions?.waitForCallback; - const isNestedCallback = nestedStepOptions?.waitForCallback === true; - + step: async (stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { + const fnType = getFunctionType(stepFn, stepOptions); if (denoContext && typeof denoContext.step === "function") { // @ts-ignore context.step exists in Deno - await denoContext.step(nestedStepName, async (deeperContext) => { - if (isNestedSimple && !isNestedCallback) { - await (nestedStepFn as SimpleStepFunction)(); - } else if (isNestedContext) { - // Recursive: create another level of nesting - const deeperWrappedContext = createNestedContext(deeperContext); - await (nestedStepFn as (context: TestContext) => void | Promise)(deeperWrappedContext); - } else { - // Callback-based nested step - const deeperWrappedContext = createNestedContext(deeperContext); - let nestedStepFnPromise = undefined; - const nestedCallbackPromise = new Promise((resolve, reject) => { - nestedStepFnPromise = (nestedStepFn as StepSubject)(deeperWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (nestedStepOptions?.waitForCallback) await nestedCallbackPromise; - await nestedStepFnPromise; - } + await denoContext.step(stepName, async (deeperContext) => { + await executeStepFn(stepFn, fnType, () => createNestedContext(deeperContext), stepOptions?.waitForCallback); }); } else { - // Fallback: execute step directly without Deno nesting when context lacks step method or is undefined - // This can occur at deeper nesting levels where Deno's context.step may not be available - if (isNestedSimple && !isNestedCallback) { - await (nestedStepFn as SimpleStepFunction)(); - } else if (isNestedContext) { - // Create a fallback context for deeper nesting - const fallbackContext = createNestedContext(undefined); - await (nestedStepFn as (context: TestContext) => void | Promise)(fallbackContext); - } else { - // Callback-based step without Deno context - const fallbackContext = createNestedContext(undefined); - let nestedStepFnPromise = undefined; - const nestedCallbackPromise = new Promise((resolve, reject) => { - nestedStepFnPromise = (nestedStepFn as StepSubject)(fallbackContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (nestedStepOptions?.waitForCallback) await nestedCallbackPromise; - await nestedStepFnPromise; - } + await executeStepFn(stepFn, fnType, () => createNestedContext(undefined), stepOptions?.waitForCallback); } }, }; } - // Adapt the context here - let testFnPromise = undefined; - const callbackPromise = new Promise((resolve, reject) => { + const wrappedContext = createNestedContext(context); + + let testFnPromise: void | Promise | undefined; + const callbackPromise = new Promise((resolve, reject) => { testFnPromise = testFn(wrappedContext, (e) => { if (e) reject(e); - else resolve(0); + else resolve(); }); }); - let timeoutId: number = -1; // Store the timeout ID + let timeoutId: number = -1; try { if (options.timeout) { - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error("Test timed out")); - }, options.timeout); + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error("Test timed out")), options.timeout); }); await Promise.race([options.waitForCallback ? callbackPromise : testFnPromise, timeoutPromise]); } else { - await options.waitForCallback ? callbackPromise : testFnPromise; + await (options.waitForCallback ? callbackPromise : testFnPromise); } - } catch (error) { - throw error; } finally { - if (timeoutId) clearTimeout(timeoutId); + if (timeoutId !== -1) clearTimeout(timeoutId); await testFnPromise; if (options.waitForCallback) await callbackPromise; } diff --git a/shims/node.ts b/shims/node.ts index 92fab76..c42cd88 100644 --- a/shims/node.ts +++ b/shims/node.ts @@ -1,6 +1,6 @@ -import { test } from "node:test"; // For type safety -import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, WrappedTestOptions } from "../mod.ts"; // Shared options -import type { TestSubject } from "../mod.ts"; +import { test } from "node:test"; +import { executeStepFn, getFunctionType } from "./shared.ts"; +import type { ContextStepFunction, SimpleStepFunction, StepOptions, StepSubject, TestContext, TestSubject, WrappedTestOptions } from "./shared.ts"; function transformOptions(options?: WrappedTestOptions) { return { @@ -9,153 +9,35 @@ function transformOptions(options?: WrappedTestOptions) { }; } -// Helper function to create a fallback context for older Node versions -// This context has a no-op step function since nested tests aren't supported -function createFallbackContext(): TestContext { - return { - // deno-lint-ignore no-explicit-any - step: (): Promise => { - // No-op: older Node versions don't support nested tests - console.warn("Warning: Nested steps are not fully supported in this Node version. Consider upgrading to Node 18.17.0+"); - return Promise.resolve(); - }, - }; -} - -export function wrappedTest( - name: string, - testFn: TestSubject, - options: WrappedTestOptions, -): Promise { +export function wrappedTest(name: string, testFn: TestSubject, options: WrappedTestOptions): Promise { // deno-lint-ignore no-explicit-any test(name, transformOptions(options), async (context: any) => { - // Create wrapped context with step method - const wrappedContext: TestContext = { - // deno-lint-ignore no-explicit-any - step: async (stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { - // Check function arity to determine how to handle it: - // - length 0: Simple function with no parameters - // - length 1: Function with context parameter for nesting - // - length 2: Function with context and done callback - const isSimpleFunction = stepFn.length === 0; - const isContextFunction = stepFn.length === 1 && !stepOptions?.waitForCallback; - const isCallbackFunction = stepOptions?.waitForCallback === true; - - // Node.js supports nested tests via test() within a test callback - // Use context.test() if available (Node 18.17.0+), otherwise use global test() - if (context && typeof context.test === "function") { - // deno-lint-ignore no-explicit-any - return await context.test(stepName, async (nestedContext: any) => { - if (isSimpleFunction && !isCallbackFunction) { - // Simple function without context or callback - await (stepFn as SimpleStepFunction)(); - } else if (isContextFunction) { - // Function with context parameter - create proper nested context - const nestedWrappedContext: TestContext = createNestedContext(nestedContext); - await (stepFn as (context: TestContext) => void | Promise)(nestedWrappedContext); - } else { - // Callback-based function - const nestedWrappedContext: TestContext = createNestedContext(nestedContext); - let stepFnPromise = undefined; - const stepCallbackPromise = new Promise((resolve, reject) => { - stepFnPromise = (stepFn as StepSubject)(nestedWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (stepOptions?.waitForCallback) await stepCallbackPromise; - await stepFnPromise; - } - }); - } else { - // Fallback for older Node versions - run the step directly without nesting - if (isSimpleFunction && !isCallbackFunction) { - // Simple function without context or callback - await (stepFn as SimpleStepFunction)(); - } else if (isContextFunction) { - // Function with context parameter - use fallback context - const nestedWrappedContext = createFallbackContext(); - await (stepFn as (context: TestContext) => void | Promise)(nestedWrappedContext); - } else { - // Callback-based function - const nestedWrappedContext = createFallbackContext(); - let stepFnPromise = undefined; - const stepCallbackPromise = new Promise((resolve, reject) => { - stepFnPromise = (stepFn as StepSubject)(nestedWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (stepOptions?.waitForCallback) await stepCallbackPromise; - await stepFnPromise; - } - } - }, - }; - - // Helper function to create nested context with proper step support // deno-lint-ignore no-explicit-any function createNestedContext(nodeContext: any): TestContext { return { // deno-lint-ignore no-explicit-any - step: async (nestedStepName: string, nestedStepFn: SimpleStepFunction | ContextStepFunction | StepSubject, nestedStepOptions?: StepOptions): Promise => { - const isNestedSimple = nestedStepFn.length === 0; - const isNestedContext = nestedStepFn.length === 1 && !nestedStepOptions?.waitForCallback; - const isNestedCallback = nestedStepOptions?.waitForCallback === true; - + step: async (stepName: string, stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, stepOptions?: StepOptions): Promise => { + const fnType = getFunctionType(stepFn, stepOptions); if (nodeContext && typeof nodeContext.test === "function") { // deno-lint-ignore no-explicit-any - return await nodeContext.test(nestedStepName, async (deeperContext: any) => { - if (isNestedSimple && !isNestedCallback) { - await (nestedStepFn as SimpleStepFunction)(); - } else if (isNestedContext) { - // Recursive: create another level of nesting - const deeperWrappedContext = createNestedContext(deeperContext); - await (nestedStepFn as (context: TestContext) => void | Promise)(deeperWrappedContext); - } else { - // Callback-based nested step - const deeperWrappedContext = createNestedContext(deeperContext); - let nestedStepFnPromise = undefined; - const nestedCallbackPromise = new Promise((resolve, reject) => { - nestedStepFnPromise = (nestedStepFn as StepSubject)(deeperWrappedContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (nestedStepOptions?.waitForCallback) await nestedCallbackPromise; - await nestedStepFnPromise; - } + return await nodeContext.test(stepName, async (deeperContext: any) => { + await executeStepFn(stepFn, fnType, () => createNestedContext(deeperContext), stepOptions?.waitForCallback); }); } else { - // Fallback for older Node versions - if (isNestedSimple && !isNestedCallback) { - await (nestedStepFn as SimpleStepFunction)(); - } else if (isNestedContext) { - const fallbackContext = createFallbackContext(); - await (nestedStepFn as (context: TestContext) => void | Promise)(fallbackContext); - } else { - const fallbackContext = createFallbackContext(); - let nestedStepFnPromise = undefined; - const nestedCallbackPromise = new Promise((resolve, reject) => { - nestedStepFnPromise = (nestedStepFn as StepSubject)(fallbackContext, (e) => { - if (e) reject(e); - else resolve(0); - }); - }); - if (nestedStepOptions?.waitForCallback) await nestedCallbackPromise; - await nestedStepFnPromise; - } + console.warn("Warning: Nested steps are not fully supported in this Node version. Consider upgrading to Node 18.17.0+"); + await executeStepFn(stepFn, fnType, () => createNestedContext(undefined), stepOptions?.waitForCallback); } }, }; } - // Adapt the context here - let testFnPromise = undefined; - const callbackPromise = new Promise((resolve, reject) => { + const wrappedContext = createNestedContext(context); + + let testFnPromise: void | Promise | undefined; + const callbackPromise = new Promise((resolve, reject) => { testFnPromise = testFn(wrappedContext, (e) => { if (e) reject(e); - else resolve(0); + else resolve(); }); }); if (options.waitForCallback) await callbackPromise; diff --git a/shims/shared.ts b/shims/shared.ts new file mode 100644 index 0000000..fb73b66 --- /dev/null +++ b/shims/shared.ts @@ -0,0 +1,96 @@ +/** + * Simple step function without context or callback + */ +export type SimpleStepFunction = () => void | Promise; + +/** + * Context step function - function with context parameter for nested steps + */ +export type ContextStepFunction = (context: TestContext) => void | Promise; + +/** + * Step subject - the function executed within a step with context and callback support + */ +export type StepSubject = (context: TestContext, done: (value?: unknown) => void) => void | Promise; + +/** + * Step options + */ +export interface StepOptions { + waitForCallback?: boolean; // Whether to wait for the done-callback to be called +} + +/** + * Step function for nested tests - supports simple functions, context functions, and callback functions + */ +export type StepFunction = { + (name: string, fn: SimpleStepFunction): Promise; + (name: string, fn: ContextStepFunction): Promise; + (name: string, fn: StepSubject, options: StepOptions): Promise; +}; + +/** + * Test context with step support + */ +export interface TestContext { + /** + * Run a sub-test as a step of the parent test + * @param name - The name of the step + * @param fn - The function to run for this step + * @param options - Optional configuration for the step + */ + step: StepFunction; +} + +/** + * Test subject + */ +export type TestSubject = (context: TestContext, done: (value?: unknown) => void) => void | Promise; + +/** + * Runtime independent test options + */ +export interface WrappedTestOptions { + timeout?: number; // Timeout duration in milliseconds (optional) + skip?: boolean; // Whether to skip the test (optional) + waitForCallback?: boolean; // Whether to wait for the done-callback to be called +} + +/** + * Determines the function type based on arity and options + * @internal + */ +export function getFunctionType(fn: SimpleStepFunction | ContextStepFunction | StepSubject, options?: StepOptions): "simple" | "context" | "callback" { + if (options?.waitForCallback) return "callback"; + if (fn.length === 0) return "simple"; + if (fn.length === 1) return "context"; + return "callback"; +} + +/** + * Executes a step function with the appropriate handling based on its type + * @internal + */ +export async function executeStepFn( + stepFn: SimpleStepFunction | ContextStepFunction | StepSubject, + fnType: "simple" | "context" | "callback", + createContext: () => TestContext, + waitForCallback?: boolean, +): Promise { + if (fnType === "simple") { + await (stepFn as SimpleStepFunction)(); + } else if (fnType === "context") { + await (stepFn as ContextStepFunction)(createContext()); + } else { + const ctx = createContext(); + let stepFnPromise: void | Promise | undefined; + const callbackPromise = new Promise((resolve, reject) => { + stepFnPromise = (stepFn as StepSubject)(ctx, (e) => { + if (e) reject(e); + else resolve(); + }); + }); + if (waitForCallback) await callbackPromise; + await stepFnPromise; + } +}