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,94 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import ProveedorNominatim from "../../src/utils/ubicacion/proveedorNominatim.js";
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("ProveedorNominatim", () => {
|
||||
const p = new ProveedorNominatim();
|
||||
|
||||
describe("detecta (fallback de texto)", () => {
|
||||
it("acepta direcciones y nombres con letras", () => {
|
||||
expect(p.detecta("Calle Mayor 5, Alcalá de Henares, Madrid")).toBe(true);
|
||||
expect(p.detecta("Bar El Olivo Sevilla")).toBe(true);
|
||||
});
|
||||
|
||||
it("rechaza URLs (las gestiona otro proveedor)", () => {
|
||||
expect(p.detecta("https://ejemplo.com/calle")).toBe(false);
|
||||
expect(p.detecta("www.ejemplo.com")).toBe(false);
|
||||
expect(p.detecta("https://maps.app.goo.gl/abc")).toBe(false);
|
||||
});
|
||||
|
||||
it("rechaza textos demasiado cortos o sin letras", () => {
|
||||
expect(p.detecta("ab")).toBe(false);
|
||||
expect(p.detecta("")).toBe(false);
|
||||
expect(p.detecta("12345")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("desdeResultado (normalización de provincia)", () => {
|
||||
it("normaliza provincia canónica insensible a acentos", () => {
|
||||
const r = p.desdeResultado({
|
||||
lat: "40.4", lon: "-3.7",
|
||||
display_name: "X, Madrid, España",
|
||||
address: { state: "Comunidad de Madrid" },
|
||||
namedetails: { name: "X" },
|
||||
});
|
||||
expect(r.provincia).toBe("Madrid");
|
||||
});
|
||||
|
||||
it("alias cooficiales: Bizkaia → Vizcaya, A Coruña → La Coruña", () => {
|
||||
const r = p.desdeResultado({ lat: "1", lon: "2", display_name: "d", address: { province: "Bizkaia" }, namedetails: {} });
|
||||
expect(r.provincia).toBe("Vizcaya");
|
||||
const r2 = p.desdeResultado({ lat: "1", lon: "2", display_name: "d", address: { city: "A Coruña" }, namedetails: {} });
|
||||
expect(r2.provincia).toBe("La Coruña");
|
||||
});
|
||||
|
||||
it("provincia fuera del listado → \"\" (selección manual)", () => {
|
||||
const r = p.desdeResultado({ lat: "1", lon: "2", display_name: "d", address: { state: "Île-de-France" }, namedetails: {} });
|
||||
expect(r.provincia).toBe("");
|
||||
});
|
||||
|
||||
it("resultado extensible: lat/lng/proveedor/nombre/direccion", () => {
|
||||
const r = p.desdeResultado({
|
||||
lat: "41.5", lon: "2.1",
|
||||
display_name: "X, Girona, España",
|
||||
address: { province: "Girona" },
|
||||
namedetails: { name: "X" },
|
||||
});
|
||||
expect(r).toEqual({ proveedor: "nominatim", lat: 41.5, lng: 2.1, nombre: "X", provincia: "Gerona", direccion: "X, Girona, España" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("extrae (búsqueda por texto)", () => {
|
||||
it("devuelve el primer resultado normalizado", async () => {
|
||||
const fetchMock = vi.fn(() => ({
|
||||
ok: true,
|
||||
json: async () => [{
|
||||
lat: "40.4828632", lon: "-3.3652613",
|
||||
display_name: "5, Calle Mayor, Alcalá de Henares, España",
|
||||
address: { city: "Alcalá de Henares", state: "Comunidad de Madrid" },
|
||||
namedetails: { name: "Casa de la Panadería" },
|
||||
}],
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const r = await p.extrae("Calle Mayor 5, Alcalá de Henares");
|
||||
expect(r.proveedor).toBe("nominatim");
|
||||
expect(r.lat).toBeCloseTo(40.4828632, 6);
|
||||
expect(r.provincia).toBe("Madrid");
|
||||
expect(r.nombre).toBe("Casa de la Panadería");
|
||||
// la búsqueda acota a España
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(decodeURIComponent(String(url).replace(/\+/g, " "))).toContain("Calle Mayor 5, Alcalá de Henares, España");
|
||||
});
|
||||
|
||||
it("sin resultados → null", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => ({ ok: true, json: async () => [] })));
|
||||
expect(await p.extrae("calle inventada zzzz")).toBeNull();
|
||||
});
|
||||
|
||||
it("fallo de red → null (degradación controlada)", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(new Error("sin red"))));
|
||||
expect(await p.extrae("Calle Mayor 5, Madrid")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user