oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1/* Drive cut, copy and paste in a real browser.
2 *
3 * The third of the drive scripts, and the one that had to exist: every
4 * bug it covers was invisible to the Go suite. The vendored grid's own
5 * paste dropped cells — a six-cell block landed five, an external
6 * two-row paste landed one — and the cells that vanished did so because
7 * the library wrote them by a path that never fires onchange, so they
8 * never reached the server at all.
9 *
10 * It also drives spec §4.2's translation pass end to end: fill `=A1*2`
11 * down and read `=A2*2` back out of the formula bar's own source.
12 *
13 * Exits non-zero on any failed check, so it can gate a deploy by hand.
14 * Not part of `make test`: it needs a browser and a running instance,
15 * and it needs clipboard permissions granted to the context.
16 *
17 * SHEETS_SOCK=$D/s.sock SHEETS_LOG=$D/s.log \
18 * PLAYWRIGHT=/path/to/playwright/index.js node tools/drive/clipboard.mjs
19 */
20import { checkCellQueue } from './batch-checks.mjs';
21import net from 'node:net';
22import fs from 'node:fs';
23const pw=await import(process.env.PLAYWRIGHT); const chromium=pw.chromium||(pw.default&&pw.default.chromium);
24const proxy=net.createServer(c=>{const u=net.connect(process.env.SHEETS_SOCK);c.pipe(u);u.pipe(c);c.on('error',()=>u.destroy());u.on('error',()=>c.destroy());});
25await new Promise(r=>proxy.listen(8799,'127.0.0.1',r));
26const base='http://127.0.0.1:8799';
27const browser=await chromium.launch({executablePath:process.env.CHROMIUM_EXECUTABLE_PATH});
28const ctx=await browser.newContext({viewport:{width:1440,height:900},permissions:['clipboard-read','clipboard-write']});
29const page=await ctx.newPage();
30const errors=[]; page.on('pageerror',e=>errors.push(String(e))); page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});
31const fails=[]; const ok=n=>console.log(' ok '+n);
32const check=(n,c,d)=>c?ok(n):(fails.push(n),console.log(' FAIL '+n+' — '+d));
33await page.goto(`${base}/signin`); await page.fill('input[type=email]','paul@example.com');
34await Promise.all([page.waitForURL(/signin\?sent=1/), page.click('form[action="/signin"] button[type=submit]')]);
35const mm=[...fs.readFileSync(process.env.SHEETS_LOG,'utf8').matchAll(/http:\/\/127\.0\.0\.1:8799\/auth\/verify\?[^\s\\"]+/g)].pop();
36await page.goto(mm[0]); await page.waitForURL(/\/sheets/);
37// Create from the top bar, then name it by renaming: the list's #title
38// field is gone, so a sheet is born "Untitled sheet" and named after.
39await Promise.all([page.waitForURL(/\/sheets\/\d+$/),page.click('[estilo-bar-actions] form[action="/sheets"] button[type=submit]')]);
40await page.fill('[estilo-bar-title-input]','Clip2');
41await Promise.all([page.waitForURL(/\/sheets\/\d+$/),page.press('[estilo-bar-title-input]','Enter')]);
42await page.waitForFunction(()=>window.SheetsGrid&&window.SheetsGrid.worksheet());
43const w=(r,c,v)=>page.evaluate(([r,c,v])=>window.SheetsGrid.write(r,c,v),[r,c,v]);
44const sel=(x1,y1,x2,y2)=>page.evaluate(([a,b,c,d])=>window.SheetsGrid.worksheet().updateSelectionFromCoords(a,b,c,d),[x1,y1,x2,y2]);
45const raw=(r,c)=>page.evaluate(([r,c])=>window.SheetsGrid.rawAt(r,c),[r,c]);
46const shown=(r,c)=>page.evaluate(([r,c])=>window.SheetsGrid.worksheet().getValueFromCoords(c,r),[r,c]);
47
48for(const [r,c,v] of [[0,0,'10'],[1,0,'20'],[2,0,'30'],[0,1,'=A1*2']]) await w(r,c,v);
49await page.waitForTimeout(500);
50
51/* A REAL MOUSE CLICK first, and this is the most important line in the
52 * file. The vendored grid acts on Ctrl+C/X/V from its own document
53 * keydown whenever jspreadsheet.current is set — which a mousedown
54 * inside a worksheet does and updateSelectionFromCoords does not. An
55 * earlier version of this script never clicked a cell, so it measured
56 * the one state a person with a mouse is never in, and reported the
57 * clipboard as working while Ctrl+C copied the library's display
58 * string. Found by review; do not replace these clicks with a
59 * programmatic selection. */
60const cellAt=(r,c)=>`[data-sheet-grid] tbody tr:nth-child(${r+1}) td:nth-child(${c+2})`;
61await page.click(cellAt(0,1));
62await page.waitForTimeout(200);
63check('a mouse click arms the vendored grid (the state this must survive)',
64 await page.evaluate(()=>{try{return !!(window.jspreadsheet&&jspreadsheet.current);}catch(e){return false;}}),
65 'jspreadsheet.current is not set, so this run is measuring the wrong state');
66await page.evaluate(async()=>{try{await navigator.clipboard.writeText('SENTINEL');}catch(e){}});
67await page.keyboard.press('Control+c');
68await page.waitForTimeout(400);
69const afterClick=await page.evaluate(async()=>{try{return await navigator.clipboard.readText();}catch(e){return 'ERR';}});
70check('after a mouse click, Ctrl+C copies the RAW input, not the display string',
71 afterClick==='=A1*2', `got ${JSON.stringify(afterClick)} — "20" means the library took the keydown`);
72
73console.log('\n— fill down translates (spec 4.2)');
74await sel(1,0,1,2); await page.waitForTimeout(200);
75await page.click('[data-menubar] > details:nth-child(2) > summary');
76await page.click('[data-cmd="fill-down"]'); await page.waitForTimeout(900);
77check('B2 is =A2*2', await raw(1,1)==='=A2*2', 'got '+await raw(1,1));
78check('B3 shows 60', await shown(2,1)==='60', 'got '+await shown(2,1));
79
80console.log('\n— copy carries every cell and paste loses none');
81await page.evaluate(()=>window.SheetsGrid.focus());
82await sel(0,0,1,2); await page.waitForTimeout(200);
83await page.keyboard.press('Control+c'); await page.waitForTimeout(300);
84const clip=await page.evaluate(async()=>navigator.clipboard.readText());
85console.log(' clipboard:',JSON.stringify(clip));
86await sel(5,0,5,0); await page.waitForTimeout(200);
87await page.keyboard.press('Control+v'); await page.waitForTimeout(1400);
88const block=await page.evaluate(()=>{const g=window.SheetsGrid;const o={};
89 for(let r=0;r<3;r++)for(let c=5;c<7;c++){const v=g.rawAt(r,c); if(v!=='')o[r+','+c]=v;} return o;});
90console.log(' pasted:',JSON.stringify(block));
91check('all six cells arrived', Object.keys(block).length===6, Object.keys(block).length+' cells');
92check('the formula translated to =F3*2', block['2,6']==='=F3*2', 'got '+block['2,6']);
93
94console.log('\n— paste from outside loses no row');
95await page.evaluate(async()=>navigator.clipboard.writeText('p\tq\nr\ts'));
96await page.evaluate(()=>window.SheetsGrid.focus());
97await sel(9,0,9,0); await page.waitForTimeout(200);
98await page.keyboard.press('Control+v'); await page.waitForTimeout(1200);
99const ext=await page.evaluate(()=>{const g=window.SheetsGrid;const o={};
100 for(let r=0;r<3;r++)for(let c=9;c<12;c++){const v=g.rawAt(r,c); if(v!=='')o[r+','+c]=v;} return o;});
101console.log(' external:',JSON.stringify(ext));
102check('external paste keeps its first row', ext['0,9']==='p'&&ext['0,10']==='q', JSON.stringify(ext));
103
104console.log('\n— quoting survives a round trip');
105await w(20,0,'has\ttab and\nnewline'); await page.waitForTimeout(400);
106await page.evaluate(()=>window.SheetsGrid.focus());
107await sel(0,20,0,20); await page.waitForTimeout(200);
108await page.keyboard.press('Control+c'); await page.waitForTimeout(300);
109await sel(3,20,3,20); await page.waitForTimeout(200);
110await page.keyboard.press('Control+v'); await page.waitForTimeout(900);
111check('a cell holding a tab and a newline round-trips', await raw(20,3)==='has\ttab and\nnewline', JSON.stringify(await raw(20,3)));
112
113console.log('\n— cut holds the source until paste');
114await sel(0,0,0,0); await page.waitForTimeout(200);
115await page.evaluate(()=>window.SheetsGrid.focus());
116await page.keyboard.press('Control+x'); await page.waitForTimeout(900);
117check('Ctrl+X kept A1', await raw(0,0)==='10', 'got '+JSON.stringify(await raw(0,0)));
118await sel(10,0,10,0); await page.keyboard.press('Control+v');
119await page.waitForFunction(()=>SheetsGrid.rawAt(0,0)==='' && SheetsGrid.rawAt(0,10)==='10');
120
121console.log('\n— the menu items');
122// B1 holds =A1*2 (sel takes col,row).
123await sel(1,0,1,0); await page.waitForTimeout(200);
124await page.click('[data-menubar] > details:nth-child(2) > summary');
125await page.click('[data-cmd="copy"]'); await page.waitForTimeout(500);
126check('menu Copy reaches the system clipboard', (await page.evaluate(async()=>navigator.clipboard.readText()))==='=A1*2', 'got '+await page.evaluate(async()=>navigator.clipboard.readText()));
127await sel(7,7,7,7); await page.waitForTimeout(200);
128await page.click('[data-menubar] > details:nth-child(2) > summary');
129await page.click('[data-cmd="paste"]'); await page.waitForTimeout(900);
130// H8 is 7 rows and 6 columns from B1, so A1 becomes G8.
131check('menu Paste writes a translated cell', await raw(7,7)==='=G8*2', 'got '+JSON.stringify(await raw(7,7)));
132console.log('\n note text:',JSON.stringify(await page.textContent('[data-sheet-note]')));
133try { await checkCellQueue(page); ok('atomic paste and failed cut queue'); }
134catch (err) { check('atomic paste and failed cut queue', false, err.message); }
135console.log('ERRORS:',JSON.stringify([...new Set(errors)]));
136console.log(fails.length?('\nFAILURES: '+fails.join('; ')):'\nall checks passed');
137await browser.close(); proxy.close(); process.exit(fails.length?1:0);
138