| 1 | import assert from 'node:assert/strict'; |
| 2 | import fs from 'node:fs'; |
| 3 | import { checkCellQueue } from './batch-checks.mjs'; |
| 4 | const pw = await import(process.env.PLAYWRIGHT || 'playwright'); |
| 5 | const base = process.env.SHEETS_CANARY_ORIGIN; |
| 6 | const state = process.env.SHEETS_CANARY_AUTH_STATE; |
| 7 | if (base !== 'https://overnight-sheets.bds.oncarlos.com' || !state) throw new Error('Missing HTTPS canary origin or authentication state'); |
| 8 | if ((fs.statSync(state).mode & 0o077) !== 0) throw new Error('Authentication state must be private'); |
| 9 | const expected = process.env.SHEETS_CANARY_VERSION; |
| 10 | if (!/^[a-f0-9]{7,40}$/.test(expected || '')) throw new Error('Expected candidate version is required'); |
| 11 | const browser = await pw.chromium.launch({headless:true,executablePath:process.env.CHROMIUM_EXECUTABLE_PATH}); |
| 12 | try { |
| 13 | const context = await browser.newContext({storageState:state, permissions:['clipboard-read','clipboard-write']}); |
| 14 | let signinPosted = false; |
| 15 | await context.route('**/signin', route => { |
| 16 | if (route.request().method() === 'POST') { signinPosted = true; return route.abort(); } |
| 17 | return route.continue(); |
| 18 | }); |
| 19 | const version = await context.request.get(base+'/api/version'); |
| 20 | assert.equal((await version.text()).trim(), expected); |
| 21 | const page = await context.newPage(); |
| 22 | const errors=[];page.on('pageerror',e=>errors.push(e.message)); |
| 23 | await page.goto(base+'/sheets'); |
| 24 | assert.equal(new URL(page.url()).pathname,'/sheets'); |
| 25 | const location = await page.evaluate(async()=>{ |
| 26 | const r=await fetch('/sheets',{method:'POST',body:new URLSearchParams({title:'Atomic batch canary '+Date.now()})}); |
| 27 | if(!r.ok)throw new Error('Fixture creation failed');return r.url; |
| 28 | }); |
| 29 | await page.goto(location); |
| 30 | await page.waitForFunction(()=>window.SheetsGrid?.worksheet()); |
| 31 | const ids=await page.locator('[data-sheet-surface]').evaluate(el=>({sheet:el.dataset.sheetId,tab:el.dataset.tabId})); |
| 32 | const api=`/api/sheets/${ids.sheet}/tabs/${ids.tab}/cells`; |
| 33 | const second=await context.newPage();second.on('pageerror',e=>errors.push(e.message)); |
| 34 | await second.goto(location);await second.waitForFunction(()=>window.SheetsGrid?.worksheet()); |
| 35 | const requests=[];page.on('request',r=>{if(r.method()==='POST'&&r.url().includes('/cells'))requests.push(r.url());}); |
| 36 | const text=Array.from({length:25},(_,r)=>Array.from({length:20},(_,c)=>String(r*20+c+1)).join('\t')).join('\n'); |
| 37 | await page.bringToFront(); |
| 38 | await page.evaluate(async text=>{await navigator.clipboard.writeText(text);},text); |
| 39 | await page.locator('[data-sheet-grid] tbody tr:first-child td:nth-child(2)').click(); |
| 40 | const start=Date.now(); |
| 41 | await page.keyboard.press('Control+v'); |
| 42 | await page.waitForFunction(()=>window.SheetsGrid.rawAt(24,19)==='500'); |
| 43 | assert.equal(requests.length,1);assert.ok(requests[0].endsWith('/cells/batch')); |
| 44 | const pasteMs=Date.now()-start; |
| 45 | await second.bringToFront(); |
| 46 | await second.waitForFunction(()=>window.SheetsGrid.rawAt(24,19)==='500'); |
| 47 | await page.bringToFront(); |
| 48 | await page.evaluate(()=>{window.SheetsGrid.worksheet().updateSelectionFromCoords(0,0,19,24);window.SheetsGrid.focus();}); |
| 49 | await page.keyboard.press('Control+x'); |
| 50 | assert.equal(await page.evaluate(()=>SheetsGrid.rawAt(0,0)), '1'); |
| 51 | await page.keyboard.press('Escape'); |
| 52 | const snap=await page.evaluate(async api=>{const r=await fetch(api);return r.json();},api); |
| 53 | assert.equal(snap.cells.length,500); |
| 54 | const before=snap.cells.map(c=>[c.r,c.c,c.raw]).sort(); |
| 55 | const refused=await page.evaluate(async api=>{ |
| 56 | const r=await fetch(api+'/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cells:[{r:0,c:0,raw:'99999'},{r:24,c:19,raw:'=SUM(('}]})});return r.status; |
| 57 | },api); |
| 58 | assert.equal(refused,400); |
| 59 | const after=await page.evaluate(async api=>{const r=await fetch(api);return r.json();},api); |
| 60 | assert.deepEqual(after.cells.map(c=>[c.r,c.c,c.raw]).sort(),before); |
| 61 | await page.reload();await page.waitForFunction(()=>window.SheetsGrid?.worksheet()&&window.SheetsGrid.rawAt(24,19)==='500'); |
| 62 | const nojs=await browser.newContext({storageState:state,javaScriptEnabled:false}); |
| 63 | const table=await nojs.newPage();await table.goto(location+'/table'); |
| 64 | assert.equal(await table.locator('a[aria-label="Edit T25"]').innerText(),'500'); |
| 65 | assert.equal(await table.locator('a[aria-label="Edit A1"]').innerText(),'1'); |
| 66 | const queue = await checkCellQueue(page); |
| 67 | assert.equal(signinPosted, false); |
| 68 | assert.deepEqual(errors,[]); |
| 69 | const result={origin:base,version:expected,queue,sheet:ids.sheet,tab:ids.tab,pasteCells:500,pasteRequests:1,pasteMs,secondPageObserved:true,clearThenPasteVisible:true,invalidBatchRolledBack:true,reloadPreserved:true,scriptlessTableLoaded:true}; |
| 70 | if(process.env.SHEETS_CANARY_RESULT)fs.writeFileSync(process.env.SHEETS_CANARY_RESULT,JSON.stringify(result,null,2)); |
| 71 | console.log(JSON.stringify(result)); |
| 72 | } finally {await browser.close();} |
| 73 | |