Ctrl K
ComunidadGuías…

tests/e2e/flows.mjs

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

Archivo original ↗Preguntar a KiBot
// Journeys across pages: links between them, figures that follow a change, keys after every page.
// BASE=http://localhost:8891 node flows.mjs   (a fresh DEMO server)
import { readFileSync } from "node:fs";
import { chromium } from "playwright";

const BASE = process.env.BASE || "http://localhost:8891";
let failed = 0;
const ok = (cond, msg) => { console.log((cond ? "  ok   " : "  FAIL ") + msg); if (!cond) failed++; };
const money = (s) => Number(String(s).replace(/[^\d.,-]/g, "").replace(/,/g, ""));

const browser = await chromium.launch();
async function open(width, height) {
  const ctx = await browser.newContext({ viewport: { width, height }, serviceWorkers: "block" });
  await ctx.addInitScript(() => localStorage.setItem("wally.person", "1"));
  const page = await ctx.newPage();
  const errors = [];
  page.on("pageerror", (e) => errors.push(e.message));
  page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
  return { ctx, page, errors };
}

{
  console.log("Opening Wally");
  const { ctx, page, errors } = await open(1440, 900);
  // index.html asks for each page's scripts up front: the list has to be what the page really imports
  const web = new URL("../../web/js/", import.meta.url);
  const closure = (file, seen = new Set()) => {
    if (seen.has(file)) return seen;
    seen.add(file);
    for (const m of readFileSync(new URL(file, web), "utf8").matchAll(/from\s+"(\.[^"]+)"/g)) closure(new URL(m[1], new URL(file, web)).pathname.split("/web/js/")[1], seen);
    return seen;
  };
  for (const [path, file] of [["/", "pages/dashboard.js"], ["/transactions", "pages/transactions.js"], ["/recurring", "pages/recurring.js"], ["/settings/people", "pages/settings.js"], ["/review", "pages/review.js"]]) {
    await page.goto(BASE + path, { waitUntil: "networkidle" });
    const listed = await page.$$eval('link[rel="modulepreload"]', (ls) => ls.map((l) => new URL(l.href).pathname.slice(4)).sort());
    const real = [...closure(file), "ui.js", "shell.js", "core.js"].filter((f, i, a) => a.indexOf(f) === i).sort();
    ok(JSON.stringify(listed) === JSON.stringify(real), path + " preloads exactly what it imports " + (JSON.stringify(listed) === JSON.stringify(real) ? "" : JSON.stringify({ listed, real })));
  }

  // A refresh: the frame is there before the data, without the logo, and holds no figures
  await page.goto(BASE + "/", { waitUntil: "networkidle" });
  const kept = await page.evaluate(() => localStorage.getItem("wally.frame"));
  ok(kept && !/\d[\d.,]*\s*(€|\$)|(€|\$)\s*\d/.test(kept), "the frame is kept, with no money in it");
  let release;
  const held = new Promise((r) => { release = r; });
  await page.route("**/api/ledger", async (route) => { await held; await route.continue(); });
  await page.reload({ waitUntil: "commit" });
  await page.waitForSelector('#side a[href="/transactions"]', { state: "visible" });
  ok(await page.locator("#boot").isHidden(), "no logo over the frame");
  ok(await page.$eval("#main", (m) => m.children.length === 0), "the frame is up before the data");
  // A click on it while the data is on its way ends up on that page
  await page.click('#side a[href="/transactions"]');
  ok(page.url().endsWith("/transactions"), "a click during loading changes the address");
  release();
  await page.waitForSelector('#main[data-page="transactions"] > *');
  await page.unroute("**/api/ledger");
  ok(await page.$eval('#side a[href="/transactions"]', (a) => a.classList.contains("on")), "and opens that page");
  ok(errors.length === 0, "no errors while opening " + errors.slice(0, 2).join(" | "));
  await ctx.close();
}

