Ctrl K
ComunidadGuías…

tests/pocketBoot.test.ts

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

Archivo original ↗Preguntar a KiBot
// THE POCKET PAGE'S FIRST LINE (mobile/src/pocket/boot.ts), piece by piece.
//
// boot() itself runs only where there is a page; what is tested here is what
// it installs: the `fetch` shim that answers `/api/*` from the pocket and
// passes everything else to the real fetch, the answer to the service
// worker's questions (an image's `/api/file`, which is not a fetch the page
// can patch), and the words of the shell's one sync line. The session is a
// stub that records what it was asked.

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { readFileSync } from "node:fs";
import { answerTheWorker, installFetchShim, syncText } from "../mobile/src/pocket/boot.ts";
import type { PocketRequest, PocketResponse } from "../mobile/src/pocket/server.ts";
import { pocketWords } from "../mobile/src/i18n.ts";
import type { SyncLine } from "../mobile/src/pocket/sync.ts";

function stubSession(answer: (req: PocketRequest) => PocketResponse | Promise<PocketResponse>) {
  const asked: PocketRequest[] = [];
  return {
    asked,
    async handle(req: PocketRequest): Promise<PocketResponse> {
      asked.push(req);
      return answer(req);
    },
  };
}

const ORIGIN = "https://localhost";

