diff --git a/apps/sim/lib/table/events.test.ts b/apps/sim/lib/table/events.test.ts index 48c8a7bd61d..5bb93f23130 100644 --- a/apps/sim/lib/table/events.test.ts +++ b/apps/sim/lib/table/events.test.ts @@ -11,7 +11,12 @@ beforeAll(() => { afterAll(resetEnvMock) import type { TableEvent } from '@/lib/table/events' -import { appendTableEvent, getLatestTableEventId, readTableEventsSince } from '@/lib/table/events' +import { + appendTableEvent, + getLatestTableEventId, + readTableEventsSince, + signalTableViewsChanged, +} from '@/lib/table/events' /** Module-level memory buffer can't be reset without vi.resetModules — use a * unique tableId per test to avoid cross-test bleed. */ @@ -75,3 +80,23 @@ describe('getLatestTableEventId (memory buffer)', () => { } }) }) + +describe('signalTableViewsChanged', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('appends a single views event carrying the tableId', async () => { + const tableId = uniqueTableId() + // The memory-buffer append is synchronous, so the fire-and-forget signal is + // observable immediately without awaiting the (unreturned) append promise. + signalTableViewsChanged(tableId) + + const result = await readTableEventsSince(tableId, 0) + expect(result.status).toBe('ok') + if (result.status === 'ok') { + expect(result.events).toHaveLength(1) + expect(result.events[0].event).toEqual({ kind: 'views', tableId }) + } + }) +}) diff --git a/apps/sim/lib/table/views/service.test.ts b/apps/sim/lib/table/views/service.test.ts index 3405f73bd90..0033d7b2ce2 100644 --- a/apps/sim/lib/table/views/service.test.ts +++ b/apps/sim/lib/table/views/service.test.ts @@ -1,9 +1,25 @@ /** * @vitest-environment node */ -import { describe, expect, it } from 'vitest' +import { tableViews } from '@sim/db/schema' +import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ColumnDefinition, TableViewConfig } from '@/lib/table/types' -import { normalizeStoredViewConfig, pruneViewConfig } from '@/lib/table/views/service' + +const { mockSignalTableViewsChanged } = vi.hoisted(() => ({ + mockSignalTableViewsChanged: vi.fn(), +})) +vi.mock('@/lib/table/events', () => ({ + signalTableViewsChanged: mockSignalTableViewsChanged, +})) + +import { + createTableView, + deleteTableView, + normalizeStoredViewConfig, + pruneViewConfig, + updateTableView, +} from '@/lib/table/views/service' const columns: ColumnDefinition[] = [ { id: 'col_a', name: 'Name', type: 'text' }, @@ -84,3 +100,87 @@ describe('normalizeStoredViewConfig', () => { expect(out.filter).toBeNull() }) }) + +describe('table-view mutations signal collaborators', () => { + const columns: ColumnDefinition[] = [] + const viewRow = { + id: 'view-1', + tableId: 'table-1', + workspaceId: 'ws-1', + name: 'My View', + config: {}, + isDefault: false, + createdBy: 'user-1', + createdAt: new Date('2026-01-01T00:00:00.000Z'), + updatedAt: new Date('2026-01-01T00:00:00.000Z'), + } + + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('createTableView signals the table after inserting', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([viewRow]) + + await createTableView({ + tableId: 'table-1', + workspaceId: 'ws-1', + name: 'My View', + config: {}, + userId: 'user-1', + columns, + }) + + expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1) + expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1') + }) + + it('updateTableView signals when the target view exists', async () => { + queueTableRows(tableViews, [{ id: 'view-1' }]) // the in-transaction existence pre-check + dbChainMockFns.returning.mockResolvedValueOnce([viewRow]) // the update returning + + const result = await updateTableView({ + viewId: 'view-1', + tableId: 'table-1', + name: 'Renamed', + columns, + }) + + expect(result).not.toBeNull() + expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1) + expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1') + }) + + it('updateTableView does NOT signal a no-op update on a missing view', async () => { + // No queued existence row → the pre-check finds nothing → returns null before any write. + const result = await updateTableView({ + viewId: 'missing', + tableId: 'table-1', + name: 'Renamed', + columns, + }) + + expect(result).toBeNull() + expect(mockSignalTableViewsChanged).not.toHaveBeenCalled() + }) + + it('deleteTableView signals when a row was actually deleted', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'view-1' }]) + + const deleted = await deleteTableView('view-1', 'table-1') + + expect(deleted).toBe(true) + expect(mockSignalTableViewsChanged).toHaveBeenCalledTimes(1) + expect(mockSignalTableViewsChanged).toHaveBeenCalledWith('table-1') + }) + + it('deleteTableView does NOT signal when nothing was deleted', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([]) + + const deleted = await deleteTableView('missing', 'table-1') + + expect(deleted).toBe(false) + expect(mockSignalTableViewsChanged).not.toHaveBeenCalled() + }) +}) diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 5cccc2c8af0..c0dfae52a13 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1136,6 +1136,17 @@ export const schemaMock = { updatedAt: 'updatedAt', completedAt: 'completedAt', }, + tableViews: { + id: 'id', + tableId: 'tableId', + workspaceId: 'workspaceId', + name: 'name', + config: 'config', + isDefault: 'isDefault', + createdBy: 'createdBy', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, tableRowExecutions: { tableId: 'tableId', rowId: 'rowId',