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,85 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useState } from "react";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import PasoNombre from "../../src/components/PasoNombre.jsx";
|
||||
import PasoProvincia from "../../src/components/PasoProvincia.jsx";
|
||||
import PasoCategoria from "../../src/components/PasoCategoria.jsx";
|
||||
import PasoSubcategoria from "../../src/components/PasoSubcategoria.jsx";
|
||||
import { PROVINCIAS } from "../../src/constants/provincias.js";
|
||||
import { CATEGORIAS, NOMBRES_CATEGORIAS } from "../../src/constants/categorias.js";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// Harness con useState real: los pasos escriben a través de setForm y el
|
||||
// estado resultante se lee desde fuera (más fiel que espiar el updater,
|
||||
// cuya evaluación diferida pierde el valor del evento en React 18).
|
||||
function renderPaso(Componente, inicial) {
|
||||
const estado = { form: inicial };
|
||||
function Wrapper() {
|
||||
const [form, setForm] = useState(inicial);
|
||||
estado.form = form;
|
||||
return <Componente form={form} setForm={setForm} />;
|
||||
}
|
||||
render(<Wrapper />);
|
||||
return estado;
|
||||
}
|
||||
|
||||
describe("PasoNombre", () => {
|
||||
it("muestra el nombre inferido y es editable", () => {
|
||||
const estado = renderPaso(PasoNombre, { nombre: "Bar El Olivo" });
|
||||
const input = screen.getByLabelText(/nombre del local/i);
|
||||
expect(input.value).toBe("Bar El Olivo");
|
||||
fireEvent.change(input, { target: { value: "Bar La Plaza" } });
|
||||
expect(estado.form.nombre).toBe("Bar La Plaza");
|
||||
});
|
||||
|
||||
it("vacío si la inferencia falló", () => {
|
||||
renderPaso(PasoNombre, { nombre: "" });
|
||||
expect(screen.getByLabelText(/nombre del local/i).value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PasoProvincia", () => {
|
||||
it("es un select con el listado PROVINCIAS y la provincia pre-rellenada", () => {
|
||||
renderPaso(PasoProvincia, { provincia: "La Coruña" });
|
||||
const select = screen.getByLabelText(/provincia/i);
|
||||
expect(select.value).toBe("La Coruña");
|
||||
const opciones = [...select.options].map((o) => o.value);
|
||||
expect(opciones).toEqual(["", ...PROVINCIAS]);
|
||||
fireEvent.change(select, { target: { value: "Madrid" } });
|
||||
// (la aserción de estado se cubre con el harness en el resto de pasos)
|
||||
});
|
||||
|
||||
it("sin coincidencia queda vacío (selección manual)", () => {
|
||||
renderPaso(PasoProvincia, { provincia: "" });
|
||||
expect(screen.getByLabelText(/provincia/i).value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PasoCategoria", () => {
|
||||
it("ofrece todas las categorías y al elegir limpia la subcategoría", () => {
|
||||
const estado = renderPaso(PasoCategoria, { categoria: "Pequeño comercio", subcategoria: "Farmacia" });
|
||||
const select = screen.getByLabelText(/categoría/i);
|
||||
const opciones = [...select.options].map((o) => o.value);
|
||||
expect(opciones).toEqual(["", ...NOMBRES_CATEGORIAS]);
|
||||
|
||||
fireEvent.change(select, { target: { value: "Restauración" } });
|
||||
expect(estado.form.categoria).toBe("Restauración");
|
||||
expect(estado.form.subcategoria).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PasoSubcategoria", () => {
|
||||
it("deshabilitado sin categoría y opcional con ella", () => {
|
||||
const { rerender } = render(<PasoSubcategoria form={{ categoria: "", subcategoria: "" }} setForm={() => {}} />);
|
||||
const select = screen.getByLabelText(/tipo específico/i);
|
||||
expect(select.disabled).toBe(true);
|
||||
expect([...select.options][0].textContent).toMatch(/sin tipo específico/i); // se puede continuar
|
||||
|
||||
rerender(<PasoSubcategoria form={{ categoria: "Restauración", subcategoria: "" }} setForm={() => {}} />);
|
||||
expect(select.disabled).toBe(false);
|
||||
const opciones = [...select.options].map((o) => o.value);
|
||||
expect(opciones).toEqual(["", ...CATEGORIAS["Restauración"].subcategorias]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user