// @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(); 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(); 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(); 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(); 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(); 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(); 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(); }); });