Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.
Closed
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
16 changes: 8 additions & 8 deletions docs/concepts/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import { test, expect } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../src/index";

test("my actor test", async () => {
const { client } = await setupTest(app);
test("my actor test", async (test) => {
const { client } = await setupTest(test, app);

// Now you can interact with your actor through the client
const myActor = await client.myActor.get();
Expand Down Expand Up @@ -76,8 +76,8 @@ import { test, expect } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../src/index";

test("actor should persist state", async () => {
const { client } = await setupTest(app);
test("actor should persist state", async (test) => {
const { client } = await setupTest(test, app);
const counter = await client.counter.get();

// Initial state
Expand Down Expand Up @@ -126,8 +126,8 @@ import { test, expect, vi } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../src/index";

test("actor should emit events", async () => {
const { client } = await setupTest(app);
test("actor should emit events", async (test) => {
const { client } = await setupTest(test, app);
const chatRoom = await client.chatRoom.get();

// Set up event handler with a mock function
Expand Down Expand Up @@ -182,9 +182,9 @@ import { test, expect, vi } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../src/index";

test("scheduled tasks should execute", async () => {
test("scheduled tasks should execute", async (test) => {
// setupTest automatically configures vi.useFakeTimers()
const { client } = await setupTest(app);
const { client } = await setupTest(test, app);
const scheduler = await client.scheduler.get();

// Set up a scheduled task
Expand Down
34 changes: 19 additions & 15 deletions examples/chat-room/tests/chat-room.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,59 @@ import { test, expect } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../actors/app";

test("chat room should handle messages", async () => {
const { client } = await setupTest(app);
test("chat room should handle messages", async (test) => {
const { client } = await setupTest(test, app);

// Connect to chat room
const chatRoom = await client.chatRoom.get();

// Initial history should be empty
const initialMessages = await chatRoom.getHistory();
expect(initialMessages).toEqual([]);

// Test event emission
let receivedUsername = "";
let receivedMessage = "";
chatRoom.on("newMessage", (username: string, message: string) => {
receivedUsername = username;
receivedMessage = message;
});

// Send a message
const testUser = "william";
const testMessage = "All the world's a stage.";
await chatRoom.sendMessage(testUser, testMessage);

// Verify event was emitted with correct data
expect(receivedUsername).toBe(testUser);
expect(receivedMessage).toBe(testMessage);

// Verify message was stored in history
const updatedMessages = await chatRoom.getHistory();
expect(updatedMessages).toEqual([
{ username: testUser, message: testMessage }
{ username: testUser, message: testMessage },
]);

// Send multiple messages and verify
const users = ["romeo", "juliet", "othello"];
const messages = ["Wherefore art thou?", "Here I am!", "The green-eyed monster."];

const messages = [
"Wherefore art thou?",
"Here I am!",
"The green-eyed monster.",
];

for (let i = 0; i < users.length; i++) {
await chatRoom.sendMessage(users[i], messages[i]);

// Verify event emission
expect(receivedUsername).toBe(users[i]);
expect(receivedMessage).toBe(messages[i]);
}

// Verify all messages are in history in correct order
const finalHistory = await chatRoom.getHistory();
expect(finalHistory).toEqual([
{ username: testUser, message: testMessage },
...users.map((username, i) => ({ username, message: messages[i] }))
...users.map((username, i) => ({ username, message: messages[i] })),
]);
});
17 changes: 8 additions & 9 deletions examples/counter/tests/counter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@ import { test, expect } from "vitest";
import { setupTest } from "actor-core/test";
import { app } from "../actors/app";

test("it should count", async () => {
const { client } = await setupTest(app);
test("it should count", async (test) => {
const { client } = await setupTest(test, app);
const counter = await client.counter.get();

// Test initial count
expect(await counter.getCount()).toBe(0);

// Test event emission
let eventCount = -1;
counter.on("newCount", (count: number) => {
eventCount = count;
});

// Test increment
const incrementAmount = 5;
const result = await counter.increment(incrementAmount);
expect(result).toBe(incrementAmount);

// Verify event was emitted with correct count
expect(eventCount).toBe(incrementAmount);

// Test multiple increments
for (let i = 1; i <= 3; i++) {
const newCount = await counter.increment(incrementAmount);
expect(newCount).toBe(incrementAmount * (i + 1));
expect(eventCount).toBe(incrementAmount * (i + 1));
}

// Verify final count
expect(await counter.getCount()).toBe(incrementAmount * 4);
});

1 change: 0 additions & 1 deletion packages/actor-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@
"@types/node": "^22.13.1",
"@types/ws": "^8",
"eventsource": "^3.0.5",
"get-port": "^7.1.0",
"tsup": "^8.4.0",
"typescript": "^5.7.3",
"vitest": "^3.1.1",
Expand Down
60 changes: 56 additions & 4 deletions packages/actor-core/src/test/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
TestActorDriver,
} from "./driver/mod";
import { type InputConfig, ConfigSchema } from "./config";
import { onTestFinished, vi } from "vitest";
import getPort from "get-port";
import { type TestContext, vi } from "vitest";
import { type Client, createClient } from "@/client/mod";
import { createServer } from "node:net";

function createRouter(
app: ActorCoreApp<any>,
Expand Down Expand Up @@ -93,7 +93,9 @@ export interface SetupTestResult<A extends ActorCoreApp<any>> {
};
}

// Must use `TestContext` since global hooks do not work when running concurrently
export async function setupTest<A extends ActorCoreApp<any>>(
c: TestContext,
app: A,
): Promise<SetupTestResult<A>> {
vi.useFakeTimers();
Expand All @@ -109,13 +111,13 @@ export async function setupTest<A extends ActorCoreApp<any>>(
// Start server with a random port
const port = await getPort();
const server = serve(app, { port });
onTestFinished(
c.onTestFinished(
async () => await new Promise((resolve) => server.close(() => resolve())),
);

// Create client
const client = createClient<A>(`http://127.0.0.1:${port}`);
onTestFinished(async () => await client.dispose());
c.onTestFinished(async () => await client.dispose());

return {
client,
Expand All @@ -126,3 +128,53 @@ export async function setupTest<A extends ActorCoreApp<any>>(
},
};
}

export async function getPort(): Promise<number> {
// Pick random port between 10000 and 65535 (avoiding well-known and registered ports)
const MIN_PORT = 10000;
const MAX_PORT = 65535;
const getRandomPort = () =>
Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT;

let port = getRandomPort();
let maxAttempts = 10;

while (maxAttempts > 0) {
try {
// Try to create a server on the port to check if it's available
const server = await new Promise<any>((resolve, reject) => {
const server = createServer();

server.once("error", (err: Error & { code?: string }) => {
if (err.code === "EADDRINUSE") {
reject(new Error(`Port ${port} is in use`));
} else {
reject(err);
}
});

server.once("listening", () => {
resolve(server);
});

server.listen(port);
});

// Close the server since we're just checking availability
await new Promise<void>((resolve) => {
server.close(() => resolve());
});

return port;
} catch (err) {
// If port is in use, try a different one
maxAttempts--;
if (maxAttempts <= 0) {
break;
}
port = getRandomPort();
}
}

throw new Error("Could not find an available port after multiple attempts");
}
5 changes: 2 additions & 3 deletions packages/actor-core/tests/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { actor, setup } from "@/mod";
import { test } from "vitest";
import { setupTest } from "@/test/mod";

test("basic actor setup", async () => {
test("basic actor setup", async (c) => {
const counter = actor({
state: { count: 0 },
actions: {
Expand All @@ -18,9 +18,8 @@ test("basic actor setup", async () => {
actors: { counter },
});

const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);

const counterInstance = await client.counter.get();
await counterInstance.increment(1);
});

20 changes: 10 additions & 10 deletions packages/actor-core/tests/vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { setupTest } from "@/test/mod";

describe("Actor Vars", () => {
describe("Static vars", () => {
test("should provide access to static vars", async () => {
test("should provide access to static vars", async (c) => {
// Define actor with static vars
const varActor = actor({
state: { value: 0 },
Expand All @@ -24,7 +24,7 @@ describe("Actor Vars", () => {
actors: { varActor },
});

const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);
const instance = await client.varActor.get();

// Test accessing vars
Expand All @@ -38,7 +38,7 @@ describe("Actor Vars", () => {
});

describe("Deep cloning of static vars", () => {
test("should deep clone static vars between actor instances", async () => {
test("should deep clone static vars between actor instances", async (c) => {
// Define actor with nested object in vars
const nestedVarActor = actor({
state: { value: 0 },
Expand Down Expand Up @@ -69,7 +69,7 @@ describe("Actor Vars", () => {
actors: { nestedVarActor },
});

const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);

// Create two separate instances
const instance1 = await client.nestedVarActor.get({
Expand All @@ -94,7 +94,7 @@ describe("Actor Vars", () => {
});

describe("createVars", () => {
test("should support dynamic vars creation", async () => {
test("should support dynamic vars creation", async (c) => {
// Define actor with createVars function
const dynamicVarActor = actor({
state: { value: 0 },
Expand All @@ -116,7 +116,7 @@ describe("Actor Vars", () => {
actors: { dynamicVarActor },
});

const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);

// Create an instance
const instance = await client.dynamicVarActor.get();
Expand All @@ -130,7 +130,7 @@ describe("Actor Vars", () => {
expect(vars.computed).toMatch(/^Actor-\d+$/);
});

test("should create different vars for different instances", async () => {
test("should create different vars for different instances", async (c) => {
// Define actor with createVars function that generates unique values
const uniqueVarActor = actor({
state: { value: 0 },
Expand All @@ -151,7 +151,7 @@ describe("Actor Vars", () => {
actors: { uniqueVarActor },
});

const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);

// Create two separate instances
const instance1 = await client.uniqueVarActor.get({
Expand All @@ -171,7 +171,7 @@ describe("Actor Vars", () => {
});

describe("Driver Context", () => {
test("should provide access to driver context", async () => {
test("should provide access to driver context", async (c) => {
// Reset timers to avoid test timeouts
vi.useRealTimers();

Expand Down Expand Up @@ -201,7 +201,7 @@ describe("Actor Vars", () => {
});

// Set up the test
const { client } = await setupTest<typeof app>(app);
const { client } = await setupTest<typeof app>(c, app);

// Create an instance
const instance = await client.driverCtxActor.get();
Expand Down
7 changes: 2 additions & 5 deletions packages/actor-core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import defaultConfig from "../../vitest.base.ts";
import { defineConfig } from "vitest/config";
import { resolve } from "path";

export default defineConfig({
test: {
globals: true,
environment: "node",
},
...defaultConfig,
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});

Loading
Loading