{
  console.log("Desktop journeys");
  const { ctx, page, errors } = await open(1440, 900);
  await page.goto(BASE + "/", { waitUntil: "networkidle" });

  // Every page, then keys on each: nothing a page leaves behind may break another
  for (const path of ["/transactions", "/recurring", "/settings/people", "/review", "/"]) {
    await page.click(`#side a[href="${path === "/review" ? "/" : path.replace(/\/people$/, "")}"]`).catch(() => page.goto(BASE + path));
    if (path === "/review") await page.goto(BASE + path, { waitUntil: "networkidle" });
    await page.waitForTimeout(300);
    for (const key of ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "1", "2", "Escape"]) await page.keyboard.press(key);
  }
  ok(errors.length === 0, "visiting every page and pressing keys leaves no errors " + errors.slice(0, 2).join(" | "));

  // Dashboard category → Transactions filtered to it, same range
  await page.goto(BASE + "/", { waitUntil: "networkidle" });
  const catLink = page.locator('[data-w="cats"] [data-go-cat]').first();
  if (await catLink.count()) {
    const name = await catLink.getAttribute("data-go-cat");
    await catLink.click();
    await page.waitForURL(/\/transactions\?/);
    ok(page.url().includes("cat=" + encodeURIComponent(name)), "a category on Summary opens Transactions filtered to " + name);
    const cats = await page.locator("#main .w-row .tx-rm p").allTextContents();
    ok(cats.length > 0 && cats.every((c) => c.includes(name)), "every row shown is " + name);
  } else ok(false, "Summary has category links");

  // Budgets are gone: the old address falls back to the Dashboard
  await page.goto(BASE + "/budgets", { waitUntil: "networkidle" });
  ok(await page.locator("#main").getAttribute("data-page") === "dashboard" && await page.locator('#side a[href="/budgets"]').count() === 0, "/budgets opens Summary, and nothing links to it");

  // Adding from the Dashboard changes its figures at once
  await page.goto(BASE + "/", { waitUntil: "networkidle" });
  const before = money(await page.locator("#s-big").textContent());
  await page.keyboard.press("n");
  await page.waitForSelector("#add-modal:not([hidden])");
  await page.keyboard.type("100");
  await page.keyboard.press("Enter");
  await page.keyboard.type("Journey test");
  await page.click("#add-cat");
  await page.keyboard.press("Enter");
  await page.keyboard.press(process.platform === "darwin" ? "Meta+Enter" : "Control+Enter");
  await page.waitForSelector("#add-modal", { state: "hidden" });
  await page.waitForTimeout(400);
  const after = money(await page.locator("#s-big").textContent());
  // Summary rounds to whole amounts, so a euro either way
  ok(Math.abs(after - before - 100) <= 1 || Math.abs(after - before - 50) <= 1, `Spent so far moved from ${before} to ${after}`);
  await page.goto(BASE + "/transactions?q=Journey", { waitUntil: "networkidle" });
  ok((await page.locator("#main .w-row").count()) >= 1, "the new expense is found by search");

  // Month in review → Summary on that month
  await page.goto(BASE + "/review", { waitUntil: "networkidle" });
  const toDash = page.locator('a[href^="/?month="]').first();
  if (await toDash.count()) {
    const href = await toDash.getAttribute("href");
    await toDash.click();
    await page.waitForTimeout(500);
    const ym = href.match(/month=(\d{4})-(\d\d)/);
    const title = await page.locator("#s-title").textContent();
    const name = new Intl.DateTimeFormat("en", { month: "long", timeZone: "UTC" }).format(Date.UTC(+ym[1], +ym[2] - 1, 15));
    ok(title.startsWith(name), `Month in review opens Summary on ${ym[1]}-${ym[2]} (${title.trim()})`);
  } else ok(false, "Month in review links to Summary");

  ok(errors.length === 0, "no errors on desktop " + errors.slice(0, 2).join(" | "));
  await ctx.close();
}

{
  console.log("Phone journeys");
  const { ctx, page, errors } = await open(390, 844);
  await page.goto(BASE + "/", { waitUntil: "networkidle" });
  ok(await page.locator("#tabbar").isVisible(), "the tab bar shows on a phone");
  ok(!(await page.locator("#side").isVisible()), "the sidebar is hidden");
  ok(await page.locator("#tabbar .w-tabpill a").count() === 4, "four tabs: Summary, Transactions, Recurring, Settings");
  for (const href of ["/transactions", "/recurring", "/settings", "/"]) {
    await page.click(`#tabbar a[href="${href}"]`);
    await page.waitForTimeout(300);
    ok((await page.locator(`#tabbar a[href="${href}"]`).getAttribute("aria-current")) === "page", "tab " + href + " is current");
    const overflow = await page.evaluate(() => document.documentElement.scrollWidth - innerWidth);
    ok(overflow <= 0, "no sideways scroll on " + href);
  }
  // Without the sidebar, the person menu is where you switch person (or sign out)
  await page.click(".w-pswitch-b");
  ok(await page.locator(".w-pswitch-m [data-switch]").isVisible(), "the person menu offers Switch person");
  await page.keyboard.press("Escape");
  const add = await page.locator("#tabbar .w-tabadd").boundingBox();
  ok(add.x >= 0 && add.x + add.width <= 390 && add.y + add.height <= 844, "the + button is whole on screen");
  await page.click("#tabbar .w-tabadd");
  ok(await page.locator("#add-modal").isVisible(), "Add in the tab bar opens the sheet");
  ok(errors.length === 0, "no errors on a phone " + errors.slice(0, 2).join(" | "));
  await ctx.close();
}

await browser.close();
console.log(failed ? failed + " failed" : "all passed");
process.exit(failed ? 1 : 0);