AdvisorSignals.integration.test.tsx280 lines · main
1import { screen, waitFor } from '@testing-library/react'
2import userEvent from '@testing-library/user-event'
3import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
5import { AdvisorSection } from '@/components/interfaces/ProjectHome/AdvisorSection'
6import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
7import { AdvisorPanel } from '@/components/ui/AdvisorPanel/AdvisorPanel'
8import { advisorState } from '@/state/advisor-state'
9import { sidebarManagerState } from '@/state/sidebar-manager-state'
10import { render } from '@/tests/helpers'
11
12const {
13 mockUseProjectLintsQuery,
14 mockUseBannedIPsQuery,
15 mockUseSelectedProjectQuery,
16 mockUseNotificationsV2Query,
17 mockUseNotificationsV2UpdateMutation,
18 mockUseTrack,
19} = vi.hoisted(() => ({
20 mockUseProjectLintsQuery: vi.fn(),
21 mockUseBannedIPsQuery: vi.fn(),
22 mockUseSelectedProjectQuery: vi.fn(),
23 mockUseNotificationsV2Query: vi.fn(),
24 mockUseNotificationsV2UpdateMutation: vi.fn(),
25 mockUseTrack: vi.fn(),
26}))
27
28vi.mock('common', async () => {
29 const actual = await vi.importActual<typeof import('common')>('common')
30
31 return {
32 ...actual,
33 useParams: () => ({ ref: 'project-ref' }),
34 }
35})
36
37vi.mock('@/data/lint/lint-query', () => ({
38 useProjectLintsQuery: mockUseProjectLintsQuery,
39}))
40
41vi.mock('@/data/banned-ips/banned-ips-query', () => ({
42 useBannedIPsQuery: mockUseBannedIPsQuery,
43}))
44
45vi.mock('@/hooks/misc/useSelectedProject', () => ({
46 useSelectedProjectQuery: mockUseSelectedProjectQuery,
47}))
48
49vi.mock('@/data/notifications/notifications-v2-query', () => ({
50 useNotificationsV2Query: mockUseNotificationsV2Query,
51}))
52
53vi.mock('@/data/notifications/notifications-v2-update-mutation', () => ({
54 useNotificationsV2UpdateMutation: mockUseNotificationsV2UpdateMutation,
55}))
56
57vi.mock('@/lib/constants', async (importOriginal) => ({
58 ...(await importOriginal<typeof import('@/lib/constants')>()),
59 IS_PLATFORM: true,
60}))
61
62vi.mock('@/lib/telemetry/track', () => ({
63 useTrack: mockUseTrack,
64}))
65
66vi.mock('@/state/ai-assistant-state', () => ({
67 useAiAssistantStateSnapshot: () => ({
68 newChat: vi.fn(),
69 }),
70}))
71
72vi.mock('@/components/ui/AiAssistantDropdown', () => ({
73 AiAssistantDropdown: () => <div data-testid="advisor-assistant-dropdown" />,
74}))
75
76vi.mock('./AdvisorFilters', () => ({
77 AdvisorFilters: () => <div data-testid="advisor-filters" />,
78}))
79
80vi.mock('./AdvisorPanelHeader', () => ({
81 AdvisorPanelHeader: () => <div data-testid="advisor-panel-header" />,
82}))
83
84describe('Advisor signals integration', () => {
85 beforeEach(() => {
86 window.localStorage.clear()
87 advisorState.reset()
88 sidebarManagerState.unregisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
89 sidebarManagerState.registerSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => null)
90 sidebarManagerState.clearActiveSidebar()
91
92 mockUseTrack.mockReturnValue(vi.fn())
93 mockUseSelectedProjectQuery.mockReturnValue({
94 data: { ref: 'project-ref' },
95 })
96 mockUseProjectLintsQuery.mockImplementation((_variables, options) => {
97 if (options?.enabled === false) {
98 return {
99 data: undefined,
100 isPending: false,
101 isError: false,
102 }
103 }
104
105 return {
106 data: [
107 {
108 cache_key: 'lint-1',
109 name: 'unknown_lint',
110 detail: 'Critical lint detail',
111 level: 'ERROR',
112 categories: ['SECURITY'],
113 metadata: {},
114 },
115 ],
116 isPending: false,
117 isError: false,
118 }
119 })
120 mockUseBannedIPsQuery.mockImplementation((_variables, options) => {
121 if (options?.enabled === false) {
122 return {
123 data: undefined,
124 isPending: false,
125 isError: false,
126 }
127 }
128
129 return {
130 data: {
131 banned_ipv4_addresses: ['203.0.113.10'],
132 },
133 isPending: false,
134 isError: false,
135 }
136 })
137 mockUseNotificationsV2Query.mockReturnValue({
138 data: { pages: [[]] },
139 isPending: false,
140 isError: false,
141 })
142 mockUseNotificationsV2UpdateMutation.mockReturnValue({
143 mutate: vi.fn(),
144 })
145 })
146
147 afterEach(() => {
148 advisorState.reset()
149 sidebarManagerState.unregisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
150 sidebarManagerState.clearActiveSidebar()
151 vi.clearAllMocks()
152 })
153
154 it('renders signal items and dismisses them across the homepage and panel', async () => {
155 render(
156 <>
157 <AdvisorSection />
158 <AdvisorPanel />
159 </>
160 )
161
162 expect(screen.getByText('Advisor found 2 issues')).toBeInTheDocument()
163 expect(screen.getByText('Banned IP address')).toBeInTheDocument()
164 expect(screen.getAllByText('Critical lint detail').length).toBeGreaterThan(0)
165 expect(
166 screen.getAllByText((_, node) =>
167 Boolean(
168 node?.textContent?.includes(
169 'The IP address 203.0.113.10 is temporarily blocked because of suspicious traffic or repeated failed password attempts.'
170 )
171 )
172 ).length
173 ).toBeGreaterThan(0)
174
175 await userEvent.click(screen.getByText('Banned IP address'))
176
177 expect(screen.getByText('Entity')).toBeInTheDocument()
178 expect(screen.getByText('Issue')).toBeInTheDocument()
179 expect(screen.getByText('Resolve')).toBeInTheDocument()
180 expect(screen.getAllByTestId('advisor-assistant-dropdown').length).toBeGreaterThan(0)
181 expect(
182 screen.getAllByText((_, node) =>
183 Boolean(
184 node?.textContent?.includes(
185 'The IP address 203.0.113.10 is temporarily blocked because of suspicious traffic or repeated failed password attempts.'
186 )
187 )
188 ).length
189 ).toBeGreaterThan(0)
190 expect(screen.getByRole('link', { name: 'Learn more' })).toHaveAttribute(
191 'href',
192 'https://supabase.com/docs/reference/cli/supabase-network-bans'
193 )
194
195 expect(screen.getAllByText('Banned IP address').length).toBeGreaterThan(0)
196
197 await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
198
199 await waitFor(() => {
200 expect(screen.queryByText('Banned IP address')).not.toBeInTheDocument()
201 })
202
203 expect(screen.getAllByText('Critical lint detail').length).toBeGreaterThan(0)
204 })
205
206 it('shows an overflow affordance when the homepage cap hides additional issues', () => {
207 mockUseProjectLintsQuery.mockImplementation((_variables, options) => {
208 if (options?.enabled === false) {
209 return {
210 data: undefined,
211 isPending: false,
212 isError: false,
213 }
214 }
215
216 return {
217 data: [
218 {
219 cache_key: 'lint-1',
220 name: 'unknown_lint',
221 detail: 'Critical lint detail 1',
222 level: 'ERROR',
223 categories: ['SECURITY'],
224 metadata: {},
225 },
226 {
227 cache_key: 'lint-2',
228 name: 'unknown_lint',
229 detail: 'Critical lint detail 2',
230 level: 'ERROR',
231 categories: ['SECURITY'],
232 metadata: {},
233 },
234 {
235 cache_key: 'lint-3',
236 name: 'unknown_lint',
237 detail: 'Critical lint detail 3',
238 level: 'ERROR',
239 categories: ['SECURITY'],
240 metadata: {},
241 },
242 {
243 cache_key: 'lint-4',
244 name: 'unknown_lint',
245 detail: 'Critical lint detail 4',
246 level: 'ERROR',
247 categories: ['SECURITY'],
248 metadata: {},
249 },
250 ],
251 isPending: false,
252 isError: false,
253 }
254 })
255
256 render(<AdvisorSection />)
257
258 expect(screen.getByText('Advisor found 5 issues')).toBeInTheDocument()
259 expect(screen.getByRole('button', { name: 'View 1 more issue in Advisor' })).toBeInTheDocument()
260 expect(screen.queryByText('Banned IP address')).not.toBeInTheDocument()
261 })
262
263 it('does not block the homepage while signal items are pending', () => {
264 mockUseProjectLintsQuery.mockReturnValue({
265 data: [],
266 isPending: false,
267 isError: false,
268 })
269 mockUseBannedIPsQuery.mockReturnValue({
270 data: undefined,
271 isPending: true,
272 isError: false,
273 })
274
275 render(<AdvisorSection />)
276
277 // Lints have loaded — homepage renders without being blocked by signal loading
278 expect(screen.getByText('Advisor found no issues')).toBeInTheDocument()
279 })
280})