@tuturuuu/ui 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +6 -5
  3. package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  5. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
  6. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  7. package/src/components/ui/custom/nav-link.tsx +69 -11
  8. package/src/components/ui/custom/navigation.tsx +1 -0
  9. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  10. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  11. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  12. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  13. package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
  14. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  15. package/src/components/ui/custom/workspace-select.tsx +17 -16
  16. package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
  17. package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
  18. package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
  19. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
  20. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
  21. package/src/components/ui/finance/invoices/hooks.ts +11 -2
  22. package/src/components/ui/finance/invoices/internal-api.ts +5 -0
  23. package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
  24. package/src/components/ui/finance/invoices/utils.test.ts +82 -0
  25. package/src/components/ui/finance/invoices/utils.ts +240 -7
  26. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
  27. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  28. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
  29. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  30. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
  31. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  32. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  33. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  34. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  35. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
  36. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
  37. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
  38. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
  39. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
  40. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
  41. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
  42. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
  43. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  44. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  45. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
  46. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
  47. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  48. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  49. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  50. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  51. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  52. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  53. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  54. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  55. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  56. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  57. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  58. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  59. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  60. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  61. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  62. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  63. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +123 -1
  64. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
  65. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
  66. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
  67. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +58 -0
  68. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  69. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  70. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  71. package/src/components/ui/tu-do/shared/board-client.tsx +16 -4
  72. package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
  73. package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
  74. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  75. package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
  76. package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
  77. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +58 -4
  78. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  79. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
  100. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
  101. package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
  102. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  103. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  104. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  105. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  106. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
  107. package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
  108. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +390 -34
  109. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  110. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  111. package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
  112. package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
  113. package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
  114. package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
  115. package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
  116. package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
  117. package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
  118. package/src/components/ui/tu-do/templates/task-templates-page.tsx +6 -11
  119. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  120. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  121. package/src/hooks/useBoardPresence.ts +364 -0
  122. package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
  123. package/src/lib/workspace-actions.ts +2 -6
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { act, renderHook } from '@testing-library/react';
6
+ import { getCurrentUserProfile } from '@tuturuuu/internal-api/users';
7
+ import { createClient } from '@tuturuuu/supabase/next/client';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import { useBoardPresence } from '../useBoardPresence';
10
+
11
+ type PresenceListener = () => void;
12
+
13
+ type MockChannel = {
14
+ on: ReturnType<typeof vi.fn>;
15
+ presenceState: ReturnType<typeof vi.fn>;
16
+ subscribe: ReturnType<typeof vi.fn>;
17
+ track: ReturnType<typeof vi.fn>;
18
+ untrack: ReturnType<typeof vi.fn>;
19
+ };
20
+
21
+ type MockSupabaseClient = {
22
+ auth: {
23
+ getUser: ReturnType<typeof vi.fn>;
24
+ };
25
+ channel: ReturnType<typeof vi.fn>;
26
+ removeChannel: ReturnType<typeof vi.fn>;
27
+ };
28
+
29
+ type MockCreateClientFn = {
30
+ (): MockSupabaseClient;
31
+ mockReturnValue: (value: MockSupabaseClient) => void;
32
+ };
33
+
34
+ vi.mock('@tuturuuu/supabase/next/client', () => ({
35
+ createClient: vi.fn(),
36
+ }));
37
+
38
+ vi.mock('@tuturuuu/internal-api/users', () => ({
39
+ getCurrentUserProfile: vi.fn(),
40
+ }));
41
+
42
+ vi.mock('@tuturuuu/utils/constants', () => ({
43
+ DEV_MODE: false,
44
+ }));
45
+
46
+ describe('useBoardPresence', () => {
47
+ let mockChannel: MockChannel;
48
+ let mockClient: MockSupabaseClient;
49
+ let presenceListeners: Map<string, PresenceListener>;
50
+
51
+ beforeEach(() => {
52
+ presenceListeners = new Map();
53
+
54
+ mockChannel = {
55
+ on: vi.fn(
56
+ (
57
+ type: string,
58
+ config: { event?: string },
59
+ callback: PresenceListener
60
+ ) => {
61
+ if (type === 'presence' && config.event) {
62
+ presenceListeners.set(config.event, callback);
63
+ }
64
+ return mockChannel;
65
+ }
66
+ ),
67
+ presenceState: vi.fn(() => ({})),
68
+ subscribe: vi.fn((callback?: (status: string) => void) => {
69
+ callback?.('SUBSCRIBED');
70
+ return mockChannel;
71
+ }),
72
+ track: vi.fn(() => Promise.resolve('ok')),
73
+ untrack: vi.fn(() => Promise.resolve('ok')),
74
+ };
75
+
76
+ mockClient = {
77
+ auth: {
78
+ getUser: vi.fn(() =>
79
+ Promise.resolve({
80
+ data: {
81
+ user: {
82
+ email: 'ada.auth@example.com',
83
+ id: 'user-1',
84
+ },
85
+ },
86
+ })
87
+ ),
88
+ },
89
+ channel: vi.fn(() => mockChannel),
90
+ removeChannel: vi.fn(() => Promise.resolve()),
91
+ };
92
+
93
+ (createClient as unknown as MockCreateClientFn).mockReturnValue(mockClient);
94
+ vi.mocked(getCurrentUserProfile).mockResolvedValue({
95
+ avatar_url: 'https://example.com/ada.png',
96
+ created_at: '2026-01-01T00:00:00.000Z',
97
+ default_workspace_id: 'user-1',
98
+ display_name: 'Ada Lovelace',
99
+ email: 'ada@example.com',
100
+ full_name: 'Ada Lovelace',
101
+ id: 'user-1',
102
+ new_email: null,
103
+ });
104
+ });
105
+
106
+ afterEach(() => {
107
+ vi.clearAllMocks();
108
+ });
109
+
110
+ it('tracks sanitized profile data on a private board realtime presence channel', async () => {
111
+ const { result } = renderHook(() => useBoardPresence('board-1'));
112
+
113
+ await act(async () => {
114
+ await (result.current.updateLocation(
115
+ { boardId: 'board-1', type: 'board' },
116
+ { listStatusFilter: 'active' }
117
+ ) as unknown as Promise<void>);
118
+ });
119
+
120
+ expect(mockClient.channel).toHaveBeenCalledWith('board-realtime-board-1', {
121
+ config: {
122
+ presence: {
123
+ enabled: true,
124
+ key: 'user-1',
125
+ },
126
+ private: true,
127
+ },
128
+ });
129
+ expect(mockChannel.track).toHaveBeenCalledWith(
130
+ expect.objectContaining({
131
+ away: false,
132
+ location: { boardId: 'board-1', type: 'board' },
133
+ metadata: { listStatusFilter: 'active' },
134
+ session_id: expect.any(String),
135
+ user: {
136
+ avatar_url: 'https://example.com/ada.png',
137
+ display_name: 'Ada Lovelace',
138
+ email: 'ada@example.com',
139
+ id: 'user-1',
140
+ },
141
+ })
142
+ );
143
+ });
144
+
145
+ it('does not create a channel while disabled', async () => {
146
+ const { result } = renderHook(() =>
147
+ useBoardPresence('board-1', { enabled: false })
148
+ );
149
+
150
+ await act(async () => {
151
+ await (result.current.updateLocation({
152
+ boardId: 'board-1',
153
+ type: 'board',
154
+ }) as unknown as Promise<void>);
155
+ });
156
+
157
+ expect(mockClient.channel).not.toHaveBeenCalled();
158
+ expect(mockChannel.track).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('exposes board viewers from presence sync events', async () => {
162
+ const viewerPresence = {
163
+ location: { boardId: 'board-1', type: 'board' as const },
164
+ online_at: '2026-01-01T00:00:00.000Z',
165
+ session_id: 'session-2',
166
+ user: {
167
+ avatar_url: null,
168
+ display_name: 'Guest Reviewer',
169
+ email: 'guest@example.com',
170
+ id: 'user-2',
171
+ },
172
+ };
173
+ mockChannel.presenceState.mockReturnValue({
174
+ 'user-2': [viewerPresence],
175
+ });
176
+ const { result } = renderHook(() => useBoardPresence('board-1'));
177
+
178
+ await act(async () => {
179
+ await (result.current.updateLocation({
180
+ boardId: 'board-1',
181
+ type: 'board',
182
+ }) as unknown as Promise<void>);
183
+ });
184
+
185
+ act(() => {
186
+ presenceListeners.get('sync')?.();
187
+ });
188
+
189
+ expect(result.current.getBoardViewers('board-1')).toEqual([viewerPresence]);
190
+ });
191
+ });
@@ -78,6 +78,7 @@ describe('useBoardRealtime', () => {
78
78
  subscribe: ReturnType<typeof vi.fn>;
79
79
  send: ReturnType<typeof vi.fn>;
80
80
  };
81
+ let mockFrom: ReturnType<typeof vi.fn>;
81
82
  let mockRemoveChannel: ReturnType<typeof vi.fn>;
82
83
  let broadcastListeners: Map<string, BroadcastListener>;
83
84
  let subscribeCallback: ((status: string, err?: unknown) => void) | undefined;
@@ -142,92 +143,14 @@ describe('useBoardRealtime', () => {
142
143
  };
143
144
 
144
145
  mockRemoveChannel = vi.fn();
146
+ mockFrom = vi.fn();
145
147
 
146
148
  const mockCreateClient = createClient as unknown as MockCreateClientFn;
147
149
 
148
150
  mockCreateClient.mockReturnValue({
149
151
  channel: vi.fn(() => mockChannel),
150
152
  removeChannel: mockRemoveChannel,
151
- from: vi.fn(() => ({
152
- select: vi.fn(() => ({
153
- in: vi.fn(() =>
154
- Promise.resolve({
155
- data: [
156
- {
157
- id: 'task-1',
158
- assignees: [
159
- {
160
- user: {
161
- id: 'u-1',
162
- display_name: 'User 1',
163
- avatar_url: null,
164
- },
165
- },
166
- ],
167
- labels: [
168
- {
169
- label: {
170
- id: 'l-1',
171
- name: 'Bug',
172
- color: 'red',
173
- created_at: '2025-01-01',
174
- },
175
- },
176
- ],
177
- projects: [
178
- {
179
- project: {
180
- id: 'p-1',
181
- name: 'Project 1',
182
- status: 'active',
183
- },
184
- },
185
- ],
186
- },
187
- ],
188
- error: null,
189
- })
190
- ),
191
- eq: vi.fn(() => ({
192
- single: vi.fn(() =>
193
- Promise.resolve({
194
- data: {
195
- id: 'task-1',
196
- assignees: [
197
- {
198
- user: {
199
- id: 'u-1',
200
- display_name: 'User 1',
201
- avatar_url: null,
202
- },
203
- },
204
- ],
205
- labels: [
206
- {
207
- label: {
208
- id: 'l-1',
209
- name: 'Bug',
210
- color: 'red',
211
- created_at: '2025-01-01',
212
- },
213
- },
214
- ],
215
- projects: [
216
- {
217
- project: {
218
- id: 'p-1',
219
- name: 'Project 1',
220
- status: 'active',
221
- },
222
- },
223
- ],
224
- },
225
- error: null,
226
- })
227
- ),
228
- })),
229
- })),
230
- })),
153
+ from: mockFrom,
231
154
  });
232
155
 
233
156
  vi.clearAllMocks();
@@ -835,11 +758,12 @@ describe('useBoardRealtime', () => {
835
758
  });
836
759
 
837
760
  describe('task:relations-changed event', () => {
838
- it('should fetch relations from DB and merge into cache', async () => {
761
+ it('reconciles relation-bearing queries without refetching visible board caches', async () => {
839
762
  queryClient.setQueryData(
840
763
  ['tasks', 'board-1'],
841
764
  [{ ...mockTaskWithRelations, id: 'task-1' }]
842
765
  );
766
+ const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
843
767
 
844
768
  renderHook(() => useBoardRealtime('board-1', { enabled: true }), {
845
769
  wrapper,
@@ -847,31 +771,29 @@ describe('useBoardRealtime', () => {
847
771
 
848
772
  const listener = broadcastListeners.get('task:relations-changed')!;
849
773
 
850
- // Fire the listener — it debounces for 150ms before fetching
774
+ // Fire the listener — it debounces for 150ms before invalidating caches.
851
775
  await act(async () => {
852
776
  listener({ payload: { taskId: 'task-1' } });
853
777
  });
854
778
 
855
- // Advance past the 150ms receiver debounce and flush microtasks
779
+ // Advance past the 150ms receiver debounce.
856
780
  await act(async () => {
857
781
  vi.advanceTimersByTime(200);
858
- // Allow the async fetch to resolve
859
- await Promise.resolve();
860
- await Promise.resolve();
861
782
  });
862
783
 
863
- const cachedTasks = queryClient.getQueryData<Task[]>([
864
- 'tasks',
865
- 'board-1',
866
- ]);
867
- const task = cachedTasks?.[0];
868
- // The mock returns one assignee, one label, one project
869
- expect(task?.assignees).toHaveLength(1);
870
- expect(task?.labels).toHaveLength(1);
871
- expect(task?.projects).toHaveLength(1);
784
+ expect(mockFrom).not.toHaveBeenCalled();
785
+ expect(invalidateQueriesSpy).not.toHaveBeenCalledWith({
786
+ queryKey: ['tasks', 'board-1'],
787
+ });
788
+ expect(invalidateQueriesSpy).not.toHaveBeenCalledWith({
789
+ queryKey: ['tasks-full', 'board-1'],
790
+ });
791
+ expect(invalidateQueriesSpy).toHaveBeenCalledWith({
792
+ queryKey: ['task-list-counts', 'board-1'],
793
+ });
872
794
  });
873
795
 
874
- it('should merge fetched relations into the full task cache when it exists', async () => {
796
+ it('keeps the full task cache warm when relations change', async () => {
875
797
  queryClient.setQueryData(
876
798
  ['tasks', 'board-1'],
877
799
  [{ ...mockTaskWithRelations, id: 'task-1' }]
@@ -880,6 +802,7 @@ describe('useBoardRealtime', () => {
880
802
  ['tasks-full', 'board-1'],
881
803
  [{ ...mockTaskWithRelations, id: 'task-1' }]
882
804
  );
805
+ const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
883
806
 
884
807
  renderHook(() => useBoardRealtime('board-1', { enabled: true }), {
885
808
  wrapper,
@@ -893,57 +816,14 @@ describe('useBoardRealtime', () => {
893
816
 
894
817
  await act(async () => {
895
818
  vi.advanceTimersByTime(200);
896
- await Promise.resolve();
897
- await Promise.resolve();
898
819
  });
899
820
 
900
- const task = queryClient.getQueryData<Task[]>([
901
- 'tasks-full',
902
- 'board-1',
903
- ])?.[0];
904
- expect(task?.assignees).toHaveLength(1);
905
- expect(task?.labels).toHaveLength(1);
906
- expect(task?.projects).toHaveLength(1);
907
- });
908
-
909
- it('should handle DB fetch errors gracefully', async () => {
910
- queryClient.setQueryData(['tasks', 'board-1'], [mockTaskWithRelations]);
911
-
912
- // Mock a batched fetch error from the .in(...) branch.
913
- (createClient as unknown as MockCreateClientFn).mockReturnValue({
914
- channel: vi.fn(() => mockChannel),
915
- removeChannel: mockRemoveChannel,
916
- from: vi.fn(() => ({
917
- select: vi.fn(() => ({
918
- in: vi.fn(() =>
919
- Promise.resolve({ data: null, error: { message: 'Not found' } })
920
- ),
921
- })),
922
- })),
923
- });
924
-
925
- renderHook(() => useBoardRealtime('board-1', { enabled: true }), {
926
- wrapper,
821
+ expect(invalidateQueriesSpy).not.toHaveBeenCalledWith({
822
+ queryKey: ['tasks-full', 'board-1'],
927
823
  });
928
-
929
- const listener = broadcastListeners.get('task:relations-changed')!;
930
-
931
- await act(async () => {
932
- listener({ payload: { taskId: 'task-1' } });
933
- });
934
-
935
- await act(async () => {
936
- vi.advanceTimersByTime(150);
937
- await Promise.resolve();
938
- await Promise.resolve();
939
- });
940
-
941
- // Cache should remain unchanged
942
- const cachedTasks = queryClient.getQueryData<Task[]>([
943
- 'tasks',
944
- 'board-1',
945
- ]);
946
- expect(cachedTasks?.[0]?.assignees).toEqual([]);
824
+ expect(
825
+ queryClient.getQueryData<Task[]>(['tasks-full', 'board-1'])?.[0]?.id
826
+ ).toBe('task-1');
947
827
  });
948
828
 
949
829
  it('calls onTaskRelationsChange for Supabase relation broadcasts', async () => {