| 1 | // shell_node — runs shell.js against a stub page in plain Node, one |
| 2 | // fresh script per page and one sessionStorage for the tab, so a record |
| 3 | // one page writes is what the next page reads, as in a browser. Only |
| 4 | // the direction is driven: what the back control's click leaves behind |
| 5 | // and which way the next page's transition is typed. |
| 6 | // |
| 7 | // check/shell_test.go is the only caller. It reads back one JSON |
| 8 | // object: each scenario's name and the transition type the last page |
| 9 | // got. |
| 10 | // |
| 11 | // This file lives under check/node/ beside the other Node drivers, so |
| 12 | // `go test` in check/ finds it by name; it is never embedded, and |
| 13 | // nothing in fiesta.go reaches for *.mjs. |
| 14 | import { readFileSync } from "node:fs"; |
| 15 | import vm from "node:vm"; |
| 16 | |
| 17 | const src = readFileSync(new URL("../../js/shell.js", import.meta.url), "utf8"); |
| 18 | |
| 19 | // A page: its URL, its view ("page" or "index"), and the href of its |
| 20 | // back control, if it has one. No Navigation API, so the click never |
| 21 | // takes the history.back() path and always leaves its record behind. |
| 22 | function open(storage, url, view, backHref) { |
| 23 | const on = { window: {}, document: {} }; |
| 24 | const location = new URL(url); |
| 25 | const root = { matches: (sel) => sel.includes(`~="${view}"`) }; |
| 26 | const back = backHref && { |
| 27 | href: new URL(backHref, url).href, |
| 28 | closest: (sel) => (sel.includes("[rst-shell-back]") ? back : null), |
| 29 | }; |
| 30 | const document = { |
| 31 | prerendering: false, |
| 32 | querySelector: (sel) => (sel.includes("[rst-shell-sidebar]") ? root : null), |
| 33 | querySelectorAll: () => [], |
| 34 | getElementById: () => null, |
| 35 | addEventListener: (type, fn) => { on.document[type] = fn; }, |
| 36 | }; |
| 37 | const window = { |
| 38 | sessionStorage: storage, |
| 39 | onpagereveal: null, |
| 40 | onpageswap: null, |
| 41 | addEventListener: (type, fn) => { on.window[type] = fn; }, |
| 42 | }; |
| 43 | vm.runInNewContext(src, { window, document, location, URL, matchMedia: () => ({ matches: true }) }); |
| 44 | return { |
| 45 | tapBack() { |
| 46 | on.document.click({ target: back, button: 0, defaultPrevented: false, preventDefault() {} }); |
| 47 | }, |
| 48 | leave() { on.window.pageswap({}); }, |
| 49 | reveal() { |
| 50 | const types = new Set(); |
| 51 | on.window.pagereveal({ viewTransition: { types } }); |
| 52 | return [...types].join(","); |
| 53 | }, |
| 54 | }; |
| 55 | } |
| 56 | |
| 57 | function tab() { |
| 58 | const m = new Map(); |
| 59 | return { |
| 60 | getItem: (k) => (m.has(k) ? m.get(k) : null), |
| 61 | setItem: (k, v) => { m.set(k, String(v)); }, |
| 62 | removeItem: (k) => { m.delete(k); }, |
| 63 | }; |
| 64 | } |
| 65 | |
| 66 | const out = {}; |
| 67 | |
| 68 | // Back is tapped and the page really goes back to its up page. |
| 69 | { |
| 70 | const s = tab(); |
| 71 | const p = open(s, "https://app.test/invoices", "page", "/#nav-invoices"); |
| 72 | p.tapBack(); |
| 73 | p.leave(); |
| 74 | out.taken = open(s, "https://app.test/#nav-invoices", "index").reveal(); |
| 75 | } |
| 76 | |
| 77 | // Back is tapped, the reader cancels the leave-page prompt, then follows |
| 78 | // a link to another content page: that is a forward navigation. |
| 79 | { |
| 80 | const s = tab(); |
| 81 | const p = open(s, "https://app.test/invoices", "page", "/"); |
| 82 | p.tapBack(); |
| 83 | p.leave(); |
| 84 | out.cancelled = open(s, "https://app.test/orders", "page", "/").reveal(); |
| 85 | } |
| 86 | |
| 87 | process.stdout.write(JSON.stringify(out)); |
| 88 | |