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
86 changes: 4 additions & 82 deletions packages/core/src/plugin/Context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,93 +7,15 @@ import {
PluginArgsProvider,
usePluginArgs,
} from "./Context";
import type { SeizenTableInstance } from "../table/useSeizenTable";
import type {
ColumnFiltersState,
SortingState,
PaginationState,
} from "@tanstack/react-table";

// =============================================================================
// Test Helpers
// =============================================================================

interface TestRow {
id: number;
name: string;
}

/**
* Create a mock SeizenTableInstance for testing
*/
function createMockTableInstance(
overrides: Partial<SeizenTableInstance<TestRow>> = {}
): SeizenTableInstance<TestRow> {
const eventListeners = new Map<string, Set<(payload: unknown) => void>>();

const mockEventBus = {
emit: vi.fn((event: string, payload: unknown) => {
const listeners = eventListeners.get(event);
if (listeners) {
listeners.forEach((cb) => cb(payload));
}
}),
subscribe: vi.fn((event: string, callback: (payload: unknown) => void) => {
if (!eventListeners.has(event)) {
eventListeners.set(event, new Set());
}
eventListeners.get(event)!.add(callback);
return () => {
eventListeners.get(event)?.delete(callback);
};
}),
};

const mockPlugin = {
open: vi.fn(),
close: vi.fn(),
isOpen: vi.fn(() => false),
getActiveId: vi.fn(() => null),
setActive: vi.fn(),
toggle: vi.fn(),
_state: { id: null, args: undefined },
};

return {
getData: () => [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
getColumns: () => [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "name", header: "Name" },
],
getSelectedRows: () => [],
setSelectedRows: vi.fn(),
clearSelection: vi.fn(),
getFilterState: () => [],
setFilter: vi.fn(),
getGlobalFilter: () => "",
setGlobalFilter: vi.fn(),
getSortingState: () => [],
setSorting: vi.fn(),
getPaginationState: () => ({ pageIndex: 0, pageSize: 10 }),
setPageIndex: vi.fn(),
setPageSize: vi.fn(),
getColumnVisibility: () => ({}),
setColumnVisibility: vi.fn(),
toggleColumnVisibility: vi.fn(),
getColumnOrder: () => [],
setColumnOrder: vi.fn(),
moveColumn: vi.fn(),
plugins: [],
plugin: mockPlugin as any,
eventBus: mockEventBus as any,
remote: false,
_tanstackTable: {} as any,
...overrides,
};
}
import {
createMockTableInstance,
type TestRow,
} from "../../tests/utils/mockTable";

// =============================================================================
// PluginContextProvider Tests
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/plugin/useEventBus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ describe("useEventBus", () => {
expect(result.current.subscribe).toBe(subscribe1);
});

it("eventBus object itself should be stable across renders", () => {
const { result, rerender } = renderHook(() => useEventBus());

const eventBus1 = result.current;

rerender();

expect(result.current).toBe(eventBus1);
});

it("should maintain subscribers across re-renders", () => {
const { result, rerender } = renderHook(() => useEventBus());
const callback = vi.fn();
Expand Down
29 changes: 16 additions & 13 deletions packages/core/src/plugin/useEventBus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef } from "react";
import { useCallback, useMemo, useRef } from "react";
import type {
ColumnFiltersState,
PaginationState,
Expand Down Expand Up @@ -171,18 +171,21 @@ export function useEventBus() {
[]
);

return {
/**
* Emit an event to all subscribers
*/
emit,

/**
* Subscribe to an event
* @returns Unsubscribe function
*/
subscribe,
};
return useMemo(
() => ({
/**
* Emit an event to all subscribers
*/
emit,

/**
* Subscribe to an event
* @returns Unsubscribe function
*/
subscribe,
}),
[emit, subscribe]
);
}

export type EventBus = ReturnType<typeof useEventBus>;
210 changes: 210 additions & 0 deletions packages/core/src/table/useSeizenTableEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useSeizenTableEvent } from "./useSeizenTableEvent";
import type { SeizenTableInstance } from "./useSeizenTable";
import type { EventBus } from "../plugin/useEventBus";
import type { SortingState, PaginationState } from "@tanstack/react-table";
import {
createMockEventBus,
createMockTableInstance,
type TestRow,
} from "../../tests/utils/mockTable";

// =============================================================================
// useSeizenTableEvent Hook Tests
// =============================================================================

