Ctrl K
ComunidadGuías…

lib/app-layout.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment jsdom
import { APP_LAYOUT_BOOTSTRAP } from "./app-layout-bootstrap.generated";
import { afterEach, describe, expect, it, vi } from "vitest";
import { APP_LAYOUT_POLICY, isMobileLayout, resolveMobileLayout, subscribeAppLayout, type AppLayoutInput } from "./app-layout";

const input = (width: number, height: number, touch = true, orientation: AppLayoutInput["orientation"] = height >= width ? "portrait" : "landscape"): AppLayoutInput => ({ width, height, touch, screenWidth: width, screenHeight: height, orientation });
const cases: [string, AppLayoutInput, boolean][] = [
  ["narrow phone cover", input(280, 653), true],
  ["phone portrait", input(390, 844), true],
  ["phone landscape", input(844, 390), true],
  ["wide phone landscape", input(1100, 500), true],
  ["foldable inner square", input(896, 896), true],
  ["foldable inner landscape", input(960, 800), true],
  ["large unfolded landscape", input(1100, 800), false],
  ["tablet portrait", input(820, 1180), true],
  ["large tablet portrait", input(1032, 1376), true],
  ["large tablet landscape", input(1376, 1032), false],
  ["small tablet landscape", input(1024, 768), false],
  ["tablet split window", input(700, 1032), true],
  ["wide tablet portrait boundary", input(1199, 1500), true],
  ["roomy portrait viewport", input(1200, 1600), false],
  ["desktop below boundary", input(1023, 900, false), true],
  ["desktop at boundary", input(1024, 900, false), false],
  ["desktop portrait window", input(1100, 1500, false), false],
  ["desktop short window", input(1366, 500, false), false],
];

afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); delete document.documentElement.dataset.appLayout; });

function emulate(value: AppLayoutInput) {
  vi.stubGlobal("innerWidth", value.width);
  vi.stubGlobal("innerHeight", value.height);
  vi.stubGlobal("navigator", { maxTouchPoints: value.touch ? 5 : 0 });
  const orientation = new EventTarget() as EventTarget & { type: string };
  orientation.type = value.orientation === "portrait" ? "portrait-primary" : "landscape-primary";
  vi.stubGlobal("screen", { width: value.screenWidth, height: value.screenHeight, orientation: value.orientation ? orientation : undefined });
  vi.stubGlobal("matchMedia", (query: string) => ({ matches: query.includes("max-width") ? value.width < 1024 : value.touch, addEventListener: vi.fn(), removeEventListener: vi.fn() }));
  return orientation;
}

describe("adaptive application layout", () => {
  it.each(cases)("selects the expected layout for %s", (_name, value, mobile) => {
    expect(resolveMobileLayout(value, APP_LAYOUT_POLICY)).toBe(mobile);
    emulate(value);
    expect(isMobileLayout()).toBe(mobile);
    // The pre-paint script must agree with the runtime decision in every case.
    new Function(APP_LAYOUT_BOOTSTRAP)();
    expect(document.documentElement.dataset.appLayout).toBe(mobile ? "mobile" : "desktop");
  });

  it("keeps tablet orientation stable when a keyboard changes viewport shape", () => {
    const portrait = { ...input(1032, 1376), height: 620 };
    expect(resolveMobileLayout(portrait, APP_LAYOUT_POLICY)).toBe(true);
    const landscape = { ...input(1376, 1032), height: 450 };
    expect(resolveMobileLayout(landscape, APP_LAYOUT_POLICY)).toBe(false);
  });

  it("falls back to screen shape without orientation APIs", () => {
    expect(resolveMobileLayout({ ...input(1032, 1376), height: 600, orientation: null }, APP_LAYOUT_POLICY)).toBe(true);
    expect(resolveMobileLayout({ ...input(1100, 800), height: 600, orientation: null }, APP_LAYOUT_POLICY)).toBe(false);
  });

  it("supports the legacy Safari orientation signal", () => {
    emulate({ ...input(1032, 1376), height: 600, orientation: null });
    vi.stubGlobal("orientation", 0);
    expect(isMobileLayout()).toBe(true);
    vi.stubGlobal("orientation", 90);
    expect(isMobileLayout()).toBe(false);
  });

  it("shares rotation/resize listeners and removes them after the final subscriber", () => {
    const orientation = emulate(input(1032, 1376));
    const first = vi.fn(), second = vi.fn();
    const add = vi.spyOn(window, "addEventListener"), remove = vi.spyOn(window, "removeEventListener");
    const stopFirst = subscribeAppLayout(first), stopSecond = subscribeAppLayout(second);
    expect(add.mock.calls.filter(([name]) => name === "resize")).toHaveLength(1);
    expect(document.documentElement.dataset.appLayout).toBe("mobile");
    orientation.type = "landscape-primary";
    vi.stubGlobal("innerWidth", 1376);
    vi.stubGlobal("matchMedia", () => ({ matches: false }));
    orientation.dispatchEvent(new Event("change"));
    expect(document.documentElement.dataset.appLayout).toBe("desktop");
    expect(second).toHaveBeenCalledOnce();
    stopFirst();
    window.dispatchEvent(new Event("resize"));
    expect(second).toHaveBeenCalledTimes(2);
    stopSecond();
    expect(remove.mock.calls.filter(([name]) => name === "resize")).toHaveLength(1);
    window.dispatchEvent(new Event("resize"));
    expect(second).toHaveBeenCalledTimes(2);
  });
});