Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default function TabFor3270({
setHighlightedRowInDisplayedData={setHighlightedRowInDisplayedData}
highlightedRowId={highlightedRowId}
setHighlightedRowId={setHighlightedRowId}
is3270CurrentlySelected={is3270CurrentlySelected}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function TableOfScreenshots({
setHighlightedRowInDisplayedData,
highlightedRowId,
setHighlightedRowId,
is3270CurrentlySelected,
}: {
runId: string;
zos3270TerminalData: TreeNodeData[];
Expand All @@ -57,6 +58,7 @@ export default function TableOfScreenshots({
setHighlightedRowInDisplayedData: React.Dispatch<React.SetStateAction<boolean>>;
highlightedRowId: string;
setHighlightedRowId: React.Dispatch<React.SetStateAction<string>>;
is3270CurrentlySelected: boolean;
}) {
const translations = useTranslations('3270Tab');
const headers = [
Expand Down Expand Up @@ -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 {
Expand All @@ -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(() => {
Expand Down
108 changes: 6 additions & 102 deletions galasa-ui/src/components/test-runs/test-run-details/ArtifactsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -41,6 +41,7 @@ export function ArtifactsTab({
setZos3270TerminalData,
}: {
artifacts: ArtifactIndexEntry[];
artifactsTreeData: FolderNode;
runId: string;
runName: string;
isLoadingArtifacts?: boolean;
Expand All @@ -49,11 +50,6 @@ export function ArtifactsTab({
setZos3270TerminalData: (zos3270TerminalData: TreeNodeData[]) => void;
}) {
const translations = useTranslations('Artifacts');
const [treeData, setTreeData] = useState<FolderNode>({
name: '',
isFile: false,
children: {},
});

const [artifactDetails, setArtifactDetails] = useState<ArtifactDetails>({
artifactFile: '',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -343,8 +247,8 @@ export function ArtifactsTab({

{!isLoadingArtifacts && !artifactsError && artifacts.length > 0 && (
<div className={styles.artifact}>
<TreeView className={styles.tree} onSelect={() => {}}>
{Object.values(treeData.children).map((child) => renderNode(child, child.name))}
<TreeView className={styles.tree} onSelect={() => { }}>
{Object.values(artifactsTreeData.children).map((child) => renderNode(child, child.name))}
</TreeView>

<div className={styles.artifactView}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { buildArtifactsTree } from '@/utils/3270/buildArtifactsTree';

interface TestRunDetailsProps {
runId: string;
Expand All @@ -62,6 +64,11 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => {
const [run, setRun] = useState<RunMetadata>();
const [methods, setMethods] = useState<TestMethod[]>([]);
const [artifacts, setArtifacts] = useState<ArtifactIndexEntry[]>([]);
const [artifactsTreeData, setArtifactsTreeData] = useState<FolderNode>({
name: '',
isFile: false,
children: {},
});
const [artifactsLoaded, setArtifactsLoaded] = useState(false);
const [artifactsLoading, setArtifactsLoading] = useState(false);
const [artifactsError, setArtifactsError] = useState<string | null>(null);
Expand Down Expand Up @@ -176,6 +183,21 @@ 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 = buildArtifactsTree(runArtifacts);

// 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');
Expand Down Expand Up @@ -229,13 +251,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(() => {
Expand All @@ -262,7 +283,6 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => {
fetchExistingTags();
}, []);


const handleShare = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
Expand Down Expand Up @@ -492,6 +512,7 @@ const TestRunDetails = ({ runId, runDetailsPromise }: TestRunDetailsProps) => {
<TabPanel>
<ArtifactsTab
artifacts={artifacts}
artifactsTreeData={artifactsTreeData}
runId={runId}
runName={run?.runName || ''}
isLoadingArtifacts={artifactsLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const defaultProps = {
setCannotSwitchToNextImage: jest.fn(),
highlightedRowInDisplayedData: true,
setHighlightedRowInDisplayedData: jest.fn(),
is3270CurrentlySelected: true,
};

jest.mock('@/utils/3270/get3270Screenshots', () => {
Expand All @@ -64,6 +65,10 @@ beforeAll(() => {
Element.prototype.scrollIntoView = jest.fn();
});

beforeEach(() => {
mockGet3270Screenshots.mockClear();
});

type MockData = {
newFlattenedZos3270TerminalData: CellFor3270[];
newAllImageData: TerminalImage[];
Expand Down Expand Up @@ -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(
<TableOfScreenshots
isLoading={true}
setIsLoading={jest.fn()}
setImageData={jest.fn()}
highlightedRowId={''}
setHighlightedRowId={jest.fn()}
{...defaultProps}
is3270CurrentlySelected={false}
/>
);
});

// 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(
<TableOfScreenshots
isLoading={true}
setIsLoading={setIsLoading}
setImageData={jest.fn()}
highlightedRowId={''}
setHighlightedRowId={jest.fn()}
{...defaultProps}
is3270CurrentlySelected={false}
/>
);
});

// Assert - should not fetch when not selected
expect(mockGet3270Screenshots).not.toHaveBeenCalled();

// Act - change to selected
await act(async () => {
rerender(
<TableOfScreenshots
isLoading={true}
setIsLoading={setIsLoading}
setImageData={jest.fn()}
highlightedRowId={''}
setHighlightedRowId={jest.fn()}
{...defaultProps}
is3270CurrentlySelected={true}
/>
);
});

// Assert - should fetch when selected
expect(mockGet3270Screenshots).toHaveBeenCalledWith([], 'testRunId');
expect(setIsLoading).toHaveBeenCalledWith(false);
});
});
Loading
Loading