From c0aeec464e1a8263ad9a5536bc2d6611a3c3f800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alvaro=20Amor=C3=B3s?= <39102625+ElMaxter99@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:38:04 +0100 Subject: [PATCH 1/3] test: cover annotation templates service --- .github/workflows/ci.yml | 22 +++ src/app/annotation-templates.service.spec.ts | 136 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 src/app/annotation-templates.service.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0db68ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + pull_request: + branches: + - '*' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies + run: npm install + - name: Run unit tests + run: npm run test -- --watch=false --browsers=ChromeHeadless + env: + CHROME_BIN: /usr/bin/google-chrome diff --git a/src/app/annotation-templates.service.spec.ts b/src/app/annotation-templates.service.spec.ts new file mode 100644 index 0000000..55e1fc4 --- /dev/null +++ b/src/app/annotation-templates.service.spec.ts @@ -0,0 +1,136 @@ +import { TestBed } from '@angular/core/testing'; +import { + AnnotationTemplate, + AnnotationTemplatesService, +} from './annotation-templates.service'; +import { PageAnnotations } from './models/annotation.model'; + +const TEMPLATES_KEY = 'pdf-annotator.templates'; + +class MockStorage implements Storage { + private store = new Map(); + + get length(): number { + return this.store.size; + } + + clear(): void { + this.store.clear(); + } + + getItem(key: string): string | null { + return this.store.has(key) ? this.store.get(key)! : null; + } + + key(index: number): string | null { + return Array.from(this.store.keys())[index] ?? null; + } + + removeItem(key: string): void { + this.store.delete(key); + } + + setItem(key: string, value: string): void { + this.store.set(key, value); + } +} + +class QuotaExceededStorage extends MockStorage { + override setItem(): void { + throw new DOMException('Storage quota exceeded', 'QuotaExceededError'); + } +} + +describe('AnnotationTemplatesService', () => { + const originalLocalStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage'); + let service: AnnotationTemplatesService; + let storage: MockStorage; + + function overrideLocalStorage(value: Storage | undefined) { + Object.defineProperty(window, 'localStorage', { + configurable: true, + value, + }); + } + + beforeEach(() => { + storage = new MockStorage(); + overrideLocalStorage(storage); + + TestBed.configureTestingModule({ + providers: [AnnotationTemplatesService], + }); + + service = TestBed.inject(AnnotationTemplatesService); + }); + + afterEach(() => { + if (originalLocalStorageDescriptor) { + Object.defineProperty(window, 'localStorage', originalLocalStorageDescriptor); + } else { + overrideLocalStorage(undefined); + } + }); + + function createTemplate(id: string, name: string): AnnotationTemplate { + return { + id, + name, + createdAt: Date.now(), + pages: [], + }; + } + + it('should return the default template first even when stored templates exist', () => { + const existingTemplate = createTemplate('stored-id', 'Guardada'); + storage.setItem(TEMPLATES_KEY, JSON.stringify([existingTemplate])); + + const templates = service.getTemplates(); + + expect(templates[0].id).toBe(service.defaultTemplateId); + expect(templates[1]).toEqual(jasmine.objectContaining({ id: 'stored-id', name: 'Guardada' })); + }); + + it('should normalize template names and update existing templates', () => { + const initialPages: PageAnnotations[] = [{ num: 1, fields: [] }]; + const updatedPages: PageAnnotations[] = [{ num: 2, fields: [] }]; + + const created = service.saveTemplate(' Plantilla Personalizada ', initialPages); + expect(created).not.toBeNull(); + expect(created!.name).toBe('Plantilla Personalizada'); + + const updated = service.saveTemplate('plantilla personalizada', updatedPages); + expect(updated).not.toBeNull(); + expect(updated!.id).toBe(created!.id); + expect(updated!.pages).toEqual(updatedPages); + + const storedRaw = storage.getItem(TEMPLATES_KEY); + expect(storedRaw).toBeTruthy(); + const storedTemplates = JSON.parse(storedRaw!) as AnnotationTemplate[]; + expect(storedTemplates.length).toBe(1); + expect(storedTemplates[0].name).toBe('Plantilla Personalizada'); + expect(storedTemplates[0].pages).toEqual(updatedPages); + }); + + it('should return null when saving without localStorage support', () => { + overrideLocalStorage(undefined); + + const result = service.saveTemplate('Sin almacenamiento', []); + + expect(result).toBeNull(); + }); + + it('should handle quota exceeded errors without throwing', () => { + const quotaStorage = new QuotaExceededStorage(); + overrideLocalStorage(quotaStorage); + const warnSpy = spyOn(console, 'warn'); + + const result = service.saveTemplate('Quota', []); + + expect(result).not.toBeNull(); + expect(warnSpy).toHaveBeenCalledWith( + 'Storage quota exceeded, persistence disabled for key:', + TEMPLATES_KEY + ); + }); +}); From 2caa30700ea8b0a207f8a9dcf0a02505eaf9e016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alvaro=20Amor=C3=B3s?= <39102625+ElMaxter99@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:45:23 +0100 Subject: [PATCH 2/3] Adjust template name normalization test --- src/app/annotation-templates.service.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/annotation-templates.service.spec.ts b/src/app/annotation-templates.service.spec.ts index 55e1fc4..29bc2bf 100644 --- a/src/app/annotation-templates.service.spec.ts +++ b/src/app/annotation-templates.service.spec.ts @@ -103,12 +103,13 @@ describe('AnnotationTemplatesService', () => { expect(updated).not.toBeNull(); expect(updated!.id).toBe(created!.id); expect(updated!.pages).toEqual(updatedPages); + expect(updated!.name).toBe('plantilla personalizada'); const storedRaw = storage.getItem(TEMPLATES_KEY); expect(storedRaw).toBeTruthy(); const storedTemplates = JSON.parse(storedRaw!) as AnnotationTemplate[]; expect(storedTemplates.length).toBe(1); - expect(storedTemplates[0].name).toBe('Plantilla Personalizada'); + expect(storedTemplates[0].name).toBe('plantilla personalizada'); expect(storedTemplates[0].pages).toEqual(updatedPages); }); From 5a6919e27084553be17bc8fc28c3f4d0b2e95306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alvaro=20Amor=C3=B3s?= <39102625+ElMaxter99@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:51:35 +0100 Subject: [PATCH 3/3] Stub localStorage safely in annotation templates tests --- .github/workflows/ci.yml | 2 +- src/app/annotation-templates.service.spec.ts | 30 +++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0db68ec..b954dcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22.12.0' - name: Install dependencies run: npm install - name: Run unit tests diff --git a/src/app/annotation-templates.service.spec.ts b/src/app/annotation-templates.service.spec.ts index 29bc2bf..159528f 100644 --- a/src/app/annotation-templates.service.spec.ts +++ b/src/app/annotation-templates.service.spec.ts @@ -42,20 +42,22 @@ class QuotaExceededStorage extends MockStorage { } describe('AnnotationTemplatesService', () => { - const originalLocalStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage'); + const originalLocalStorage = window.localStorage; let service: AnnotationTemplatesService; let storage: MockStorage; + let localStorageGetterSpy!: jasmine.Spy<() => Storage>; - function overrideLocalStorage(value: Storage | undefined) { - Object.defineProperty(window, 'localStorage', { - configurable: true, - value, - }); + function stubLocalStorage(value: Storage | undefined) { + localStorageGetterSpy.and.callFake(() => value as unknown as Storage); } + beforeAll(() => { + localStorageGetterSpy = spyOnProperty(window, 'localStorage', 'get'); + }); + beforeEach(() => { storage = new MockStorage(); - overrideLocalStorage(storage); + stubLocalStorage(storage); TestBed.configureTestingModule({ providers: [AnnotationTemplatesService], @@ -65,11 +67,11 @@ describe('AnnotationTemplatesService', () => { }); afterEach(() => { - if (originalLocalStorageDescriptor) { - Object.defineProperty(window, 'localStorage', originalLocalStorageDescriptor); - } else { - overrideLocalStorage(undefined); - } + stubLocalStorage(originalLocalStorage); + }); + + afterAll(() => { + localStorageGetterSpy.and.callThrough(); }); function createTemplate(id: string, name: string): AnnotationTemplate { @@ -114,7 +116,7 @@ describe('AnnotationTemplatesService', () => { }); it('should return null when saving without localStorage support', () => { - overrideLocalStorage(undefined); + stubLocalStorage(undefined); const result = service.saveTemplate('Sin almacenamiento', []); @@ -123,7 +125,7 @@ describe('AnnotationTemplatesService', () => { it('should handle quota exceeded errors without throwing', () => { const quotaStorage = new QuotaExceededStorage(); - overrideLocalStorage(quotaStorage); + stubLocalStorage(quotaStorage); const warnSpy = spyOn(console, 'warn'); const result = service.saveTemplate('Quota', []);