import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import type { Application } from '@spinnaker/core'; import { AmazonImageSelectInput } from './AmazonImageSelectInput'; import { setupUser } from '../../../../core/src/utils/testUtils/userEvent'; import type { IAmazonImage } from '../../image'; import { AwsImageReader } from '../../image'; function makeImage(imageName: string, amiId: string, region = 'us-east-1'): IAmazonImage { return { imageName, amis: { [region]: [amiId] }, attributes: { virtualizationType: 'hvm', architecture: 'x86_64', creationDate: '2024-01-01T00:00:00.000Z' }, } as IAmazonImage; } describe('AmazonImageSelectInput', () => { const application = ({ name: 'app' } as unknown) as Application; const image1 = makeImage('app-package-1.0', 'ami-111'); const image2 = makeImage('app-package-2.0', 'ami-222'); beforeEach(() => { vi.spyOn(AwsImageReader.prototype, 'findImages').mockImplementation(({ q }: { q: string }) => Promise.resolve(q === 'app-package-2' ? [image2] : [image1, image2]), ); vi.spyOn(AwsImageReader.prototype, 'getImage').mockReturnValue(Promise.resolve(null)); }); function renderInput(onChange: (image: IAmazonImage) => void, value: IAmazonImage = null) { return render( , ); } async function waitForPackageImages(): Promise { expect(await screen.findByText('Pick an image')).toBeInTheDocument(); } function openMenu(container: HTMLElement): void { fireEvent.mouseDown(container.querySelector('.Select-control')); } it('renders a single options menu wrapper, not a nested duplicate', async () => { const { container } = renderInput(vi.fn()); await waitForPackageImages(); openMenu(container); // Regression test: buildImageMenu used to re-wrap its options in a second // ".Select-menu-outer > .Select-menu" pair on top of react-select's own wrapper. The inner // duplicate kept its default `position: absolute` styling instead of the `position: static` // override TetheredSelect applies to the outermost wrapper, which broke the sizing/hit-testing // of the real, Tether-positioned menu: the list was visible but clicks landed on nothing. expect(await screen.findAllByRole('option')).toHaveLength(2); expect(document.querySelectorAll('.Select-menu-outer')).toHaveLength(1); }); it('selects the clicked image from the package images dropdown', async () => { const onChange = vi.fn(); const { container } = renderInput(onChange); await waitForPackageImages(); openMenu(container); const options = await screen.findAllByRole('option'); expect(options).toHaveLength(2); fireEvent.mouseDown(screen.getByRole('option', { name: new RegExp(image1.imageName) })); expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ imageName: image1.imageName })); }); it('provides a way back from "Search All Images" to the package images dropdown', async () => { const user = setupUser(); renderInput(vi.fn()); await waitForPackageImages(); await user.click(screen.getByRole('button', { name: 'Search All Images' })); expect(screen.getByText('Search for an image...')).toBeInTheDocument(); // Regression test: there used to be no control to switch back out of search-all-images mode. await user.click(screen.getByRole('button', { name: 'Back to Package Images' })); expect(screen.getByText('Pick an image')).toBeInTheDocument(); }); it('selects the clicked image while searching all images', async () => { const user = setupUser(); const onChange = vi.fn(); const { container } = renderInput(onChange); await waitForPackageImages(); await user.click(screen.getByRole('button', { name: 'Search All Images' })); await user.type(screen.getByRole('combobox'), 'app-package-2'); await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(1), { timeout: 2000 }); expect(container.querySelector('.Select')).toHaveClass('is-open'); fireEvent.mouseDown(screen.getByRole('option', { name: new RegExp(image2.imageName) })); expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ imageName: image2.imageName })); }); });