Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 5 additions & 55 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,10 @@
import { CurrentRuntime, Runtime } from "@cross/runtime";

/**
* Simple step function without context or callback
*/
export type SimpleStepFunction = () => void | Promise<void>;

/**
* Context step function - function with context parameter for nested steps
*/
export type ContextStepFunction = (context: TestContext) => void | Promise<void>;

/**
* Step subject - the function executed within a step with context and callback support
*/
export type StepSubject = (context: TestContext, done: (value?: unknown) => void) => void | Promise<void>;

/**
* 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<void>;
(name: string, fn: ContextStepFunction): Promise<void>;
(name: string, fn: StepSubject, options: StepOptions): Promise<void>;
};

/**
* 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<void>;
// Internal utilities are not re-exported to keep the public API clean
// They are used internally by the shims

/**
* Runtime independent test function
Expand All @@ -56,14 +13,7 @@ export interface WrappedTest {
(name: string, testFn: TestSubject, options?: WrappedTestOptions): Promise<void>;
}

/**
* 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) {
Expand Down
100 changes: 16 additions & 84 deletions shims/bun.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
export async function wrappedTest(name: string, testFn: TestSubject, options: WrappedTestOptions): Promise<void> {
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<any> => {
// 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<void>)(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<any> => {
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<void>)(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<any> => {
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<void> | undefined;
const callbackPromise = new Promise<void>((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<void>((_, 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;
}
Expand Down
116 changes: 17 additions & 99 deletions shims/deno.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,51 @@
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<void> {
// @ts-ignore The Deno namespace isn't available in Node or Bun
Deno.test({
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<any> => {
// 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<void>)(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<any> => {
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<any> => {
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<void>)(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<void>)(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<void> | undefined;
const callbackPromise = new Promise<void>((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<void>((_, 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;
}
Expand Down
Loading
Loading