Oregami
Repositories/oxedyne/daimond

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
11import path from 'node:path';
12import { fileURLToPath } from 'node:url';
13import { open, shot, scratch } from './harness.mjs';
14
15const HERE = path.dirname(fileURLToPath(import.meta.url));
16const PROFILE = scratch('pw', 'typstphone');
17
18const 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.
31const s = await open({ name: 'typstphone', profile: PROFILE, touch: true });
32const { page } = s;
33const sleep = (ms) => new Promise(r => setTimeout(r, ms));
34
35const 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`.
65async 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
77try {
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}