diff --git a/libs/designer/src/lib/ui/panel/recommendation/__test__/browseView.spec.tsx b/libs/designer/src/lib/ui/panel/recommendation/__test__/browseView.spec.tsx
new file mode 100644
index 00000000000..c26d67921e6
--- /dev/null
+++ b/libs/designer/src/lib/ui/panel/recommendation/__test__/browseView.spec.tsx
@@ -0,0 +1,412 @@
+// @vitest-environment jsdom
+import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import type { Connector } from '@microsoft/logic-apps-shared';
+
+// --- Mocks ---
+
+const mockDispatch = vi.fn();
+vi.mock('react-redux', () => ({
+ useDispatch: () => mockDispatch,
+}));
+
+vi.mock('../../../../core/queries/browse', () => ({
+ useAllConnectors: vi.fn(() => ({ data: [], isLoading: false })),
+}));
+
+vi.mock('../../../../core/state/panel/panelSlice', () => ({
+ selectOperationGroupId: vi.fn((id: string) => ({ type: 'panel/selectOperationGroupId', payload: id })),
+}));
+
+vi.mock('../../../../core/state/panel/panelSelectors', () => ({
+ useDiscoveryPanelRelationshipIds: vi.fn(() => ({ graphId: 'root', parentId: undefined, childId: undefined })),
+}));
+
+vi.mock('../../../../core/state/designerView/designerViewSelectors', () => ({
+ useIsA2AWorkflow: vi.fn(() => false),
+}));
+
+vi.mock('@microsoft/designer-ui', () => ({
+ BrowseGrid: vi.fn(({ operationsData, isLoading, onConnectorSelected }) => (
+
+ {operationsData?.map((c: Connector) => (
+
+ ))}
+
+ )),
+ isBuiltInConnector: vi.fn((c: Connector) => c.id.startsWith('builtin/')),
+ isCustomConnector: vi.fn((c: Connector) => c.id.startsWith('custom/')),
+ RuntimeFilterTagList: vi.fn(() => ),
+}));
+
+vi.mock('@microsoft/logic-apps-shared', async (importOriginal) => {
+ const actual = (await importOriginal()) as Record;
+ return {
+ ...actual,
+ equals: (a?: string, b?: string) => a?.toLowerCase() === b?.toLowerCase(),
+ getRecordEntry: (record: Record | undefined, key: string) => record?.[key],
+ };
+});
+
+// --- Imports after mocks ---
+
+import { BrowseView } from '../browseView';
+import { useAllConnectors } from '../../../../core/queries/browse';
+import { useIsA2AWorkflow } from '../../../../core/state/designerView/designerViewSelectors';
+import { useDiscoveryPanelRelationshipIds } from '../../../../core/state/panel/panelSelectors';
+import { selectOperationGroupId } from '../../../../core/state/panel/panelSlice';
+import { fireEvent } from '@testing-library/react';
+
+const mockUseAllConnectors = vi.mocked(useAllConnectors);
+const mockUseIsA2AWorkflow = vi.mocked(useIsA2AWorkflow);
+const mockUseDiscoveryPanelRelationshipIds = vi.mocked(useDiscoveryPanelRelationshipIds);
+const mockSelectOperationGroupId = vi.mocked(selectOperationGroupId);
+
+// --- Test helpers ---
+
+const makeConnector = (overrides: Partial & { id: string }): Connector =>
+ ({
+ id: overrides.id,
+ name: overrides.name ?? overrides.id.split('/').pop(),
+ type: overrides.type ?? 'Microsoft.Web/locations/managedApis',
+ properties: {
+ displayName: overrides.properties?.displayName ?? overrides.id,
+ capabilities: overrides.properties?.capabilities ?? [],
+ ...(overrides.properties ?? {}),
+ },
+ }) as unknown as Connector;
+
+const defaultProps = {
+ filters: {} as Record,
+ displayRuntimeInfo: false,
+ setFilters: vi.fn(),
+ onConnectorCardSelected: vi.fn(),
+};
+
+describe('BrowseView', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseAllConnectors.mockReturnValue({ data: [], isLoading: false });
+ mockUseIsA2AWorkflow.mockReturnValue(false);
+ mockUseDiscoveryPanelRelationshipIds.mockReturnValue({
+ graphId: 'root',
+ parentId: undefined,
+ childId: undefined,
+ });
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ // --- Rendering ---
+
+ describe('Rendering', () => {
+ test('should render BrowseGrid and RuntimeFilterTagList', () => {
+ render();
+ expect(screen.getByTestId('browse-grid')).toBeDefined();
+ expect(screen.getByTestId('runtime-filter-tag-list')).toBeDefined();
+ });
+
+ test('should pass isLoading from useAllConnectors to BrowseGrid', () => {
+ mockUseAllConnectors.mockReturnValue({ data: [], isLoading: true });
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.loading).toBe('true');
+ });
+
+ test('should show connectors returned by useAllConnectors', () => {
+ const connectors = [makeConnector({ id: 'shared/sql' }), makeConnector({ id: 'shared/office365' })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('2');
+ });
+ });
+
+ // --- Agent connector filter ---
+
+ describe('Agent connector filter', () => {
+ test('should filter out the agent connector', () => {
+ const connectors = [
+ makeConnector({ id: 'connectionProviders/agent', name: 'agent' }),
+ makeConnector({ id: 'shared/sql', name: 'sql' }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.queryByTestId('connector-connectionProviders/agent')).toBeNull();
+ expect(screen.getByTestId('connector-shared/sql')).toBeDefined();
+ });
+ });
+
+ // --- Runtime filter ---
+
+ describe('Runtime filter', () => {
+ const connectors = [
+ makeConnector({ id: 'builtin/compose', properties: { displayName: 'Compose' } as any }),
+ makeConnector({ id: 'shared/sql', properties: { displayName: 'SQL' } as any }),
+ makeConnector({ id: 'custom/myConnector', properties: { displayName: 'My Custom' } as any }),
+ ];
+
+ beforeEach(() => {
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+ });
+
+ test('should show all connectors when no runtime filter is set', () => {
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('3');
+ });
+
+ test('should show only built-in connectors when inapp filter is set', () => {
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.getByTestId('connector-builtin/compose')).toBeDefined();
+ });
+
+ test('should show only custom connectors when custom filter is set', () => {
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.getByTestId('connector-custom/myConnector')).toBeDefined();
+ });
+
+ test('should show only shared connectors when shared filter is set', () => {
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.getByTestId('connector-shared/sql')).toBeDefined();
+ });
+ });
+
+ // --- ActionType filter ---
+
+ describe('ActionType filter', () => {
+ test('should show all connectors when no actionType filter is set', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/a', properties: { displayName: 'A', capabilities: ['actions'] } as any }),
+ makeConnector({ id: 'shared/b', properties: { displayName: 'B', capabilities: ['triggers'] } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('2');
+ });
+
+ test('should filter to only triggers when actionType filter is triggers', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/a', properties: { displayName: 'A', capabilities: ['actions'] } as any }),
+ makeConnector({ id: 'shared/b', properties: { displayName: 'B', capabilities: ['triggers'] } as any }),
+ makeConnector({ id: 'shared/c', properties: { displayName: 'C', capabilities: ['actions', 'triggers'] } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('2');
+ expect(screen.queryByTestId('connector-shared/a')).toBeNull();
+ expect(screen.getByTestId('connector-shared/b')).toBeDefined();
+ expect(screen.getByTestId('connector-shared/c')).toBeDefined();
+ });
+
+ test('should filter to only actions when actionType filter is actions', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/a', properties: { displayName: 'A', capabilities: ['actions'] } as any }),
+ makeConnector({ id: 'shared/b', properties: { displayName: 'B', capabilities: ['triggers'] } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.getByTestId('connector-shared/a')).toBeDefined();
+ });
+
+ test('should include connectors with no capabilities (assume supports both)', () => {
+ const connectors = [makeConnector({ id: 'shared/a', properties: { displayName: 'A', capabilities: [] } as any })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ });
+
+ test('should include connectors with non-action capabilities (assume supports both)', () => {
+ const connectors = [makeConnector({ id: 'shared/a', properties: { displayName: 'A', capabilities: ['blob'] } as any })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ });
+ });
+
+ // --- A2A workflow filter ---
+
+ describe('A2A workflow filter', () => {
+ test('should not filter connectors when not an A2A workflow', () => {
+ mockUseIsA2AWorkflow.mockReturnValue(false);
+ const connectors = [makeConnector({ id: 'shared/someRandom', name: 'someRandom', type: 'SomeOtherType' })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ });
+
+ test('should not filter connectors when A2A but not adding to root', () => {
+ mockUseIsA2AWorkflow.mockReturnValue(true);
+ mockUseDiscoveryPanelRelationshipIds.mockReturnValue({
+ graphId: 'someSubgraph',
+ parentId: undefined,
+ childId: undefined,
+ });
+ const connectors = [makeConnector({ id: 'shared/someRandom', name: 'someRandom', type: 'SomeOtherType' })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ });
+
+ test('should allow connectors with allowed A2A names in A2A workflow', () => {
+ mockUseIsA2AWorkflow.mockReturnValue(true);
+ mockUseDiscoveryPanelRelationshipIds.mockReturnValue({
+ graphId: 'root',
+ parentId: undefined,
+ childId: undefined,
+ });
+ const connectors = [
+ makeConnector({ id: 'some/http', name: 'http', type: 'SomeOtherType' }),
+ makeConnector({ id: 'some/variable', name: 'variable', type: 'SomeOtherType' }),
+ makeConnector({ id: 'some/agent', name: 'agent', type: 'SomeOtherType' }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ // 'agent' is in ALLOWED_A2A_CONNECTOR_NAMES but also filtered by isAgentConnectorAllowed
+ // since id is 'some/agent' not 'connectionProviders/agent', agent filter won't exclude it
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('3');
+ });
+
+ test('should allow ManagedApi and ServiceProvider types in A2A workflow', () => {
+ mockUseIsA2AWorkflow.mockReturnValue(true);
+ mockUseDiscoveryPanelRelationshipIds.mockReturnValue({
+ graphId: 'root',
+ parentId: undefined,
+ childId: undefined,
+ });
+ const connectors = [
+ makeConnector({ id: 'shared/sql', name: 'sql', type: 'Microsoft.Web/locations/managedApis' }),
+ makeConnector({ id: 'builtin/sb', name: 'sb', type: 'ServiceProvider' }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('2');
+ });
+
+ test('should exclude connectors with disallowed type and name in A2A workflow', () => {
+ mockUseIsA2AWorkflow.mockReturnValue(true);
+ mockUseDiscoveryPanelRelationshipIds.mockReturnValue({
+ graphId: 'root',
+ parentId: undefined,
+ childId: undefined,
+ });
+ const connectors = [makeConnector({ id: 'shared/something', name: 'something', type: 'SomeOtherType' })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('0');
+ });
+ });
+
+ // --- Sorting ---
+
+ describe('Sorting', () => {
+ test('should sort priority connectors first', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/random', properties: { displayName: 'Random' } as any }),
+ makeConnector({ id: '/managedApis/sql', properties: { displayName: 'SQL' } as any }),
+ makeConnector({ id: 'connectionproviders/request', properties: { displayName: 'Request' } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ const grid = screen.getByTestId('browse-grid');
+ const buttons = grid.querySelectorAll('button');
+ // Request has priority index 0, SQL has priority index 4, Random has no priority
+ expect(buttons[0].textContent).toBe('Request');
+ expect(buttons[1].textContent).toBe('SQL');
+ expect(buttons[2].textContent).toBe('Random');
+ });
+
+ test('should sort built-in before shared for same priority', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/aaa', properties: { displayName: 'AAA Shared' } as any }),
+ makeConnector({ id: 'builtin/aaa', properties: { displayName: 'AAA BuiltIn' } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ const grid = screen.getByTestId('browse-grid');
+ const buttons = grid.querySelectorAll('button');
+ expect(buttons[0].textContent).toBe('AAA BuiltIn');
+ expect(buttons[1].textContent).toBe('AAA Shared');
+ });
+
+ test('should sort alphabetically by displayName when priority and runtime are same', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/zebra', properties: { displayName: 'Zebra' } as any }),
+ makeConnector({ id: 'shared/apple', properties: { displayName: 'Apple' } as any }),
+ makeConnector({ id: 'shared/mango', properties: { displayName: 'Mango' } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ const grid = screen.getByTestId('browse-grid');
+ const buttons = grid.querySelectorAll('button');
+ expect(buttons[0].textContent).toBe('Apple');
+ expect(buttons[1].textContent).toBe('Mango');
+ expect(buttons[2].textContent).toBe('Zebra');
+ });
+
+ test('should handle null displayName gracefully during sorting', () => {
+ const connectors = [
+ makeConnector({ id: 'shared/b', properties: { displayName: 'B' } as any }),
+ makeConnector({ id: 'shared/a', properties: { displayName: undefined } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ // Should not throw
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('2');
+ });
+ });
+
+ // --- Connector selection ---
+
+ describe('Connector selection', () => {
+ test('should dispatch selectOperationGroupId when connector card is clicked', () => {
+ const connectors = [makeConnector({ id: 'shared/sql', properties: { displayName: 'SQL' } as any })];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ fireEvent.click(screen.getByTestId('connector-shared/sql'));
+
+ expect(mockSelectOperationGroupId).toHaveBeenCalledWith('shared/sql');
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+ });
+
+ // --- Combined filters ---
+
+ describe('Combined filters', () => {
+ test('should apply both runtime and actionType filters', () => {
+ const connectors = [
+ makeConnector({ id: 'builtin/a', properties: { displayName: 'A', capabilities: ['actions'] } as any }),
+ makeConnector({ id: 'builtin/b', properties: { displayName: 'B', capabilities: ['triggers'] } as any }),
+ makeConnector({ id: 'shared/c', properties: { displayName: 'C', capabilities: ['actions'] } as any }),
+ ];
+ mockUseAllConnectors.mockReturnValue({ data: connectors, isLoading: false });
+
+ render();
+ expect(screen.getByTestId('browse-grid').dataset.count).toBe('1');
+ expect(screen.getByTestId('connector-builtin/a')).toBeDefined();
+ });
+ });
+});
diff --git a/libs/designer/src/lib/ui/panel/recommendation/browseView.tsx b/libs/designer/src/lib/ui/panel/recommendation/browseView.tsx
index 85791d80e16..90f8e5ed7d5 100644
--- a/libs/designer/src/lib/ui/panel/recommendation/browseView.tsx
+++ b/libs/designer/src/lib/ui/panel/recommendation/browseView.tsx
@@ -54,7 +54,7 @@ const defaultSortConnectors = (connectors: Connector[]): Connector[] => {
return (
getPriorityValue(b) - getPriorityValue(a) ||
getRunTimeValue(a) - getRunTimeValue(b) ||
- a.properties.displayName?.localeCompare(b.properties.displayName)
+ (a.properties.displayName ?? '').localeCompare(b.properties.displayName ?? '')
);
});
};