// Run against a disposable local preview with ?debug=1 and a fresh persistent // profile: playwright-cli -s=pet run-code --filename=test/browser-flow.js // Uses the real configured brain. The only forced model effects are explicitly // marked debug checks; nothing here should run against a personal browser save. async function browserFlow(page) { const check = (condition, message) => { if (!condition) throw new Error(message); }; const result = []; await page.waitForFunction(() => !!window.tokenling); check(await page.locator("#setup-owner").isVisible(), "Use a fresh test profile"); await page.fill("#owner-name", "Sam"); await page.click("#setup-owner button"); await page.fill("#pet-name", ""); await page.waitForTimeout(1100); check((await page.inputValue("#pet-name")) === "", "Progress must not restore a cleared name"); await page.fill("#pet-name", "Mochi"); await page.waitForFunction(() => ["ready", "error"].includes(tokenling.brain.state), null, { timeout: 60000, }); check( await page.evaluate(() => tokenling.brain.state === "ready"), await page.locator("#setup-hatch-note").innerText(), ); check((await page.inputValue("#pet-name")) === "Mochi", "Progress changed the name"); await page.click("#hatch-btn"); await page.waitForFunction( () => tokenling.state().chat.some((l) => l.startsWith("Mochi:")) && !tokenling.brain.busy, null, { timeout: 60000 }, ); result.push("fresh download, edited name, hatch and real generation"); await page.click("#tab-care"); await page.uncheck("#chatter-toggle"); check((await page.inputValue("#owner-name-edit")) === "Sam", "Settings owner is stale"); await page.click("#tab-chat"); const before = await page.evaluate(() => tokenling.state().chat.length); await page.fill("#chat-input", "hello little friend"); await page.press("#chat-input", "Enter"); await page.waitForFunction( (n) => tokenling.state().chat.length > n + 1 && !tokenling.brain.busy, before, { timeout: 60000 }, ); check( await page .locator("#chat-log") .innerText() .then((t) => t.includes("hello little friend")), "Missing owner message", ); // The engine owns the save; debug edits go through it. await page.evaluate(() => tokenling.debug.edit((s) => { s.stats.tummy = 15; }), ); await page.click("#act-feed"); await page.getByRole("button", { name: "cookie", exact: true }).click(); await page.waitForFunction(() => !tokenling.state().offer && !tokenling.brain.busy, null, { timeout: 60000, }); check( await page.evaluate(() => tokenling.state().today.log.some((l) => /ate a cookie|said no to a cookie/.test(l)), ), "Offer was not resolved", ); await page.click("#act-meds"); await page.waitForFunction(() => !tokenling.state().offer && !tokenling.brain.busy, null, { timeout: 60000, }); result.push("chat, food offer and healthy medicine availability"); // Deterministic debug effects verify UI wiring independently of model sampling. await page.evaluate(() => { tokenling.debug.applyTags("[remember:Sam likes frogs] [gift:flower]"); }); await page.click("#tab-shelf"); check( await page .locator("#memory-list") .innerText() .then((t) => t.includes("Sam likes frogs")), "Memory missing", ); check( await page .locator("#shelf-items") .innerText() .then((t) => t.includes("flower")), "Gift missing", ); await page.getByRole("button", { name: "forget: Sam likes frogs" }).click(); check( await page.evaluate(() => !tokenling.state().memories.includes("Sam likes frogs")), "Forget did not persist", ); await page.click("#tab-debug"); await page.locator("[data-debug-diary]").click(); await page.waitForFunction(() => tokenling.state().diary.length && !tokenling.brain.busy, null, { timeout: 60000, }); await page.click("#tab-diary"); check((await page.locator(".diary-page").count()) > 0, "Diary page missing"); result.push("memory, shelf and real diary generation"); await page.evaluate(() => { tokenling.debug.edit((s) => { s.asleep = false; s.offer = { kind: "play", item: null }; }); tokenling.debug.applyTags("[yes]"); }); await page.waitForFunction(() => !!tokenling.view.game); const x = await page.evaluate(() => tokenling.view.x); const pad = await page.locator("#pad-right").boundingBox(); await page.mouse.move(pad.x + pad.width / 2, pad.y + pad.height / 2); await page.mouse.down(); await page.waitForTimeout(200); await page.mouse.up(); check( await page.evaluate((start) => tokenling.view.x > start && tokenling.view.game.dir === 0, x), "Held pad/release failed", ); await page.locator("#pad-left").focus(); await page.keyboard.down("Space"); await page.waitForTimeout(100); await page.keyboard.up("Space"); check(await page.evaluate(() => tokenling.view.game.dir === 0), "Keyboard release failed"); await page.evaluate(() => { tokenling.view.game.timeLeft = 0; }); await page.waitForFunction(() => !tokenling.view.game && !tokenling.brain.busy, null, { timeout: 60000, }); result.push("pointer and keyboard game controls"); await page.locator("#tab-chat").focus(); await page.keyboard.press("ArrowRight"); check( (await page.locator("#tab-diary").getAttribute("aria-selected")) === "true", "Arrow tab navigation failed", ); await page.keyboard.press("End"); check( (await page.locator("#tab-debug").getAttribute("aria-selected")) === "true", "End tab navigation failed", ); for (const width of [1280, 768, 360]) { await page.setViewportSize({ width, height: 900 }); for (const id of ["chat", "diary", "shelf", "care", "debug"]) { await page.click("#tab-" + id); check( await page.evaluate(() => document.documentElement.scrollWidth === innerWidth), `Overflow in ${id} at ${width}`, ); } } result.push("keyboard tabs and all panels at 1280/768/360px"); await page.reload(); await page.waitForFunction(() => window.tokenling?.brain.state === "ready", null, { timeout: 60000, }); check( await page.evaluate( () => tokenling.state().pet.name === "Mochi" && tokenling.state().diary.length > 0, ), "Reload lost the pet", ); const other = await page.context().newPage(); await other.goto(page.url()); await other.locator("#pet-elsewhere").waitFor(); await other.click("#elsewhere-here"); await other.waitForFunction(() => window.tokenling?.brain.state === "ready", null, { timeout: 60000, }); await page.locator("#pet-elsewhere").waitFor(); check(await page.evaluate(() => tokenling.brain.worker === null), "Old tab still owns a worker"); await other.close(); await page.waitForFunction( async () => !(await navigator.locks.query()).held.some((lock) => lock.name === "tokenling.v1"), ); await page.reload(); await page.waitForFunction(() => window.tokenling?.brain.state === "ready", null, { timeout: 60000, }); result.push("cached reload and Web Lock takeover"); return result; }