feat(propuesta): pasarela paso a paso con autodetección de ubicación y FAB
deploy-branch / deploy (push) Failing after 6s
deploy-branch / teardown (push) Skipped

- 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:
2026-08-16 01:35:02 +02:00
parent d48b74cbd4
commit 003b1e209f
53 changed files with 4896 additions and 253 deletions
+94
View File
@@ -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();
});
});
});