// Browser-level component checks with the real vendored grid and mocked transport. // PLAYWRIGHT=/path/to/playwright/index.mjs node tools/drive/editing.mjs import assert from 'node:assert/strict'; import fs from 'node:fs'; const pw = await import(process.env.PLAYWRIGHT || 'playwright'); const browser = await pw.chromium.launch({headless: true}); const page = await browser.newPage(); const root = new URL('../../internal/sheets/static/', import.meta.url); let cells, batches, fail; const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.route('https://sheets.test/**', async route => { const req = route.request(), path = new URL(req.url()).pathname; if (path === '/') return route.fulfill({contentType:'text/html', body:`
${['vendor/jsuites.js','vendor/jspreadsheet.js','tsv.js','cellqueue.js','grid.js','formulabar.js','formulapick.js','clipboard.js'].map(x=>``).join('')}`}); if (path.startsWith('/static/')) return route.fulfill({body:fs.readFileSync(new URL(path.slice(8),root)), contentType:path.endsWith('.css')?'text/css':'application/javascript'}); if (req.method() === 'POST') { const data=req.postDataJSON(); const jobs=data.cells || [data]; batches.push(jobs); if(fail) return route.fulfill({status:400,body:'rejected'}); jobs.forEach(c=>cells.set(`${c.r},${c.c}`, {...c,display:c.raw})); } return route.fulfill({json:{cells:[...cells.values()],version:batches.length+1}}); }); async function reset(seed=[]) { cells=new Map(seed.map(([r,c,raw])=>[`${r},${c}`,{r,c,raw,display:raw}]));batches=[];fail=false; await page.goto('https://sheets.test/'); await page.waitForFunction(()=>window.SheetsGrid?.worksheet()); } const td=(r,c)=>page.locator(`td[data-x="${c}"][data-y="${r}"]`); async function select(r,c,r2=r,c2=c) { await page.evaluate(([r,c,r2,c2])=>{SheetsGrid.worksheet().updateSelectionFromCoords(c,r,c2,r2);SheetsGrid.focus();},[r,c,r2,c2]); } async function clip(type,text) { return page.evaluate(([type,text])=>{ const data=new DataTransfer();if(text!==undefined)data.setData('text/plain',text); document.activeElement.dispatchEvent(new ClipboardEvent(type,{bubbles:true,cancelable:true,clipboardData:data})); return data.getData('text/plain'); },[type,text]); } async function settled() {await page.evaluate(()=>SheetsCellQueue([],SheetsGrid.post));} try { await reset([[0,0,'one'],[1,0,'two']]);await select(0,0,1,0); const text=await clip('cut');await settled();assert.equal(batches.length,0); assert.equal(cells.get('0,0').raw,'one'); await select(1,0);await clip('paste',text);await settled(); assert.equal(batches.length,1);assert.equal(cells.get('0,0').raw,''); assert.equal(cells.get('1,0').raw,'one');assert.equal(cells.get('2,0').raw,'two'); assert.equal(new Set(batches[0].map(c=>`${c.r},${c.c}`)).size,batches[0].length); console.log('ok deferred cut and overlapping atomic move'); await reset([[0,0,'=B1']]);await select(0,0);const formula=await clip('cut'); await select(1,0);fail=true;await clip('paste',formula);await settled(); assert.equal(cells.get('0,0').raw,'=B1');fail=false; await clip('paste',formula);await settled();assert.equal(cells.get('0,0').raw,''); assert.equal(cells.get('1,0').raw,'=B1');assert.ok(!('fromR' in batches.at(-1)[0])); console.log('ok rejected paste preserves cut for retry; moved formulas stay verbatim'); await reset([[0,0,'hello']]);await select(0,0);await clip('cut'); await page.keyboard.press('Escape');await select(1,0);await clip('paste','hello');await settled(); assert.equal(cells.get('0,0').raw,'hello'); await select(0,0);await clip('cut');await page.locator('[data-formula-input]').focus(); await clip('copy');await select(2,0);await clip('paste','hello');await settled(); assert.equal(cells.get('0,0').raw,'hello');console.log('ok Escape and text-field copy cancel pending cuts'); await reset([[0,0,'one']]);await select(0,0);await clip('cut'); cells.set('0,0',{r:0,c:0,raw:'changed',display:'changed'}); await page.evaluate(()=>SheetsGrid.refetch());await select(1,0);await clip('paste','one');await settled(); assert.equal(batches.length,0);console.log('ok changed source rejects stale cut'); await reset([[0,0,'one'],[1,0,'two']]);await select(0,0,1,0);await clip('cut'); await select(29,0);await clip('paste','one\ntwo');await settled();assert.equal(batches.length,0); console.log('ok out-of-bounds move keeps source'); await reset([[0,0,'Room three']]);await td(0,0).dblclick();await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); const input=td(0,0).locator('input,textarea');await input.waitFor(); const fonts=await input.evaluate(el=>[getComputedStyle(el).fontSize,getComputedStyle(el.parentElement).fontSize]); assert.equal(fonts[0],fonts[1]);console.log('ok editor font matches cell'); await input.fill('=SUM('); const a=await td(0,1).boundingBox(),b=await td(2,2).boundingBox(); await page.mouse.move(a.x+10,a.y+10);await page.mouse.down(); await page.mouse.move(b.x+10,b.y+10);await page.mouse.up(); assert.equal(await input.inputValue(),'=SUM(B1:C3)'.slice(0,-1)); assert.equal(batches.length,0);await input.press('End');await input.type(')');await input.press('Enter');await page.waitForFunction(()=>SheetsGrid.rawAt(0,0)==='=SUM(B1:C3)'); assert.equal(cells.get('0,0').raw,'=SUM(B1:C3)');console.log('ok in-cell formula range drag and commit'); await select(3,0);const bar=page.locator('[data-formula-input]');await bar.fill('=SUM('); await td(0,1).click();assert.equal(await bar.inputValue(),'=SUM(B1'); await bar.type(')');assert.equal(await bar.inputValue(),'=SUM(B1)');await bar.press('Enter');await page.waitForFunction(()=>SheetsGrid.rawAt(3,0)==='=SUM(B1)');assert.equal(cells.get('3,0').raw,'=SUM(B1)'); console.log('ok formula bar inserts cell reference and commits to original cell'); await reset([[0,0,'=SUM(B1:B3)']]);await td(0,0).dblclick();await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); await td(0,0).locator('input,textarea').waitFor(); await page.waitForFunction(()=>document.activeElement.selectionStart===0); await td(0,1).click();assert.equal(cells.get('0,0').raw,'=SUM(B1:B3)'); console.log('ok clicking away from a selected existing formula preserves it'); await reset([[0,0,'original']]);await td(0,0).dblclick();await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); const cancel=td(0,0).locator('input,textarea'); await page.waitForFunction(()=>document.activeElement.value==='original' && document.activeElement.selectionEnd===8); await cancel.fill('=SUM('); await td(0,1).click();await cancel.press('Escape'); assert.equal(cells.get('0,0').raw,'original');assert.equal(batches.length,0); console.log('ok Escape discards formula reference edit'); assert.deepEqual(errors,[]); } finally {await browser.close();}