248 lines
8.9 KiB
TypeScript
248 lines
8.9 KiB
TypeScript
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<string | null>(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<typeof createTenantServiceStub> = createTenantServiceStub(),
|
|
authService: ReturnType<typeof createAuthServiceStub> = 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');
|
|
});
|
|
});
|