feat(propuesta): pasarela paso a paso con autodetección de ubicación y FAB
- Campo universal de ubicación (Google Maps/Waze/OSM/coordenadas/dirección) sobre proveedores extensibles (src/utils/ubicacion) con resolver e inferencia inversa Nominatim (nombre/provincia pre-rellenados, alias cooficiales) - FAB flotante abajo a la derecha que abre la pasarela en un modal accesible (Escape, foco contenido, responsive); sin descripción ni puntuación; avisos de duplicado y filtro de palabras dentro del modal - Aviso PWA reposicionado: tarjeta abajo a la izquierda en móvil, botón compacto arriba a la izquierda en PC (no tapa el FAB) - Suite de pruebas: 88 unitarias/componente (vitest + RTL, offline) y 10 de integración (supertest + SQLite :memory: y Nominatim real opt-in) - CI: workflow de Gitea Actions con npm test + build en push/PR e integración manual
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import PasoResumen from "../../src/components/PasoResumen.jsx";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); // los mocks de propsBase se comparten entre pruebas
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
const FORM_BASE = {
|
||||
nombre: "Bar El Olivo", provincia: "Madrid", categoria: "Restauración", subcategoria: "Tapas y raciones",
|
||||
direccion: "Calle Mayor 5, Madrid", enlaceGoogleMaps: "", lat: 40.4168, lng: -3.7038,
|
||||
entradaUbicacion: "40.4168, -3.7038", proveedorUbicacion: "coordenadas",
|
||||
};
|
||||
|
||||
const propsBase = {
|
||||
form: FORM_BASE,
|
||||
aceptaPrivacidad: false,
|
||||
setAceptaPrivacidad: vi.fn(),
|
||||
onAbrirPrivacidad: vi.fn(),
|
||||
onEnviar: vi.fn(),
|
||||
onConfirmarDuplicado: vi.fn(),
|
||||
onDescartarDuplicado: vi.fn(),
|
||||
errorFiltro: "",
|
||||
posibleDuplicado: null,
|
||||
comprobandoDuplicado: false,
|
||||
saving: false,
|
||||
};
|
||||
|
||||
describe("PasoResumen", () => {
|
||||
it("muestra un resumen legible de todos los datos", () => {
|
||||
render(<PasoResumen {...propsBase} />);
|
||||
expect(screen.getByText("Bar El Olivo")).toBeTruthy();
|
||||
expect(screen.getByText("Madrid")).toBeTruthy();
|
||||
expect(screen.getByText(/🍽️ Restauración/)).toBeTruthy();
|
||||
expect(screen.getByText("Tapas y raciones")).toBeTruthy();
|
||||
expect(screen.getByText("Calle Mayor 5, Madrid")).toBeTruthy();
|
||||
expect(screen.getByText(/Coordenadas/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("botón de envío deshabilitado sin aceptar la privacidad", () => {
|
||||
render(<PasoResumen {...propsBase} />);
|
||||
const boton = screen.getByRole("button", { name: /enviar propuesta/i });
|
||||
expect(boton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("al marcar la aceptación se habilita el envío y dispara onEnviar", () => {
|
||||
render(<PasoResumen {...propsBase} />);
|
||||
fireEvent.click(screen.getByRole("checkbox"));
|
||||
expect(propsBase.setAceptaPrivacidad).toHaveBeenCalledWith(true);
|
||||
|
||||
// con aceptación ya aplicada por el padre
|
||||
cleanup();
|
||||
render(<PasoResumen {...propsBase} aceptaPrivacidad />);
|
||||
const boton = screen.getByRole("button", { name: /enviar propuesta/i });
|
||||
expect(boton.disabled).toBe(false);
|
||||
fireEvent.click(boton);
|
||||
expect(propsBase.onEnviar).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("el enlace de privacidad abre el modal sin marcar el checkbox", () => {
|
||||
render(<PasoResumen {...propsBase} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /política de privacidad/i }));
|
||||
expect(propsBase.onAbrirPrivacidad).toHaveBeenCalledTimes(1);
|
||||
expect(propsBase.setAceptaPrivacidad).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renderiza el aviso de palabra prohibida dentro del modal", () => {
|
||||
render(<PasoResumen {...propsBase} errorFiltro="Tu propuesta contiene palabras no permitidas." />);
|
||||
expect(screen.getByRole("alert").textContent).toContain("palabras no permitidas");
|
||||
});
|
||||
|
||||
it("renderiza el aviso de duplicado con las opciones del flujo", () => {
|
||||
render(
|
||||
<PasoResumen
|
||||
{...propsBase}
|
||||
posibleDuplicado={{
|
||||
coincidencias: [{ id: "1", nombre: "Bar El Olivo", provincia: "Madrid", direccion: "", estado: "publicado" }],
|
||||
lat: 40.4168, lng: -3.7038,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const aviso = screen.getByRole("alert");
|
||||
expect(aviso.textContent).toContain("Ya existe un local parecido");
|
||||
expect(aviso.textContent).toContain("Bar El Olivo");
|
||||
fireEvent.click(screen.getByRole("button", { name: /enviar de todos modos/i }));
|
||||
expect(propsBase.onConfirmarDuplicado).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByRole("button", { name: /revisar datos/i }));
|
||||
expect(propsBase.onDescartarDuplicado).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("estados Comprobando…/Enviando… deshabilitan el envío", () => {
|
||||
const { rerender } = render(<PasoResumen {...propsBase} aceptaPrivacidad comprobandoDuplicado />);
|
||||
let boton = screen.getByRole("button", { name: /comprobando/i });
|
||||
expect(boton.disabled).toBe(true);
|
||||
|
||||
rerender(<PasoResumen {...propsBase} aceptaPrivacidad saving />);
|
||||
boton = screen.getByRole("button", { name: /enviando/i });
|
||||
expect(boton.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user