import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { App } from './app'; describe('App studio flow', () => { let fixture: ComponentFixture; beforeEach(async () => { vi.stubGlobal('crypto', { randomUUID: vi.fn(() => `image-${Math.random()}`) }); vi.stubGlobal('fetch', vi.fn()); vi.stubGlobal('URL', { createObjectURL: vi.fn((file: File) => `blob:${file.name}`), revokeObjectURL: vi.fn(), }); await TestBed.configureTestingModule({ imports: [App], }).compileComponents(); fixture = TestBed.createComponent(App); fixture.detectChanges(); }); afterEach(() => { vi.unstubAllGlobals(); }); it('shows the empty starting state', () => { expect(text()).toContain('Maak een menu-afbeelding.'); expect(fixture.nativeElement.querySelector('.primary').disabled).toBe(true); expect(fixture.nativeElement.querySelectorAll('.image-card')).toHaveLength(0); }); it('loads multiple images from the picker without showing result cards yet', () => { selectFiles([ new File(['one'], 'broodje.png', { type: 'image/png' }), new File(['two'], 'koffie.jpg', { type: 'image/jpeg' }), new File(['skip'], 'artikelen.csv', { type: 'text/csv' }), ]); expect(text()).not.toContain('artikelen.csv'); expect(fixture.nativeElement.querySelectorAll('.image-card')).toHaveLength(0); expect(fixture.nativeElement.querySelector('.primary').disabled).toBe(false); }); it('updates settings, generates variants and shows the generated result', async () => { mockCompletedGenerationJob('job-1', { images: [ { originalName: 'cola.webp', variants: [ { label: 'FLUX.2 Flex', imageBase64: 'data:image/png;base64,generated', }, ], }, ], }); selectFiles([new File(['one'], 'cola.webp', { type: 'image/webp' })]); const selects = fixture.nativeElement.querySelectorAll('select') as NodeListOf; selects[0].value = 'premium'; selects[0].dispatchEvent(new Event('change')); selects[1].value = '9:16'; selects[1].dispatchEvent(new Event('change')); const textarea = fixture.nativeElement.querySelector('textarea') as HTMLTextAreaElement; textarea.value = 'Maak een realistische koeling achtergrond'; textarea.dispatchEvent(new Event('input')); fixture.detectChanges(); click('.primary'); expect(text()).toContain('Beelden maken...'); await waitForGeneration(); fixture.detectChanges(); expect(text()).toContain('Maak nieuwe beelden'); expect(text()).toContain('cola.webp'); expect(text()).toContain('9:16'); expect(text()).toContain('FLUX.2 Flex beeld is klaar voor controle.'); expect(fixture.nativeElement.querySelector('.generated img').src).toBe('data:image/png;base64,generated'); expect(fetch).toHaveBeenCalledWith('/api/generations', expect.objectContaining({ method: 'POST' })); expect(fetch).toHaveBeenCalledWith('/api/generations/job-1'); }); it('submits menu mode with multiple products', async () => { mockCompletedGenerationJob('job-menu', { images: [ { originalName: 'burger.jpg', variants: [{ label: 'Menu deal', imageBase64: 'data:image/png;base64,menu' }], }, ], }); selectFiles([ new File(['burger'], 'burger.jpg', { type: 'image/jpeg' }), new File(['cola'], 'cola-bottle.jpg', { type: 'image/jpeg' }), new File(['fries'], 'fries.jpg', { type: 'image/jpeg' }), ]); click('.primary'); await waitForGeneration(); fixture.detectChanges(); const form = vi.mocked(fetch).mock.calls[0][1]?.body as FormData; expect(form.get('mode')).toBe('menu'); expect(form.getAll('images')).toHaveLength(3); expect(text()).toContain('Menu deal beeld is klaar voor controle.'); }); it('shows an error when backend generation fails', async () => { vi.mocked(fetch).mockResolvedValue(new Response(undefined, { status: 502 })); selectFiles([new File(['one'], 'cola.webp', { type: 'image/webp' })]); click('.primary'); await waitForGeneration(); fixture.detectChanges(); expect(text()).toContain('De beeldgenerator kon het verzoek niet verwerken:'); }); it('shows an error when an async job fails', async () => { vi.mocked(fetch).mockResolvedValueOnce(new Response( JSON.stringify({ jobId: 'job-failed', provider: 'kaggle', status: 'queued' }), { status: 202, headers: { 'Content-Type': 'application/json' } }, )); vi.mocked(fetch).mockResolvedValueOnce(new Response( JSON.stringify({ jobId: 'job-failed', provider: 'kaggle', status: 'failed', error: 'Kaggle failed' }), { status: 200, headers: { 'Content-Type': 'application/json' } }, )); selectFiles([new File(['one'], 'cola.webp', { type: 'image/webp' })]); click('.primary'); await waitForGeneration(); fixture.detectChanges(); expect(text()).toContain('De beeldgenerator kon het verzoek niet verwerken: Kaggle failed'); }); it('keeps selected images hidden until a result is generated', () => { selectFiles([new File(['one'], 'pizza.png', { type: 'image/png' })]); expect(text()).not.toContain('pizza.png'); expect(fixture.nativeElement.querySelectorAll('.image-card')).toHaveLength(0); }); function mockCompletedGenerationJob(jobId: string, result: object): void { vi.mocked(fetch).mockResolvedValueOnce(new Response( JSON.stringify({ jobId, provider: 'kaggle', status: 'queued' }), { status: 202, headers: { 'Content-Type': 'application/json' } }, )); vi.mocked(fetch).mockResolvedValueOnce(new Response( JSON.stringify({ jobId, provider: 'kaggle', status: 'completed', result }), { status: 200, headers: { 'Content-Type': 'application/json' } }, )); } function selectFiles(files: File[]): void { const input = fixture.nativeElement.querySelector('input[type="file"]'); Object.defineProperty(input, 'files', { configurable: true, value: files, }); input.dispatchEvent(new Event('change')); fixture.detectChanges(); } function click(selector: string): void { fixture.debugElement.query(By.css(selector)).nativeElement.click(); fixture.detectChanges(); } function text(): string { return fixture.nativeElement.textContent.replace(/\s+/g, ' ').trim(); } async function waitForGeneration(): Promise { await fixture.whenStable(); await new Promise((resolve) => window.setTimeout(resolve)); await fixture.whenStable(); } });