|
| 1 | +// @vitest-environment jsdom |
| 2 | +import '@testing-library/jest-dom/vitest'; |
| 3 | +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; |
| 4 | +import { render, screen, cleanup } from '@testing-library/react'; |
| 5 | +import { Provider } from 'react-redux'; |
| 6 | +import { configureStore } from '@reduxjs/toolkit'; |
| 7 | +import { IntlProvider } from 'react-intl'; |
| 8 | +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; |
| 9 | +import { ConnectorBrowse } from '../connectorBrowse'; |
| 10 | +import type { Connector } from '@microsoft/logic-apps-shared'; |
| 11 | + |
| 12 | +// --- Mocks --- |
| 13 | + |
| 14 | +const mockDispatch = vi.fn(); |
| 15 | +vi.mock('react-redux', async () => { |
| 16 | + const actual = await vi.importActual('react-redux'); |
| 17 | + return { ...actual, useDispatch: () => mockDispatch }; |
| 18 | +}); |
| 19 | + |
| 20 | +const mockUseAllConnectors = vi.fn(); |
| 21 | +vi.mock('../../../../../core/queries/browse', () => ({ |
| 22 | + useAllConnectors: () => mockUseAllConnectors(), |
| 23 | +})); |
| 24 | + |
| 25 | +vi.mock('../../../../../core/state/panel/panelSelectors', () => ({ |
| 26 | + useDiscoveryPanelRelationshipIds: vi.fn(() => ({ |
| 27 | + graphId: 'root', |
| 28 | + parentId: undefined, |
| 29 | + childId: undefined, |
| 30 | + })), |
| 31 | +})); |
| 32 | + |
| 33 | +vi.mock('../../../../../core/state/designerView/designerViewSelectors', () => ({ |
| 34 | + useIsA2AWorkflow: vi.fn(() => false), |
| 35 | +})); |
| 36 | + |
| 37 | +vi.mock('../../../../../core/state/panel/panelSlice', () => ({ |
| 38 | + selectOperationGroupId: vi.fn((id: string) => ({ type: 'panel/selectOperationGroupId', payload: id })), |
| 39 | +})); |
| 40 | + |
| 41 | +vi.mock('@microsoft/designer-ui', () => ({ |
| 42 | + isBuiltInConnector: vi.fn((c: Connector) => c.id.includes('builtin')), |
| 43 | + isCustomConnector: vi.fn((c: Connector) => c.id.includes('custom')), |
| 44 | +})); |
| 45 | + |
| 46 | +vi.mock('../connectorCard', () => ({ |
| 47 | + ConnectorCard: vi.fn(({ connector }: { connector: Connector }) => ( |
| 48 | + <div data-testid={`connector-card-${connector.id}`}>{connector.properties.displayName}</div> |
| 49 | + )), |
| 50 | +})); |
| 51 | + |
| 52 | +vi.mock('../styles/ConnectorBrowse.styles', () => ({ |
| 53 | + useConnectorBrowseStyles: vi.fn(() => ({ |
| 54 | + loadingContainer: 'loading-container', |
| 55 | + emptyStateContainer: 'empty-state-container', |
| 56 | + })), |
| 57 | +})); |
| 58 | + |
| 59 | +vi.mock('react-window', () => ({ |
| 60 | + List: vi.fn(({ rowCount, rowComponent: Row }: any) => ( |
| 61 | + <div data-testid="virtualized-list"> |
| 62 | + {Array.from({ length: rowCount }, (_, i) => ( |
| 63 | + <Row key={i} index={i} style={{}} /> |
| 64 | + ))} |
| 65 | + </div> |
| 66 | + )), |
| 67 | +})); |
| 68 | + |
| 69 | +// --- Helpers --- |
| 70 | + |
| 71 | +const makeConnector = (id: string, displayName: string, overrides?: Partial<Connector>): Connector => |
| 72 | + ({ |
| 73 | + id, |
| 74 | + name: id.split('/').pop() ?? id, |
| 75 | + type: 'Microsoft.Web/locations/managedApis', |
| 76 | + properties: { |
| 77 | + displayName, |
| 78 | + capabilities: [], |
| 79 | + ...overrides?.properties, |
| 80 | + }, |
| 81 | + ...overrides, |
| 82 | + }) as unknown as Connector; |
| 83 | + |
| 84 | +const createWrapper = () => { |
| 85 | + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); |
| 86 | + const store = configureStore({ reducer: { stub: (s = {}) => s } }); |
| 87 | + |
| 88 | + return ({ children }: { children: React.ReactNode }) => ( |
| 89 | + <QueryClientProvider client={queryClient}> |
| 90 | + <Provider store={store}> |
| 91 | + <IntlProvider locale="en">{children}</IntlProvider> |
| 92 | + </Provider> |
| 93 | + </QueryClientProvider> |
| 94 | + ); |
| 95 | +}; |
| 96 | + |
| 97 | +// --- Tests --- |
| 98 | + |
| 99 | +describe('ConnectorBrowse', () => { |
| 100 | + beforeEach(() => { |
| 101 | + vi.clearAllMocks(); |
| 102 | + }); |
| 103 | + |
| 104 | + afterEach(() => { |
| 105 | + cleanup(); |
| 106 | + }); |
| 107 | + |
| 108 | + test('renders loading spinner when data is loading', () => { |
| 109 | + mockUseAllConnectors.mockReturnValue({ data: undefined, isLoading: true }); |
| 110 | + |
| 111 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 112 | + |
| 113 | + expect(screen.getByText('Loading connectors...')).toBeInTheDocument(); |
| 114 | + }); |
| 115 | + |
| 116 | + test('renders empty state when no connectors match', () => { |
| 117 | + mockUseAllConnectors.mockReturnValue({ data: [], isLoading: false }); |
| 118 | + |
| 119 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 120 | + |
| 121 | + expect(screen.getByText('No connectors found for this category')).toBeInTheDocument(); |
| 122 | + }); |
| 123 | + |
| 124 | + test('renders connector cards when connectors are available', () => { |
| 125 | + const connectors = [makeConnector('shared/sql', 'SQL'), makeConnector('shared/outlook', 'Outlook')]; |
| 126 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 127 | + |
| 128 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 129 | + |
| 130 | + expect(screen.getByText('SQL')).toBeInTheDocument(); |
| 131 | + expect(screen.getByText('Outlook')).toBeInTheDocument(); |
| 132 | + }); |
| 133 | + |
| 134 | + test('filters out agent connector', () => { |
| 135 | + const connectors = [makeConnector('connectionProviders/agent', 'Agent'), makeConnector('shared/sql', 'SQL')]; |
| 136 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 137 | + |
| 138 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 139 | + |
| 140 | + expect(screen.queryByTestId('connector-card-connectionProviders/agent')).not.toBeInTheDocument(); |
| 141 | + expect(screen.getByText('SQL')).toBeInTheDocument(); |
| 142 | + }); |
| 143 | + |
| 144 | + test('filters out ACA session connector', () => { |
| 145 | + const connectors = [makeConnector('/serviceProviders/acasession', 'ACA Session'), makeConnector('shared/sql', 'SQL')]; |
| 146 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 147 | + |
| 148 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 149 | + |
| 150 | + expect(screen.queryByTestId('connector-card-/serviceProviders/acasession')).not.toBeInTheDocument(); |
| 151 | + expect(screen.getByText('SQL')).toBeInTheDocument(); |
| 152 | + }); |
| 153 | + |
| 154 | + test('filters by runtime=inapp to show only built-in connectors', () => { |
| 155 | + const connectors = [makeConnector('builtin/http', 'HTTP'), makeConnector('shared/sql', 'SQL')]; |
| 156 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 157 | + |
| 158 | + render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'inapp' }} />, { wrapper: createWrapper() }); |
| 159 | + |
| 160 | + expect(screen.getByText('HTTP')).toBeInTheDocument(); |
| 161 | + expect(screen.queryByText('SQL')).not.toBeInTheDocument(); |
| 162 | + }); |
| 163 | + |
| 164 | + test('filters by runtime=custom to show only custom connectors', () => { |
| 165 | + const connectors = [makeConnector('custom/myConnector', 'My Custom'), makeConnector('shared/sql', 'SQL')]; |
| 166 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 167 | + |
| 168 | + render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'custom' }} />, { wrapper: createWrapper() }); |
| 169 | + |
| 170 | + expect(screen.getByText('My Custom')).toBeInTheDocument(); |
| 171 | + expect(screen.queryByText('SQL')).not.toBeInTheDocument(); |
| 172 | + }); |
| 173 | + |
| 174 | + test('filters by runtime=shared to exclude built-in and custom connectors', () => { |
| 175 | + const connectors = [ |
| 176 | + makeConnector('builtin/http', 'HTTP'), |
| 177 | + makeConnector('custom/myConnector', 'My Custom'), |
| 178 | + makeConnector('shared/sql', 'SQL'), |
| 179 | + ]; |
| 180 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 181 | + |
| 182 | + render(<ConnectorBrowse categoryKey="all" filters={{ runtime: 'shared' }} />, { wrapper: createWrapper() }); |
| 183 | + |
| 184 | + expect(screen.queryByText('HTTP')).not.toBeInTheDocument(); |
| 185 | + expect(screen.queryByText('My Custom')).not.toBeInTheDocument(); |
| 186 | + expect(screen.getByText('SQL')).toBeInTheDocument(); |
| 187 | + }); |
| 188 | + |
| 189 | + test('filters by actionType=triggers', () => { |
| 190 | + const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', { |
| 191 | + properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] }, |
| 192 | + } as any); |
| 193 | + const actionsConnector = makeConnector('shared/action', 'Action Connector', { |
| 194 | + properties: { displayName: 'Action Connector', capabilities: ['actions'] }, |
| 195 | + } as any); |
| 196 | + |
| 197 | + mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false }); |
| 198 | + |
| 199 | + render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'triggers' }} />, { wrapper: createWrapper() }); |
| 200 | + |
| 201 | + expect(screen.getByText('Trigger Connector')).toBeInTheDocument(); |
| 202 | + expect(screen.queryByText('Action Connector')).not.toBeInTheDocument(); |
| 203 | + }); |
| 204 | + |
| 205 | + test('filters by actionType=actions', () => { |
| 206 | + const triggersConnector = makeConnector('shared/trigger', 'Trigger Connector', { |
| 207 | + properties: { displayName: 'Trigger Connector', capabilities: ['triggers'] }, |
| 208 | + } as any); |
| 209 | + const actionsConnector = makeConnector('shared/action', 'Action Connector', { |
| 210 | + properties: { displayName: 'Action Connector', capabilities: ['actions'] }, |
| 211 | + } as any); |
| 212 | + |
| 213 | + mockUseAllConnectors.mockReturnValue({ data: [triggersConnector, actionsConnector], isLoading: false }); |
| 214 | + |
| 215 | + render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'actions' }} />, { wrapper: createWrapper() }); |
| 216 | + |
| 217 | + expect(screen.queryByText('Trigger Connector')).not.toBeInTheDocument(); |
| 218 | + expect(screen.getByText('Action Connector')).toBeInTheDocument(); |
| 219 | + }); |
| 220 | + |
| 221 | + test('connectors with no capabilities pass actionType filter', () => { |
| 222 | + const noCapsConnector = makeConnector('shared/nocaps', 'No Caps', { |
| 223 | + properties: { displayName: 'No Caps', capabilities: [] }, |
| 224 | + } as any); |
| 225 | + |
| 226 | + mockUseAllConnectors.mockReturnValue({ data: [noCapsConnector], isLoading: false }); |
| 227 | + |
| 228 | + render(<ConnectorBrowse categoryKey="all" filters={{ actionType: 'triggers' }} />, { wrapper: createWrapper() }); |
| 229 | + |
| 230 | + expect(screen.getByText('No Caps')).toBeInTheDocument(); |
| 231 | + }); |
| 232 | + |
| 233 | + test('sorts priority connectors before others', () => { |
| 234 | + const regularConnector = makeConnector('shared/random', 'Random'); |
| 235 | + const priorityConnector = makeConnector('shared/managedApis/office365', 'Office 365'); |
| 236 | + |
| 237 | + mockUseAllConnectors.mockReturnValue({ data: [regularConnector, priorityConnector], isLoading: false }); |
| 238 | + |
| 239 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 240 | + |
| 241 | + const cards = screen.getAllByTestId(/connector-card-/); |
| 242 | + expect(cards[0]).toHaveTextContent('Office 365'); |
| 243 | + expect(cards[1]).toHaveTextContent('Random'); |
| 244 | + }); |
| 245 | + |
| 246 | + test('uses virtualized list for rendering', () => { |
| 247 | + const connectors = [makeConnector('shared/sql', 'SQL')]; |
| 248 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 249 | + |
| 250 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 251 | + |
| 252 | + expect(screen.getAllByTestId('virtualized-list').length).toBeGreaterThan(0); |
| 253 | + }); |
| 254 | + |
| 255 | + test('does not render loading spinner after data has loaded', () => { |
| 256 | + const connectors = [makeConnector('shared/sql', 'SQL')]; |
| 257 | + mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false }); |
| 258 | + |
| 259 | + render(<ConnectorBrowse categoryKey="all" />, { wrapper: createWrapper() }); |
| 260 | + |
| 261 | + expect(screen.queryByText('Loading connectors...')).not.toBeInTheDocument(); |
| 262 | + }); |
| 263 | +}); |
0 commit comments