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
+84
View File
@@ -0,0 +1,84 @@
import { describe, expect, it, vi } from "vitest";
import ProveedorUbicacion from "../../src/utils/ubicacion/ProveedorUbicacion.js";
describe("ProveedorUbicacion (clase base)", () => {
it("normaliza: minúsculas, sin tildes, sin espacios extremos", () => {
const p = new ProveedorUbicacion();
expect(p.normalizar(" ÁLAVA ")).toBe("alava");
expect(p.normalizar("Guipúzcoa")).toBe("guipuzcoa");
expect(ProveedorUbicacion.normalizar("MA DRID")).toBe("ma drid");
expect(p.normalizar(null)).toBe("");
});
describe("emparejarProvincia", () => {
const p = new ProveedorUbicacion();
it("coincidencia exacta insensible a acentos y mayúsculas", () => {
expect(p.emparejarProvincia("ALAVA")).toBe("Álava");
expect(p.emparejarProvincia("Guipuzcoa")).toBe("Guipúzcoa");
});
it("coincidencia parcial dentro de un texto (Comunidad de Madrid)", () => {
expect(p.emparejarProvincia("Comunidad de Madrid")).toBe("Madrid");
expect(p.emparejarProvincia("Santiago, A Coruña, Galicia")).toBe("La Coruña");
});
it("tolera artículos anteuestos (A Coruña → La Coruña)", () => {
expect(p.emparejarProvincia("A Coruña")).toBe("La Coruña");
expect(p.emparejarProvincia("Ourense")).toBe("Orense");
});
it("alias de nombres cooficiales (D12)", () => {
expect(p.emparejarProvincia("Bizkaia")).toBe("Vizcaya");
expect(p.emparejarProvincia("Gipuzkoa")).toBe("Guipúzcoa");
expect(p.emparejarProvincia("Girona")).toBe("Gerona");
expect(p.emparejarProvincia("Lleida")).toBe("Lérida");
expect(p.emparejarProvincia("Illes Balears")).toBe("Islas Baleares");
expect(p.emparejarProvincia("Asturies")).toBe("Asturias");
});
it("devuelve \"\" cuando no casa con ninguna provincia", () => {
expect(p.emparejarProvincia("Île-de-France")).toBe("");
expect(p.emparejarProvincia("")).toBe("");
expect(p.emparejarProvincia("Lisboa")).toBe("");
});
});
it("el contrato base no acepta ninguna entrada", () => {
const p = new ProveedorUbicacion();
expect(p.detecta("41.38, 2.17")).toBe(false);
expect(p.detecta("https://maps.google.com")).toBe(false);
});
it("extrae() de la base lanza \"sin implementar\" (método abstracto)", async () => {
const p = new ProveedorUbicacion();
await expect(p.extrae("x")).rejects.toThrow("sin implementar");
});
describe("fetchNominatim degrada errores a null", () => {
it("devuelve null si fetch lanza (sin red)", async () => {
const p = new ProveedorUbicacion();
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(new Error("sin red"))));
expect(await p.fetchNominatim("https://nominatim.openstreetmap.org/x")).toBeNull();
vi.unstubAllGlobals();
});
it("devuelve null si la respuesta HTTP no es ok", async () => {
const p = new ProveedorUbicacion();
vi.stubGlobal("fetch", vi.fn(() => Promise.resolve({ ok: false, status: 503 })));
expect(await p.fetchNominatim("https://nominatim.openstreetmap.org/x")).toBeNull();
vi.unstubAllGlobals();
});
it("devuelve el JSON cuando todo va bien y manda cabeceras", async () => {
const p = new ProveedorUbicacion();
const fetchMock = vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve([{ lat: "1" }]) }));
vi.stubGlobal("fetch", fetchMock);
expect(await p.fetchNominatim("https://nominatim.openstreetmap.org/x")).toEqual([{ lat: "1" }]);
const [, init] = fetchMock.mock.calls[0];
expect(init.headers["Accept-Language"]).toBe("es");
expect(init.headers["User-Agent"]).toBe("LocalesEspanoles/1.0");
vi.unstubAllGlobals();
});
});
});