Skip to content
Merged
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
57 changes: 57 additions & 0 deletions packages/common/test/_nodeSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Polyfills for Node.js test environment
// The @evolu/common package uses ES2024+ features that aren't available in Node.js

// IMPORTANT: Install polyfills FIRST before any other imports
// This provides AsyncDisposableStack, DisposableStack, Symbol.dispose, etc.
import { installPolyfills } from "../src/Polyfills.js";
installPolyfills();

// Polyfill WebSocket for Node.js tests (after polyfills are installed)
import { WebSocket } from "ws";

if (!globalThis.WebSocket) {
// @ts-expect-error - ws WebSocket is compatible enough for our needs
globalThis.WebSocket = WebSocket;
}

// Polyfill Promise.try for Node.js/Bun versions that don't support it (ES2024)
if (!Promise.try) {
// @ts-ignore - Adding ES2024 Promise.try polyfill
Promise.try = function <T, Args extends readonly unknown[]>(
callback: (...args: Args) => T | PromiseLike<T>,
...args: Args
): Promise<T> {
return Promise.resolve().then(() => callback(...args));
};
}

// Polyfill Promise.withResolvers for Node.js/Bun versions that don't support it (ES2024)
if (!Promise.withResolvers) {
// @ts-ignore - Adding ES2024 Promise.withResolvers polyfill
Promise.withResolvers = function <T>(): {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: unknown) => void;
} {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}

// Polyfill Set.prototype.difference for Node.js/Bun (ES2025)
if (!Set.prototype.difference) {
// @ts-ignore - Adding ES2025 Set.prototype.difference polyfill
Set.prototype.difference = function <T>(this: Set<T>, other: Set<T>): Set<T> {
const result = new Set(this);
for (const elem of other) {
result.delete(elem);
}
return result;
};
}

1 change: 1 addition & 0 deletions packages/common/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default defineConfig({
include: ["test/**/*.test.ts"],
name: "unit",
environment: "node",
setupFiles: ["./test/_nodeSetup.ts"],
},
},
{
Expand Down
52 changes: 35 additions & 17 deletions packages/nodejs/test/Task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ import { type MainTask, ok, testCreateConsole } from "@evolu/common";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { runMain } from "../src/Task.js";

/**
* Helper function to create a deferred promise (similar to Promise.withResolvers).
* This is needed for compatibility with environments that don't support Promise.withResolvers.
*/
const withResolvers = <T>(): {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: unknown) => void;
} => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};

describe("runMain", () => {
beforeEach(() => {
// Clean up any signal listeners from previous tests
Expand All @@ -25,7 +43,7 @@ describe("runMain", () => {

test("executes main task", async () => {
let called = false;
const executed = Promise.withResolvers<void>();
const executed = withResolvers<void>();

runMain({})(() => {
called = true;
Expand All @@ -39,7 +57,7 @@ describe("runMain", () => {
});

test("passes custom deps", async () => {
const depsValue = Promise.withResolvers<number>();
const depsValue = withResolvers<number>();
const customDep = { myValue: 42 };

interface MyDep {
Expand All @@ -60,7 +78,7 @@ describe("runMain", () => {

test("handles aborted runner", async () => {
let taskRan = false;
const taskCompleted = Promise.withResolvers<void>();
const taskCompleted = withResolvers<void>();

runMain({})(async (run) => {
taskRan = true;
Expand All @@ -78,8 +96,8 @@ describe("runMain", () => {

test("disposes returned Disposable after signal", async () => {
let disposed = false;
const taskStarted = Promise.withResolvers<void>();
const disposeCalled = Promise.withResolvers<void>();
const taskStarted = withResolvers<void>();
const disposeCalled = withResolvers<void>();

runMain({})(() => {
taskStarted.resolve();
Expand All @@ -100,8 +118,8 @@ describe("runMain", () => {

test("disposes returned AsyncDisposable after signal", async () => {
let disposed = false;
const taskStarted = Promise.withResolvers<void>();
const disposeCalled = Promise.withResolvers<void>();
const taskStarted = withResolvers<void>();
const disposeCalled = withResolvers<void>();

runMain({})(() => {
taskStarted.resolve();
Expand All @@ -123,7 +141,7 @@ describe("runMain", () => {

test("handles void return without disposal", async () => {
let called = false;
const taskStarted = Promise.withResolvers<void>();
const taskStarted = withResolvers<void>();

runMain({})(() => {
called = true;
Expand All @@ -139,8 +157,8 @@ describe("runMain", () => {

test("responds to SIGTERM", async () => {
let disposed = false;
const taskStarted = Promise.withResolvers<void>();
const disposeCalled = Promise.withResolvers<void>();
const taskStarted = withResolvers<void>();
const disposeCalled = withResolvers<void>();

runMain({})(() => {
taskStarted.resolve();
Expand All @@ -161,8 +179,8 @@ describe("runMain", () => {

test("responds to SIGHUP", async () => {
let disposed = false;
const taskStarted = Promise.withResolvers<void>();
const disposeCalled = Promise.withResolvers<void>();
const taskStarted = withResolvers<void>();
const disposeCalled = withResolvers<void>();

runMain({})(() => {
taskStarted.resolve();
Expand All @@ -182,7 +200,7 @@ describe("runMain", () => {
});

test("cleans up signal listeners after signal", async () => {
const taskCompleted = Promise.withResolvers<void>();
const taskCompleted = withResolvers<void>();
const initialSigintCount = process.listenerCount("SIGINT");

runMain({})(() =>
Expand All @@ -207,8 +225,8 @@ describe("runMain", () => {
});

test("sets exitCode to 1 on uncaughtException", async () => {
const disposed = Promise.withResolvers<void>();
const taskStarted = Promise.withResolvers<void>();
const disposed = withResolvers<void>();
const taskStarted = withResolvers<void>();
const console = testCreateConsole();

runMain({ console })(() => {
Expand Down Expand Up @@ -238,8 +256,8 @@ describe("runMain", () => {
});

test("sets exitCode to 1 on unhandledRejection", async () => {
const disposed = Promise.withResolvers<void>();
const taskStarted = Promise.withResolvers<void>();
const disposed = withResolvers<void>();
const taskStarted = withResolvers<void>();
const console = testCreateConsole();

runMain({ console })(() => {
Expand Down
24 changes: 24 additions & 0 deletions packages/nodejs/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Polyfills for Node.js test environment
// The @evolu/common package uses ES2024+ features

// Polyfill Promise.try for Node.js/Bun versions that don't support it (ES2024)
// The @evolu/common package uses Promise.try in Task.ts
if (!Promise.try) {
// @ts-ignore - Adding ES2024 Promise.try polyfill
Promise.try = function <T, Args extends readonly unknown[]>(
callback: (...args: Args) => T | PromiseLike<T>,
...args: Args
): Promise<T> {
return Promise.resolve().then(() => callback(...args));
};
}

// Polyfill WebSocket for Node.js tests
// IMPORTANT: Import WebSocket AFTER other polyfills to avoid circular dependency issues
import { WebSocket } from "ws";

if (!globalThis.WebSocket) {
// @ts-expect-error - ws WebSocket is compatible enough for our needs
globalThis.WebSocket = WebSocket;
}

1 change: 1 addition & 0 deletions packages/nodejs/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
setupFiles: ["./test/setup.ts"],
exclude: ["**/node_modules/**", "**/dist/**"],
include: ["test/**/*.test.ts"],
coverage: {
Expand Down
Loading