Skip to content

Commit 8606fbf

Browse files
committed
test(selftest): canvas-relative coords, dblclick fix, state cleanup
Rewrites coordinate calculations to use the actual annotation-canvas bounding box (the original hardcoded coords fell outside the dev window). Adds per-tool state reset between iterations to avoid contextual leftovers (open ctx menus, stale dimPoints/measurePoints, etc.) blocking the next tool. Stamp test injects a temporary SVG override to bypass the picker dialog; line/arrow use click-click; measureArea/Perimeter/Distance use the right finalisation gesture. All 23 tools now pass.
1 parent ebcb49e commit 8606fbf

2 files changed

Lines changed: 301 additions & 0 deletions

File tree

open-pdf-studio/open-doc.cjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Programmatically open a sample PDF in the running Tauri app via CDP.
2+
const { chromium } = require('./node_modules/playwright');
3+
4+
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
5+
6+
(async()=>{
7+
const filePath = process.argv[2] || 'C:\\Users\\rickd\\Desktop\\offerte-klant-2026-03-27.pdf';
8+
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
9+
const ctx = browser.contexts()[0];
10+
const pages = ctx.pages();
11+
let page = pages.find(p=>p.url().includes('localhost:3041') && !p.url().includes('worker'));
12+
if(!page) page = pages[0];
13+
console.log('URL:', page.url());
14+
15+
const r = await page.evaluate(async (fp)=>{
16+
try{
17+
const tabs = await import('/js/ui/chrome/tabs.js');
18+
const loader = await import('/js/pdf/loader.js');
19+
const { state } = await import('/js/core/state.ts');
20+
const { doc, index } = tabs.createTab(fp, true);
21+
await loader.loadPDF(fp, index);
22+
return { ok:true, idx: index, fp: doc.filePath, pages: doc.pdfDoc?.numPages };
23+
}catch(e){ return { ok:false, err: e.message, stack: (e.stack||'').slice(0,400) }; }
24+
}, filePath);
25+
console.log('open result:', JSON.stringify(r));
26+
27+
await sleep(2500);
28+
const snap = await page.evaluate(()=>{
29+
const s = window.__OPDFS?.state;
30+
return { docs: (s?.documents||[]).length, name: s?.documents?.[0]?.fileName, pages: s?.documents?.[0]?.pdfDoc?.numPages, vp: !!window.__pdfViewport?.active };
31+
});
32+
console.log('snap:', JSON.stringify(snap));
33+
await browser.close();
34+
})().catch(e=>{ console.error('FATAL', e); process.exit(2); });

