oxedyne/daimond/dev/probe_typstphone.mjs
8.0 KiB, 1 run
created by r2519314175:103, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // probe_typstphone.mjs — the watched live document inside a phone sheet, at 390 px. |
| 2 | // |
| 3 | // `.tl-scroll` is a nested scroller inside a draggable sheet, which is where |
| 4 | // gestures fight: a drag near the top of the pages can either scroll the document |
| 5 | // or move the sheet, and whichever loses becomes impossible. This drives it with |
| 6 | // REAL TOUCH EVENTS and reports; it asserts nothing. The rule it settles is written |
| 7 | // up beside the sheet's own drag handler in `www/js/mobile.js`. |
| 8 | // |
| 9 | // eval "$(bash dev/world.sh 13 --env)" |
| 10 | // node dev/probe_typstphone.mjs |
| 11 | import path from 'node:path'; |
| 12 | import { fileURLToPath } from 'node:url'; |
| 13 | import { open, shot, scratch } from './harness.mjs'; |
| 14 | |
| 15 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 16 | const PROFILE = scratch('pw', 'typstphone'); |
| 17 | |
| 18 | const MAIN = `#set page(width: 120mm, height: 160mm, margin: 12mm) |
| 19 | #set text(size: 10pt) |
| 20 | #set par(justify: true) |
| 21 | = The book |
| 22 | #lorem(400) |
| 23 | #pagebreak() |
| 24 | #lorem(400) |
| 25 | #pagebreak() |
| 26 | #lorem(400) |
| 27 | `; |
| 28 | |
| 29 | // `touch: true` so the sheet's pointer handlers see a finger, and the browser does |
| 30 | // its own gesture arbitration between the nested scroller and its ancestors. |
| 31 | const s = await open({ name: 'typstphone', profile: PROFILE, touch: true }); |
| 32 | const { page } = s; |
| 33 | const sleep = (ms) => new Promise(r => setTimeout(r, ms)); |
| 34 | |
| 35 | const look = (tag) => page.evaluate((tag) => { |
| 36 | const q = (sel) => document.querySelector(sel); |
| 37 | const box = (e) => { |
| 38 | if (!e) return null; |
| 39 | const r = e.getBoundingClientRect(); |
| 40 | return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }; |
| 41 | }; |
| 42 | const sh = document.getElementById('msheet'); |
| 43 | const sb = document.getElementById('msheet-body'); |
| 44 | const sc = q('#typst-live .tl-scroll'); |
| 45 | const w = window.DaimondTypstWatch ? window.DaimondTypstWatch.state() : null; |
| 46 | return { |
| 47 | tag, |
| 48 | sheetH: sh ? Math.round(sh.getBoundingClientRect().height) : null, |
| 49 | guest: window.DaimondSheet ? window.DaimondSheet.guest() : null, |
| 50 | liveIn: !!(sb && q('#typst-live') && sb.contains(q('#typst-live'))), |
| 51 | sheetBody: sb ? { ...box(sb), scrollH: sb.scrollHeight, scrollTop: sb.scrollTop } : null, |
| 52 | pageY: Math.round(window.scrollY), |
| 53 | scroller: sc ? { ...box(sc), scrollH: sc.scrollHeight, scrollTop: Math.round(sc.scrollTop), |
| 54 | overscroll: getComputedStyle(sc).overscrollBehaviorY, |
| 55 | touchAction: getComputedStyle(sc).touchAction } : null, |
| 56 | ask: box(document.getElementById('msheet-ask')), |
| 57 | grab: box(document.getElementById('msheet-grab')), |
| 58 | watch: w ? { mode: w.mode, drawn: w.drawn, pages: w.pages, |
| 59 | at: w.at ? { page: w.at.page, into: Math.round(w.at.into) } : null } : null, |
| 60 | }; |
| 61 | }, tag); |
| 62 | |
| 63 | /// A real touch drag through CDP, so the browser's own scrolling and gesture |
| 64 | /// arbitration decide what moves — not a scripted `scrollTop`. |
| 65 | async function swipe(cdp, fromX, fromY, toX, toY, steps = 14) { |
| 66 | const pt = (x, y) => [{ x: Math.round(x), y: Math.round(y), radiusX: 8, radiusY: 8, force: 1, id: 1 }]; |
| 67 | await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: pt(fromX, fromY) }); |
| 68 | for (let i = 1; i <= steps; i++) { |
| 69 | await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', |
| 70 | touchPoints: pt(fromX + (toX - fromX) * i / steps, fromY + (toY - fromY) * i / steps) }); |
| 71 | await sleep(18); |
| 72 | } |
| 73 | await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] }); |
| 74 | await sleep(500); |
| 75 | } |
| 76 | |
| 77 | try { |
| 78 | await page.waitForTimeout(1200); |
| 79 | await page.evaluate(async (text) => { |
| 80 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 81 | await m.write_file('proj/main.typ', text); |
| 82 | }, MAIN); |
| 83 | |
| 84 | // The harness connects the model through the Admin dialog and leaves it up. |
| 85 | // It covers the whole screen, so every touch below would land on it and the |
| 86 | // run would report that nothing moves — which is true, and about the dialog. |
| 87 | await page.keyboard.press('Escape'); |
| 88 | await sleep(500); |
| 89 | console.log('modal after Escape: ' + await page.evaluate(() => |
| 90 | !!document.querySelector('.modal-card'))); |
| 91 | |
| 92 | await page.setViewportSize({ width: 390, height: 844 }); |
| 93 | await page.waitForTimeout(600); |
| 94 | |
| 95 | await page.evaluate(() => window.DaimondDoc.show('proj/main.typ')); |
| 96 | await sleep(1500); |
| 97 | console.log(JSON.stringify(await look('the .typ open in the sheet'), null, 1)); |
| 98 | |
| 99 | // WHAT IS ON TOP OF ⚙ COMPILE. A button a finger cannot reach is a feature that |
| 100 | // does not exist on a phone, and `elementFromPoint` is the only thing that says |
| 101 | // so — the button is visible, sized and enabled either way. |
| 102 | console.log('compile button: ' + JSON.stringify(await page.evaluate(() => { |
| 103 | const b = document.querySelector('[data-act="compile"]'); |
| 104 | if (!b) return 'ABSENT'; |
| 105 | const r = b.getBoundingClientRect(); |
| 106 | const cx = r.left + r.width / 2, cy = r.top + r.height / 2; |
| 107 | const stack = document.elementsFromPoint(cx, cy).slice(0, 4).map(e => |
| 108 | e.tagName.toLowerCase() + (e.id ? '#' + e.id : '') |
| 109 | + (e.className && typeof e.className === 'string' ? '.' + e.className.split(' ')[0] : '')); |
| 110 | return { at: { x: Math.round(cx), y: Math.round(cy) }, stack, reaches: stack[0] }; |
| 111 | }))); |
| 112 | |
| 113 | await page.evaluate(() => document.querySelector('[data-act="compile"]').click()); |
| 114 | for (let i = 0; i < 90; i++) { |
| 115 | const w = await page.evaluate(() => window.DaimondTypstWatch.state()); |
| 116 | if (w.drawn) break; |
| 117 | await sleep(500); |
| 118 | } |
| 119 | await sleep(1000); |
| 120 | const drawn = await look('live view drawn on a phone'); |
| 121 | console.log(JSON.stringify(drawn, null, 1)); |
| 122 | await shot(s, 'typstphone-drawn'); |
| 123 | |
| 124 | const cdp = await page.context().newCDPSession(page); |
| 125 | if (drawn.scroller) { |
| 126 | const sc = drawn.scroller; |
| 127 | const top = sc.y + 30; // 30px into the pages: the contested band |
| 128 | const mid = sc.y + Math.round(sc.h / 2); |
| 129 | |
| 130 | // 1. A drag UP from the middle of the pages. Uncontested: it must scroll. |
| 131 | await swipe(cdp, 195, mid, 195, mid - 280); |
| 132 | console.log(JSON.stringify(await look('drag UP from the middle of the pages'), null, 1)); |
| 133 | |
| 134 | // 2. A drag DOWN from near the TOP of the pages, with the document already |
| 135 | // scrolled. The scroller has somewhere to go, so it should take it. |
| 136 | await swipe(cdp, 195, top, 195, top + 240); |
| 137 | console.log(JSON.stringify(await look('drag DOWN near the top, document scrolled'), null, 1)); |
| 138 | |
| 139 | // 3. The same drag with the document AT the top: the scroller has nowhere to |
| 140 | // go, so the gesture chains — to the sheet, the sheet body, or the page. |
| 141 | await page.evaluate(() => { document.querySelector('#typst-live .tl-scroll').scrollTop = 0; }); |
| 142 | await sleep(300); |
| 143 | await swipe(cdp, 195, top, 195, top + 300); |
| 144 | console.log(JSON.stringify(await look('drag DOWN at the very top of the document'), null, 1)); |
| 145 | |
| 146 | // 4. The grabber still does what it is for, and the reader keeps his place |
| 147 | // across it: a sheet detent is a resize, and a resize is a repaint at a |
| 148 | // new scale rather than a rebuild. |
| 149 | await swipe(cdp, 195, mid, 195, mid - 400); |
| 150 | const before = await look('scrolled into the book, before the sheet moves'); |
| 151 | console.log(JSON.stringify(before, null, 1)); |
| 152 | const g = drawn.grab; |
| 153 | await swipe(cdp, 195, g.y + Math.round(g.h / 2), 195, g.y + Math.round(g.h / 2) + 260); |
| 154 | console.log(JSON.stringify(await look('drag DOWN on the grabber — a smaller sheet'), null, 1)); |
| 155 | const small = await look('x'); |
| 156 | await swipe(cdp, 195, small.grab.y + Math.round(small.grab.h / 2), 195, 120); |
| 157 | console.log(JSON.stringify(await look('drag UP on the grabber — full again'), null, 1)); |
| 158 | |
| 159 | // 5. A rebuild, while the sheet is up: the whole feature, on a phone. |
| 160 | await page.evaluate(async () => { |
| 161 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 162 | await m.write_file('proj/main.typ', |
| 163 | '#set page(width: 120mm, height: 160mm, margin: 12mm)\n' |
| 164 | + '#set text(size: 10pt)\n#set par(justify: true)\n= The book\n' |
| 165 | + 'ZULU #lorem(400)\n#pagebreak()\n#lorem(400)\n#pagebreak()\n#lorem(400)\n'); |
| 166 | }); |
| 167 | for (let i = 0; i < 30; i++) { |
| 168 | const w = await page.evaluate(() => window.DaimondTypstWatch.state()); |
| 169 | if (w.drawn > 1) break; |
| 170 | await sleep(300); |
| 171 | } |
| 172 | await sleep(600); |
| 173 | console.log(JSON.stringify(await look('after a rebuild, sheet up'), null, 1)); |
| 174 | } |
| 175 | await cdp.detach(); |
| 176 | await shot(s, 'typstphone-end'); |
| 177 | } finally { |
| 178 | await s.close(); |
| 179 | } |