| 1 | // Actual packaged renderer + matching Go host, with disposable data and no provider calls. |
| 2 | import assert from "node:assert/strict"; |
| 3 | import {mkdtempSync,mkdirSync,writeFileSync} from "node:fs"; |
| 4 | import {tmpdir} from "node:os"; |
| 5 | import path from "node:path"; |
| 6 | import {_electron as electron} from "playwright"; |
| 7 | const executablePath=process.argv[2]; |
| 8 | if(!executablePath)throw Error("usage: composer-wiring-smoke.mjs <packaged executable>"); |
| 9 | const home=mkdtempSync(path.join(tmpdir(),"reasonix-composer-wiring-")); |
| 10 | const evidence=process.env.REASONIX_WIRING_EVIDENCE || "/tmp/reasonix-composer-wiring-evidence"; |
| 11 | mkdirSync(evidence,{recursive:true}); |
| 12 | const env={...process.env,REASONIX_HOME:home,REASONIX_STATE_HOME:home,REASONIX_CACHE_HOME:path.join(home,"cache")}; |
| 13 | delete env.REASONIX_DEV;delete env.REASONIX_DESKTOP_SERVICE; |
| 14 | let shell; |
| 15 | const rpc=(page,command,...args)=>page.evaluate(({command,args})=>window.reasonixDesktop.invoke(command,args),{command,args}); |
| 16 | async function launch(){ |
| 17 | shell=await electron.launch({executablePath,env,timeout:60000}); |
| 18 | const page=await shell.firstWindow(); |
| 19 | await page.waitForURL("reasonix://app/index.html",{timeout:60000}); |
| 20 | await page.locator(".app").waitFor({timeout:60000}); |
| 21 | await page.addLocatorHandler(page.locator(".management-screen__back:visible"),async b=>b.click()); |
| 22 | await page.locator(".sidebar__quick-action:visible").first().click({trial:true,timeout:60000}); |
| 23 | return page; |
| 24 | } |
| 25 | try { |
| 26 | let page=await launch(); |
| 27 | await page.locator(".sidebar__quick-action:visible").first().click(); |
| 28 | const composer=page.locator("textarea.composer__input:not([aria-hidden=true])"); |
| 29 | await composer.waitFor(); |
| 30 | await page.waitForFunction(()=>document.querySelector("textarea.composer__input:not([aria-hidden=true])")?.disabled===false); |
| 31 | await composer.fill("Keep this image across restart 中文"); |
| 32 | // Valid source used by the host fixtures; use the renderer's normal paste path. |
| 33 | const image="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; |
| 34 | await composer.evaluate((element,data)=>{ |
| 35 | const file=new File([Uint8Array.from(atob(data),c=>c.charCodeAt(0))],"persisted.png",{type:"image/png"}); |
| 36 | const transfer=new DataTransfer();transfer.items.add(file); |
| 37 | element.dispatchEvent(new ClipboardEvent("paste",{bubbles:true,cancelable:true,clipboardData:transfer})); |
| 38 | },image); |
| 39 | await page.locator(".composer-context__item").waitFor(); |
| 40 | const active=(await rpc(page,"ListTabs")).find(tab=>tab.active); |
| 41 | assert.ok(active?.sessionId); |
| 42 | const ref={hostId:"local",sessionId:active.sessionId}; |
| 43 | let saved; |
| 44 | const deadline=Date.now()+30000; |
| 45 | do { |
| 46 | saved=JSON.parse((await rpc(page,"GetSessionComposerState",ref)).contentJson); |
| 47 | if(saved.attachments?.[0]?.recoveryPath)break; |
| 48 | await new Promise(resolve=>setTimeout(resolve,100)); |
| 49 | } while(Date.now()<deadline); |
| 50 | assert.ok(saved.attachments?.[0]?.recoveryPath,"wait for the host's acknowledged attachment source"); |
| 51 | for(let i=0;i<3;i++)assert.deepEqual(JSON.parse((await rpc(page,"GetSessionComposerState",ref)).contentJson),saved,"acknowledged data remains visible to repeated RPC reads"); |
| 52 | assert.equal(saved.attachments[0].draftId,undefined); |
| 53 | assert.equal(saved.attachments[0].previewUrl,undefined); |
| 54 | await shell.close();shell=undefined; |
| 55 | page=await launch(); |
| 56 | await page.locator(".composer-context__item img").waitFor({timeout:30000}); |
| 57 | await page.waitForFunction(()=>Array.from(document.querySelectorAll(".composer-context__item img")).some(img=>img.complete && img.naturalWidth>0)); |
| 58 | assert.equal(await page.locator("textarea.composer__input:not([aria-hidden=true])").inputValue(),saved.text); |
| 59 | const current=(await rpc(page,"ListTabs")).find(tab=>tab.sessionId===ref.sessionId); |
| 60 | const captured=await rpc(page,"CaptureAttachmentTarget",{kind:"session",tabId:current.id,session:ref}); |
| 61 | const data=await rpc(page,"AttachmentDataURLForTarget",captured.token,saved.attachments[0].recoveryPath); |
| 62 | const restored=await rpc(page,"StageImageForTarget",captured.token,"restart-image-check","persisted.png","image/png",data); |
| 63 | assert.ok(restored.draftId,"restored image must acquire a fresh runtime credential"); |
| 64 | assert.match(await rpc(page,"ReadDraftImageForTarget",captured.token,restored.draftId),/^data:image\//); |
| 65 | await rpc(page,"ReleaseAttachmentTarget",captured.token); |
| 66 | await page.screenshot({path:path.join(evidence,"restored-image.png")}); |
| 67 | await shell.close();shell=undefined; |
| 68 | writeFileSync(path.join(evidence,"results.json"),JSON.stringify({home,executablePath,passed:["actual manual New click","actual renderer image paste","durable source without transient credential","normal packaged exit and restart","restored text and image preview","fresh typed image credential after restart"]},null,2)); |
| 69 | console.log(`PASS packaged composer wiring: ${evidence}`); |
| 70 | } catch(error) { |
| 71 | if(shell){const page=await shell.firstWindow();await page.screenshot({path:path.join(evidence,"failure.png")});writeFileSync(path.join(evidence,"failure.txt"),await page.locator("body").innerText());} |
| 72 | throw error; |
| 73 | } finally {if(shell)await shell.close();} |
| 74 |