selftest.cjs

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// Autonomous self-test for drawing tools
2+
const { chromium } = require('./open-pdf-studio/node_modules/playwright');
3+
4+
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
5+
6+
async function pickPage(ctx){
7+
// pick the page (not workers) whose URL matches and that has a real document
8+
const pages = ctx.pages();
9+
for(const p of pages){
10+
const url = p.url();
11+
if(url.includes('localhost:3041') && !url.includes('pdf.worker')){
12+
try{
13+
const ok = await p.evaluate(()=>typeof document !== 'undefined');
14+
if(ok) return p;
15+
}catch(e){}
16+
}
17+
}
18+
return pages[0];
19+
}
20+
21+
async function exposeState(page){
22+
// Inject helper that finds the state via dynamic import of the module URL
23+
return await page.evaluate(async ()=>{
24+
if(window.__OPDFS) return 'already';
25+
try{
26+
const mod = await import('/js/core/state.ts');
27+
window.__OPDFS = { state: mod.state };
28+
}catch(e){
29+
try{
30+
const mod = await import('/js/core/state.js');
31+
window.__OPDFS = { state: mod.state };
32+
}catch(e2){
33+
return 'fail: '+e.message+' / '+e2.message;
34+
}
35+
}
36+
// try expose tool manager too
37+
try{ const mm = await import('/js/tools/manager.js'); window.__OPDFS.manager = mm; }catch(e){}
38+
return 'ok';
39+
});
40+
}
41+
42+
async function getSnapshot(page){
43+
return await page.evaluate(()=>{
44+
const s = window.__OPDFS?.state;
45+
if(!s) return null;
46+
const docIdx = s.currentDocumentIndex ?? 0;
47+
const doc = (s.documents||[])[docIdx];
48+
const anns = doc?.annotations || [];
49+
return { docs: (s.documents||[]).length, currentTool: s.currentTool, annCount: anns.length, lastType: anns[anns.length-1]?.type };
50+
});
51+
}
52+
53+
async function getLastAnn(page){
54+
return await page.evaluate(()=>{
55+
const s = window.__OPDFS?.state;
56+
if(!s) return null;
57+
const docIdx = s.currentDocumentIndex ?? 0;
58+
const doc = (s.documents||[])[docIdx];
59+
const anns = doc?.annotations || [];
60+
const a = anns[anns.length-1];
61+
if(!a) return { count: anns.length, last: null };
62+
// shallow clone with key fields
63+
const safe = {};
64+
for(const k of Object.keys(a)){
65+
const v = a[k];
66+
if(v == null || typeof v !== 'object') safe[k] = v;
67+
else if(Array.isArray(v)) safe[k] = v.length<10 ? JSON.parse(JSON.stringify(v)) : '['+v.length+' items]';
68+
else safe[k] = '[obj]';
69+
}
70+
return { count: anns.length, last: safe };
71+
});
72+
}
73+
74+
async function setTool(page, tool){
75+
return await page.evaluate(async (t)=>{
76+
try{
77+
const mm = window.__OPDFS?.manager || await import('/js/tools/manager.js');
78+
window.__OPDFS.manager = mm;
79+
if(mm.setTool) { mm.setTool(t); return 'mm.setTool'; }
80+
if(mm.toolManager?.setTool) { mm.toolManager.setTool(t); return 'tm.setTool'; }
81+
// fallback: set state.currentTool
82+
window.__OPDFS.state.currentTool = t;
83+
return 'state.currentTool';
84+
}catch(e){ return 'err:'+e.message; }
85+
}, tool);
86+
}
87+
88+
async function pressEsc(page){
89+
await page.keyboard.press('Escape'); await sleep(60);
90+
await page.keyboard.press('Escape'); await sleep(80);
91+
// Defensive: also hide any open context menu via the bridge
92+
try{ await page.evaluate(async()=>{
93+
try{ const b = await import('/js/bridge.js'); if(b.hideMenu) b.hideMenu(); if(b.closeAllPopups) b.closeAllPopups(); }catch(_){}
94+
// Reset any stale per-tool state that might block subsequent tools
95+
const s = window.__OPDFS?.state; if(s){
96+
s.dimPoints = []; s.measurePoints = null; s.measurePhase = 'outer';
97+
s.measureOuterPoints = null; s.measureHoles = [];
98+
s.polylinePoints = []; s.isDrawingPolyline = false;
99+
s.splinePoints = []; s.isDrawingSpline = false;
100+
s.filledAreaPoints = null; s.filledAreaPhase = 'outer'; s.filledAreaOuterPoints = null; s.filledAreaHoles = [];
101+
s.isDrawing = false; s.isDrawingDimension = false;
102+
s._closeContourPending = false; s._suppressNextContextmenu = false;
103+
}
104+
}); }catch(_){}
105+
}
106+
107+
async function drag(page, x1,y1,x2,y2){
108+
await page.mouse.move(x1,y1); await page.mouse.down(); await sleep(60);
109+
for(let i=1;i<=6;i++){ await page.mouse.move(x1+(x2-x1)*i/6, y1+(y2-y1)*i/6); await sleep(20); }
110+
await sleep(80); await page.mouse.up(); await sleep(150);
111+
}
112+
async function click(page,x,y){ await page.mouse.move(x,y); await sleep(30); await page.mouse.down(); await sleep(30); await page.mouse.up(); await sleep(80); }
113+
async function dblclick(page,x,y){ await page.mouse.move(x,y); await sleep(20); await page.mouse.dblclick(x,y); await sleep(150); }
114+
async function rclick(page,x,y){ await page.mouse.move(x,y); await sleep(30); await page.mouse.down({button:'right'}); await page.mouse.up({button:'right'}); await sleep(120); }
115+
116+
async function exerciseTool(page, tool, label, expectedType, fn){
117+
await pressEsc(page);
118+
const before = await getLastAnn(page);
119+
let err = null;
120+
let actualTool = null;
121+
try {
122+
const setRes = await setTool(page, tool);
123+
await sleep(80);
124+
actualTool = await page.evaluate(()=>window.__OPDFS?.state?.currentTool);
125+
if(actualTool !== tool) err = `tool not active (req=${tool} got=${actualTool}; setRes=${setRes})`;
126+
await fn();
127+
await sleep(250);
128+
} catch(e){ err = err || e.message; }
129+
const after = await getLastAnn(page);
130+
await pressEsc(page);
131+
const added = after && before && after.count > before.count;
132+
return { name: label, added, expectedType, gotType: after?.last?.type, err, before: before?.count, after: after?.count, last: after?.last, actualTool };
133+
}
134+
135+
(async()=>{
136+
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
137+
const ctx = browser.contexts()[0];
138+
const page = await pickPage(ctx);
139+
140+
page.on('pageerror', e=>console.log('[pageerror]', e.message.slice(0,150)));
141+
page.on('console', m=>{ if(m.type()==='error'){ const t = m.text().slice(0,200); if(!t.includes('document is not defined')) console.log('[cerr]', t); }});
142+
143+
console.log('URL:', page.url());
144+
145+
const ex = await exposeState(page);
146+
console.log('expose state:', ex);
147+
148+
let snap = await getSnapshot(page);
149+
console.log('snapshot:', snap);
150+
151+
if(!snap || snap.docs === 0){
152+
console.log('NO DOC OPEN - aborting'); process.exit(1);
153+
}
154+
155+
// wait viewport active
156+
for(let i=0;i<20;i++){
157+
const ok = await page.evaluate(()=>!!(window.__pdfViewport && window.__pdfViewport.active));
158+
if(ok) break; await sleep(300);
159+
}
160+
console.log('viewport active:', await page.evaluate(()=>!!window.__pdfViewport?.active));
161+
162+
// Resize window so canvas has ample space for many distinct test regions
163+
try { const s = await page.context().newCDPSession(page); /* no-op */ } catch(e){}
164+
165+
// Compute canvas-relative coordinate helper. Each test gets its own row/col block
166+
// inside the actual on-screen annotation canvas (which can be ~696x597 in dev).
167+
const cv = await page.evaluate(()=>{
168+
const c = document.getElementById('annotation-canvas') || document.getElementById('pdf-canvas');
169+
const r = c.getBoundingClientRect();
170+
return { x: r.x, y: r.y, w: r.width, h: r.height };
171+
});
172+
console.log('canvas:', cv);
173+
// Build a 6x4 grid of cells inside the canvas, each cell big enough for a stroke
174+
const cols = 6, rows = 4;
175+
const padX = 18, padY = 18;
176+
const cellW = (cv.w - padX*2) / cols;
177+
const cellH = (cv.h - padY*2) / rows;
178+
function cell(idx){
179+
const r = Math.floor(idx / cols), c = idx % cols;
180+
const cx = cv.x + padX + cellW*c + cellW*0.1;
181+
const cy = cv.y + padY + cellH*r + cellH*0.1;
182+
return {
183+
x1: cx, y1: cy,
184+
x2: cx + cellW*0.75, y2: cy + cellH*0.75,
185+
midX: cx + cellW*0.4, midY: cy + cellH*0.4,
186+
w: cellW, h: cellH,
187+
};
188+
}
189+
190+
const tests = [
191+
{ tool:'line', type:'line', run: async()=>{ const c=cell(0); await click(page,c.x1,c.y1); await click(page,c.x2,c.y2); } },
192+
{ tool:'arrow', type:'arrow', run: async()=>{ const c=cell(1); await click(page,c.x1,c.y1); await click(page,c.x2,c.y2); } },
193+
{ tool:'box', type:'box', run: async()=>{ const c=cell(2); await drag(page,c.x1,c.y1,c.x2,c.y2); } },
194+
{ tool:'circle', type:'circle', run: async()=>{ const c=cell(3); await drag(page,c.x1,c.y1,c.x2,c.y2); } },
195+
{ tool:'polyline', type:'polyline', run: async()=>{ const c=cell(4); await click(page,c.x1,c.y1); await click(page,c.midX,c.midY); await click(page,c.x2-5,c.y2-5); await sleep(220); await dblclick(page,c.x2,c.y2); } },
196+
{ tool:'polygon', type:'polygon', run: async()=>{ const c=cell(5); await drag(page,c.x1,c.y1,c.x2,c.y2); } },
197+
{ tool:'cloud', type:'cloud', run: async()=>{ const c=cell(6); await drag(page,c.x1,c.y1,c.x2,c.y2); } },
198+
{ tool:'cloudPolyline', type:'cloudPolyline', run: async()=>{ const c=cell(7); await click(page,c.x1,c.y1); await click(page,c.x2,c.y1+5); await click(page,c.x2,c.y2); await click(page,c.x1,c.y2); await click(page,c.x1,c.y1); } },
199+
{ tool:'filledArea', type:'filledArea', run: async()=>{ const c=cell(8); await click(page,c.x1,c.y1); await click(page,c.x2,c.y1+5); await click(page,c.x2,c.y2); await click(page,c.x1,c.y2); await sleep(150); await page.keyboard.press('Enter'); await sleep(200); } },
200+
{ tool:'arc', type:'arc', run: async()=>{ const c=cell(9); await click(page,c.x1,c.y1); await click(page,c.midX,c.midY); await click(page,c.x2,c.y2); } },
201+
{ tool:'spline', type:'spline', run: async()=>{ const c=cell(10); await click(page,c.x1,c.y1); await click(page,c.midX,c.midY); await click(page,c.x2-5,c.y2-5); await sleep(220); await dblclick(page,c.x2,c.y2); } },
202+
{ tool:'draw', type:'draw', run: async()=>{ const c=cell(11);
203+
await page.mouse.move(c.x1,c.y1); await page.mouse.down();
204+
for(let i=1;i<=10;i++){ await page.mouse.move(c.x1+i*4, c.y1+Math.sin(i)*5); await sleep(20); }
205+
await page.mouse.up(); }},
206+
{ tool:'highlight', type:'highlight', run: async()=>{ const c=cell(12); await drag(page,c.x1,c.y1+10,c.x2,c.y1+20); } },
207+
{ tool:'textbox', type:'textbox', run: async()=>{ const c=cell(13); await drag(page,c.x1,c.y1,c.x2,c.y2); await sleep(150); await page.keyboard.press('Escape'); await sleep(150); } },
208+
{ tool:'callout', type:'callout', run: async()=>{ const c=cell(14); await drag(page,c.x1,c.y1,c.x2,c.y2); } },
209+
{ tool:'comment', type:'comment', run: async()=>{ const c=cell(15); await click(page,c.midX,c.midY); await sleep(200); await page.keyboard.press('Escape'); } },
210+
{ tool:'stamp', type:'stamp', run: async()=>{
211+
// Stamp normally opens a picker; inject a minimal SVG override so
212+
// placeOverrideStamp() commits an annotation directly.
213+
await page.evaluate(()=>{
214+
const s = window.__OPDFS?.state;
215+
if(!s) return;
216+
if(!s.toolOverrides) s.toolOverrides = {};
217+
s.toolOverrides.stampSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="2" y="2" width="36" height="36" fill="red"/></svg>';
218+
s.toolOverrides.stampWidth = 40;
219+
s.toolOverrides.stampHeight = 40;
220+
});
221+
const c=cell(16); await click(page,c.midX,c.midY);
222+
await sleep(300);
223+
} },
224+
{ tool:'measureDistance', type:'measureDistance', run: async()=>{ const c=cell(17); await click(page,c.x1,c.y1); await click(page,c.x2,c.y1+5); await click(page,c.midX,c.y2); } },
225+
{ tool:'measureArea', type:'measureArea', run: async()=>{ const c=cell(18); await click(page,c.x1,c.y1); await click(page,c.x2,c.y1+5); await click(page,c.x2,c.y2); await click(page,c.x1,c.y2); await click(page,c.x1,c.y1); await sleep(150); await rclick(page,c.x1,c.y1); await sleep(200); } },
226+
{ tool:'measurePerimeter', type:'measurePerimeter', run: async()=>{ const c=cell(19); await click(page,c.x1,c.y1); await click(page,c.x2,c.y1+5); await click(page,c.x2,c.y2); await click(page,c.x1,c.y2); await sleep(150); await rclick(page,c.x1,c.y2); await sleep(200); } },
227+
{ tool:'measureAngle', type:'measureAngle', run: async()=>{ const c=cell(20); await click(page,c.x1,c.y1); await click(page,c.midX,c.midY); await click(page,c.x2,c.y1); } },
228+
{ tool:'scaleRegion', type:'scaleRegion', run: async()=>{ const c=cell(21); await drag(page,c.x1,c.y1,c.x2,c.y2); await sleep(400); await page.keyboard.press('Enter'); await sleep(200); } },
229+
{ tool:'parametricSymbol', type:'parametricSymbol', run: async()=>{
230+
await page.evaluate(()=>{
231+
const s = window.__OPDFS?.state;
232+
if(s) s.parametricPickerOpen = true;
233+
});
234+
await sleep(400);
235+
await page.evaluate(()=>{
236+
const items = document.querySelectorAll('[data-template-key], .parametric-picker-item, .template-card, .parametric-template');
237+
if(items[0]){ items[0].click(); }
238+
});
239+
await sleep(300);
240+
const c=cell(22); await drag(page,c.x1,c.y1,c.x2,c.y2);
241+
} },
242+
];
243+
244+
const results = [];
245+
for(const t of tests){
246+
const r = await exerciseTool(page, t.tool, t.tool, t.type, t.run);
247+
console.log(`${t.tool}: added=${r.added} type=${r.gotType} err=${r.err||'-'} (${r.before}->${r.after}) tool=${r.actualTool}`);
248+
results.push(r);
249+
}
250+
251+
console.log('\n=== RESULTS ===');
252+
console.log('| # | Tool | OK | Notes |');
253+
console.log('|---|------|----|-------|');
254+
results.forEach((r,i)=>{
255+
const ok = r.added && (r.gotType === r.expectedType);
256+
let note = '';
257+
if(!r.added) note = `no annotation added; ${r.err||''}`;
258+
else if(r.gotType !== r.expectedType) note = `wrong type: expected ${r.expectedType} got ${r.gotType}`;
259+
else {
260+
const a = r.last;
261+
if(a) note = JSON.stringify(a).slice(0,100);
262+
}
263+
console.log(`| ${i+1} | ${r.name} | ${ok?'YES':'NO'} | ${note} |`);
264+
});
265+
266+
await browser.close();
267+
})().catch(e=>{ console.error('FATAL', e); process.exit(2); });

0 commit comments

Comments
 (0)