mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 17:47:22 +00:00
chore: update documentation links to be used from product.json (#16912)
* chore: update documentation links to be used from product.json Signed-off-by: Sonia Sandler <ssandler@redhat.com> * chore: adjust types in product.json Signed-off-by: Sonia Sandler <ssandler@redhat.com>
This commit is contained in:
parent
1095fded1c
commit
a7f05e926a
3 changed files with 359 additions and 188 deletions
|
|
@ -20,6 +20,7 @@ import type { ApiSenderType } from '@podman-desktop/core-api/api-sender';
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { DocumentationService } from '/@/plugin/documentation/documentation-service.js';
|
||||
import product from '/@product.json' with { type: 'json' };
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
|
|
@ -30,10 +31,35 @@ const mockApiSender = {
|
|||
send: vi.fn(),
|
||||
} as unknown as ApiSenderType;
|
||||
|
||||
const fallbackDocumentation = [
|
||||
{
|
||||
id: 'item1',
|
||||
name: 'Item 1',
|
||||
description: 'Some description 1',
|
||||
url: '/url/1',
|
||||
category: 'Documentation',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
name: 'Item 2',
|
||||
description: 'Some description 2',
|
||||
url: '/url/2',
|
||||
category: 'Tutorial',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock(import('/@product.json'));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
documentationService = new DocumentationService(mockApiSender);
|
||||
console.error = vi.fn();
|
||||
vi.mocked(product).documentation.links = [
|
||||
{ link: '/docs/link', category: 'category 1' },
|
||||
{ link: '/tutorial/link', category: 'category 2' },
|
||||
{ link: '/some/link', category: 'category 3' },
|
||||
];
|
||||
vi.mocked(product).documentation.fallback = fallbackDocumentation;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -42,42 +68,54 @@ afterEach(() => {
|
|||
|
||||
describe('fetchDocumentation', () => {
|
||||
test('should fetch documentation and tutorials successfully', async () => {
|
||||
const mockDocsHtml = `
|
||||
<html>
|
||||
<body>
|
||||
<a href="/docs/intro">Introduction</a>
|
||||
<a href="/docs/containers">Containers Guide</a>
|
||||
<a href="/docs/kubernetes">Kubernetes Guide</a>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: '/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Containers Guide',
|
||||
url: '/docs/containers',
|
||||
},
|
||||
{
|
||||
name: 'Kubernetes Guide',
|
||||
url: '/docs/kubernetes',
|
||||
},
|
||||
];
|
||||
|
||||
const mockTutorialHtml = `
|
||||
<html>
|
||||
<body>
|
||||
<a href="/tutorial/getting-started">Getting Started Tutorial</a>
|
||||
<a href="/tutorial/kubernetes-cluster">Kubernetes Cluster Tutorial</a>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Getting Started Tutorial',
|
||||
url: '/tutorial/getting-started',
|
||||
},
|
||||
{
|
||||
name: 'Kubernetes Cluster Tutorial',
|
||||
url: '/tutorial/kubernetes-cluster',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
|
||||
// Verify fetch was called with correct URLs
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledWith('https://podman-desktop.io/docs.json');
|
||||
expect(fetchSpy).toHaveBeenCalledWith('https://podman-desktop.io/tutorials.json');
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
expect(fetchSpy).toHaveBeenCalledWith('/docs/link');
|
||||
expect(fetchSpy).toHaveBeenCalledWith('/tutorial/link');
|
||||
expect(fetchSpy).toHaveBeenCalledWith('/some/link');
|
||||
|
||||
// Verify service is initialized
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
|
@ -92,14 +130,18 @@ describe('fetchDocumentation', () => {
|
|||
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
expect(items).toBeDefined();
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
expect(items.length).toBe(2);
|
||||
|
||||
// Should include fallback items
|
||||
const introItem = items.find(item => item.id === 'docs-intro');
|
||||
expect(introItem).toBeDefined();
|
||||
expect(introItem?.name).toBe('Introduction & Getting Started');
|
||||
const firstItem = items.find(item => item.id === 'item1');
|
||||
expect(firstItem).toBeDefined();
|
||||
expect(firstItem?.name).toBe('Item 1');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
const secondItem = items.find(item => item.id === 'item2');
|
||||
expect(secondItem).toBeDefined();
|
||||
expect(secondItem?.name).toBe('Item 2');
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test('should use fallback when HTTP response is not ok', async () => {
|
||||
|
|
@ -115,291 +157,389 @@ describe('fetchDocumentation', () => {
|
|||
expect(items).toBeDefined();
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocumentationItems', () => {
|
||||
test('should initialize automatically if not initialized', async () => {
|
||||
const mockDocsHtml = '<a href="/docs/auto">Auto Init</a>';
|
||||
const mockTutorialHtml = '<a href="/tutorial/auto">Auto Tutorial</a>';
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Auto Init',
|
||||
url: '/docs/auto',
|
||||
},
|
||||
];
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Auto Tutorial',
|
||||
url: '/tutorial/auto',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
expect(items).toBeDefined();
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should return cached items after initialization', async () => {
|
||||
const mockDocsHtml = '<a href="/docs/cached">Cached</a>';
|
||||
const mockTutorialHtml = '<a href="/tutorial/cached">Cached Tutorial</a>';
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Cached',
|
||||
url: '/docs/cached',
|
||||
},
|
||||
];
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Cached Tutorial',
|
||||
url: '/tutorial/cached',
|
||||
},
|
||||
];
|
||||
|
||||
// Only mock once - should be cached after first call
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const firstCall = await documentationService.getDocumentationItems();
|
||||
const secondCall = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(firstCall).toStrictEqual(secondCall); // Same content
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2); // Only called once for initialization
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3); // Only called once for initialization
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshDocumentation', () => {
|
||||
test('should re-fetch documentation and send update notification', async () => {
|
||||
const mockDocsHtml = '<a href="/docs/refresh">Refresh Test</a>';
|
||||
const mockTutorialHtml = '<a href="/tutorial/refresh">Refresh Tutorial</a>';
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Refresh Test',
|
||||
url: '/docs/refresh',
|
||||
},
|
||||
];
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Refresh Tutorial',
|
||||
url: '/tutorial/refresh',
|
||||
},
|
||||
];
|
||||
|
||||
// Initial fetch
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Refresh fetch - add more mock calls
|
||||
fetchSpy
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.refreshDocumentation();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(4); // 2 initial + 2 refresh
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(6); // 3 initial + 3 refresh
|
||||
expect(mockApiSender.send).toHaveBeenCalledWith('documentation-updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDocumentationContent', () => {
|
||||
test('should parse documentation and tutorial links correctly', async () => {
|
||||
const mockDocsHtml = `
|
||||
<html>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/docs/intro">Introduction & Getting Started</a>
|
||||
<a href="/docs/containers">Working with Containers</a>
|
||||
<a href="/docs/kubernetes">Kubernetes Integration</a>
|
||||
<a href="/docs/troubleshooting">Troubleshooting Guide</a>
|
||||
<a href="#edit">Edit this page</a> <!-- Should be filtered out -->
|
||||
<a href="#next">Next</a> <!-- Should be filtered out -->
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Introduction & Getting Started',
|
||||
url: '/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Working with Containers',
|
||||
url: '/docs/containers',
|
||||
},
|
||||
{
|
||||
name: 'Kubernetes Integration',
|
||||
url: '/docs/kubernetes',
|
||||
},
|
||||
{
|
||||
name: 'Troubleshooting Guide',
|
||||
url: '/docs/troubleshooting',
|
||||
},
|
||||
];
|
||||
|
||||
const mockTutorialHtml = `
|
||||
<html>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/tutorial/getting-started">Getting Started</a>
|
||||
<a href="/tutorial/kubernetes-cluster">Creating a Kubernetes Cluster</a>
|
||||
<a href="/tutorial/compose">Using Docker Compose</a>
|
||||
<a href="#edit">Edit this page</a> <!-- Should be filtered out -->
|
||||
</nav>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Getting Started',
|
||||
url: '/tutorial/getting-started',
|
||||
},
|
||||
{
|
||||
name: 'Creating a Kubernetes Cluster',
|
||||
url: '/tutorial/kubernetes-cluster',
|
||||
},
|
||||
{
|
||||
name: 'Using Docker Compose',
|
||||
url: '/tutorial/compose',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Check that we have core pages plus parsed pages
|
||||
const docItems = items.filter(item => item.category === 'Documentation');
|
||||
const tutorialItems = items.filter(item => item.category === 'Tutorial');
|
||||
console.log(items);
|
||||
|
||||
expect(docItems.length).toBeGreaterThanOrEqual(4); // Core + parsed
|
||||
expect(tutorialItems.length).toBeGreaterThanOrEqual(1); // Core + parsed
|
||||
// Check that we have expected parsed items and categories
|
||||
const cat1Items = items.filter(item => item.category === 'category 1');
|
||||
const cat2Items = items.filter(item => item.category === 'category 2');
|
||||
|
||||
expect(cat1Items.length).toBe(4); // parsed
|
||||
expect(cat2Items.length).toBe(3); // parsed
|
||||
|
||||
// Check specific parsed items
|
||||
const introItem = items.find(item => item.name === 'Introduction & Getting Started' && item.id === 'docs-intro');
|
||||
const introItem = items.find(item => item.name === 'Introduction & Getting Started');
|
||||
expect(introItem).toBeDefined();
|
||||
expect(introItem?.category).toBe('Documentation');
|
||||
expect(introItem?.url).toBe('https://podman-desktop.io/docs/intro');
|
||||
expect(introItem?.category).toBe('category 1');
|
||||
expect(introItem?.url).toBe('/docs/intro');
|
||||
|
||||
// Tutorial item may not be parsed if HTML doesn't match regex, so just check core tutorial exists
|
||||
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
|
||||
const coreTutorialItem = items.find(item => item.name === 'Getting Started');
|
||||
expect(coreTutorialItem).toBeDefined();
|
||||
expect(coreTutorialItem?.category).toBe('Tutorial');
|
||||
|
||||
// Check that filtered items are not included
|
||||
const editItem = items.find(item => item.name.includes('Edit this page'));
|
||||
expect(editItem).toBeUndefined();
|
||||
|
||||
const nextItem = items.find(item => item.name.includes('Next'));
|
||||
expect(nextItem).toBeUndefined();
|
||||
expect(coreTutorialItem?.category).toBe('category 2');
|
||||
});
|
||||
|
||||
test('should handle relative and absolute URLs correctly', async () => {
|
||||
const mockDocsHtml = `
|
||||
<a href="/docs/relative">Relative Link</a>
|
||||
<a href="https://podman-desktop.io/docs/absolute">Absolute Link</a>
|
||||
`;
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Relative Link',
|
||||
url: '/docs/relative',
|
||||
},
|
||||
{
|
||||
name: 'Absolute Link',
|
||||
url: 'https://podman-desktop.io/docs/absolute',
|
||||
},
|
||||
];
|
||||
|
||||
const mockTutorialHtml = `
|
||||
<a href="/tutorial/relative">Relative Tutorial</a>
|
||||
<a href="https://podman-desktop.io/tutorial/absolute">Absolute Tutorial</a>
|
||||
`;
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Relative Tutorial',
|
||||
url: '/tutorial/relative',
|
||||
},
|
||||
{
|
||||
name: 'Absolute Tutorial',
|
||||
url: 'https://podman-desktop.io/tutorial/absolute',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Since the simple HTML may not match complex regex patterns,
|
||||
// we should mainly verify core documentation pages exist
|
||||
const coreDocItem = items.find(item => item.id === 'docs-intro');
|
||||
expect(coreDocItem).toBeDefined();
|
||||
expect(coreDocItem?.url).toBe('https://podman-desktop.io/docs/intro');
|
||||
const docItem = items.find(item => item.name === 'Relative Link');
|
||||
expect(docItem).toBeDefined();
|
||||
expect(docItem?.url).toBe('/docs/relative');
|
||||
|
||||
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
|
||||
expect(coreTutorialItem).toBeDefined();
|
||||
expect(coreTutorialItem?.url).toBe('https://podman-desktop.io/tutorial');
|
||||
const tutorialItem = items.find(item => item.name === 'Absolute Tutorial');
|
||||
expect(tutorialItem).toBeDefined();
|
||||
expect(tutorialItem?.url).toBe('https://podman-desktop.io/tutorial/absolute');
|
||||
});
|
||||
|
||||
test('should handle empty or malformed HTML gracefully', async () => {
|
||||
test('should handle empty JSON gracefully', async () => {
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(''),
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('<html><body></body></html>'),
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Should still have core documentation pages
|
||||
expect(items.length).toBeGreaterThan(0);
|
||||
const coreItem = items.find(item => item.id === 'docs-intro');
|
||||
expect(coreItem).toBeDefined();
|
||||
// Should fallback to predefined documentation when JSON lists are empty
|
||||
expect(items).toStrictEqual(fallbackDocumentation);
|
||||
});
|
||||
|
||||
test('should remove duplicate items', async () => {
|
||||
const mockDocsHtml = `
|
||||
<a href="/docs/duplicate">Duplicate Item</a>
|
||||
<a href="/docs/duplicate">Duplicate Item</a>
|
||||
<a href="/docs/unique">Unique Item</a>
|
||||
`;
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Duplicate Item',
|
||||
url: '/docs/duplicate',
|
||||
},
|
||||
{
|
||||
name: 'Duplicate Item',
|
||||
url: '/docs/duplicate',
|
||||
},
|
||||
{
|
||||
name: 'Unique Item',
|
||||
url: '/docs/unique',
|
||||
},
|
||||
];
|
||||
|
||||
const mockTutorialHtml = '<a href="/tutorial/test">Test Tutorial</a>';
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Test Tutorial',
|
||||
url: '/tutorial/test',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Since simple HTML may not be parsed, just check core documentation exists
|
||||
const coreDocItems = items.filter(item => item.category === 'Documentation');
|
||||
expect(coreDocItems.length).toBeGreaterThanOrEqual(1);
|
||||
const docItems = items.filter(item => item.category === 'category 1');
|
||||
|
||||
// Just verify we have the expected core documentation
|
||||
const coreTutorialItems = items.filter(item => item.category === 'Tutorial');
|
||||
expect(coreTutorialItems.length).toBeGreaterThanOrEqual(1);
|
||||
expect(docItems.length).toBe(2);
|
||||
|
||||
const tutorialItems = items.filter(item => item.category === 'category 2');
|
||||
expect(tutorialItems.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateId', () => {
|
||||
test('should generate consistent IDs for documentation items', async () => {
|
||||
const mockDocsHtml = '<a href="/docs/test">Test Documentation Item</a>';
|
||||
const mockTutorialHtml = '<a href="/tutorial/test">Test Tutorial Item</a>';
|
||||
const mockDocsJson = [
|
||||
{
|
||||
name: 'Test Documentation Item',
|
||||
url: '/docs/test',
|
||||
},
|
||||
];
|
||||
const mockTutorialJson = [
|
||||
{
|
||||
name: 'Test Tutorial Item',
|
||||
url: '/tutorial/test',
|
||||
},
|
||||
];
|
||||
|
||||
const fetchSpy = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockDocsHtml),
|
||||
json: () => Promise.resolve(mockDocsJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(mockTutorialHtml),
|
||||
json: () => Promise.resolve(mockTutorialJson),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
await documentationService.fetchDocumentation();
|
||||
const items = await documentationService.getDocumentationItems();
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2); // Only called during fetchDocumentation, cached for getDocumentationItems
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3); // Only called during fetchDocumentation, cached for getDocumentationItems
|
||||
|
||||
// Check that we have core documentation IDs
|
||||
const coreDocItem = items.find(item => item.id === 'docs-intro');
|
||||
expect(coreDocItem).toBeDefined();
|
||||
expect(coreDocItem?.id).toBe('docs-intro');
|
||||
// IDs are generated from item names (sha256)
|
||||
const docItem = items.find(item => item.name === 'Test Documentation Item');
|
||||
expect(docItem).toBeDefined();
|
||||
expect(docItem?.id).toMatch(/^[a-f0-9]{64}$/);
|
||||
|
||||
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
|
||||
expect(coreTutorialItem).toBeDefined();
|
||||
expect(coreTutorialItem?.id).toBe('tutorial-index');
|
||||
const tutorialItem = items.find(item => item.name === 'Test Tutorial Item');
|
||||
expect(tutorialItem).toBeDefined();
|
||||
expect(tutorialItem?.id).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(docItem?.id).not.toBe(tutorialItem?.id);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import { DocumentationBaseInfo, DocumentationInfo } from '@podman-desktop/core-a
|
|||
import { ApiSenderType } from '@podman-desktop/core-api/api-sender';
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
import fallbackDocumentation from '/@/assets/fallback-documentation.json' with { type: 'json' };
|
||||
import { Disposable } from '/@/plugin/types/disposable.js';
|
||||
import product from '/@product.json' with { type: 'json' };
|
||||
|
||||
@injectable()
|
||||
export class DocumentationService extends Disposable {
|
||||
|
|
@ -42,21 +42,18 @@ export class DocumentationService extends Disposable {
|
|||
|
||||
async fetchDocumentation(): Promise<void> {
|
||||
try {
|
||||
const [docsJson, tutorialsJson] = await Promise.all([
|
||||
this.fetchJsonContent('https://podman-desktop.io/docs.json'),
|
||||
this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
|
||||
]);
|
||||
|
||||
if (docsJson && tutorialsJson) {
|
||||
this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
|
||||
} else {
|
||||
throw new Error('Failed to fetch documentation JSON files');
|
||||
}
|
||||
const documentationJsons = await Promise.all(
|
||||
[...product.documentation.links].map(async item => {
|
||||
const data = await this.fetchJsonContent(item.link);
|
||||
return { category: item.category, data };
|
||||
}),
|
||||
);
|
||||
this.documentation = this.parseDocumentationFromJson(documentationJsons);
|
||||
this.isInitialized = true;
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch documentation at startup:', error);
|
||||
// Fallback to predefined documentation if fetching fails
|
||||
this.documentation = fallbackDocumentation as DocumentationInfo[];
|
||||
this.documentation = product.documentation.fallback as DocumentationInfo[];
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -99,53 +96,37 @@ export class DocumentationService extends Disposable {
|
|||
}
|
||||
|
||||
private parseDocumentationFromJson(
|
||||
docsJson: DocumentationBaseInfo[],
|
||||
tutorialsJson: DocumentationBaseInfo[],
|
||||
documentationJsons: { category?: string; data: DocumentationBaseInfo[] }[],
|
||||
): DocumentationInfo[] {
|
||||
const documentation: DocumentationInfo[] = [];
|
||||
|
||||
// Validate input parameters
|
||||
if (!docsJson || !tutorialsJson) {
|
||||
// Check that there is some data
|
||||
if (!documentationJsons.some(item => item.data.length > 0)) {
|
||||
console.warn('Missing JSON content for parsing documentation');
|
||||
return fallbackDocumentation as DocumentationInfo[];
|
||||
return product.documentation.fallback as DocumentationInfo[];
|
||||
}
|
||||
|
||||
// Parse both docs and tutorials using generic logic
|
||||
const parseConfigs = [
|
||||
{
|
||||
data: docsJson,
|
||||
category: 'Documentation',
|
||||
errorMessage: 'Error parsing documentation JSON:',
|
||||
},
|
||||
{
|
||||
data: tutorialsJson,
|
||||
category: 'Tutorial',
|
||||
errorMessage: 'Error parsing tutorials JSON:',
|
||||
},
|
||||
];
|
||||
|
||||
for (const config of parseConfigs) {
|
||||
try {
|
||||
for (const item of config.data) {
|
||||
if (item.name && item.url) {
|
||||
for (const documentationJson of documentationJsons) {
|
||||
for (const item of documentationJson.data) {
|
||||
if (item.name && item.url) {
|
||||
const id = createHash('sha256').update(item.name).digest('hex');
|
||||
if (!documentation.find(doc => doc.id === id)) {
|
||||
documentation.push({
|
||||
id: createHash('sha256').update(item.name).digest('hex'),
|
||||
id: id,
|
||||
name: item.name,
|
||||
description: `${config.category}: ${item.name}`,
|
||||
description: `${documentationJson.category}: ${item.name}`,
|
||||
url: item.url,
|
||||
category: config.category,
|
||||
category: documentationJson.category ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(config.errorMessage, error);
|
||||
}
|
||||
}
|
||||
|
||||
// If no documentation was parsed, use fallback
|
||||
if (documentation.length === 0) {
|
||||
console.error('DocumentationService: No items parsed, using fallback documentation');
|
||||
return fallbackDocumentation as DocumentationInfo[];
|
||||
return product.documentation.fallback as DocumentationInfo[];
|
||||
}
|
||||
|
||||
return documentation;
|
||||
|
|
|
|||
50
product.json
50
product.json
|
|
@ -83,5 +83,55 @@
|
|||
"title": "",
|
||||
"image": "",
|
||||
"summary": ""
|
||||
},
|
||||
"documentation": {
|
||||
"links": [
|
||||
{ "link": "https://podman-desktop.io/docs.json", "category": "Documentation" },
|
||||
{ "link": "https://podman-desktop.io/tutorials.json", "category": "Tutorial" }
|
||||
],
|
||||
"fallback": [
|
||||
{
|
||||
"id": "docs-intro",
|
||||
"name": "Introduction & Getting Started",
|
||||
"description": "Learn the basics of Podman Desktop",
|
||||
"url": "https://podman-desktop.io/docs/intro",
|
||||
"category": "Documentation"
|
||||
},
|
||||
{
|
||||
"id": "tutorial-index",
|
||||
"name": "Tutorials & Guides",
|
||||
"description": "Step-by-step tutorials for common tasks",
|
||||
"url": "https://podman-desktop.io/tutorial",
|
||||
"category": "Tutorial"
|
||||
},
|
||||
{
|
||||
"id": "docs-containers",
|
||||
"name": "Containers Documentation",
|
||||
"description": "Working with containers, images, and pods",
|
||||
"url": "https://podman-desktop.io/docs/containers",
|
||||
"category": "Documentation"
|
||||
},
|
||||
{
|
||||
"id": "docs-kubernetes",
|
||||
"name": "Kubernetes Documentation",
|
||||
"description": "Deploy and manage Kubernetes applications",
|
||||
"url": "https://podman-desktop.io/docs/kubernetes",
|
||||
"category": "Documentation"
|
||||
},
|
||||
{
|
||||
"id": "docs-extensions",
|
||||
"name": "Extensions Development",
|
||||
"description": "Create and develop extensions",
|
||||
"url": "https://podman-desktop.io/docs/extensions/developing",
|
||||
"category": "Documentation"
|
||||
},
|
||||
{
|
||||
"id": "docs-troubleshooting",
|
||||
"name": "Troubleshooting Guide",
|
||||
"description": "Solve common issues and problems",
|
||||
"url": "https://podman-desktop.io/docs/troubleshooting",
|
||||
"category": "Documentation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue