import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import React from 'react'; import { AccountService } from '@spinnaker/core'; import type { IManifestSelector } from './IManifestSelector'; import { SelectorMode } from './IManifestSelector'; import { ManifestKindSearchService } from '../ManifestKindSearch'; import { ManifestSelector } from './ManifestSelector'; import { getFormGroupByLabel } from '../../../../core/src/utils/testUtils/rtl'; import { setupUser } from '../../../../core/src/utils/testUtils/userEvent'; describe('', () => { const accounts = [ { name: 'my-account', namespaces: ['default', 'kube-system', 'other-default'], spinnakerKindMap: { configMap: 'unclassified', deployment: 'serverGroupManagers', replicaSet: 'serverGroups', statefulSet: 'serverGroups', }, }, { name: 'my-other-account', namespaces: ['other-default'], spinnakerKindMap: { deployment: 'serverGroupManagers' }, }, ] as any; let accountService: ReturnType; let searchService: ReturnType; beforeEach(() => { searchService = vi .spyOn(ManifestKindSearchService, 'search') .mockResolvedValue([{ name: 'configMap my-config-map' }, { name: 'deployment my-deployment' }] as any); accountService = vi.spyOn(AccountService, 'getAllAccountDetailsForProvider').mockResolvedValue(accounts); }); const renderSelector = async ( selector: Partial, props: Partial> = {}, ) => { const onChange = props.onChange || vi.fn(); const rendered = render( , ); await waitFor(() => expect(accountService).toHaveBeenCalled()); await waitFor(() => expect(within(getFormGroupByLabel('Account', rendered.container)).getByRole('combobox')).toBeInTheDocument(), ); return { ...rendered, onChange }; }; const getField = (label: string, root: HTMLElement) => getFormGroupByLabel(label, root); const getSelectedValue = (label: string, value: string, root: HTMLElement) => within(getField(label, root)).getByText(value, { selector: '.Select-value-label' }); const chooseReactSelectOption = async (label: string, option: string, root: HTMLElement) => { const user = setupUser(); const field = getField(label, root); await user.click(within(field).getByRole('combobox')); await user.click(within(field).getByText(option, { selector: '.Select-option' })); }; describe('initialization', () => { it('renders namespace from input props', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', }); expect(getSelectedValue('Namespace', 'default', container)).toBeInTheDocument(); }); it('renders kind from input props', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', }); expect(getSelectedValue('Kind', 'configMap', container)).toBeInTheDocument(); }); it('renders name from input props', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', }); expect(getSelectedValue('Name', 'my-config-map', container)).toBeInTheDocument(); }); it('renders kinds from input props', async () => { const { container } = await renderSelector({ account: 'my-account', kinds: ['configMap', 'deployment'], location: 'default', mode: SelectorMode.Label, }); const kinds = getField('Kinds', container); expect(within(kinds).getByText('configMap', { selector: '.Select-value-label' })).toBeInTheDocument(); expect(within(kinds).getByText('deployment', { selector: '.Select-value-label' })).toBeInTheDocument(); }); it('renders labels from input props', async () => { const { container } = await renderSelector({ account: 'my-account', kinds: ['configMap', 'deployment'], labelSelectors: { selectors: [{ key: 'label-key', kind: 'EQUALS', values: ['label-value'] }], }, location: 'default', mode: SelectorMode.Label, }); const labels = getField('Labels', container); expect(within(labels).getByDisplayValue('label-key')).toBeInTheDocument(); expect(within(labels).getByDisplayValue('label-value')).toBeInTheDocument(); }); describe('cluster dropdown', () => { const buildPropsWithApplicationData = (data: any[]) => ({ modes: [SelectorMode.Static, SelectorMode.Dynamic], application: { getDataSource: () => ({ data }) } as any, }); const clusterOptions = async (selector: Partial, data: any[]) => { const { container } = await renderSelector(selector, buildPropsWithApplicationData(data)); return within(getField('Cluster', container)); }; it("includes cluster if selected kind matches the cluster's server groups' kind", async () => { const cluster = await clusterOptions( { kind: 'replicaSet', account: 'my-account', location: 'default', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'replicaSet my-replica-set', }, ], ); expect(cluster.getByRole('option', { name: 'replicaSet my-replica-set' })).toBeInTheDocument(); }); it("does not include cluster if selected kind does not match cluster's server groups' kind", async () => { const cluster = await clusterOptions( { kind: 'statefulSet', account: 'my-account', location: 'default', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'replicaSet my-replica-set', }, ], ); expect(cluster.queryByRole('option', { name: 'replicaSet my-replica-set' })).not.toBeInTheDocument(); }); it('handles case in which a cluster has two different kinds of server groups', async () => { const cluster = await clusterOptions( { kind: 'statefulSet', account: 'my-account', location: 'default', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'my-cluster', }, { name: 'statefulSet my-stateful-set-v000', account: 'my-account', region: 'default', cluster: 'my-cluster', }, ], ); expect(cluster.getByRole('option', { name: 'my-cluster' })).toBeInTheDocument(); }); it("does not include cluster if the cluster's server groups are managed", async () => { const cluster = await clusterOptions( { kind: 'replicaSet', account: 'my-account', location: 'default', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'my-cluster', serverGroupManagers: ['deployment my-deployment'], }, ], ); expect(cluster.queryByRole('option', { name: 'my-cluster' })).not.toBeInTheDocument(); }); it('filters clusters by account', async () => { const cluster = await clusterOptions( { kind: 'replicaSet', account: 'my-other-account', location: 'other-default', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'my-cluster', }, ], ); expect(cluster.queryByRole('option', { name: 'my-cluster' })).not.toBeInTheDocument(); }); it('filters clusters by namespace', async () => { const cluster = await clusterOptions( { kind: 'replicaSet', account: 'my-account', location: 'kube-system', mode: SelectorMode.Dynamic }, [ { name: 'replicaSet my-replica-set-v000', account: 'my-account', region: 'default', cluster: 'my-cluster', }, ], ); expect(cluster.queryByRole('option', { name: 'my-cluster' })).not.toBeInTheDocument(); }); }); }); describe('change handlers', () => { it('calls the search service after updating the Kind field', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }); searchService.mockClear(); await chooseReactSelectOption('Kind', 'deployment', container); expect(searchService).toHaveBeenCalledWith('deployment', 'default', 'my-account'); }); it('calls the search service after updating the Namespace field', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }); searchService.mockClear(); await chooseReactSelectOption('Namespace', 'kube-system', container); expect(searchService).toHaveBeenCalledWith('configMap', 'kube-system', 'my-account'); }); it('uses the Kubernetes provider and searches after updating the Account field', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'other-default', mode: SelectorMode.Static, }); await waitFor(() => expect(accountService).toHaveBeenCalledTimes(2)); expect(accountService.mock.calls.every(([provider]) => provider === 'kubernetes')).toBe(true); searchService.mockClear(); fireEvent.change(within(getField('Account', container)).getByRole('combobox'), { target: { value: 'my-other-account' }, }); expect(searchService).toHaveBeenCalledWith('configMap', 'other-default', 'my-other-account'); }); it('waits for complete manifest search criteria', async () => { const { container } = await renderSelector({ account: 'my-account', mode: SelectorMode.Static }); searchService.mockClear(); fireEvent.change(within(getField('Account', container)).getByRole('combobox'), { target: { value: 'my-other-account' }, }); expect(searchService).not.toHaveBeenCalled(); await chooseReactSelectOption('Namespace', 'other-default', container); expect(searchService).not.toHaveBeenCalled(); await chooseReactSelectOption('Kind', 'deployment', container); expect(searchService).toHaveBeenCalledExactlyOnceWith('deployment', 'other-default', 'my-other-account'); }); it('clears namespace when changing account if account does not have selected namespace', async () => { const { container } = await renderSelector({ manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }); expect(getSelectedValue('Namespace', 'default', container)).toBeInTheDocument(); fireEvent.change(within(getField('Account', container)).getByRole('combobox'), { target: { value: 'my-other-account' }, }); expect( within(getField('Namespace', container)).queryByText('default', { selector: '.Select-value-label' }), ).not.toBeInTheDocument(); }); }); describe('mode change', () => { const modes = [SelectorMode.Dynamic, SelectorMode.Static, SelectorMode.Label]; it('renders the static kind in dynamic mode after a static to dynamic transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }, { modes }, ); await user.click(screen.getByRole('radio', { name: 'Choose a target dynamically' })); expect(onChange).toHaveBeenLastCalledWith( expect.objectContaining({ kind: 'configMap', mode: SelectorMode.Dynamic }), ); expect(getSelectedValue('Kind', 'configMap', container)).toBeInTheDocument(); expect(screen.getByText('Cluster', { selector: '.label-text' })).toBeInTheDocument(); expect(screen.queryByText('Name', { selector: '.label-text' })).not.toBeInTheDocument(); }); it('renders label fields after a static to label transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }, { modes }, ); (onChange as ReturnType).mockClear(); await user.click(screen.getByRole('radio', { name: 'Match target(s) by label' })); expect(within(container).getByText('Kinds', { selector: '.label-text' })).toBeInTheDocument(); expect(within(container).getByText('Labels', { selector: '.label-text' })).toBeInTheDocument(); expect(within(container).queryByText('Name', { selector: '.label-text' })).not.toBeInTheDocument(); expect(onChange).toHaveBeenLastCalledWith({ account: 'my-account', cluster: null, criteria: null, kind: null, kinds: [], labelSelectors: { selectors: [] }, location: 'default', manifestName: null, mode: SelectorMode.Label, }); }); it('renders a static name after a dynamic to static transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { account: 'my-account', location: 'default', kind: 'configMap', mode: SelectorMode.Dynamic }, { modes }, ); (onChange as ReturnType).mockClear(); await user.click(screen.getByRole('radio', { name: 'Choose a static target' })); expect(within(container).getByText('Name', { selector: '.label-text' })).toBeInTheDocument(); expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ manifestName: 'configMap' })); }); it('renders label fields after a dynamic to label transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { account: 'my-account', location: 'default', kind: 'configMap', mode: SelectorMode.Dynamic }, { modes }, ); (onChange as ReturnType).mockClear(); await user.click(screen.getByRole('radio', { name: 'Match target(s) by label' })); expect(within(container).getByText('Kinds', { selector: '.label-text' })).toBeInTheDocument(); expect(within(container).getByText('Labels', { selector: '.label-text' })).toBeInTheDocument(); expect(onChange).toHaveBeenLastCalledWith({ account: 'my-account', cluster: null, criteria: null, kind: null, kinds: [], labelSelectors: { selectors: [] }, location: 'default', manifestName: null, mode: SelectorMode.Label, }); }); it('renders static fields after a label to static transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { account: 'my-account', location: 'default', kinds: ['configMap'], labelSelectors: { selectors: [] }, mode: SelectorMode.Label, }, { modes }, ); (onChange as ReturnType).mockClear(); await user.click(screen.getByRole('radio', { name: 'Choose a static target' })); expect(within(container).getByText('Name', { selector: '.label-text' })).toBeInTheDocument(); expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ kind: null, kinds: null })); }); it('renders dynamic fields after a label to dynamic transition', async () => { const user = setupUser(); const { container, onChange } = await renderSelector( { account: 'my-account', location: 'default', kinds: ['configMap'], labelSelectors: { selectors: [] }, mode: SelectorMode.Label, }, { modes }, ); (onChange as ReturnType).mockClear(); await user.click(screen.getByRole('radio', { name: 'Choose a target dynamically' })); expect(within(container).getByText('Cluster', { selector: '.label-text' })).toBeInTheDocument(); expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ kind: null, kinds: null })); }); it('does not mutate prior callback payloads during later mode transitions', async () => { const user = setupUser(); const onChange = vi.fn(); await renderSelector( { manifestName: 'configMap my-config-map', account: 'my-account', location: 'default', mode: SelectorMode.Static, }, { modes, onChange }, ); onChange.mockClear(); await user.click(screen.getByRole('radio', { name: 'Choose a target dynamically' })); const dynamicPayload = onChange.mock.calls.at(-1)![0]; expect(dynamicPayload.mode).toBe(SelectorMode.Dynamic); await user.click(screen.getByRole('radio', { name: 'Match target(s) by label' })); expect(dynamicPayload.mode).toBe(SelectorMode.Dynamic); expect(dynamicPayload.kinds).toBeNull(); expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ mode: SelectorMode.Label, kinds: [] })); }); }); });