From f80433a6f3ab0e46182bad0d5cbbe58cea2a4385 Mon Sep 17 00:00:00 2001 From: James Cocker Date: Mon, 8 Jun 2026 14:47:04 +0100 Subject: [PATCH 1/5] Correct logic Signed-off-by: James Cocker --- .../test-run-details/ArtifactsTab.tsx | 108 +----------------- .../test-run-details/TestRunDetails.tsx | 86 ++++++++++++-- 2 files changed, 84 insertions(+), 110 deletions(-) diff --git a/galasa-ui/src/components/test-runs/test-run-details/ArtifactsTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/ArtifactsTab.tsx index c05fed07..aca09704 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/ArtifactsTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/ArtifactsTab.tsx @@ -7,7 +7,7 @@ import { ArtifactIndexEntry } from '@/generated/galasaapi'; import { TreeView, TreeNode, InlineLoading, InlineNotification } from '@carbon/react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import styles from '@/styles/test-runs/test-run-details/Artifacts.module.css'; import { CarbonIconType, @@ -20,7 +20,7 @@ import { } from '@carbon/icons-react'; import { downloadArtifactFromServer } from '@/actions/runsAction'; import { Tile } from '@carbon/react'; -import { cleanArtifactPath, handleDownload } from '@/utils/artifacts'; +import { handleDownload } from '@/utils/artifacts'; import { useTranslations } from 'next-intl'; import { Button } from '@carbon/react'; import { @@ -29,10 +29,10 @@ import { TreeNodeData, DownloadResult, } from '@/utils/functions/artifacts'; -import { checkForZosTerminalFolderStructure } from '@/utils/3270/checkFor3270FolderStructure'; export function ArtifactsTab({ artifacts, + artifactsTreeData, runId, runName, isLoadingArtifacts = false, @@ -41,6 +41,7 @@ export function ArtifactsTab({ setZos3270TerminalData, }: { artifacts: ArtifactIndexEntry[]; + artifactsTreeData: FolderNode; runId: string; runName: string; isLoadingArtifacts?: boolean; @@ -49,11 +50,6 @@ export function ArtifactsTab({ setZos3270TerminalData: (zos3270TerminalData: TreeNodeData[]) => void; }) { const translations = useTranslations('Artifacts'); - const [treeData, setTreeData] = useState({ - name: '', - isFile: false, - children: {}, - }); const [artifactDetails, setArtifactDetails] = useState({ artifactFile: '', @@ -164,98 +160,6 @@ export function ArtifactsTab({ return result; } - useEffect(() => { - // Only build tree if we have artifacts - if (artifacts.length === 0) { - setTreeData({ name: '', isFile: false, children: {} }); - return; - } - - // Build the root node, which holds top-level folders and files - const root: FolderNode = { name: '', isFile: false, children: {} }; - - artifacts.forEach((artifact) => { - // 1) Get the raw path string, default to empty if undefined - const rawPath = artifact.path ?? ''; - - // 2) Remove a leading "/" or "./" if present - const cleanedPath = cleanArtifactPath(rawPath); - - // 3) Split into segments and drop any empty strings - let segments = cleanedPath.split('/').filter((seg) => seg !== ''); - - // 4) If the first segment is "artifact" or "artifacts", drop it so that - // "artifact/framework" becomes ["framework", ...] - const segmentValue = segments[0]?.toLocaleLowerCase(); - - if (segmentValue === 'artifact' || segmentValue === 'artifacts') { - segments = segments.slice(1); - } - - if (segments.length > 0) { - const currentNode: FolderNode = root; - createFolderSegments(segments, currentNode, artifact); - } - }); - - setTreeData(root); - - // This checks for the terminal structure, and sets the zosTerminalData hook in TestRunDetails for later use in get3270Screenshots.ts. - checkForZosTerminalFolderStructure( - root, - setZos3270TerminalFolderExists, - setZos3270TerminalData - ); - - // If you're adding extra state to this hook, make sure to review the dependency array due to the warning suppression: - // eslint-disable-next-line - }, [artifacts, checkForZosTerminalFolderStructure]); - - const createFolderSegments = ( - segments: string[], - currentNode: FolderNode, - artifact: ArtifactIndexEntry - ) => { - return segments.forEach((segment, idx) => { - const isLast = idx === segments.length - 1; - - if (isLast) { - // It’s a file: insert a FileNode under currentNode.children - currentNode.children[segment] = { - name: segment, - runId: artifact.runId ?? '', - url: artifact.path ?? '', - isFile: true, - children: {}, - }; - } else { - // It’s a folder: create or reuse a FolderNode - const existing = currentNode.children[segment]; - - if (!existing) { - // Create new folder if it doesn’t exist - currentNode.children[segment] = { - name: segment, - isFile: false, - children: {}, - }; - currentNode = currentNode.children[segment] as FolderNode; - } else if (existing.isFile) { - // Conflict: a file was created here before. Replace it with a folder. - currentNode.children[segment] = { - name: segment, - isFile: false, - children: {}, - }; - currentNode = currentNode.children[segment] as FolderNode; - } else { - // Descend into existing folder - currentNode = existing as FolderNode; - } - } - }); - }; - const renderFileIcon = (path: string) => { const pathSplit = path.split('.'); const extension = pathSplit[pathSplit.length - 1]; // get the last split e.g some.file.ts -> we need the extension (ts) @@ -343,8 +247,8 @@ export function ArtifactsTab({ {!isLoadingArtifacts && !artifactsError && artifacts.length > 0 && (
- {}}> - {Object.values(treeData.children).map((child) => renderNode(child, child.name))} + { }}> + {Object.values(artifactsTreeData.children).map((child) => renderNode(child, child.name))}
diff --git a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx index c6a3ed16..b2f5b256 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx @@ -43,9 +43,11 @@ import { TEST_RUN_TAB_NAMES, } from '@/utils/constants/common'; import { NotificationType } from '@/utils/types/common'; -import { TreeNodeData } from '@/utils/functions/artifacts'; +import { TreeNodeData, FolderNode } from '@/utils/functions/artifacts'; import TestRunsSearch from '../TestRunsSearch'; import { getExistingTagObjects } from '@/actions/runsAction'; +import { checkForZosTerminalFolderStructure } from '@/utils/3270/checkFor3270FolderStructure'; +import { cleanArtifactPath } from '@/utils/artifacts'; interface TestRunDetailsProps { runId: string; @@ -62,6 +64,11 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { const [run, setRun] = useState(); const [methods, setMethods] = useState([]); const [artifacts, setArtifacts] = useState([]); + const [artifactsTreeData, setArtifactsTreeData] = useState({ + name: '', + isFile: false, + children: {}, + }); const [artifactsLoaded, setArtifactsLoaded] = useState(false); const [artifactsLoading, setArtifactsLoading] = useState(false); const [artifactsError, setArtifactsError] = useState(null); @@ -150,9 +157,9 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { duration: runDetails.testStructure?.startTime && runDetails.testStructure?.endTime ? getIsoTimeDifference( - runDetails.testStructure?.startTime, - runDetails.testStructure?.endTime - ) + runDetails.testStructure?.startTime, + runDetails.testStructure?.endTime + ) : '-', tags: runDetails.testStructure?.tags || [], }; @@ -176,6 +183,69 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { const runArtifacts = await response.json(); setArtifacts(runArtifacts); setArtifactsLoaded(true); + + // Build tree structure to check for 3270 terminal folder + if (runArtifacts.length > 0) { + const root: FolderNode = { name: '', isFile: false, children: {} }; + + runArtifacts.forEach((artifact: ArtifactIndexEntry) => { + const rawPath = artifact.path ?? ''; + const cleanedPath = cleanArtifactPath(rawPath); + let segments = cleanedPath.split('/').filter((seg) => seg !== ''); + + const segmentValue = segments[0]?.toLocaleLowerCase(); + if (segmentValue === 'artifact' || segmentValue === 'artifacts') { + segments = segments.slice(1); + } + + if (segments.length > 0) { + let currentNode: FolderNode = root; + segments.forEach((segment, idx) => { + const isLast = idx === segments.length - 1; + + if (isLast) { + currentNode.children[segment] = { + name: segment, + runId: artifact.runId ?? '', + url: artifact.path ?? '', + isFile: true, + children: {}, + }; + } else { + const existing = currentNode.children[segment]; + + if (!existing) { + currentNode.children[segment] = { + name: segment, + isFile: false, + children: {}, + }; + currentNode = currentNode.children[segment] as FolderNode; + } else if (existing.isFile) { + currentNode.children[segment] = { + name: segment, + isFile: false, + children: {}, + }; + currentNode = currentNode.children[segment] as FolderNode; + } else { + currentNode = existing as FolderNode; + } + } + }); + } + }); + + // Store the built tree for use in ArtifactsTab + setArtifactsTreeData(root); + + // Check for 3270 terminal folder structure + checkForZosTerminalFolderStructure( + root, + setZos3270TerminalFolderExists, + setZos3270TerminalData + ); + } } catch (err: unknown) { console.error('Error loading artifacts:', err); setArtifactsError(err instanceof Error ? err.message : 'Failed to load artifacts'); @@ -229,13 +299,12 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { loadRunDetails(); }, [run, runDetailsPromise, extractRunDetails]); - // If artifacts tab is selected in URL on initial load, fetch artifacts + // Load artifacts immediately when run details are loaded to check for 3270 terminal folder useEffect(() => { - const artifactsTabIndex = TEST_RUN_PAGE_TABS.indexOf(TEST_RUN_TAB_NAMES.ARTIFACTS); - if (selectedTabIndex === artifactsTabIndex && !artifactsLoaded && !artifactsLoading && run) { + if (run && !artifactsLoaded && !artifactsLoading && !artifactsError) { loadArtifacts(); } - }, [selectedTabIndex, artifactsLoaded, artifactsLoading, run, loadArtifacts]); + }, [run, artifactsLoaded, artifactsLoading, artifactsError, loadArtifacts]); // If log tab is selected in URL on initial load, fetch logs useEffect(() => { @@ -492,6 +561,7 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { Date: Mon, 8 Jun 2026 15:02:13 +0100 Subject: [PATCH 2/5] Move functionality to create file tree into its own file Signed-off-by: James Cocker --- .../test-run-details/TestRunDetails.tsx | 52 +------------ .../src/utils/3270/buildArtifactsTree.ts | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+), 50 deletions(-) create mode 100644 galasa-ui/src/utils/3270/buildArtifactsTree.ts diff --git a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx index b2f5b256..59239b7b 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx @@ -47,7 +47,7 @@ import { TreeNodeData, FolderNode } from '@/utils/functions/artifacts'; import TestRunsSearch from '../TestRunsSearch'; import { getExistingTagObjects } from '@/actions/runsAction'; import { checkForZosTerminalFolderStructure } from '@/utils/3270/checkFor3270FolderStructure'; -import { cleanArtifactPath } from '@/utils/artifacts'; +import { buildArtifactsTree } from '@/utils/3270/buildArtifactsTree'; interface TestRunDetailsProps { runId: string; @@ -186,55 +186,7 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { // Build tree structure to check for 3270 terminal folder if (runArtifacts.length > 0) { - const root: FolderNode = { name: '', isFile: false, children: {} }; - - runArtifacts.forEach((artifact: ArtifactIndexEntry) => { - const rawPath = artifact.path ?? ''; - const cleanedPath = cleanArtifactPath(rawPath); - let segments = cleanedPath.split('/').filter((seg) => seg !== ''); - - const segmentValue = segments[0]?.toLocaleLowerCase(); - if (segmentValue === 'artifact' || segmentValue === 'artifacts') { - segments = segments.slice(1); - } - - if (segments.length > 0) { - let currentNode: FolderNode = root; - segments.forEach((segment, idx) => { - const isLast = idx === segments.length - 1; - - if (isLast) { - currentNode.children[segment] = { - name: segment, - runId: artifact.runId ?? '', - url: artifact.path ?? '', - isFile: true, - children: {}, - }; - } else { - const existing = currentNode.children[segment]; - - if (!existing) { - currentNode.children[segment] = { - name: segment, - isFile: false, - children: {}, - }; - currentNode = currentNode.children[segment] as FolderNode; - } else if (existing.isFile) { - currentNode.children[segment] = { - name: segment, - isFile: false, - children: {}, - }; - currentNode = currentNode.children[segment] as FolderNode; - } else { - currentNode = existing as FolderNode; - } - } - }); - } - }); + const root = buildArtifactsTree(runArtifacts); // Store the built tree for use in ArtifactsTab setArtifactsTreeData(root); diff --git a/galasa-ui/src/utils/3270/buildArtifactsTree.ts b/galasa-ui/src/utils/3270/buildArtifactsTree.ts new file mode 100644 index 00000000..318a3983 --- /dev/null +++ b/galasa-ui/src/utils/3270/buildArtifactsTree.ts @@ -0,0 +1,78 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { ArtifactIndexEntry } from '@/generated/galasaapi'; +import { FolderNode } from '@/utils/functions/artifacts'; +import { cleanArtifactPath } from '@/utils/artifacts'; + +/** + * Builds a tree structure from a list of artifacts. + * This function processes artifact paths and creates a hierarchical folder/file structure. + * + * @param artifacts - Array of artifact entries to process + * @returns A FolderNode representing the root of the tree structure + */ +export function buildArtifactsTree(artifacts: ArtifactIndexEntry[]): FolderNode { + const root: FolderNode = { name: '', isFile: false, children: {} }; + + artifacts.forEach((artifact) => { + const rawPath = artifact.path ?? ''; + const cleanedPath = cleanArtifactPath(rawPath); + let segments = cleanedPath.split('/').filter((seg) => seg !== ''); + + // Remove "artifact" or "artifacts" prefix from paths + const segmentValue = segments[0]?.toLocaleLowerCase(); + if (segmentValue === 'artifact' || segmentValue === 'artifacts') { + segments = segments.slice(1); + } + + if (segments.length > 0) { + let currentNode: FolderNode = root; + segments.forEach((segment, idx) => { + const isLast = idx === segments.length - 1; + + if (isLast) { + // It's a file: insert a FileNode under currentNode.children + currentNode.children[segment] = { + name: segment, + runId: artifact.runId ?? '', + url: artifact.path ?? '', + isFile: true, + children: {}, + }; + } else { + // It's a folder: create or reuse a FolderNode + const existing = currentNode.children[segment]; + + if (!existing) { + // Create new folder if it doesn't exist + currentNode.children[segment] = { + name: segment, + isFile: false, + children: {}, + }; + currentNode = currentNode.children[segment] as FolderNode; + } else if (existing.isFile) { + // Conflict: a file was created here before. Replace it with a folder. + currentNode.children[segment] = { + name: segment, + isFile: false, + children: {}, + }; + currentNode = currentNode.children[segment] as FolderNode; + } else { + // Descend into existing folder + currentNode = existing as FolderNode; + } + } + }); + } + }); + + return root; +} + +// Made with Bob From 23b617b8a0bdc3e5e068658337974fb3328f093b Mon Sep 17 00:00:00 2001 From: James Cocker Date: Mon, 8 Jun 2026 15:02:50 +0100 Subject: [PATCH 3/5] Add/ fix unit tests Signed-off-by: James Cocker --- .../test-run-details/ArtifactsTab.test.tsx | 19 ++ .../test-run-details/TestRunDetails.test.tsx | 19 +- .../utils/3270/buildArtifactsTree.test.ts | 170 ++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 galasa-ui/src/tests/utils/3270/buildArtifactsTree.test.ts diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/ArtifactsTab.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/ArtifactsTab.test.tsx index 6432934f..1d83d714 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/ArtifactsTab.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/ArtifactsTab.test.tsx @@ -9,6 +9,7 @@ import { ArtifactsTab } from '@/components/test-runs/test-run-details/ArtifactsT import { downloadArtifactFromServer } from '@/actions/runsAction'; import { handleDownload } from '@/utils/artifacts'; import { FeatureFlagProvider } from '@/contexts/FeatureFlagContext'; +import { buildArtifactsTree } from '@/utils/3270/buildArtifactsTree'; // Mock dependencies jest.mock('@/actions/runsAction'); @@ -179,6 +180,7 @@ describe('ArtifactsTab', () => { { { { { { { { { { { { { { { { { { { const runDetailsDeferred = setup<{ testStructure: Record }>(); const runLogDeferred = setup(); - // Mock the successful fetch response + // Mock the artifacts fetch response (called automatically on mount) + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue([]), + }); + + // Mock the successful zip fetch response const mockBlob = new Blob(['mock-zip-content'], { type: 'application/zip' }); (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, @@ -650,9 +656,14 @@ describe('TestRunDetails', () => { { timeout: 500 } ); - // Verify the correct API endpoint was called - expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith( + // Verify the correct API endpoints were called (artifacts + zip) + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(global.fetch).toHaveBeenNthCalledWith( + 1, + `/internal-api/test-runs/${runId}/artifacts` + ); + expect(global.fetch).toHaveBeenNthCalledWith( + 2, `http://localhost/internal-api/test-runs/${runId}/zip?runName=TestRun` ); diff --git a/galasa-ui/src/tests/utils/3270/buildArtifactsTree.test.ts b/galasa-ui/src/tests/utils/3270/buildArtifactsTree.test.ts new file mode 100644 index 00000000..3e442033 --- /dev/null +++ b/galasa-ui/src/tests/utils/3270/buildArtifactsTree.test.ts @@ -0,0 +1,170 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { buildArtifactsTree } from '@/utils/3270/buildArtifactsTree'; +import { ArtifactIndexEntry } from '@/generated/galasaapi'; +import { FolderNode, FileNode } from '@/utils/functions/artifacts'; + +describe('buildArtifactsTree', () => { + it('should return an empty root node for empty artifacts array', () => { + const result = buildArtifactsTree([]); + + expect(result).toEqual({ + name: '', + isFile: false, + children: {}, + }); + }); + + it('should build a simple tree structure with one file', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/framework/test.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.isFile).toBe(false); + expect(framework.children['test.txt']).toBeDefined(); + expect(framework.children['test.txt'].isFile).toBe(true); + }); + + it('should build a nested tree structure', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/framework/logs/debug.log', + runId: 'run-123', + }, + { + path: '/framework/images/screenshot.png', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + const logs = framework.children.logs as FolderNode; + expect(logs).toBeDefined(); + expect(logs.children['debug.log']).toBeDefined(); + const images = framework.children.images as FolderNode; + expect(images).toBeDefined(); + expect(images.children['screenshot.png']).toBeDefined(); + }); + + it('should remove "artifact" prefix from paths', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/artifact/framework/test.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + expect(result.children.artifact).toBeUndefined(); + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.children['test.txt']).toBeDefined(); + }); + + it('should remove "artifacts" prefix from paths', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/artifacts/framework/test.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + expect(result.children.artifacts).toBeUndefined(); + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.children['test.txt']).toBeDefined(); + }); + + it('should handle paths with leading slashes and dots', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: './framework/test.txt', + runId: 'run-123', + }, + { + path: '/framework/test2.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.children['test.txt']).toBeDefined(); + expect(framework.children['test2.txt']).toBeDefined(); + }); + + it('should handle undefined paths gracefully', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: undefined, + runId: 'run-123', + }, + { + path: '/framework/test.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.children['test.txt']).toBeDefined(); + }); + + it('should handle undefined runId gracefully', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/framework/test.txt', + runId: undefined, + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + const testFile = framework.children['test.txt'] as FileNode; + expect(testFile).toBeDefined(); + expect(testFile.runId).toBe(''); + }); + + it('should replace file with folder if path conflict occurs', () => { + const artifacts: ArtifactIndexEntry[] = [ + { + path: '/framework', + runId: 'run-123', + }, + { + path: '/framework/test.txt', + runId: 'run-123', + }, + ]; + + const result = buildArtifactsTree(artifacts); + + const framework = result.children.framework as FolderNode; + expect(framework).toBeDefined(); + expect(framework.isFile).toBe(false); + expect(framework.children['test.txt']).toBeDefined(); + }); +}); From aecdfcfe23062f456d95e0486a897f00e1127b7c Mon Sep 17 00:00:00 2001 From: James Cocker Date: Mon, 8 Jun 2026 15:07:45 +0100 Subject: [PATCH 4/5] Code formatting Signed-off-by: James Cocker --- .../test-runs/test-run-details/TestRunDetails.test.tsx | 5 +---- galasa-ui/src/utils/3270/buildArtifactsTree.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/TestRunDetails.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/TestRunDetails.test.tsx index c24c66b2..e6f119bb 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/TestRunDetails.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/TestRunDetails.test.tsx @@ -658,10 +658,7 @@ describe('TestRunDetails', () => { // Verify the correct API endpoints were called (artifacts + zip) expect(global.fetch).toHaveBeenCalledTimes(2); - expect(global.fetch).toHaveBeenNthCalledWith( - 1, - `/internal-api/test-runs/${runId}/artifacts` - ); + expect(global.fetch).toHaveBeenNthCalledWith(1, `/internal-api/test-runs/${runId}/artifacts`); expect(global.fetch).toHaveBeenNthCalledWith( 2, `http://localhost/internal-api/test-runs/${runId}/zip?runName=TestRun` diff --git a/galasa-ui/src/utils/3270/buildArtifactsTree.ts b/galasa-ui/src/utils/3270/buildArtifactsTree.ts index 318a3983..7489e0d2 100644 --- a/galasa-ui/src/utils/3270/buildArtifactsTree.ts +++ b/galasa-ui/src/utils/3270/buildArtifactsTree.ts @@ -11,7 +11,7 @@ import { cleanArtifactPath } from '@/utils/artifacts'; /** * Builds a tree structure from a list of artifacts. * This function processes artifact paths and creates a hierarchical folder/file structure. - * + * * @param artifacts - Array of artifact entries to process * @returns A FolderNode representing the root of the tree structure */ From d5b5a4d61e848f87687284e37156d8e63d7bbc36 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:48:22 +0100 Subject: [PATCH 5/5] fix: Only render 3270 terminals when the terminal tab is selected Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .../test-run-details/3270Tab/TabFor3270.tsx | 1 + .../3270Tab/TableOfScreenshots.tsx | 12 +++- .../test-run-details/TestRunDetails.tsx | 7 +- .../3270Tab/TableOfScreenshots.test.tsx | 71 +++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TabFor3270.tsx b/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TabFor3270.tsx index 06a01225..2d6c495a 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TabFor3270.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TabFor3270.tsx @@ -69,6 +69,7 @@ export default function TabFor3270({ setHighlightedRowInDisplayedData={setHighlightedRowInDisplayedData} highlightedRowId={highlightedRowId} setHighlightedRowId={setHighlightedRowId} + is3270CurrentlySelected={is3270CurrentlySelected} />
diff --git a/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.tsx b/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.tsx index 8465bf94..52856888 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.tsx @@ -42,6 +42,7 @@ export default function TableOfScreenshots({ setHighlightedRowInDisplayedData, highlightedRowId, setHighlightedRowId, + is3270CurrentlySelected, }: { runId: string; zos3270TerminalData: TreeNodeData[]; @@ -57,6 +58,7 @@ export default function TableOfScreenshots({ setHighlightedRowInDisplayedData: React.Dispatch>; highlightedRowId: string; setHighlightedRowId: React.Dispatch>; + is3270CurrentlySelected: boolean; }) { const translations = useTranslations('3270Tab'); const headers = [ @@ -145,8 +147,12 @@ export default function TableOfScreenshots({ }, [filteredRows, highlightedRowId]); useEffect(() => { - // Ensure screenshots are only collected once. - if (!screenshotsCollected.current?.valueOf() && flattenedZos3270TerminalData.length === 0) { + // Only fetch screenshots when the 3270 tab is selected + if ( + is3270CurrentlySelected && + !screenshotsCollected.current?.valueOf() && + flattenedZos3270TerminalData.length === 0 + ) { screenshotsCollected.current = true; const fetchData = async () => { try { @@ -171,7 +177,7 @@ export default function TableOfScreenshots({ // If you're adding extra state to this hook, make sure to review the dependency array due to the warning suppression: // eslint-disable-next-line - }, []); + }, [is3270CurrentlySelected]); // When highlighted image changes, update image data. useEffect(() => { diff --git a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx index 59239b7b..444f1811 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/TestRunDetails.tsx @@ -157,9 +157,9 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { duration: runDetails.testStructure?.startTime && runDetails.testStructure?.endTime ? getIsoTimeDifference( - runDetails.testStructure?.startTime, - runDetails.testStructure?.endTime - ) + runDetails.testStructure?.startTime, + runDetails.testStructure?.endTime + ) : '-', tags: runDetails.testStructure?.tags || [], }; @@ -283,7 +283,6 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => { fetchExistingTags(); }, []); - const handleShare = async () => { try { await navigator.clipboard.writeText(window.location.href); diff --git a/galasa-ui/src/tests/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.test.tsx b/galasa-ui/src/tests/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.test.tsx index 56e671f3..21b3e663 100644 --- a/galasa-ui/src/tests/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/test-run-details/3270Tab/TableOfScreenshots.test.tsx @@ -48,6 +48,7 @@ const defaultProps = { setCannotSwitchToNextImage: jest.fn(), highlightedRowInDisplayedData: true, setHighlightedRowInDisplayedData: jest.fn(), + is3270CurrentlySelected: true, }; jest.mock('@/utils/3270/get3270Screenshots', () => { @@ -64,6 +65,10 @@ beforeAll(() => { Element.prototype.scrollIntoView = jest.fn(); }); +beforeEach(() => { + mockGet3270Screenshots.mockClear(); +}); + type MockData = { newFlattenedZos3270TerminalData: CellFor3270[]; newAllImageData: TerminalImage[]; @@ -233,4 +238,70 @@ describe('TableOfScreenshots', () => { const dropdown = screen.getByRole('combobox') as HTMLSelectElement; expect(dropdown).toBeInTheDocument(); }); + + test('does not fetch screenshots when tab is not selected', async () => { + // Arrange + mockGet3270Screenshots.mockResolvedValue(emptyMockData); + + // Act + await act(async () => { + render( + + ); + }); + + // Assert + expect(mockGet3270Screenshots).not.toHaveBeenCalled(); + }); + + test('fetches screenshots when tab becomes selected', async () => { + // Arrange + const setIsLoading = jest.fn(); + mockGet3270Screenshots.mockResolvedValue(someMockData); + + // Act + const { rerender } = await act(async () => { + return render( + + ); + }); + + // Assert - should not fetch when not selected + expect(mockGet3270Screenshots).not.toHaveBeenCalled(); + + // Act - change to selected + await act(async () => { + rerender( + + ); + }); + + // Assert - should fetch when selected + expect(mockGet3270Screenshots).toHaveBeenCalledWith([], 'testRunId'); + expect(setIsLoading).toHaveBeenCalledWith(false); + }); });