import '@angular/compiler'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; const sourcePath = resolve( __dirname, '../../frontend/src/app/features/admin/blog/blog-form-modal.component.ts', ); const source = readFileSync(sourcePath, 'utf8'); describe('BlogFormModalComponent — Escape-to-close contract', () => { it('imports HostListener from @angular/core', () => { const importBlock = source.match( /import\s*\{[\s\S]*?\}\s*from\s*'@angular\/core';/, ); expect(importBlock).not.toBeNull(); expect(importBlock![0]).toMatch(/\bHostListener\b/); }); it('registers a @HostListener for document:keydown.escape', () => { expect(source).toMatch( /@HostListener\(\s*['"]document:keydown\.escape['"]\s*\)/, ); }); it('guards the Escape handler with open() so a closed modal is a no-op', () => { const handler = extractEscapeHandler(source); expect(handler).not.toBeNull(); expect(handler).toMatch(/this\.open\(\)/); }); it('guards the Escape handler against in-flight saves (no cancel while saving)', () => { const handler = extractEscapeHandler(source); expect(handler).not.toBeNull(); expect(handler).toMatch(/!this\.saving\(\)/); }); it('routes Escape to the existing onCancel() so the parent closes the modal without saving', () => { const handler = extractEscapeHandler(source); expect(handler).not.toBeNull(); expect(handler).toMatch(/this\.onCancel\(\)/); }); it('keeps the existing onCancel() emit intact as a regression guard', () => { const onCancel = extractMethod(source, 'onCancel'); expect(onCancel).not.toBeNull(); expect(onCancel).toMatch(/this\.cancel\.emit\(\s*\)/); }); }); function extractEscapeHandler(src: string): string | null { // Capture the onEscape handler body — must contain the guard + onCancel call. const m = src.match( /@HostListener\(\s*['"]document:keydown\.escape['"]\s*\)\s*onEscape\s*\(\s*\)\s*:\s*void\s*\{([^}]*)\}/, ); return m ? m[1] : null; } function extractMethod(src: string, name: string): string | null { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Match the method header (with optional TypeScript return-type annotation) and capture the body. const re = new RegExp( `(?:public\\s+|private\\s+|protected\\s+)?${escaped}\\s*\\([^)]*\\)\\s*(?::\\s*[A-Za-z_<>|[\\]\\s,]+)?\\s*\\{([\\s\\S]*?)\\n\\s*\\}`, ); const m = src.match(re); return m ? m[1] : null; }