- 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
107 lines
4.3 KiB
React
107 lines
4.3 KiB
React
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
|
|
// Control del hook de instalación sin depender de beforeinstallprompt
|
|
const estadoHook = {
|
|
isInstalled: false,
|
|
canPromptInstall: false,
|
|
promptInstall: vi.fn(async () => "accepted"),
|
|
isIos: false,
|
|
};
|
|
vi.mock("../../src/hooks/usePwaInstall.js", () => ({ default: () => estadoHook }));
|
|
|
|
import BannerMovil from "../../src/components/BannerMovil.jsx";
|
|
|
|
const matchMediaMock = (esMovil) => (_query) => ({
|
|
matches: esMovil,
|
|
media: _query,
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
addListener: () => {},
|
|
removeListener: () => {},
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
estadoHook.isInstalled = false;
|
|
estadoHook.canPromptInstall = false;
|
|
estadoHook.isIos = false;
|
|
localStorage.clear();
|
|
localStorage.setItem("localesp_visitas", "5"); // ya superó el mínimo de 2 visitas
|
|
});
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
cleanup();
|
|
});
|
|
|
|
describe("BannerMovil en PC", () => {
|
|
beforeEach(() => vi.stubGlobal("matchMedia", matchMediaMock(false)));
|
|
|
|
it("muestra un botón compacto arriba a la izquierda, sin la tarjeta", () => {
|
|
render(<BannerMovil />);
|
|
const boton = screen.getByRole("button", { name: /instalar app/i });
|
|
expect(boton).toBeTruthy();
|
|
|
|
const contenedor = boton.closest("div").parentElement;
|
|
expect(contenedor.style.position).toBe("fixed");
|
|
expect(contenedor.style.top).toBe("20px");
|
|
expect(contenedor.style.left).toBe("20px");
|
|
expect(screen.queryByText("App para móvil")).toBeNull(); // tarjeta plegada
|
|
});
|
|
|
|
it("sin diálogo nativo, el botón despliega la tarjeta con instrucciones; el ✕ de la tarjeta solo la recoge", () => {
|
|
render(<BannerMovil />);
|
|
const boton = screen.getByRole("button", { name: /instalar app/i });
|
|
expect(boton.getAttribute("aria-expanded")).toBe("false");
|
|
|
|
fireEvent.click(boton);
|
|
expect(screen.getByText("App para móvil")).toBeTruthy();
|
|
expect(screen.getByText(/Instálala desde el menú de tu navegador/i)).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Cerrar aviso de instalación" }));
|
|
expect(screen.queryByText("App para móvil")).toBeNull();
|
|
// la tarjeta se cierra sin activar la regla de 14 días
|
|
expect(localStorage.getItem("localesp_banner_cerrado_hasta")).toBeNull();
|
|
});
|
|
|
|
it("con diálogo nativo, el botón instala directamente sin desplegar la tarjeta", async () => {
|
|
estadoHook.canPromptInstall = true;
|
|
render(<BannerMovil />);
|
|
fireEvent.click(screen.getByRole("button", { name: /instalar app/i }));
|
|
expect(estadoHook.promptInstall).toHaveBeenCalledTimes(1);
|
|
expect(screen.queryByText("App para móvil")).toBeNull();
|
|
});
|
|
|
|
it("el ✕ del botón aplica la regla de 14 días y oculta el aviso", () => {
|
|
render(<BannerMovil />);
|
|
fireEvent.click(screen.getByRole("button", { name: /ocultar aviso de instalación/i }));
|
|
expect(localStorage.getItem("localesp_banner_cerrado_hasta")).toBeTruthy();
|
|
expect(screen.queryByRole("button", { name: /instalar app/i })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("BannerMovil en móvil", () => {
|
|
beforeEach(() => vi.stubGlobal("matchMedia", matchMediaMock(true)));
|
|
|
|
it("muestra la tarjeta abajo a la izquierda (sin invadir el FAB de la derecha)", () => {
|
|
render(<BannerMovil />);
|
|
expect(screen.getByText("App para móvil")).toBeTruthy();
|
|
const fijos = [...document.querySelectorAll("div")].filter((d) => d.style.position === "fixed");
|
|
expect(fijos.length).toBe(1);
|
|
const contenedor = fijos[0];
|
|
expect(contenedor.style.position).toBe("fixed");
|
|
expect(contenedor.style.left).toBe("16px");
|
|
expect(contenedor.style.bottom).toBe("0px");
|
|
// (el paddingBottom: env(safe-area-inset-bottom) no se serializa en jsdom; se verifica visualmente)
|
|
expect(contenedor.style.right).toBe(""); // nunca a la derecha, ahí vive el FAB
|
|
});
|
|
|
|
it("el ✕ de la tarjeta aplica la regla de 14 días", () => {
|
|
render(<BannerMovil />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Cerrar aviso de instalación" }));
|
|
expect(localStorage.getItem("localesp_banner_cerrado_hasta")).toBeTruthy();
|
|
expect(screen.queryByText("App para móvil")).toBeNull();
|
|
});
|
|
});
|