63c5d0a7c0
Set up testing with Vitest and jsdom and add unit tests for importers (jellyfin, playnite, stashapp, xbvr). Add typedoc configuration and update vite.config.ts and importer source files to support the tests. Ignore generated docs by adding /docs to .gitignore and add test-related devDependencies (vitest, @vitest/ui, jsdom, typedoc) in package.json.
525 lines
14 KiB
TypeScript
525 lines
14 KiB
TypeScript
/**
|
|
* Tests for XBVR Importer
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { importFromXBVR, XBVRConfig, ImportProgress } from '../xbvrImporter';
|
|
|
|
// Mock global fetch
|
|
global.fetch = vi.fn();
|
|
|
|
describe('xbvrImporter', () => {
|
|
const mockConfig: XBVRConfig = {
|
|
url: 'http://localhost:9999',
|
|
apiKey: 'test-api-key',
|
|
updateExisting: false
|
|
};
|
|
|
|
const mockLogCallback = vi.fn();
|
|
const mockProgressCallback = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(fetch).mockClear();
|
|
});
|
|
|
|
describe('importFromXBVR', () => {
|
|
it('should successfully import videos and actors from XBVR', async () => {
|
|
// Mock existing media and cast check
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
// Mock scene list fetch
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: 'Test Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'Favorites',
|
|
list: []
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
// Mock video detail fetch
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: 'Test Video',
|
|
description: 'A test VR video',
|
|
date: 1704067200, // 2024-01-01
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
rating_avg: 8.5,
|
|
screenType: '180',
|
|
stereoMode: 'sbs',
|
|
videoLength: 1800,
|
|
paysite: { name: 'Test Studio' },
|
|
actors: [
|
|
{ id: 1, name: 'Actor 1' },
|
|
{ id: 2, name: 'Actor 2' }
|
|
],
|
|
categories: [
|
|
{ tag: { name: 'VR' } },
|
|
{ tag: { name: '180°' } }
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
// Mock actor creation
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'cast-1' })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'cast-2' })
|
|
} as Response);
|
|
|
|
// Mock media creation
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'media-1' })
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.stage).toBe('complete');
|
|
expect(result.videosImported).toBe(1);
|
|
expect(result.actorsImported).toBe(2);
|
|
expect(result.errors).toHaveLength(0);
|
|
expect(mockLogCallback).toHaveBeenCalledWith('Starting DeoVR import...');
|
|
});
|
|
|
|
it('should handle connection errors', async () => {
|
|
vi.mocked(fetch).mockRejectedValueOnce(new Error('Connection failed'));
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.stage).toBe('error');
|
|
expect(result.errors).toContain('Connection failed');
|
|
});
|
|
|
|
it('should handle API response errors', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: false,
|
|
statusText: 'Unauthorized'
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.stage).toBe('error');
|
|
expect(result.errors).toContain('Failed to connect to DeoVR API: Unauthorized');
|
|
});
|
|
|
|
it('should skip videos starting with aka:', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: 'aka: Test Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: 'aka: Test Video',
|
|
date: 1704067200,
|
|
videoLength: 1800,
|
|
actors: [],
|
|
categories: []
|
|
})
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.videosImported).toBe(0);
|
|
expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped \'aka:\' video: aka: Test Video');
|
|
});
|
|
|
|
it('should skip actors containing aka:', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: 'Test Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: 'Test Video',
|
|
date: 1704067200,
|
|
videoLength: 1800,
|
|
actors: [
|
|
{ id: 1, name: 'Actor 1' },
|
|
{ id: 2, name: 'aka: Actor 2' }
|
|
],
|
|
categories: []
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'cast-1' })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'media-1' })
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.actorsImported).toBe(1);
|
|
expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped \'aka:\' actor: aka: Actor 2');
|
|
});
|
|
|
|
it('should skip existing videos when updateExisting is false', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
data: {
|
|
items: [
|
|
{ id: 'media-1', title: 'Test Video' }
|
|
]
|
|
}
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: 'Test Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: 'Test Video',
|
|
date: 1704067200,
|
|
videoLength: 1800,
|
|
actors: [],
|
|
categories: []
|
|
})
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.videosImported).toBe(0);
|
|
expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped duplicate: Test Video (updateExisting is false)');
|
|
});
|
|
|
|
it('should determine aspect ratio based on screenType and stereoMode', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: '360 Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: '360 Video',
|
|
date: 1704067200,
|
|
videoLength: 1800,
|
|
screenType: '360',
|
|
stereoMode: 'sbs',
|
|
actors: [],
|
|
categories: []
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'media-1' })
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.videosImported).toBe(1);
|
|
});
|
|
|
|
it('should convert Unix timestamp to date', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Recent',
|
|
list: [
|
|
{
|
|
title: 'Test Video',
|
|
videoLength: 1800,
|
|
thumbnailUrl: 'http://example.com/thumb.jpg',
|
|
video_url: 'http://example.com/api/video/1'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 1,
|
|
title: 'Test Video',
|
|
date: 1704067200, // 2024-01-01
|
|
videoLength: 1800,
|
|
actors: [],
|
|
categories: []
|
|
})
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: 'media-1' })
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.videosImported).toBe(1);
|
|
});
|
|
|
|
it('should handle missing Recent scene group', async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ data: { items: [] } })
|
|
} as Response);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
scenes: [
|
|
{
|
|
name: 'Favorites',
|
|
list: []
|
|
}
|
|
]
|
|
})
|
|
} as Response);
|
|
|
|
const result = await importFromXBVR(
|
|
mockConfig,
|
|
mockLogCallback,
|
|
mockProgressCallback
|
|
);
|
|
|
|
expect(result.videosImported).toBe(0);
|
|
expect(result.actorsImported).toBe(0);
|
|
expect(mockLogCallback).toHaveBeenCalledWith('Found 0 videos in \'Recent\' scene group');
|
|
});
|
|
});
|
|
|
|
describe('XBVRConfig', () => {
|
|
it('should accept valid configuration', () => {
|
|
const config: XBVRConfig = {
|
|
url: 'http://localhost:9999',
|
|
apiKey: 'test-api-key'
|
|
};
|
|
|
|
expect(config.url).toBe('http://localhost:9999');
|
|
expect(config.apiKey).toBe('test-api-key');
|
|
expect(config.updateExisting).toBeUndefined();
|
|
});
|
|
|
|
it('should accept configuration with optional fields', () => {
|
|
const config: XBVRConfig = {
|
|
url: 'http://localhost:9999',
|
|
apiKey: 'test-api-key',
|
|
updateExisting: true
|
|
};
|
|
|
|
expect(config.updateExisting).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('ImportProgress', () => {
|
|
it('should have correct structure', () => {
|
|
const progress: ImportProgress = {
|
|
current: 5,
|
|
total: 10,
|
|
stage: 'importing',
|
|
message: 'Importing...',
|
|
videosImported: 5,
|
|
actorsImported: 3,
|
|
errors: []
|
|
};
|
|
|
|
expect(progress.current).toBe(5);
|
|
expect(progress.total).toBe(10);
|
|
expect(progress.stage).toBe('importing');
|
|
expect(progress.videosImported).toBe(5);
|
|
expect(progress.actorsImported).toBe(3);
|
|
expect(progress.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|