- 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
128 lines
5.5 KiB
React
128 lines
5.5 KiB
React
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
|
|
vi.mock("../../src/utils/ubicacion/index.js", () => ({
|
|
ETIQUETAS_PROVEEDOR: {
|
|
"google-maps": "Google Maps",
|
|
coordenadas: "Coordenadas",
|
|
nominatim: "Dirección (OpenStreetMap)",
|
|
},
|
|
resolverUbicacion: vi.fn(),
|
|
inferirDesdeCoords: vi.fn(),
|
|
}));
|
|
|
|
import PasoUbicacion from "../../src/components/PasoUbicacion.jsx";
|
|
import { resolverUbicacion, inferirDesdeCoords } from "../../src/utils/ubicacion/index.js";
|
|
|
|
const FORM = {
|
|
nombre: "", provincia: "", categoria: "", subcategoria: "",
|
|
direccion: "", enlaceGoogleMaps: "", lat: null, lng: null,
|
|
entradaUbicacion: "", proveedorUbicacion: "",
|
|
};
|
|
|
|
// setForm que aplica los updater sobre un objeto (como haría useState) sin
|
|
// provocar re-render: suficiente para inspeccionar lo que el paso escribe.
|
|
function renderPaso(formInicial = { ...FORM }) {
|
|
const estado = { form: formInicial };
|
|
const setForm = (updater) => {
|
|
estado.form = typeof updater === "function" ? updater(estado.form) : updater;
|
|
};
|
|
render(<PasoUbicacion form={estado.form} setForm={setForm} />);
|
|
return estado;
|
|
}
|
|
|
|
const escribir = (texto) =>
|
|
fireEvent.change(screen.getByLabelText(/ubicación del local/i), { target: { value: texto } });
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
cleanup();
|
|
});
|
|
|
|
describe("PasoUbicacion", () => {
|
|
it("no resuelve antes del debounce (~600ms)", async () => {
|
|
resolverUbicacion.mockResolvedValue(null);
|
|
renderPaso();
|
|
escribir("https://maps.google.com/@41.3851,2.1734");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(300); });
|
|
expect(resolverUbicacion).not.toHaveBeenCalled();
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(400); });
|
|
expect(resolverUbicacion).toHaveBeenCalledWith("https://maps.google.com/@41.3851,2.1734");
|
|
});
|
|
|
|
it("al resolver muestra el chip con el proveedor y coordenadas y pre-rellena el form", async () => {
|
|
resolverUbicacion.mockResolvedValue({ proveedor: "google-maps", lat: 41.3851, lng: 2.1734, nombre: "Bar X", provincia: "Barcelona", direccion: "Bar X, Barcelona" });
|
|
inferirDesdeCoords.mockResolvedValue(null); // ya trae nombre+provincia: no debe llamarse
|
|
const estado = renderPaso();
|
|
escribir("https://maps.google.com/@41.3851,2.1734");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(601); });
|
|
|
|
expect(screen.getByText(/Google Maps detectado/)).toBeTruthy();
|
|
expect(screen.getByText(/· 41\.3851, 2\.1734/)).toBeTruthy();
|
|
expect(estado.form.lat).toBe(41.3851);
|
|
expect(estado.form.lng).toBe(2.1734);
|
|
expect(estado.form.proveedorUbicacion).toBe("google-maps");
|
|
expect(estado.form.nombre).toBe("Bar X");
|
|
expect(estado.form.provincia).toBe("Barcelona");
|
|
expect(estado.form.direccion).toBe("Bar X, Barcelona");
|
|
// es un enlace: se guarda como enlaceGoogleMaps del local
|
|
expect(estado.form.enlaceGoogleMaps).toBe("https://maps.google.com/@41.3851,2.1734");
|
|
expect(inferirDesdeCoords).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("con solo coordenadas lanza la inferencia inversa y pre-rellena lo inferido", async () => {
|
|
resolverUbicacion.mockResolvedValue({ proveedor: "coordenadas", lat: 40.4168, lng: -3.7038, nombre: "", provincia: "" });
|
|
inferirDesdeCoords.mockResolvedValue({ nombre: "Puerta del Sol", provincia: "Madrid", direccion: "Puerta del Sol, Madrid" });
|
|
const estado = renderPaso();
|
|
escribir("40.4168, -3.7038");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(601); });
|
|
|
|
expect(inferirDesdeCoords).toHaveBeenCalledWith(40.4168, -3.7038);
|
|
expect(estado.form.nombre).toBe("Puerta del Sol");
|
|
expect(estado.form.provincia).toBe("Madrid");
|
|
// coordenadas sueltas: no es un enlace
|
|
expect(estado.form.enlaceGoogleMaps).toBe("");
|
|
expect(screen.queryByRole("alert")).toBeNull();
|
|
});
|
|
|
|
it("entrada no reconocida → aviso y sin coordenadas en el form", async () => {
|
|
resolverUbicacion.mockResolvedValue(null);
|
|
const estado = renderPaso();
|
|
escribir("https://maps.app.goo.gl/xyz");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(601); });
|
|
|
|
const aviso = screen.getByRole("alert");
|
|
expect(aviso.textContent).toContain("No se pudo interpretar la ubicación");
|
|
expect(estado.form.lat).toBeNull();
|
|
});
|
|
|
|
it("si la inferencia inversa falla, avisa pero mantiene la resolución válida", async () => {
|
|
resolverUbicacion.mockResolvedValue({ proveedor: "coordenadas", lat: 1, lng: 2, nombre: "", provincia: "" });
|
|
inferirDesdeCoords.mockResolvedValue(null); // fallo controlado
|
|
renderPaso();
|
|
escribir("1, 2");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(601); });
|
|
|
|
expect(screen.getByText(/No se pudieron inferir el nombre y la provincia/i)).toBeTruthy();
|
|
expect(screen.getByText(/Coordenadas detectado/)).toBeTruthy();
|
|
});
|
|
|
|
it("al volver a editar invalida la resolución anterior", async () => {
|
|
resolverUbicacion.mockResolvedValueOnce({ proveedor: "coordenadas", lat: 1, lng: 2 });
|
|
const estado = renderPaso();
|
|
escribir("1, 2");
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(601); });
|
|
expect(estado.form.lat).toBe(1);
|
|
expect(screen.getByText(/Coordenadas detectado/)).toBeTruthy();
|
|
|
|
escribir("1, 2 y algo más");
|
|
expect(estado.form.lat).toBeNull(); // se limpia inmediatamente
|
|
expect(screen.queryByText(/detectado/)).toBeNull(); // el chip desaparece
|
|
});
|
|
});
|