describe("useSeizenTableEvent", () => {
let mockEventBus: EventBus;
let mockTable: SeizenTableInstance<TestRow>;

beforeEach(() => {
mockEventBus = createMockEventBus();
mockTable = createMockTableInstance({ eventBus: mockEventBus });
});

describe("subscription", () => {
it("should subscribe to events on mount", () => {
const callback = vi.fn();

renderHook(() =>
useSeizenTableEvent(mockTable, "sorting-change", callback)
);

expect(mockEventBus.subscribe).toHaveBeenCalledWith(
"sorting-change",
expect.any(Function)
);
});

it("should call callback when event is emitted", async () => {
const callback = vi.fn();
const sorting: SortingState = [{ id: "name", desc: false }];

renderHook(() =>
useSeizenTableEvent(mockTable, "sorting-change", callback)
);

act(() => {
mockEventBus.emit("sorting-change", sorting);
});

await waitFor(() => {
expect(callback).toHaveBeenCalledWith(sorting);
});
});

it("should call callback for pagination-change event", async () => {
const callback = vi.fn();
const pagination: PaginationState = { pageIndex: 2, pageSize: 20 };

renderHook(() =>
useSeizenTableEvent(mockTable, "pagination-change", callback)
);

act(() => {
mockEventBus.emit("pagination-change", pagination);
});

await waitFor(() => {
expect(callback).toHaveBeenCalledWith(pagination);
});
});

it("should not call callback for different events", () => {
const callback = vi.fn();

renderHook(() =>
useSeizenTableEvent(mockTable, "sorting-change", callback)
);

act(() => {
mockEventBus.emit("pagination-change", { pageIndex: 0, pageSize: 10 });
});

expect(callback).not.toHaveBeenCalled();
});
});

describe("unsubscription", () => {
it("should unsubscribe on unmount", () => {
const callback = vi.fn();

const { unmount } = renderHook(() =>
useSeizenTableEvent(mockTable, "sorting-change", callback)
);

unmount();

// After unmount, emitting should not call the callback
act(() => {
mockEventBus.emit("sorting-change", [{ id: "name", desc: true }]);
});

expect(callback).not.toHaveBeenCalled();
});
});

describe("callback updates", () => {
it("should use latest callback without re-subscribing", async () => {
const callback1 = vi.fn();
const callback2 = vi.fn();

const { rerender } = renderHook(
({ callback }) =>
useSeizenTableEvent(mockTable, "sorting-change", callback),
{ initialProps: { callback: callback1 } }
);

// First emit
act(() => {
mockEventBus.emit("sorting-change", [{ id: "name", desc: false }]);
});

await waitFor(() => {
expect(callback1).toHaveBeenCalledTimes(1);
});

// Update callback
rerender({ callback: callback2 });

// Second emit should use new callback
act(() => {
mockEventBus.emit("sorting-change", [{ id: "name", desc: true }]);
});

await waitFor(() => {
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback1).toHaveBeenCalledTimes(1); // Still 1, not called again
});

// Should only subscribe once (not re-subscribe on callback change)
expect(mockEventBus.subscribe).toHaveBeenCalledTimes(1);
});
});

describe("multiple subscriptions", () => {
it("should handle multiple useSeizenTableEvent calls for different events", async () => {
const sortingCallback = vi.fn();
const paginationCallback = vi.fn();

renderHook(() => {
useSeizenTableEvent(mockTable, "sorting-change", sortingCallback);
useSeizenTableEvent(mockTable, "pagination-change", paginationCallback);
});

act(() => {
mockEventBus.emit("sorting-change", [{ id: "name", desc: false }]);
mockEventBus.emit("pagination-change", { pageIndex: 1, pageSize: 20 });
});

await waitFor(() => {
expect(sortingCallback).toHaveBeenCalledWith([
{ id: "name", desc: false },
]);
expect(paginationCallback).toHaveBeenCalledWith({
pageIndex: 1,
pageSize: 20,
});
});
});
});

describe("event bus stability", () => {
it("should not re-subscribe when table reference changes but eventBus is same", () => {
const callback = vi.fn();

const { rerender } = renderHook(
({ table }) => useSeizenTableEvent(table, "sorting-change", callback),
{ initialProps: { table: mockTable } }
);

// Create new table with same eventBus
const newTable = createMockTableInstance({ eventBus: mockEventBus });

rerender({ table: newTable });

// Should only have subscribed once since eventBus is the same
expect(mockEventBus.subscribe).toHaveBeenCalledTimes(1);
});

it("should re-subscribe when eventBus changes", () => {
const callback = vi.fn();
const newEventBus = createMockEventBus();

const { rerender } = renderHook(
({ table }) => useSeizenTableEvent(table, "sorting-change", callback),
{ initialProps: { table: mockTable } }
);

// Create new table with different eventBus
const newTable = createMockTableInstance({ eventBus: newEventBus });

rerender({ table: newTable });

// Should have subscribed to both event buses
expect(mockEventBus.subscribe).toHaveBeenCalledTimes(1);
expect(newEventBus.subscribe).toHaveBeenCalledTimes(1);
});
});
});
Loading