Spaces:
Sleeping
Sleeping
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |
| import { By } from '@angular/platform-browser'; | |
| import { App } from './app'; | |
| describe('App studio flow', () => { | |
| let fixture: ComponentFixture<App>; | |
| 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<HTMLSelectElement>; | |
| 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 = <HTMLInputElement>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<void> { | |
| await fixture.whenStable(); | |
| await new Promise((resolve) => window.setTimeout(resolve)); | |
| await fixture.whenStable(); | |
| } | |
| }); | |