Files
reactbin/ui/src/app/detail/detail.component.spec.ts
agatha aaacfae653 Feat: Serve images directly from Cloudflare R2 CDN
API responses now include file_url and thumbnail_url fields. When
S3_PUBLIC_BASE_URL is configured, these point to the CDN domain;
when unset, they fall back to the existing API proxy paths so local
dev requires no additional setup. UI updated to use response URL
fields directly instead of constructing proxy URLs client-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:17:22 +00:00

147 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, provideRouter, Router } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { of, throwError, Subject } from 'rxjs';
import { DetailComponent } from './detail.component';
import { ImageService } from '../services/image.service';
import { routes } from '../app.routes';
const MOCK_IMAGE = {
id: 'img-1', hash: 'abc', filename: 'test.jpg', mime_type: 'image/jpeg',
size_bytes: 100, width: 10, height: 10, storage_key: 'abc',
thumbnail_key: null, file_url: '/api/v1/images/img-1/file', thumbnail_url: null,
created_at: '2026-01-01T00:00:00Z', tags: ['cat', 'funny'],
};
describe('DetailComponent', () => {
function setup(imageId = 'img-1', imageResponse = of(MOCK_IMAGE)) {
TestBed.configureTestingModule({
imports: [DetailComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
provideRouter(routes),
{ provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: () => imageId } } } },
],
}).compileComponents();
const fixture = TestBed.createComponent(DetailComponent);
const imgSvc = TestBed.inject(ImageService);
spyOn(imgSvc, 'get').and.returnValue(imageResponse);
fixture.detectChanges();
return { fixture, component: fixture.componentInstance, imgSvc };
}
// Existing tests preserved
it('should call PATCH with removed tag absent when chip × is clicked', () => {
const { component, imgSvc } = setup();
spyOn(imgSvc, 'updateTags').and.returnValue(of({ ...MOCK_IMAGE, tags: ['funny'] }));
component.removeTag('cat');
expect(imgSvc.updateTags).toHaveBeenCalledWith('img-1', ['funny']);
});
it('should call PATCH with new tag included on addTag', () => {
const { component, imgSvc } = setup();
spyOn(imgSvc, 'updateTags').and.returnValue(of({ ...MOCK_IMAGE, tags: ['cat', 'funny', 'new'] }));
component.addTag('new');
expect(imgSvc.updateTags).toHaveBeenCalledWith('img-1', ['cat', 'funny', 'new']);
});
it('should call DELETE and navigate to library on confirm delete', () => {
const { component, imgSvc } = setup();
const router = TestBed.inject(Router);
spyOn(router, 'navigate');
spyOn(imgSvc, 'delete').and.returnValue(of(undefined));
component.confirmDelete();
expect(imgSvc.delete).toHaveBeenCalledWith('img-1');
expect(router.navigate).toHaveBeenCalledWith(['/']);
});
it('should NOT call DELETE when cancel is clicked', () => {
const { component, imgSvc } = setup();
spyOn(imgSvc, 'delete').and.returnValue(of(undefined));
component.showDeleteDialog = true;
component.cancelDelete();
expect(imgSvc.delete).not.toHaveBeenCalled();
expect(component.showDeleteDialog).toBeFalse();
});
it('back button should navigate to library', () => {
const { component } = setup();
const router = TestBed.inject(Router);
spyOn(router, 'navigate');
component.goBack();
expect(router.navigate).toHaveBeenCalledWith(['/']);
});
// New polish tests
it('skeleton is visible while loading is true', () => {
TestBed.configureTestingModule({
imports: [DetailComponent],
providers: [
provideHttpClient(), provideHttpClientTesting(), provideRouter(routes),
{ provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: () => 'img-1' } } } },
],
}).compileComponents();
const fixture = TestBed.createComponent(DetailComponent);
const imgSvc = TestBed.inject(ImageService);
// Don't emit — keep loading state
spyOn(imgSvc, 'get').and.returnValue(new Subject());
fixture.componentInstance.loading = true;
fixture.detectChanges();
expect((fixture.nativeElement as HTMLElement).querySelector('.image-skeleton')).not.toBeNull();
});
it('error card shown when error is true and loading is false', () => {
const fixture = (() => {
TestBed.configureTestingModule({
imports: [DetailComponent],
providers: [
provideHttpClient(), provideHttpClientTesting(), provideRouter(routes),
{ provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: () => 'img-1' } } } },
],
}).compileComponents();
return TestBed.createComponent(DetailComponent);
})();
const imgSvc = TestBed.inject(ImageService);
spyOn(imgSvc, 'get').and.returnValue(throwError(() => ({ status: 500 })));
fixture.detectChanges();
expect((fixture.nativeElement as HTMLElement).querySelector('.fetch-error-card')).not.toBeNull();
});
it('not-found card shown when image is null, loading is false, error is false', () => {
const { fixture, component } = setup('img-1', of(MOCK_IMAGE));
component.image = null;
component.loading = false;
component.error = false;
fixture.detectChanges();
expect((fixture.nativeElement as HTMLElement).querySelector('.not-found-card')).not.toBeNull();
});
it('tag error element uses danger styling class', () => {
const { fixture, component } = setup();
component.tagError = 'Invalid tag: special characters not allowed';
fixture.detectChanges();
const errEl = (fixture.nativeElement as HTMLElement).querySelector('.tag-error');
expect(errEl).not.toBeNull();
});
it('onImgError sets src to placeholder SVG', () => {
const { fixture } = setup();
const imgEl = document.createElement('img');
imgEl.src = 'http://example.com/image.jpg';
fixture.componentInstance.onImgError({ target: imgEl } as unknown as Event);
expect(imgEl.src).toContain('data:image/svg+xml');
});
it('onImgError does not recurse when src already is a data URI', () => {
const { fixture } = setup();
const imgEl = document.createElement('img');
imgEl.src = 'data:image/svg+xml,placeholder';
const before = imgEl.src;
fixture.componentInstance.onImgError({ target: imgEl } as unknown as Event);
expect(imgEl.src).toBe(before);
});
});