function page() {
  const passed: string[] = [];
  const target = {
    location: { href: `${ORIGIN}/Ideas/Note`, origin: ORIGIN },
    fetch: (async (input: RequestInfo | URL) => {
      passed.push(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
      return new Response("from the network");
    }) as typeof fetch,
  };
  return { target, passed };
}

describe("the fetch shim", () => {
  it("answers /api/* on this origin from the pocket, with the method, the headers and the body", async () => {
    const session = stubSession(() => ({ status: 201, headers: { "Content-Type": "application/json", "X-Pocket": "yes" }, body: '{"ok":true}' }));
    const { target, passed } = page();
    installFetchShim(session, target);
    const res = await target.fetch("/api/note?path=Welcome.md", { method: "put", headers: { "Content-Type": "application/json", "X-Astrolabe-Preview": "1" }, body: '{"content":"x"}' });
    assert.equal(res.status, 201);
    assert.equal(res.headers.get("x-pocket"), "yes");
    assert.deepEqual(await res.json(), { ok: true });
    assert.deepEqual(passed, []);
    const [req] = session.asked;
    assert.equal(req.method, "PUT");
    assert.equal(req.url, `${ORIGIN}/api/note?path=Welcome.md`);
    assert.deepEqual(req.headers, { "content-type": "application/json", "x-astrolabe-preview": "1" });
    assert.equal(req.body, '{"content":"x"}');
  });

  it("reads a Request's own method, headers and body", async () => {
    const session = stubSession(() => ({ status: 200, headers: {}, body: new TextEncoder().encode("bytes") }));
    const { target } = page();
    installFetchShim(session, target);
    const res = await target.fetch(new Request(`${ORIGIN}/api/capture`, { method: "POST", headers: { "X-One": "1" }, body: "captured" }));
    assert.equal(await res.text(), "bytes");
    const [req] = session.asked;
    assert.equal(req.method, "POST");
    assert.equal(req.headers?.["x-one"], "1");
    assert.equal(req.body, "captured");
  });

  it("a GET carries no body, and a URL object works like a string", async () => {
    const session = stubSession(() => ({ status: 200, headers: {}, body: null }));
    const { target } = page();
    installFetchShim(session, target);
    await target.fetch(new URL(`${ORIGIN}/api/tree`));
    assert.equal(session.asked[0].method, "GET");
    assert.equal(session.asked[0].body, null);
  });

  it("everything else goes to the real fetch: the client's chunks, another origin, a path that only looks like the API", async () => {
    const session = stubSession(() => {
      throw new Error("the pocket must not be asked");
    });
    const { target, passed } = page();
    installFetchShim(session, target);
    for (const url of ["/assets/index.js", "https://github.com/api/x", `${ORIGIN}/apiary`, "/fonts/api/face.woff2"]) {
      assert.equal(await (await target.fetch(url)).text(), "from the network");
    }
    assert.deepEqual(passed, ["/assets/index.js", "https://github.com/api/x", `${ORIGIN}/apiary`, "/fonts/api/face.woff2"]);
    assert.deepEqual(session.asked, []);
  });
});

describe("the service worker's questions", () => {
  /** The worker posts `{ pocket: "request", request }` with a port to answer on. */
  function ask(container: EventTarget, data: unknown): Promise<{ data: any; transfer: unknown[] }> {
    return new Promise((resolve) => {
      const port = {
        postMessage(message: unknown, transfer: unknown[] = []) {
          resolve({ data: message, transfer });
        },
      };
      const event = Object.assign(new Event("message"), { data, ports: [port] });
      container.dispatchEvent(event);
    });
  }

  it("answers a request with the pocket's answer, handing binary bodies over as a transferred buffer", async () => {
    const png = new Uint8Array([137, 80, 78, 71]);
    const session = stubSession((req): PocketResponse => (req.url.includes("/api/file") ? { status: 200, headers: { "Content-Type": "image/png" }, body: png } : { status: 200, headers: {}, body: "text" }));
    const container = new EventTarget();
    answerTheWorker(session, container);
    const request: PocketRequest = { method: "GET", url: `${ORIGIN}/api/file?path=Media/a.png`, headers: {}, body: null };
    const { data, transfer } = await ask(container, { pocket: "request", request });
    assert.equal(data.status, 200);
    assert.equal(data.headers["Content-Type"], "image/png");
    assert.ok(data.body instanceof ArrayBuffer);
    assert.deepEqual(new Uint8Array(data.body), png);
    assert.deepEqual(transfer, [data.body]);
    const text = await ask(container, { pocket: "request", request: { ...request, url: `${ORIGIN}/api/tree` } });
    assert.equal(text.data.body, "text");
    assert.deepEqual(text.transfer, []);
  });

  it("a pocket that throws is a 500 with its sentence, not a worker left waiting", async () => {
    const session = stubSession(() => {
      throw new Error("The repository is not part of the vault.");
    });
    const container = new EventTarget();
    answerTheWorker(session, container);
    const { data } = await ask(container, { pocket: "request", request: { method: "GET", url: `${ORIGIN}/api/x`, headers: {}, body: null } });
    assert.equal(data.status, 500);
    assert.deepEqual(JSON.parse(data.body), { error: "The repository is not part of the vault." });
  });

  it("ignores a message that is not a pocket request", () => {
    const session = stubSession(() => ({ status: 200, headers: {}, body: null }));
    const container = new EventTarget();
    answerTheWorker(session, container);
    container.dispatchEvent(Object.assign(new Event("message"), { data: { pocket: "event" }, ports: [{ postMessage: () => assert.fail("answered") }] }));
    container.dispatchEvent(Object.assign(new Event("message"), { data: { pocket: "request", request: { method: "GET", url: "/api/x" } }, ports: [] }));
    assert.deepEqual(session.asked, []);
  });

  it("does nothing where there is no worker to answer", () => {
    assert.doesNotThrow(() => answerTheWorker(stubSession(() => ({ status: 200, headers: {}, body: null })), undefined));
  });
});

describe("the sync line's words", () => {
  it("each line the rule can choose is a sentence of the shell's dictionary", () => {
    const words = pocketWords();
    const lines: [SyncLine, string][] = [
      [{ key: "conflicts", count: 2 }, words.syncConflicts(2)],
      [{ key: "toPush", count: 3 }, words.syncToPush(3)],
      [{ key: "syncedAgo", minutes: 4 }, words.syncedAgo(4)],
      [{ key: "failed", message: "remote-moved" }, words.syncFailed],
      [{ key: "syncing" }, words.syncing],
      [{ key: "pushing" }, words.pushing],
      [{ key: "offline" }, words.offline],
      [{ key: "never" }, words.never],
      [{ key: "syncedJustNow" }, words.syncedJustNow],
    ];
    for (const [line, want] of lines) assert.equal(syncText(line), want, line.key);
  });
});

describe("the sync line's place on the phone (3.34)", () => {
  // Read from the source: the strip's stylesheet is written by boot() into a
  // page this test does not have. The line hides itself for "synced" — and
  // its own `display: flex` used to outrank the browser's [hidden] rule, so
  // "Synced just now." sat over the phone's tab bar for good.
  const src = readFileSync(new URL("../mobile/src/pocket/boot.ts", import.meta.url), "utf8");
  it("a hidden line is not drawn", () => {
    assert.match(src, /#pocket-sync\[hidden\] \{ display: none; \}/);
  });
  it("it stands clear of the phone shell's tab bar and rail", () => {
    assert.match(src, /\.s-ph-doc:has\(\.s-ph--tabs\) #pocket-sync \{ inset-block-end: calc\(env\(safe-area-inset-bottom, 0px\) \+ 68px\); \}/);
    assert.match(src, /\.s-ph-doc:has\(\.s-ph--split\) #pocket-sync \{ inset-inline-start: calc\(72px \+ 0\.5rem\); \}/);
  });
  it("pulling a list down asks the pocket for a pull", () => {
    assert.match(src, /addEventListener\("astrolabe:refresh", \(\) => void session\.pull\(\)\)/);
  });
});