// Run against TEMPORARY tracker data only. Requires an existing Playwright installation. // node e2e/usecases-check.mjs [playwright-module-path] import {pathToFileURL} from 'node:url'; import {readFileSync,writeFileSync} from 'node:fs'; const [chrome,base,out,modulePath='playwright',dataDir]=process.argv.slice(2); if(!chrome||!base||!out)throw new Error('chrome base-url output-dir required (temporary server data only)'); const {chromium}=await import(modulePath.startsWith('/')?pathToFileURL(modulePath).href:modulePath); const browser=await chromium.launch({executablePath:chrome,headless:true,args:['--no-sandbox']}); const page=await browser.newPage({viewport:{width:1600,height:1000}}); await page.route('https://fonts.googleapis.com/**',r=>r.abort());await page.route('https://fonts.gstatic.com/**',r=>r.abort()); const errors=[];page.on('pageerror',e=>errors.push(e.message)); let passed=0; function check(ok,msg){if(!ok)throw new Error(msg);passed++;console.log('PASS '+msg);} const go=async path=>{console.log('OPEN '+path);await page.goto(base+path,{waitUntil:'domcontentloaded'});}; const wait=expression=>page.waitForFunction(expression); const shot=name=>page.screenshot({path:out+'/'+name+'.png'}); try{ await go('/use-cases');check(await page.locator('.uc-row').count()===162,'table shows all 162 use cases');await shot('usecases-table'); check(await page.locator('.uc-tabs a').count()===4,'catalog has all, Frontend, Backoffice and Integration tabs'); check(await page.evaluate(()=>{const t=document.querySelector('.uc-table-wrap');return t.clientHeight>=t.scrollHeight-1&&getComputedStyle(t).maxHeight==='none'}),'table extends down the page without a height limit'); const group=page.locator('[data-uc-module]').first(),groupButton=group.locator('[data-uc-toggle]'); const totalVisible=await page.locator('.uc-row:visible').count(),inGroup=await group.locator('.uc-row').count(); check(await group.locator('.uc-module-name').textContent().then(t=>t.length>5),'module groups show the full module name'); await groupButton.click();check(await page.locator('.uc-row:visible').count()===totalVisible-inGroup,'module group can collapse its use-case rows'); await go('/use-cases');check(await page.locator('[data-uc-module]').first().locator('[data-uc-toggle]').getAttribute('aria-expanded')==='false','collapsed module state survives reload'); await page.click('[data-uc-collapse-all]');check(await page.locator('.uc-row:visible').count()===0,'Collapse all hides every module row'); await page.click('[data-uc-expand-all]');check(await page.locator('.uc-row:visible').count()===162,'Expand all restores every use case'); await go('/use-cases?channel=frontend&module=REG&q=W-49');check(await page.locator('.uc-row').count()===1&&await page.locator('.uc-tabs [aria-current=page]').textContent().then(t=>t.includes('Frontend')),'channel tabs combine with module and search filters'); await go('/use-cases?channel=integration');check(await page.locator('.uc-row .uc-code').allTextContents().then(ids=>ids.includes('W-36')),'Integration tab includes explicitly named external-system workflows'); await go('/use-cases?module=REG&q=W-49');check(await page.locator('.uc-row').count()===1&&await page.locator('.uc-name').textContent()==='ลงทะเบียนเรียน','module and search filters preserve source'); await go('/use-cases?module=NOTI');check(await page.locator('.uc-row').count()===0,'background module shows empty results'); await go('/use-cases/W-49');await page.locator('.uc-node').first().waitFor();check(await page.locator('.uc-node').count()===8&&await page.locator('.uc-edge').count()===7,'draft includes all six source steps');await page.waitForFunction(()=>document.querySelector('#uc-history').getAttribute('aria-busy')==='false');check(await page.locator('#uc-history-empty').isVisible(),'revision history has a clear empty state');await shot('usecases-light'); check(await page.evaluate(()=>[...document.querySelectorAll('.uc-source ol > li')].every((li,i)=>document.querySelector(`[data-node="step-${i+1}"] .uc-node-desc`).textContent===li.querySelector('p').textContent)),'every source description is displayed in full'); check(await page.evaluate(()=>[...document.querySelectorAll('.uc-node-desc tspan')].every(line=>line.getComputedTextLength()<=219)),'description lines wrap within the shape width'); check(await page.evaluate(()=>{const nodes=[...document.querySelectorAll('.uc-node')].map(n=>n.getBoundingClientRect());return nodes.every((n,i)=>!i||n.left>nodes[i-1].right)}),'initial draft flows left to right with space between shapes'); check(await page.evaluate(()=>{const area=document.querySelector('#uc-canvas').getBoundingClientRect(),nodes=[...document.querySelectorAll('.uc-node')].map(n=>n.getBoundingClientRect());return nodes.every(r=>r.left>=area.left&&r.right<=area.right&&r.top>=area.top&&r.bottom<=area.bottom)}),'default zoom fits every shape in the canvas'); check(await page.evaluate(()=>{const svg=document.querySelector('#uc-svg'),box=svg.querySelector('.uc-graph').getBBox(),p=svg.createSVGPoint();p.x=box.x+box.width/2;p.y=box.y+box.height/2;const c=p.matrixTransform(svg.getScreenCTM()),area=svg.getBoundingClientRect();return Math.abs(c.x-area.x-area.width/2)<2&&Math.abs(c.y-area.y-area.height/2)<2}),'diagram content is centered horizontally and vertically'); await page.locator('#uc-canvas').scrollIntoViewIfNeeded(); const canvasBox=await page.locator('#uc-canvas').boundingBox(); const wheelX=Math.round(canvasBox.x+canvasBox.width*0.7),wheelY=Math.round(canvasBox.y+canvasBox.height*0.3); const wheelState=()=>page.evaluate(({x,y})=>{const svg=document.querySelector('#uc-svg'),p=svg.createSVGPoint();p.x=x;p.y=y;const w=p.matrixTransform(svg.getScreenCTM().inverse());return {scale:svg.getScreenCTM().a,x:w.x,y:w.y,pixelRatio:devicePixelRatio};},{x:wheelX,y:wheelY}); await page.mouse.move(wheelX,wheelY);const beforeWheel=await wheelState(); await page.keyboard.down('Control');await page.mouse.wheel(0,-120); await page.waitForFunction(old=>document.querySelector('#uc-svg').getScreenCTM().a>old,beforeWheel.scale); const zoomed=await wheelState();check(zoomed.scale>beforeWheel.scale,'Ctrl + scroll up zooms in'); check(Math.abs(zoomed.x-beforeWheel.x)<0.001&&Math.abs(zoomed.y-beforeWheel.y)<0.001,'wheel zoom keeps the world point under the mouse fixed'); check(Math.abs(Number(await page.inputValue('#uc-zoom'))-zoomed.scale)<0.001&&zoomed.pixelRatio===beforeWheel.pixelRatio,'zoom selector follows wheel zoom without zooming the browser'); await page.mouse.wheel(0,120);await page.keyboard.up('Control'); await page.waitForFunction(old=>document.querySelector('#uc-svg').getScreenCTM().a1,'scroll without Ctrl still pans the diagram'); check(await page.isDisabled('#uc-save'),'zoom and pan do not mark the diagram as edited'); await page.click('#uc-fit'); await page.locator('#uc-canvas').scrollIntoViewIfNeeded(); const matrixScale=await page.evaluate(()=>document.querySelector('#uc-svg').getScreenCTM().a); const startWorld=await page.evaluate(()=>{const m=document.querySelector('[data-node=start]').transform.baseVal.getItem(0).matrix;return {x:m.e,y:m.f}}); const start=await page.locator('[data-node=start]').boundingBox(); await page.mouse.move(start.x+start.width/2,start.y+start.height/2);await page.mouse.down();await page.mouse.move(start.x+start.width/2+45*matrixScale,start.y+start.height/2+35*matrixScale,{steps:5});await page.mouse.up(); check(await page.locator('[data-node=start]').getAttribute('transform')===`translate(${Math.round((startWorld.x+45)/20)*20} ${Math.round((startWorld.y+35)/20)*20})`,'shape dragging snaps position to the 20-unit grid'); check(await page.evaluate(()=>[...document.querySelectorAll('.uc-edge-line')].every(el=>{const d=el.getAttribute('d');if(/[CQAST]/i.test(d))return false;const nums=d.match(/-?\d+(?:\.\d+)?/g).map(Number);for(let i=2;iMath.abs(document.querySelector('#uc-svg').getScreenCTM().a-1)<0.01),'user can zoom in to 100 percent'); await page.click('#uc-fit');check(await page.evaluate(()=>document.querySelector('#uc-zoom').value==='fit'),'fit button returns to the whole diagram'); for(const type of ['start','end','process','decision']){ await page.selectOption('#uc-add-type',type);await page.click('#uc-add');await page.fill('#uc-title','ทดสอบ '+type);await page.fill('#uc-description','รายละเอียด