// 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();}