/* Drive cut, copy and paste in a real browser. * * The third of the drive scripts, and the one that had to exist: every * bug it covers was invisible to the Go suite. The vendored grid's own * paste dropped cells — a six-cell block landed five, an external * two-row paste landed one — and the cells that vanished did so because * the library wrote them by a path that never fires onchange, so they * never reached the server at all. * * It also drives spec §4.2's translation pass end to end: fill `=A1*2` * down and read `=A2*2` back out of the formula bar's own source. * * Exits non-zero on any failed check, so it can gate a deploy by hand. * Not part of `make test`: it needs a browser and a running instance, * and it needs clipboard permissions granted to the context. * * SHEETS_SOCK=$D/s.sock SHEETS_LOG=$D/s.log \ * PLAYWRIGHT=/path/to/playwright/index.js node tools/drive/clipboard.mjs */ import { checkCellQueue } from './batch-checks.mjs'; import net from 'node:net'; import fs from 'node:fs'; const pw=await import(process.env.PLAYWRIGHT); const chromium=pw.chromium||(pw.default&&pw.default.chromium); const 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());}); await new Promise(r=>proxy.listen(8799,'127.0.0.1',r)); const base='http://127.0.0.1:8799'; const browser=await chromium.launch({executablePath:process.env.CHROMIUM_EXECUTABLE_PATH}); const ctx=await browser.newContext({viewport:{width:1440,height:900},permissions:['clipboard-read','clipboard-write']}); const page=await ctx.newPage(); const errors=[]; page.on('pageerror',e=>errors.push(String(e))); page.on('console',m=>{if(m.type()==='error')errors.push(m.text());}); const fails=[]; const ok=n=>console.log(' ok '+n); const check=(n,c,d)=>c?ok(n):(fails.push(n),console.log(' FAIL '+n+' — '+d)); await page.goto(`${base}/signin`); await page.fill('input[type=email]','paul@example.com'); await Promise.all([page.waitForURL(/signin\?sent=1/), page.click('form[action="/signin"] button[type=submit]')]); const mm=[...fs.readFileSync(process.env.SHEETS_LOG,'utf8').matchAll(/http:\/\/127\.0\.0\.1:8799\/auth\/verify\?[^\s\\"]+/g)].pop(); await page.goto(mm[0]); await page.waitForURL(/\/sheets/); // Create from the top bar, then name it by renaming: the list's #title // field is gone, so a sheet is born "Untitled sheet" and named after. await Promise.all([page.waitForURL(/\/sheets\/\d+$/),page.click('[estilo-bar-actions] form[action="/sheets"] button[type=submit]')]); await page.fill('[estilo-bar-title-input]','Clip2'); await Promise.all([page.waitForURL(/\/sheets\/\d+$/),page.press('[estilo-bar-title-input]','Enter')]); await page.waitForFunction(()=>window.SheetsGrid&&window.SheetsGrid.worksheet()); const w=(r,c,v)=>page.evaluate(([r,c,v])=>window.SheetsGrid.write(r,c,v),[r,c,v]); const sel=(x1,y1,x2,y2)=>page.evaluate(([a,b,c,d])=>window.SheetsGrid.worksheet().updateSelectionFromCoords(a,b,c,d),[x1,y1,x2,y2]); const raw=(r,c)=>page.evaluate(([r,c])=>window.SheetsGrid.rawAt(r,c),[r,c]); const shown=(r,c)=>page.evaluate(([r,c])=>window.SheetsGrid.worksheet().getValueFromCoords(c,r),[r,c]); for(const [r,c,v] of [[0,0,'10'],[1,0,'20'],[2,0,'30'],[0,1,'=A1*2']]) await w(r,c,v); await page.waitForTimeout(500); /* A REAL MOUSE CLICK first, and this is the most important line in the * file. The vendored grid acts on Ctrl+C/X/V from its own document * keydown whenever jspreadsheet.current is set — which a mousedown * inside a worksheet does and updateSelectionFromCoords does not. An * earlier version of this script never clicked a cell, so it measured * the one state a person with a mouse is never in, and reported the * clipboard as working while Ctrl+C copied the library's display * string. Found by review; do not replace these clicks with a * programmatic selection. */ const cellAt=(r,c)=>`[data-sheet-grid] tbody tr:nth-child(${r+1}) td:nth-child(${c+2})`; await page.click(cellAt(0,1)); await page.waitForTimeout(200); check('a mouse click arms the vendored grid (the state this must survive)', await page.evaluate(()=>{try{return !!(window.jspreadsheet&&jspreadsheet.current);}catch(e){return false;}}), 'jspreadsheet.current is not set, so this run is measuring the wrong state'); await page.evaluate(async()=>{try{await navigator.clipboard.writeText('SENTINEL');}catch(e){}}); await page.keyboard.press('Control+c'); await page.waitForTimeout(400); const afterClick=await page.evaluate(async()=>{try{return await navigator.clipboard.readText();}catch(e){return 'ERR';}}); check('after a mouse click, Ctrl+C copies the RAW input, not the display string', afterClick==='=A1*2', `got ${JSON.stringify(afterClick)} — "20" means the library took the keydown`); console.log('\n— fill down translates (spec 4.2)'); await sel(1,0,1,2); await page.waitForTimeout(200); await page.click('[data-menubar] > details:nth-child(2) > summary'); await page.click('[data-cmd="fill-down"]'); await page.waitForTimeout(900); check('B2 is =A2*2', await raw(1,1)==='=A2*2', 'got '+await raw(1,1)); check('B3 shows 60', await shown(2,1)==='60', 'got '+await shown(2,1)); console.log('\n— copy carries every cell and paste loses none'); await page.evaluate(()=>window.SheetsGrid.focus()); await sel(0,0,1,2); await page.waitForTimeout(200); await page.keyboard.press('Control+c'); await page.waitForTimeout(300); const clip=await page.evaluate(async()=>navigator.clipboard.readText()); console.log(' clipboard:',JSON.stringify(clip)); await sel(5,0,5,0); await page.waitForTimeout(200); await page.keyboard.press('Control+v'); await page.waitForTimeout(1400); const block=await page.evaluate(()=>{const g=window.SheetsGrid;const o={}; 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;}); console.log(' pasted:',JSON.stringify(block)); check('all six cells arrived', Object.keys(block).length===6, Object.keys(block).length+' cells'); check('the formula translated to =F3*2', block['2,6']==='=F3*2', 'got '+block['2,6']); console.log('\n— paste from outside loses no row'); await page.evaluate(async()=>navigator.clipboard.writeText('p\tq\nr\ts')); await page.evaluate(()=>window.SheetsGrid.focus()); await sel(9,0,9,0); await page.waitForTimeout(200); await page.keyboard.press('Control+v'); await page.waitForTimeout(1200); const ext=await page.evaluate(()=>{const g=window.SheetsGrid;const o={}; 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;}); console.log(' external:',JSON.stringify(ext)); check('external paste keeps its first row', ext['0,9']==='p'&&ext['0,10']==='q', JSON.stringify(ext)); console.log('\n— quoting survives a round trip'); await w(20,0,'has\ttab and\nnewline'); await page.waitForTimeout(400); await page.evaluate(()=>window.SheetsGrid.focus()); await sel(0,20,0,20); await page.waitForTimeout(200); await page.keyboard.press('Control+c'); await page.waitForTimeout(300); await sel(3,20,3,20); await page.waitForTimeout(200); await page.keyboard.press('Control+v'); await page.waitForTimeout(900); check('a cell holding a tab and a newline round-trips', await raw(20,3)==='has\ttab and\nnewline', JSON.stringify(await raw(20,3))); console.log('\n— cut holds the source until paste'); await sel(0,0,0,0); await page.waitForTimeout(200); await page.evaluate(()=>window.SheetsGrid.focus()); await page.keyboard.press('Control+x'); await page.waitForTimeout(900); check('Ctrl+X kept A1', await raw(0,0)==='10', 'got '+JSON.stringify(await raw(0,0))); await sel(10,0,10,0); await page.keyboard.press('Control+v'); await page.waitForFunction(()=>SheetsGrid.rawAt(0,0)==='' && SheetsGrid.rawAt(0,10)==='10'); console.log('\n— the menu items'); // B1 holds =A1*2 (sel takes col,row). await sel(1,0,1,0); await page.waitForTimeout(200); await page.click('[data-menubar] > details:nth-child(2) > summary'); await page.click('[data-cmd="copy"]'); await page.waitForTimeout(500); check('menu Copy reaches the system clipboard', (await page.evaluate(async()=>navigator.clipboard.readText()))==='=A1*2', 'got '+await page.evaluate(async()=>navigator.clipboard.readText())); await sel(7,7,7,7); await page.waitForTimeout(200); await page.click('[data-menubar] > details:nth-child(2) > summary'); await page.click('[data-cmd="paste"]'); await page.waitForTimeout(900); // H8 is 7 rows and 6 columns from B1, so A1 becomes G8. check('menu Paste writes a translated cell', await raw(7,7)==='=G8*2', 'got '+JSON.stringify(await raw(7,7))); console.log('\n note text:',JSON.stringify(await page.textContent('[data-sheet-note]'))); try { await checkCellQueue(page); ok('atomic paste and failed cut queue'); } catch (err) { check('atomic paste and failed cut queue', false, err.message); } console.log('ERRORS:',JSON.stringify([...new Set(errors)])); console.log(fails.length?('\nFAILURES: '+fails.join('; ')):'\nall checks passed'); await browser.close(); proxy.close(); process.exit(fails.length?1:0);