import { computed, signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideRouter, Router } from '@angular/router'; import { of } from 'rxjs'; import { App } from './app'; import { AuthService } from './core/services/auth/auth.service'; import { CartService } from './core/services/cart/cart.service'; import { Tenant } from './core/services/tenant.interface'; import { TenantService } from './core/services/tenant.service'; import { routes } from './app.routes'; const tenant: Tenant = { id: 1, codigo: 'test', nombre: 'Test Tenant', dominio: 'localhost', primary_color: '#6376F3', secondary_color: '#A0A0A0', danger_color: '#FF8888', success_color: '#198754', header_bg_color: '#313131', footer_bg_color: '#313131', header_logo: 'https://example.com/header.png', footer_logo: 'https://example.com/footer.png', categories: [] }; function createTenantServiceStub( status: 'ready' | 'not-found' = 'ready', currentTenant: Tenant | null = tenant ) { const tenantState = signal(currentTenant); const statusState = signal(status); return { tenant: tenantState.asReadonly(), status: statusState.asReadonly(), getTenant: () => tenantState(), getTenantApiUrl: () => { const tenantVal = tenantState(); if (!tenantVal) { throw new Error('No se pudo resolver el tenant activo.'); } return `http://localhost:8000/api/tenants/${tenantVal.codigo}`; }, bootstrap: vi.fn().mockResolvedValue(undefined) }; } function createAuthServiceStub(isAuthenticated = false) { const tokenState = signal(isAuthenticated ? 'test-token' : null); const userState = signal( isAuthenticated ? { id: 1, nombre_apellido: 'Ada Lovelace', email: 'ada@example.com' } : null ); return { user: userState.asReadonly(), token: tokenState.asReadonly(), isAuthenticated: computed(() => tokenState() !== null), login: vi.fn(), register: vi.fn(), logout: vi.fn(), loadCurrentUser: vi.fn(), hydrateSession: vi.fn(), bootstrap: vi.fn().mockResolvedValue(undefined), clearSession: vi.fn(() => { userState.set(null); tokenState.set(null); }) }; } async function renderAppAt( url: string, tenantService: ReturnType = createTenantServiceStub(), authService: ReturnType = createAuthServiceStub() ) { TestBed.resetTestingModule(); await TestBed.configureTestingModule({ imports: [App], providers: [ provideRouter(routes), { provide: TenantService, useValue: tenantService }, { provide: CartService, useValue: { cart: signal(null).asReadonly(), loadCart: () => of({ id: 1, items: [], subtotal: '0' }) } }, { provide: AuthService, useValue: authService } ] }).compileComponents(); const router = TestBed.inject(Router); const fixture = TestBed.createComponent(App); fixture.detectChanges(); await router.navigateByUrl(url); await fixture.whenStable(); fixture.detectChanges(); return { fixture, router }; } describe('app routes', () => { it('loads the store layout at /', async () => { const { fixture, router } = await renderAppAt('/'); const compiled = fixture.nativeElement as HTMLElement; expect(router.url).toBe('/'); expect(compiled.querySelector('.store-layout')).not.toBeNull(); expect(compiled.querySelector('app-store-home-page')).not.toBeNull(); expect(compiled.textContent).toContain('Productos'); expect(compiled.style.getPropertyValue('--tenant-primary')).toBe(tenant.primary_color); }); it('loads componentes-test under a valid tenant', async () => { const { fixture, router } = await renderAppAt('/'); await router.navigateByUrl('/componentes-test/reutilizables'); await fixture.whenStable(); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(router.url).toBe('/componentes-test/reutilizables'); expect(compiled.textContent).toContain('Componentes reutilizables'); }); it('loads the login page at /login and shows the create account CTA', async () => { const { fixture, router } = await renderAppAt('/login'); const compiled = fixture.nativeElement as HTMLElement; expect(router.url).toBe('/login'); expect(compiled.querySelector('app-login-page')).not.toBeNull(); expect(compiled.textContent).toContain('Iniciar sesion'); expect(compiled.textContent).toContain('Crear cuenta'); }); it('loads the register page at /register and renders the shared auth fields', async () => { const { fixture, router } = await renderAppAt('/register'); const compiled = fixture.nativeElement as HTMLElement; const registerPage = compiled.querySelector('app-register-page'); const placeholders = Array.from(registerPage?.querySelectorAll('input') ?? []).map((input) => input.getAttribute('placeholder') ); expect(router.url).toBe('/register'); expect(registerPage).not.toBeNull(); expect(compiled.textContent).toContain('CREAR CUENTA'); expect(compiled.textContent).toContain('Volver'); expect(placeholders).toEqual([ 'Nombre y Apellido', 'Email', 'Contraseña', 'Repetir Contraseña' ]); }); it('loads the recover password page inside the simple layout', async () => { const { fixture, router } = await renderAppAt('/recuperar-contrasena'); const compiled = fixture.nativeElement as HTMLElement; const recoverPasswordPage = compiled.querySelector('app-recover-password-page'); expect(router.url).toBe('/recuperar-contrasena'); expect(compiled.querySelector('.simple-layout')).not.toBeNull(); expect(recoverPasswordPage).not.toBeNull(); expect(recoverPasswordPage?.textContent).toContain('Recuperar contraseña'); expect(recoverPasswordPage?.querySelector('input')?.getAttribute('placeholder')).toBe('Email'); expect(recoverPasswordPage?.textContent).toContain('Recuperar acceso'); expect(recoverPasswordPage?.textContent).toContain('Volver'); }); it('loads the recovery code page and shows the entered email', async () => { const { fixture, router } = await renderAppAt( '/recuperar-contrasena/codigo?email=ada%40example.com' ); const compiled = fixture.nativeElement as HTMLElement; const codePage = compiled.querySelector('app-recover-password-code-page'); expect(router.url).toBe('/recuperar-contrasena/codigo?email=ada%40example.com'); expect(compiled.querySelector('.simple-layout')).not.toBeNull(); expect(codePage).not.toBeNull(); expect(codePage?.textContent).toContain('ada@example.com'); expect(codePage?.querySelectorAll('app-input')).toHaveLength(4); expect(codePage?.textContent).toContain('Validar'); }); it('loads the reset password page with the shared password inputs', async () => { const { fixture, router } = await renderAppAt( '/recuperar-contrasena/restablecer?email=ada%40example.com&code=1234' ); const compiled = fixture.nativeElement as HTMLElement; const resetPage = compiled.querySelector('app-reset-password-page'); const placeholders = Array.from(resetPage?.querySelectorAll('input') ?? []).map((input) => input.getAttribute('placeholder') ); expect(router.url).toBe( '/recuperar-contrasena/restablecer?email=ada%40example.com&code=1234' ); expect(compiled.querySelector('.simple-layout')).not.toBeNull(); expect(resetPage).not.toBeNull(); expect(resetPage?.textContent).toContain('Restablecer contraseña'); expect(placeholders).toEqual(['Nueva Contraseña', 'Repetir Nueva Contraseña']); expect(resetPage?.textContent).toContain('Guardar'); }); it('redirects authenticated users away from /login', async () => { const { router } = await renderAppAt('/login', createTenantServiceStub(), createAuthServiceStub(true)); expect(router.url).toBe('/'); }); it('renders the tenant not found screen for any route when the tenant is missing', async () => { const { fixture } = await renderAppAt( '/componentes-test/reutilizables', createTenantServiceStub('not-found', null), createAuthServiceStub() ); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('.tenant-status')).not.toBeNull(); expect(compiled.textContent).toContain('No encontramos una tienda para este dominio'); }); it('redirects unauthenticated users from /checkout to /login', async () => { const { router } = await renderAppAt('/checkout', createTenantServiceStub(), createAuthServiceStub(false)); expect(router.url).toBe('/login?returnUrl=%2Fcheckout'); }); it('allows authenticated users to access /checkout', async () => { const { router } = await renderAppAt('/checkout', createTenantServiceStub(), createAuthServiceStub(true)); expect(router.url).toBe('/checkout'); }); });