diff --git a/website/package-lock.json b/website/package-lock.json index 3636792f64..1a283beeca 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -160,8 +160,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@astrojs/internal-helpers": { "version": "0.7.5", @@ -363,7 +362,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -505,7 +503,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -933,7 +930,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -977,7 +973,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "devOptional": true, - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -3868,7 +3863,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3977,7 +3971,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.42.0.tgz", "integrity": "sha512-j0tiofkzE3CSrYKmVRaKuwGgvCE+P2OOEDlhmfjeZf5ufcuFHwYwwgw3j08n4WYPVZ+OpsHblcFYezhKA3jDwg==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "4.41.0", "use-sync-external-store": "^1.2.0" @@ -4033,7 +4026,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4356,7 +4348,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4400,7 +4391,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4412,7 +4402,6 @@ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4542,7 +4531,6 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -5005,7 +4993,6 @@ "resolved": "https://registry.npmjs.org/@zodios/core/-/core-10.9.6.tgz", "integrity": "sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==", "license": "MIT", - "peer": true, "peerDependencies": { "axios": "^0.x || ^1.0.0", "zod": "^3.x" @@ -5027,7 +5014,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5397,7 +5383,6 @@ "resolved": "https://registry.npmjs.org/astro/-/astro-5.16.5.tgz", "integrity": "sha512-QeuM4xzTR0QuXFDNlGVW0BW7rcquKFIkylaPeM4ufii0/RRiPTYtwxDYVZ3KfiMRuuc+nbLD0214kMKTvz/yvQ==", "license": "MIT", - "peer": true, "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", @@ -5856,7 +5841,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -6074,7 +6058,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -6281,7 +6264,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7579,7 +7561,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -10059,7 +10040,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12705,7 +12685,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12860,7 +12839,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12877,7 +12855,6 @@ "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", @@ -13058,7 +13035,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13091,7 +13067,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13708,7 +13683,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13931,7 +13905,6 @@ "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -14864,7 +14837,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15323,7 +15295,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15800,7 +15771,6 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16625,7 +16595,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16773,7 +16742,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/website/src/components/SearchPage/SeqPreviewModal.spec.tsx b/website/src/components/SearchPage/SeqPreviewModal.spec.tsx new file mode 100644 index 0000000000..a6ce925776 --- /dev/null +++ b/website/src/components/SearchPage/SeqPreviewModal.spec.tsx @@ -0,0 +1,463 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { SeqPreviewModal } from './SeqPreviewModal'; +import { testOrganism } from '../../../vitest.setup.ts'; +import type { Group } from '../../types/backend.ts'; +import type { SequenceFlaggingConfig } from '../../types/config.ts'; +import type { DetailsJson } from '../../types/detailsJson.ts'; +import { SINGLE_REFERENCE, type ReferenceGenomesLightweightSchema } from '../../types/referencesGenomes.ts'; + +// Mock fetch for loading sequence details +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +// Mock the logger +vi.mock('../../clientLogger.ts', () => ({ + getClientLogger: () => ({ + error: vi.fn(), + }), +})); + +const mockAccession = 'LOC_123456'; +const mockDetailsJson: DetailsJson = { + accessionVersion: mockAccession, + displayName: 'Test Sequence', + organism: testOrganism, + sequenceEntryHistory: [ + { + accessionVersion: mockAccession, + version: 1, + isRevocation: false, + }, + ], + metadata: [ + { + key: 'country', + value: 'Switzerland', + }, + { + key: 'date', + value: '2024-01-01', + }, + ], + isRevocation: false, +}; + +const defaultReferenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema = { + [SINGLE_REFERENCE]: { + nucleotideSegmentNames: ['main'], + geneNames: ['gene1', 'gene2'], + insdcAccessionFull: [ + { + name: 'main', + insdcAccessionFull: undefined, + }, + ], + }, +}; + +const mockGroups: Group[] = []; + +interface RenderSeqPreviewModalProps { + seqId?: string; + accessToken?: string; + isOpen?: boolean; + onClose?: () => void; + isHalfScreen?: boolean; + setIsHalfScreen?: (value: boolean) => void; + setPreviewedSeqId?: (seqId: string | null) => void; + sequenceFlaggingConfig?: SequenceFlaggingConfig; +} + +function renderSeqPreviewModal({ + seqId = mockAccession, + accessToken, + isOpen = true, + onClose = vi.fn(), + isHalfScreen = false, + setIsHalfScreen = vi.fn(), + setPreviewedSeqId = vi.fn(), + sequenceFlaggingConfig, +}: RenderSeqPreviewModalProps = {}) { + return render( + , + ); +} + +describe('SeqPreviewModal', () => { + beforeEach(() => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockDetailsJson), + }); + }); + + it('should open and display the modal when isOpen is true', async () => { + renderSeqPreviewModal({ isOpen: true }); + + await waitFor(() => { + expect(screen.getByTestId('sequence-preview-modal')).toBeInTheDocument(); + }); + }); + + it('should not display the modal when isOpen is false', () => { + renderSeqPreviewModal({ isOpen: false }); + + expect(screen.queryByTestId('sequence-preview-modal')).not.toBeInTheDocument(); + }); + + it('should display the correct accession in the modal header', async () => { + renderSeqPreviewModal({ seqId: mockAccession }); + + await waitFor(() => { + expect(screen.getByText(mockAccession)).toBeInTheDocument(); + }); + }); + + it('should fetch and display sequence details', async () => { + renderSeqPreviewModal(); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalled(); + // Check that fetch was called with the correct URL + const fetchCall = mockFetch.mock.calls[0][0]; + const url = typeof fetchCall === 'string' ? fetchCall : fetchCall.url; + expect(url).toContain(`/seq/${mockAccession}/details.json`); + }); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + }); + + it('should call onClose when the close button is clicked', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByTestId('close-preview-button')); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('should display loading state initially', () => { + renderSeqPreviewModal(); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('should display error state when fetch fails', async () => { + mockFetch.mockRejectedValueOnce(new Error('Failed to fetch')); + + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.getByText('Failed to load sequence data')).toBeInTheDocument(); + }); + }); + + it('should toggle between half screen and full screen modes', async () => { + const setIsHalfScreen = vi.fn(); + renderSeqPreviewModal({ isHalfScreen: false, setIsHalfScreen }); + + await waitFor(() => { + expect(screen.getByTestId('toggle-half-screen-button')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByTestId('toggle-half-screen-button')); + + expect(setIsHalfScreen).toHaveBeenCalledWith(true); + }); + + it('should render in half-screen mode when isHalfScreen is true', async () => { + renderSeqPreviewModal({ isHalfScreen: true }); + + await waitFor(() => { + expect(screen.getByTestId('half-screen-preview')).toBeInTheDocument(); + }); + expect(screen.queryByTestId('sequence-preview-modal')).not.toBeInTheDocument(); + }); + + it('should render in full-screen mode when isHalfScreen is false', async () => { + renderSeqPreviewModal({ isHalfScreen: false }); + + await waitFor(() => { + expect(screen.getByTestId('sequence-preview-modal')).toBeInTheDocument(); + }); + expect(screen.queryByTestId('half-screen-preview')).not.toBeInTheDocument(); + }); + + describe('URL state preservation', () => { + beforeEach(() => { + // Set up URL with various query parameters to test preservation + const searchParams = new URLSearchParams({ + page: '3', + orderBy: 'date', + order: 'descending', + country: 'Switzerland', + organism: testOrganism, + }); + window.history.replaceState({}, '', `?${searchParams.toString()}`); + }); + + it('should not modify URL parameters when modal opens', async () => { + const initialUrl = window.location.search; + renderSeqPreviewModal({ isOpen: true }); + + await waitFor(() => { + expect(screen.getByTestId('sequence-preview-modal')).toBeInTheDocument(); + }); + + // URL should remain unchanged + expect(window.location.search).toBe(initialUrl); + }); + + it('should preserve pagination state when closing modal', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + const initialUrl = window.location.search; + await userEvent.click(screen.getByTestId('close-preview-button')); + + // URL should remain unchanged after closing + expect(window.location.search).toBe(initialUrl); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('should preserve search filters when closing modal', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get('country')).toBe('Switzerland'); + + await userEvent.click(screen.getByTestId('close-preview-button')); + + // Search filter should still be present + const newSearchParams = new URLSearchParams(window.location.search); + expect(newSearchParams.get('country')).toBe('Switzerland'); + }); + + it('should preserve ordering when closing modal', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get('orderBy')).toBe('date'); + expect(searchParams.get('order')).toBe('descending'); + + await userEvent.click(screen.getByTestId('close-preview-button')); + + // Ordering should still be present + const newSearchParams = new URLSearchParams(window.location.search); + expect(newSearchParams.get('orderBy')).toBe('date'); + expect(newSearchParams.get('order')).toBe('descending'); + }); + + it('should preserve organism parameter when closing modal', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get('organism')).toBe(testOrganism); + + await userEvent.click(screen.getByTestId('close-preview-button')); + + // Organism should still be present + const newSearchParams = new URLSearchParams(window.location.search); + expect(newSearchParams.get('organism')).toBe(testOrganism); + }); + + it('regression test for issue #5783: pagination should not be lost when opening modal', async () => { + // This test specifically addresses issue #5783 + // Setup: Start on page 3 with other parameters + const searchParams = new URLSearchParams({ + page: '3', + orderBy: 'date', + order: 'descending', + country: 'Switzerland', + }); + window.history.replaceState({}, '', `?${searchParams.toString()}`); + + const initialUrl = window.location.search; + const initialPage = new URLSearchParams(initialUrl).get('page'); + expect(initialPage).toBe('3'); + + // Open modal + renderSeqPreviewModal({ isOpen: true }); + + await waitFor(() => { + expect(screen.getByTestId('sequence-preview-modal')).toBeInTheDocument(); + }); + + // Assert: Page parameter should still be '3' + const afterOpenUrl = new URLSearchParams(window.location.search); + expect(afterOpenUrl.get('page')).toBe('3'); + expect(afterOpenUrl.get('orderBy')).toBe('date'); + expect(afterOpenUrl.get('order')).toBe('descending'); + expect(afterOpenUrl.get('country')).toBe('Switzerland'); + }); + }); + + describe('Modal closing methods', () => { + it('should call onClose when clicking outside the modal (backdrop click)', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose, isHalfScreen: false }); + + await waitFor(() => { + expect(screen.getByTestId('sequence-preview-modal')).toBeInTheDocument(); + }); + + // Find the backdrop (Dialog component handles this automatically in headlessui) + // The backdrop is the fixed inset div with bg-black opacity-30 + const backdrop = document.querySelector('.fixed.inset-0.bg-black.opacity-30'); + expect(backdrop).toBeInTheDocument(); + + // Click on backdrop + if (backdrop) { + await userEvent.click(backdrop); + } + + // onClose should be called by the Dialog component + // Note: HeadlessUI's Dialog handles backdrop clicks automatically + // The actual behavior depends on the Dialog's onClose prop + expect(onClose).toHaveBeenCalled(); + }); + + it('should call onClose when clicking the X button', async () => { + const onClose = vi.fn(); + renderSeqPreviewModal({ onClose }); + + await waitFor(() => { + expect(screen.getByTestId('close-preview-button')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByTestId('close-preview-button')); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + describe('Sequence Entry History', () => { + it('should display sequence entry history menu when multiple versions exist', async () => { + const detailsWithHistory: DetailsJson = { + ...mockDetailsJson, + sequenceEntryHistory: [ + { + accessionVersion: `${mockAccession}.1`, + version: 1, + isRevocation: false, + }, + { + accessionVersion: `${mockAccession}.2`, + version: 2, + isRevocation: false, + }, + ], + }; + + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(detailsWithHistory), + }); + + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + // The sequence entry history menu should be rendered + // when there are multiple versions + await waitFor(() => { + // Check that the component has loaded and history exists + expect(mockFetch).toHaveBeenCalled(); + }); + }); + + it('should not display sequence entry history menu when only one version exists', async () => { + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + // With only one version in history, the menu should not be displayed + // The component structure doesn't render the menu component when length <= 1 + await waitFor(() => { + expect(mockFetch).toHaveBeenCalled(); + }); + }); + }); + + describe('Download functionality', () => { + it('should display download button with dropdown options', async () => { + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + }); + + // The download button should be present + const downloadButtons = document.querySelectorAll('.dropdown'); + expect(downloadButtons.length).toBeGreaterThan(0); + }); + }); + + describe('Error handling', () => { + it('should handle JSON parsing errors gracefully', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.reject(new Error('JSON parse error')), + }); + + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.getByText('Failed to load sequence data')).toBeInTheDocument(); + }); + }); + + it('should handle network errors gracefully', async () => { + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + renderSeqPreviewModal(); + + await waitFor(() => { + expect(screen.getByText('Failed to load sequence data')).toBeInTheDocument(); + }); + }); + }); +});