oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Real server-rendered menus and assets, mocked transport; no live user data.
2// SHEETS_MENU_FIXTURE=/tmp/menus.json go test ./internal/sheets -run TestBrowserMenuFixture
3// PLAYWRIGHT=/path/to/playwright/index.mjs SHEETS_MENU_FIXTURE=/tmp/menus.json node tools/drive/menu-actions.mjs
4import assert from 'node:assert/strict';
5import fs from 'node:fs';
6const pw = await import(process.env.PLAYWRIGHT || 'playwright');
7const fixture = JSON.parse(fs.readFileSync(process.env.SHEETS_MENU_FIXTURE));
8const origin = 'https://sheets.test';
9for (const name of ['chromium', 'webkit']) {
10 const options = name === 'webkit' && process.env.WEBKIT_EXECUTABLE_PATH ? {executablePath:process.env.WEBKIT_EXECUTABLE_PATH} : {};
11 const browser = await pw[name].launch(options);
12 try {
13 const page = await browser.newPage({viewport:{width:1440,height:1000}});
14 let requests = [], errors = [];
15 page.on('pageerror', e=>errors.push(e.message));
16 // Force the Clipboard API fallback so command dispatch is deterministic in
17 // both engines. Real clipboard behavior is covered by clipboard.mjs.
18 await page.addInitScript(()=>{
19 window.menuClipboard = '';
20 document.execCommand = ()=>false;
21 Object.defineProperty(navigator,'clipboard',{value:{
22 writeText:async text=>{window.menuClipboard=text;},
23 readText:async ()=>'pasted'
24 }});
25 });
26 await page.route(origin+'/**',async route=>{
27 const req=route.request(),path=new URL(req.url()).pathname;
28 if(path in fixture.assets) return route.fulfill({body:fixture.assets[path],contentType:
29 path.endsWith('.js')?'application/javascript':path.endsWith('.css')?'text/css':'image/svg+xml'});
30 if(path.endsWith('/stream'))return route.fulfill({body:'',contentType:'text/event-stream'});
31 requests.push({method:req.method(),path,body:req.postData()});
32 if(path.startsWith('/api/'))return route.fulfill({json:{version:1,cells:[
33 {r:0,c:0,raw:'10',display:'10'},{r:1,c:0,raw:'20',display:'20'},
34 {r:2,c:0,raw:'30',display:'30'}]}});
35 return route.fulfill({body:fixture.html,contentType:'text/html'});
36 });
37 async function load() {
38 await page.goto(origin+'/sheets/1');
39 await page.waitForFunction(()=>window.SheetsGrid?.worksheet());
40 await page.locator('td[data-x="0"][data-y="0"]').click();
41 await page.evaluate(()=>SheetsGrid.worksheet().updateSelectionFromCoords(0,0,1,1));
42 requests=[];
43 }
44 await load();
45 const inventory=await page.locator('[data-menubar] a, [data-menubar] button').evaluateAll(els=>els.map((el,index)=>{
46 const top=el.closest('[data-menubar] > details'), sub=el.closest('[rst-menu-group]');
47 return {index,label:el.textContent.trim(),cmd:el.dataset.cmd,href:el.getAttribute('href'),
48 action:el.form?.getAttribute('action'),axis:el.form?.dataset.axis,
49 menu:[...top.parentElement.children].indexOf(top),
50 sub:sub?[...top.querySelectorAll('[rst-menu-group]')].indexOf(sub):null};
51 }));
52 for(const item of inventory) {
53 await load();
54 if(item.cmd==='fn')await page.evaluate(()=>SheetsGrid.worksheet().updateSelectionFromCoords(0,3,0,3));
55 const menu=page.locator('[data-menubar] > details').nth(item.menu);
56 await menu.locator(':scope > summary').click();
57 if(item.sub!==null)await menu.locator('[rst-menu-group]').nth(item.sub).locator(':scope > summary').click();
58 if(item.axis)await menu.locator(`[data-axis="${item.axis}"] input`).fill('48');
59 const button=page.locator('[data-menubar] a, [data-menubar] button').nth(item.index);
60 if(item.href || item.action) {
61 const method=item.action?'POST':'GET',path=item.action||item.href;
62 const sent=page.waitForRequest(req=>req.method()===method&&new URL(req.url()).pathname===path,{timeout:3000});
63 await button.click();await sent;
64 } else {
65 const suffix=item.axis || item.cmd==='autofit-columns'?'/size':item.cmd==='paste'?'/cells/batch':
66 ['fn','clear','fill-down','fill-right'].includes(item.cmd)?'/cells':'/format';
67 const posted=['copy','cut','select-all'].includes(item.cmd)?null:
68 page.waitForRequest(req=>req.method()==='POST'&&new URL(req.url()).pathname.endsWith(suffix),{timeout:3000});
69 await button.click();
70 if(['copy','cut'].includes(item.cmd)) {
71 await page.waitForFunction(()=>window.menuClipboard!== '',{},{timeout:3000});
72 assert.equal(requests.filter(r=>r.method==='POST').length,0,item.label+' must not write cells');
73 } else if(item.cmd==='select-all') {
74 assert.equal(await page.evaluate(()=>SheetsGrid.selection().r2===SheetsGrid.rows-1&&SheetsGrid.selection().c2===SheetsGrid.cols-1),true);
75 } else {
76 await posted;
77 assert.ok(requests.some(r=>r.method==='POST'&&r.path.endsWith(suffix)),`${name}: ${item.label} must POST ${suffix}`);
78 }
79 }
80 console.log(`${name}: OK ${item.label.replace(/\s+/g,' ')}`);
81 }
82 // Keyboard field editing must not be swallowed by menu navigation.
83 await load();
84 const format=page.locator('[data-menubar] > details').filter({has:page.locator(':scope > summary', {hasText:'Format'})});
85 await format.locator(':scope > summary').click();
86 const width=page.locator('#colw');await width.fill('48');await width.press('ArrowUp');
87 assert.equal(await width.inputValue(),'49');
88 await width.press('Enter');
89 assert.ok(requests.some(r=>r.path.endsWith('/size')&&JSON.parse(r.body).size===49));
90 await load();
91 const file=page.locator('[data-menubar] > details').first();
92 await file.locator('summary').press('Enter');
93 const submitted=page.waitForRequest(req=>req.method()==='POST'&&new URL(req.url()).pathname==='/sheets');
94 await page.keyboard.press('Enter');await submitted;
95 await load();await file.locator('summary').click();await page.keyboard.press('Escape');
96 assert.equal(await file.getAttribute('open'),null);
97 await file.locator('summary').click();await page.locator('[data-formula-input]').click();
98 assert.equal(await file.getAttribute('open'),null);
99 assert.deepEqual(errors,[]);
100 console.log(`${name}: ${inventory.length} actions, keyboard submission, resize, Escape, and outside click passed`);
101 } finally {await browser.close();}
102}
103