Files
reactbin/ui/src/app/detail/detail.component.spec.ts
agatha ebfef1b783
Some checks failed
Pipeline / UI Lint (push) Successful in 57s
Pipeline / API Unit Tests (push) Failing after 3s
Pipeline / API Lint (push) Failing after 2s
Pipeline / API Integration Tests (push) Failing after 0s
Pipeline / UI Tests (push) Successful in 1m28s
Pipeline / Build & Push API Image (push) Has been skipped
Pipeline / Build & Push UI Image (push) Has been skipped
Fix: Clean up lint errors introduced in test fixes
- Remove unused NEVER import from detail.component.spec.ts
- Replace `null as unknown as ImageRecord` with `null as unknown as typeof MOCK_IMAGE`
  to match the narrower inferred type (thumbnail_key: null) that setup() expects

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

195 lines
8.1 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 { ToastService } from '../services/toast.service';
import { routes } from '../app.routes';
const MOCK_IMAGE = {
id: 'img-1', short_id: 'AbCd1234', hash: 'abc', filename: 'test.jpg', mime_type: 'image/jpeg',
size_bytes: 100, width: 10, height: 10, storage_key: 'AbCd1234',
thumbnail_key: null, file_url: '/api/v1/i/AbCd1234/file', thumbnail_url: null,
created_at: '2026-01-01T00:00:00Z', tags: ['cat', 'funny'],
};
const MOCK_IMAGE_ABS = { ...MOCK_IMAGE, file_url: 'https://cdn.example.com/img-1.jpg' };
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('AbCd1234', ['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('AbCd1234', ['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('AbCd1234');
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', () => {
// Service returns null → fetchImage sets image=null, loading=false, markForCheck()
const { fixture } = setup('img-1', of(null as unknown as typeof MOCK_IMAGE));
expect((fixture.nativeElement as HTMLElement).querySelector('.not-found-card')).not.toBeNull();
});
it('tag error element uses danger styling class', () => {
const { fixture, component, imgSvc } = setup();
spyOn(imgSvc, 'updateTags').and.returnValue(throwError(() => ({ error: { detail: 'Invalid tag' } })));
component.addTag('bad#tag');
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);
});
// ---- Copy URL ----
it('Copy URL button is present when image is loaded', () => {
const { fixture } = setup();
expect((fixture.nativeElement as HTMLElement).querySelector('.copy-url-btn')).not.toBeNull();
});
it('copyUrl() calls writeText with file_url when it is already absolute', async () => {
const { component } = setup('img-1', of(MOCK_IMAGE_ABS));
const toast = TestBed.inject(ToastService);
spyOn(toast, 'show');
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
component.copyUrl();
await Promise.resolve();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(MOCK_IMAGE_ABS.file_url);
});
it('copyUrl() prepends window.location.origin when file_url is relative', async () => {
const { component } = setup();
const toast = TestBed.inject(ToastService);
spyOn(toast, 'show');
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
component.copyUrl();
await Promise.resolve();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(window.location.origin + MOCK_IMAGE.file_url);
});
it('copyUrl() calls toast.show with success message on writeText resolve', async () => {
const { component } = setup();
const toast = TestBed.inject(ToastService);
spyOn(toast, 'show');
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
component.copyUrl();
await Promise.resolve();
expect(toast.show).toHaveBeenCalledWith('URL copied!');
});
it('copyUrl() calls toast.show with error message on writeText reject', async () => {
const { component } = setup();
const toast = TestBed.inject(ToastService);
spyOn(toast, 'show');
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.reject(new Error('denied')));
component.copyUrl();
await Promise.resolve();
await Promise.resolve();
expect(toast.show).toHaveBeenCalledWith('Failed to copy URL', 'error');
});
});