task(F4): library providers - builtin catalogs (event/architecture/landscape 15 blocks each), user-library and global-library via LibraryProviderExtension, panel UI with existing Drop-Channel

This commit is contained in:
Agent Zero
2026-08-28 23:20:11 +02:00
parent ec7cb4043b
commit 2efbbf9bd2
11 changed files with 2093 additions and 1 deletions
+143
View File
@@ -0,0 +1,143 @@
/**
* Task F4 LibraryProviderExtension Tests.
*
* Testet die drei Bibliotheksquellen:
* - builtin-catalog (statische JSON-Startersets event/architecture/landscape)
* - user-library (zeichnungsscoped Blöcke via /api/drawings/:id/blocks)
* - global-library (globale Blöcke via /api/global-blocks)
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { builtinCatalogProvider } from '../src/plugins/builtin/library/providers';
import { makeUserLibraryProvider, makeGlobalLibraryProvider } from '../src/plugins/builtin/library/providers';
import { builtinLibraryPlugin } from '../src/plugins/builtin/library';
import { pluginRegistry } from '../src/plugins/PluginRegistry';
import LibraryProviderPanel from '../src/components/LibraryProviderPanel';
describe('F4: builtin catalog provider', () => {
it('listet genau die 3 Katalog-Ordner', async () => {
const folders = await builtinCatalogProvider.listFolders();
expect(folders.map(f => f.id).sort()).toEqual(['cat-arch', 'cat-event', 'cat-land']);
expect(folders.every(f => f.parentId === null)).toBe(true);
});
it('Startersets: je 15-30 Blöcke mit gültiger Payload und SVG-Thumbnail', async () => {
const folders = await builtinCatalogProvider.listFolders();
for (const folder of folders) {
const blocks = await builtinCatalogProvider.listBlocks(folder.id);
expect(blocks.length).toBeGreaterThanOrEqual(15);
expect(blocks.length).toBeLessThanOrEqual(30);
expect(blocks.every(b => b.folderId === folder.id)).toBe(true);
for (const block of blocks) {
expect(block.thumbnail).toContain('<svg');
expect(block.payload).toBeTruthy();
const elements = JSON.parse(block.payload as string);
expect(Array.isArray(elements)).toBe(true);
expect(elements.length).toBeGreaterThan(0);
for (const el of elements) {
expect(typeof el.id).toBe('string');
expect(typeof el.type).toBe('string');
expect('x' in el && 'y' in el).toBe(true);
expect(el.properties).toBeTypeOf('object');
}
}
}
});
it('listBlocks filtert nach Ordner, getBlock liefert den Block mit Payload', async () => {
const eventBlocks = await builtinCatalogProvider.listBlocks('cat-event');
expect(eventBlocks.length).toBeGreaterThanOrEqual(15);
const first = eventBlocks[0];
const fetched = await builtinCatalogProvider.getBlock(first.id);
expect(fetched.id).toBe(first.id);
expect(fetched.payload).toBe(first.payload);
});
});
describe('F4: Registry-Anbindung', () => {
afterEach(() => {
pluginRegistry.unregisterV2(builtinLibraryPlugin.manifest.id);
});
it('Plugin registriert Provider, enableV2 macht ihn sichtbar, disableV2 entfernt ihn', () => {
pluginRegistry.registerV2(builtinLibraryPlugin);
pluginRegistry.enableV2(builtinLibraryPlugin.manifest.id);
const providers = pluginRegistry.getLibraryProviders();
expect(providers.some(p => p.id === 'builtin-catalog')).toBe(true);
pluginRegistry.disableV2(builtinLibraryPlugin.manifest.id);
expect(pluginRegistry.getLibraryProviders().some(p => p.id === 'builtin-catalog')).toBe(false);
});
});
describe('F4: user-library Provider', () => {
it('mappt zeichnungsscoped Blöcke auf LibBlock mit Thumbnail+Payload', async () => {
const provider = makeUserLibraryProvider([
{
id: 'b1', drawing_id: 'd1', name: 'Bühne A', description: null, category: 'stage',
elements_json: JSON.stringify([
{ id: 'e1', type: 'rect', layerId: 'layer-0', x: 0, y: 0, width: 100, height: 50, properties: { fill: '#3a3a4a' } },
]),
thumbnail: null,
},
]);
const folders = await provider.listFolders();
expect(folders.length).toBe(1);
const blocks = await provider.listBlocks(folders[0].id);
expect(blocks).toHaveLength(1);
expect(blocks[0].name).toBe('Bühne A');
expect(blocks[0].thumbnail).toContain('<svg');
const els = JSON.parse(blocks[0].payload as string);
expect(els[0].id).toBe('e1');
expect((await provider.getBlock(blocks[0].id)).name).toBe('Bühne A');
});
});
describe('F4: global-library Provider', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('lädt Ordner und Blöcke der globalen Bibliothek über die API', async () => {
const mockFetch = vi.fn()
.mockResolvedValueOnce({ ok: true, json: async () => [
{ id: 'gf1', name: 'Event', parent_id: null, created_at: '2026-01-01' },
] })
.mockResolvedValueOnce({ ok: true, json: async () => [
{ id: 'gb1', folder_id: 'gf1', name: 'Stuhl-Reihe', block_data: '[]', svg_data: null, is_favorite: false, created_at: '2026-01-01', updated_at: '2026-01-01' },
] });
vi.stubGlobal('fetch', mockFetch);
const provider = makeGlobalLibraryProvider('tok');
const folders = await provider.listFolders();
expect(folders).toHaveLength(1);
expect(folders[0].label).toBe('Event');
const blocks = await provider.listBlocks('gf1');
expect(blocks).toHaveLength(1);
expect(blocks[0].name).toBe('Stuhl-Reihe');
});
});
describe('F4: LibraryProviderPanel UI', () => {
it('rendert Provider-Sektion mit Katalog-Ordnern und Blöcken', async () => {
render(<LibraryProviderPanel providers={[builtinCatalogProvider]} />);
await waitFor(() => {
expect(screen.getAllByText(/Event-Katalog/).length).toBeGreaterThan(0);
});
// Jede Sektion listet Blöcke
expect(screen.getAllByRole('button', { name: /Stuhl/ }).length).toBeGreaterThan(0);
});
it('DragStart legt die Element-Array-Payload in den Datentransfer', async () => {
const setData = vi.fn();
render(<LibraryProviderPanel providers={[builtinCatalogProvider]} />);
const blockBtn = await screen.findByRole('button', { name: /Stuhl/ });
fireEvent.dragStart(blockBtn, { dataTransfer: { setData } });
expect(setData).toHaveBeenCalledWith('text/global-block-data', expect.any(String));
const payload = JSON.parse(setData.mock.calls[0][1] as string);
expect(Array.isArray(payload)).toBe(true);
expect(payload[0].id).toBeTypeOf('string');
});
});