Skip to content
Open
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
21 changes: 5 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 82 additions & 0 deletions src/__tests__/TagPage/HistoryLayers.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ const mockLayersList = [
}
];

const mockArtifactLayers = [
{
mediaType: 'text/plain',
size: 12,
digest: 'sha256:a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447',
annotations: [{ key: 'org.opencontainers.image.title', value: 'artifact.txt' }]
},
{
mediaType: 'application/octet-stream',
size: 1024,
digest: 'sha256:b948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a448',
annotations: []
}
];

afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
Expand All @@ -49,3 +64,70 @@ describe('Layers page', () => {
expect(await screen.findAllByText(/DIGEST/i)).toHaveLength(1);
});
});

describe('Artifact files display', () => {
it('renders "Artifact Files" title for artifact manifests', async () => {
render(
<HistoryLayers
name="hello-artifact:v1"
history={[]}
artifactType="application/vnd.acme.rocket.config"
layers={mockArtifactLayers}
/>
);
expect(await screen.findByText('Artifact Files')).toBeInTheDocument();
});

it('renders artifact file cards for artifact manifests', async () => {
render(
<HistoryLayers
name="hello-artifact:v1"
history={[]}
artifactType="application/vnd.acme.rocket.config"
layers={mockArtifactLayers}
/>
);
expect(await screen.findAllByTestId('artifact-file-card')).toHaveLength(2);
});

it('shows artifact file title from org.opencontainers.image.title annotation', async () => {
render(
<HistoryLayers
name="hello-artifact:v1"
history={[]}
artifactType="application/vnd.acme.rocket.config"
layers={mockArtifactLayers}
/>
);
expect(await screen.findByText('artifact.txt')).toBeInTheDocument();
});

it('shows media type for each artifact file', async () => {
render(
<HistoryLayers
name="hello-artifact:v1"
history={[]}
artifactType="application/vnd.acme.rocket.config"
layers={mockArtifactLayers}
/>
);
expect(await screen.findByText('text/plain')).toBeInTheDocument();
});

it('shows "No artifact files available" when artifact has no layers', async () => {
render(
<HistoryLayers
name="hello-artifact:v1"
history={[]}
artifactType="application/vnd.acme.rocket.config"
layers={[]}
/>
);
expect(await screen.findByText(/No artifact files available/i)).toBeInTheDocument();
});

it('renders "Layers" title for regular (non-artifact) manifests', async () => {
render(<HistoryLayers name="alpine:latest" history={mockLayersList} />);
expect(await screen.findByText('Layers')).toBeInTheDocument();
});
});
73 changes: 73 additions & 0 deletions src/__tests__/TagPage/TagDetails.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1111,3 +1111,76 @@ describe('Tags details', () => {
await waitFor(() => expect(screen.queryAllByTestId('successPulled-buton')).toHaveLength(0), { timeout: 4500 });
});
});

const mockArtifactImage = {
Image: {
RepoName: 'hello-artifact',
Tag: 'v1',
TaggedTimestamp: '2026-05-05T16:40:57Z',
Manifests: [
{
Digest: 'sha256:3f41230db4272fb8909e21c431d8e589682e3d743ed47d98e76a3925cff06976',
LastUpdated: '2026-05-05T16:40:57Z',
Size: '574',
ArtifactType: 'application/vnd.acme.rocket.config',
Platform: {},
Layers: [
{
MediaType: 'text/plain',
Size: '12',
Digest: 'sha256:a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447',
Annotations: [{ Key: 'org.opencontainers.image.title', Value: 'artifact.txt' }]
}
],
History: []
}
],
Vulnerabilities: { MaxSeverity: 'NONE', Count: 0 }
}
};

describe('Artifact tag details', () => {
it('should show ORAS tab in pull dropdown for artifact manifests', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockArtifactImage } });
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText(
`Pull ${mockArtifactImage.Image.RepoName}:${mockArtifactImage.Image.Tag}`
);
userEvent.click(dropdown);
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
expect(await screen.findByText('ORAS')).toBeInTheDocument();
});

it('should copy the oras pull string to clipboard for artifact manifests', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockArtifactImage } });
render(<TagDetailsThemeWrapper />);
const dropdown = await screen.findByText(
`Pull ${mockArtifactImage.Image.RepoName}:${mockArtifactImage.Image.Tag}`
);
userEvent.click(dropdown);
await waitFor(() => expect(screen.queryAllByTestId('pull-menuItem')).toHaveLength(1));
fireEvent.click(await screen.findByTestId('orasPullcopy-btn'));
await waitFor(() =>
expect(mockCopyToClipboard).toHaveBeenCalledWith(
`oras pull localhost/${mockArtifactImage.Image.RepoName}:${mockArtifactImage.Image.Tag}`
)
);
});

it('should show artifact type in metadata for artifact manifests', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockArtifactImage } });
render(<TagDetailsThemeWrapper />);
expect(await screen.findByText('Artifact Type')).toBeInTheDocument();
expect(await screen.findByTestId('artifact-type')).toHaveTextContent(
'application/vnd.acme.rocket.config'
);
});

it('should show "Artifact Files" tab content for artifact manifests', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockArtifactImage } });
render(<TagDetailsThemeWrapper />);
expect(await screen.findByText('Artifact Files')).toBeInTheDocument();
expect(await screen.findByText('artifact.txt')).toBeInTheDocument();
});
});

2 changes: 1 addition & 1 deletion src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const endpoints = {
detailedRepoInfo: (name) =>
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
detailedImageInfo: (name, tag) =>
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag TaggedTimestamp Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}}} Vendor Licenses }}`,
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag TaggedTimestamp Manifests {ArtifactType Layers {MediaType Size Digest Annotations{Key Value}} History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}}} Vendor Licenses }}`,
vulnerabilitiesForRepo: (
name,
{ pageNumber = 1, pageSize = 15 },
Expand Down
Loading
Loading