// Synthetic browser verification; install Playwright separately or provide NODE_PATH. // No Oracle requests, credentials or real customer data are used. const { chromium } = require("playwright"); const { spawn } = require("node:child_process"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); (async () => { const server = spawn(process.execPath, ["scripts/preview.mjs"], { cwd: path.resolve(__dirname, ".."), stdio: ["ignore", "pipe", "inherit"] }); let browser; const output = fs.mkdtempSync(path.join(os.tmpdir(), "org-chart-verification-")); try { const url = await new Promise((resolve, reject) => { server.once("error", reject); server.once("exit", code => reject(new Error("Preview exited: " + code))); server.stdout.on("data", chunk => { const found = String(chunk).match(/http:\/\/127\.0\.0\.1:\d+\//); if (found) resolve(found[0]); }); }); browser = await chromium.launch({ headless: true, ...(process.env.TEST_BROWSER_CHANNEL ? { channel: process.env.TEST_BROWSER_CHANNEL } : {}) }); const page = await browser.newPage({ viewport: { width: 1440, height: 960 } }); const errors = []; page.on("pageerror", error => errors.push(error.message)); // Any external request is a fixture regression, not a permitted live test. await page.route("**/*", route => new URL(route.request().url()).origin === new URL(url).origin ? route.continue() : route.abort()); page.setDefaultTimeout(10000); await page.goto(url); await page.evaluate(() => new Promise((resolve,reject) => { const request=indexedDB.open("opportunities-extension-organizations",1); request.onupgradeneeded=()=>request.result.createObjectStore("charts",{keyPath:"key"}); request.onerror=()=>reject(request.error); request.onsuccess=()=>{ const db=request.result, tx=db.transaction("charts","readwrite"); tx.objectStore("charts").put({key:"legacy-migration",schemaVersion:1,revision:3,nodes:[{id:"legacy",parentId:null,name:"Legacy",role:"Old title",emailAddress:"old@example.invalid",notes:"Retain notes",collapsed:false}]}); tx.oncomplete=()=>{db.close();resolve();}; tx.onerror=()=>reject(tx.error); }; })); const open = () => page.getByRole("button", { name: "View customer organization chart", exact: true }).first().click(); const dialog = page.locator(".oe-org-dialog"); const button = label => dialog.getByRole("button", { name: label, exact: true }); const editor = page.locator(".oe-org-editor"); async function addPerson(name, role, notes = "", parent) { await button(parent ? "Add report to " + parent : "Add person").click(); const search = editor.getByRole("combobox", { name: "Name", exact: true }); await search.fill(name); await page.locator("#oe-org-name-options").getByRole("option").filter({ hasText: name }).click(); assert.equal(await editor.getByRole("textbox", { name: "Job Title", exact: true }).inputValue(), role); assert.equal(await editor.getByRole("textbox", { name: "Job Title", exact: true }).evaluate(input => input.readOnly), true); await editor.getByRole("textbox", { name: "Notes", exact: true }).fill(notes); await editor.getByRole("button", { name: "Add person", exact: true }).click(); } async function save() { await button("Save chart").click(); await page.waitForFunction(() => document.querySelector(".oe-org-footer .oe-org-status")?.textContent.startsWith("Saved locally:")); } await open(); await dialog.getByRole("heading", { name: "No people yet" }).waitFor(); assert.deepEqual(await page.evaluate(()=>window.fixtureContactQueries || []),[]); await button("Add person").click(); const nameSearch=editor.getByRole("combobox",{name:"Name",exact:true}); await editor.getByRole("button", { name: "Create Contact", exact: true }).click(); const contactDialog = page.locator(".oe-contact-dialog"); await contactDialog.getByRole("button", { name: "Save", exact: true }).click(); assert.equal(await contactDialog.locator('[aria-invalid="true"]').count(), 3); assert.equal(await contactDialog.locator('#oe-contact-validation').textContent(), "Complete the highlighted required fields with valid values."); await contactDialog.getByRole("textbox", { name: "First Name", exact: true }).fill("Test"); assert.equal(await contactDialog.locator('[aria-invalid="true"]').count(), 2); assert.equal(await contactDialog.locator("#oe-contact-workCountry option").count(), 246); assert.equal(await contactDialog.locator("#oe-contact-mobileCountry option").count(), 246); const country = contactDialog.getByRole("combobox", { name: "Work Phone country code", exact: true }); await country.fill("Brazil"); await contactDialog.getByRole("option", { name: "BR ยท Brazil (+55)", exact: true }).click(); assert.equal(await contactDialog.locator("#oe-contact-workCountry").inputValue(), "BR"); assert.equal(await contactDialog.locator("#oe-contact-mobileCountry").inputValue(), ""); assert.equal(await contactDialog.locator('input[name="primaryPhone"]').count(), 0); await page.screenshot({path:path.join(output,"create-contact.png")}); await contactDialog.getByRole("button", { name: "Cancel", exact: true }).click(); assert.equal(await editor.getByRole("button", { name: "Create Contact", exact: true }).evaluate(item => item === document.activeElement), true); await nameSearch.fill("A"); await nameSearch.fill("Al"); await nameSearch.fill("Alex"); await page.locator("#oe-org-name-options [role=option]").waitFor(); assert.deepEqual(await page.evaluate(()=>window.fixtureContactQueries),["Alex"]); await page.evaluate(()=>{window.fixtureContactCompleted=[]; window.fixtureContactDelay={Alex:1400};}); await nameSearch.fill("Alex"); await page.waitForFunction(()=>window.fixtureContactQueries.length===2); await nameSearch.fill("Taylor"); await page.locator("#oe-org-name-options [role=option]").filter({hasText:"Taylor Chen"}).waitFor(); await page.waitForFunction(()=>window.fixtureContactCompleted.includes("Alex")); assert.equal(await page.locator("#oe-org-name-options [role=option]").filter({hasText:"Alex Morgan"}).count(),0); await page.evaluate(()=>{window.fixtureContactsFailure=true; window.fixtureContactDelay={};}); await nameSearch.fill("Alex"); await page.locator("#oe-org-name-options [role=status]").filter({hasText:"failed with status 503"}).waitFor(); await page.evaluate(()=>window.fixtureContactsFailure=false); await nameSearch.fill("Jordan"); await page.locator("#oe-org-name-options [role=option]").filter({hasText:"Jordan Silva"}).waitFor(); await editor.getByRole("button", {name:"Cancel",exact:true}).click(); await button("Add person").click(); await editor.getByRole("combobox", { name: "Name", exact: true }).fill("Not a contact"); await editor.getByRole("button", { name: "Add person", exact: true }).click(); assert.equal(await page.locator(".oe-org-node").count(), 0); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); await addPerson("Alex Morgan", "Chief Technology Officer", "Decision maker for the cloud program."); await addPerson("Taylor Chen", "Infrastructure Director", "Owns the migration roadmap.", "Alex Morgan"); await addPerson("Jordan Silva", "Data and Analytics Director", "Coordinates the analytics team.", "Alex Morgan"); await addPerson("Sam Rivera", "Cloud Architect", "Technical workshop: November 12.", "Taylor Chen"); const queriesBeforeOpportunities = await page.evaluate(() => window.fixtureContactQueries.length); await page.evaluate(() => { window.fixtureOpportunitiesDelay = 400; }); await button("View opportunities for Alex Morgan").click(); const contactOpportunities = page.locator(".oe-contact-opportunities"); await contactOpportunities.locator('table[aria-busy="true"]').waitFor(); assert.equal(await contactOpportunities.locator("tbody .oe-contact-skeleton").count(),35); await contactOpportunities.getByText("6 opportunities", {exact:true}).waitFor(); assert.equal(await contactOpportunities.locator(".oe-contact-skeleton").count(),0); await page.evaluate(() => { window.fixtureOpportunitiesDelay = 0; }); assert.equal(await page.evaluate(() => window.fixtureContactQueries.length), queriesBeforeOpportunities, "Opportunity lookup must reuse the person's PartyNumber without searching contacts"); assert.equal(await contactOpportunities.locator("tbody tr").count(), 6); const presentation = await contactOpportunities.locator("table").evaluate(table => ({ marker: table.getAttribute("data-table-layout"), textHeader: getComputedStyle(table.querySelector("th")).textAlign, moneyHeader: getComputedStyle(table.querySelector('th[data-align="money"]')).textAlign, moneyCell: getComputedStyle(table.querySelector('td[data-align="money"]')).textAlign, nowrap: getComputedStyle(table.querySelector(".opportunities-extension-sort-label")).whiteSpace })); assert.deepEqual(presentation,{marker:"standard",textHeader:"left",moneyHeader:"right",moneyCell:"right",nowrap:"nowrap"}); await page.screenshot({path:path.join(output,"shared-table.png")}); assert.equal(await contactOpportunities.getByRole("link", {name:"Contact opportunity 0",exact:true}).getAttribute("target"), "_blank"); assert.ok((await contactOpportunities.getByRole("link", {name:"Contact opportunity 0",exact:true}).getAttribute("href")).endsWith("?puid=CONTACT0")); assert.deepEqual((await page.evaluate(()=>window.fixtureOpportunitiesByContact)).map(url=>new URL(url).searchParams.get("offset")), ["0","5"]); await contactOpportunities.getByRole("button",{name:"Sort by OptyNumber",exact:true}).click(); await contactOpportunities.getByRole("button",{name:"Sort by OptyNumber, ascending",exact:true}).click(); assert.equal(await contactOpportunities.locator("tbody tr").first().locator("td").first().textContent(),"CONTACT5"); assert.equal(new URL((await page.evaluate(()=>window.fixtureOpportunitiesByContact))[0]).searchParams.get("q"), "(OpportunityContact.ContactPartyNumber='18000')"); await contactOpportunities.getByRole("button", {name:"Close opportunities",exact:true}).click(); await button("View opportunities for Alex Morgan").click(); await contactOpportunities.getByText("6 opportunities",{exact:true}).waitFor(); assert.equal(await contactOpportunities.locator("tbody tr").first().locator("td").first().textContent(),"CONTACT5"); await contactOpportunities.getByRole("button", {name:"Close opportunities",exact:true}).click(); await button("Fit chart to view").click(); assert.equal(await page.locator(".oe-org-node").count(), 4); await save(); await button("Edit Alex Morgan").hover(); assert.equal(await page.locator(".oe-org-tooltip").innerText(), "Decision maker for the cloud program."); assert.equal(await dialog.getByRole("button", { name: /inline/i }).count(), 0); await button("Edit Alex Morgan").click(); await page.waitForFunction(() => document.querySelector("#oe-org-emailAddress").value === "contact0@example.invalid"); assert.equal(await editor.getByRole("combobox", { name: "Name", exact: true }).inputValue(), "Alex Morgan"); await editor.getByRole("textbox", { name: "Notes", exact: true }).fill("Updated contact notes"); await button("Apply changes").click(); await page.evaluate(() => { window.fixtureOriginalOrgSave = window.fixtureOrgStorage.save; window.fixtureOrgStorage.save = async () => { throw new Error("Synthetic storage failure"); }; }); await button("Save chart").click(); await page.locator(".oe-org-status.oe-org-error").filter({ hasText: "Synthetic storage failure" }).waitFor(); assert.equal(await button("Save chart").isEnabled(), true); await page.evaluate(() => { window.fixtureOrgStorage.save = window.fixtureOriginalOrgSave; }); await button("Collapse reports for Taylor Chen").click(); assert.equal(await page.locator(".oe-org-node").count(), 3); await save(); await page.reload(); await open(); await button("Expand reports for Taylor Chen").waitFor(); assert.equal(await page.locator(".oe-org-node").count(), 3); await button("Edit Alex Morgan").click(); await page.waitForFunction(() => document.querySelector("#oe-org-role").value === "Chief Technology Officer"); await editor.getByRole("button", {name:"Cancel",exact:true}).click(); await button("Expand reports for Taylor Chen").click(); await button("Edit Taylor Chen").click(); await page.waitForFunction(() => document.querySelector("#oe-org-emailAddress").value === "contact1@example.invalid"); assert.deepEqual(await editor.locator("#oe-org-parent option").allTextContents(), ["No manager (top level)", "Alex Morgan - Chief Technology Officer", "Jordan Silva - Data and Analytics Director"]); await editor.getByRole("textbox", { name: "Notes", exact: true }).fill("Unapplied note"); await button("Close organization chart").click(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); assert.equal(await editor.getByRole("textbox", { name: "Notes", exact: true }).inputValue(), "Unapplied note"); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); // Reparent through the searchable select, then restore the original manager. await button("Edit Sam Rivera").click(); await editor.getByRole("combobox", { name: "Reports to", exact: true }).click(); await page.getByRole("option", { name: "Jordan Silva - Data and Analytics Director", exact: true }).click(); await editor.getByRole("button", { name: "Apply changes", exact: true }).click(); await button("Edit Sam Rivera").click(); await editor.getByRole("combobox", { name: "Reports to", exact: true }).click(); await page.getByRole("option", { name: "Taylor Chen - Infrastructure Director", exact: true }).click(); await editor.getByRole("button", { name: "Apply changes", exact: true }).click(); await button("Fit chart to view").click(); await save(); const boxes = await page.locator(".oe-org-node").evaluateAll(nodes => nodes.map(node => { const r = node.getBoundingClientRect(); return { x:r.x,y:r.y,w:r.width,h:r.height }; })); for (let a=0;a node.dataset.theme="dark"); await button("Edit Jordan Silva").click(); await page.keyboard.press("Control+d"); const debug = page.locator(".opportunities-extension-debug-panel"); await debug.getByRole("button", { name: "Close debug", exact: true }).waitFor(); assert.equal(await debug.evaluate(node => !node.closest("[inert]") && node.parentElement.classList.contains("opportunities-extension-modal")), true); assert.equal(await page.evaluate(() => !!document.elementFromPoint(innerWidth / 2, innerHeight / 2).closest(".opportunities-extension-debug-panel")), true); await debug.getByRole("button", { name: "Close debug", exact: true }).click(); assert.equal(await editor.isVisible(), true); await editor.getByRole("button", { name: "Delete person", exact: true }).click(); await page.keyboard.press("Control+d"); assert.equal(await page.evaluate(() => !!document.elementFromPoint(innerWidth / 2, innerHeight / 2).closest(".opportunities-extension-debug-panel")), true); await page.keyboard.press("Escape"); assert.equal(await page.getByRole("alertdialog").isVisible(), true); await page.getByRole("alertdialog").getByRole("button", { name: "Keep editing", exact: true }).click(); await page.screenshot({ path: path.join(output, "dark-editor.png") }); await page.setViewportSize({width:390,height:844}); await page.screenshot({ path: path.join(output, "mobile-editor.png") }); const bounds = await dialog.boundingBox(); assert.ok(bounds.x>=0 && bounds.x+bounds.width<=390 && bounds.y+bounds.height<=844); await page.setViewportSize({width:1440,height:960}); await editor.getByRole("button", { name: "Cancel", exact: true }).click(); await button("Delete Taylor Chen").click(); await page.getByRole("alertdialog").getByRole("button", { name: "Keep editing", exact: true }).click(); assert.equal(await page.locator(".oe-org-node").count(), 4); await button("Delete Taylor Chen").click(); await page.getByRole("alertdialog").getByRole("button", { name: "Delete person", exact: true }).click(); await save(); const storageResult = await page.evaluate(async () => { const identity={principal:"example@example.invalid",customerId:"100000810210835"}; const store=window.fixtureOrgStorage, chart=(await store.load(identity)).chart; const sam=chart.nodes.find(n=>n.name==="Sam Rivera"), alex=chart.nodes.find(n=>n.name==="Alex Morgan"); let conflict=false; try { await store.save({...identity,nodes:[],revision:0}); } catch { conflict=true; } return {conflict,promoted:sam.parentId===alex.id,notes:sam.notes,contactPartyId:sam.contactPartyId, independentUser:(await store.load({...identity,principal:"different@example.invalid"})).chart.nodes.length, independentCustomer:(await store.load({...identity,customerId:"another-customer"})).chart.nodes.length, afterConflict:(await store.load(identity)).chart.nodes.length}; }); assert.deepEqual(storageResult,{conflict:true,promoted:true,notes:"Technical workshop: November 12.",contactPartyId:"2003",independentUser:0,independentCustomer:0,afterConflict:3}); const rawRecords = await page.evaluate(() => new Promise((resolve,reject) => { const request=indexedDB.open("opportunities-extension-organizations",3); request.onerror=()=>reject(request.error); request.onsuccess=()=>{ const db=request.result, tx=db.transaction("charts","readonly"), all=tx.objectStore("charts").getAll(); all.onsuccess=()=>{resolve(all.result);db.close();}; all.onerror=()=>reject(all.error); }; })); assert.ok(rawRecords.every(record=>record.nodes.every(node=>!("role" in node)&&!("emailAddress" in node)&&typeof node.jobTitle === "string"))); assert.equal(rawRecords.find(record=>record.key==="legacy-migration").nodes[0].notes,"Retain notes"); assert.equal(rawRecords.find(record=>record.key==="legacy-migration").revision,3); assert.equal(rawRecords.find(record=>record.key==="legacy-migration").nodes[0].jobTitle,"Old title"); await button("Close organization chart").click(); await page.getByRole("button", { name: "View customer organization chart", exact: true }).nth(1).click(); await button("Edit Sam Rivera").waitFor(); // same customer, different opportunity const queriesBefore=await page.evaluate(()=>window.fixtureContactQueries.filter(q=>q==="Sam Rivera").length); await button("Edit Sam Rivera").click(); await page.waitForFunction(()=>document.querySelector("#oe-org-emailAddress").value==="contact3@example.invalid"); await editor.getByRole("button",{name:"Cancel",exact:true}).click(); await button("Edit Sam Rivera").click(); await page.waitForFunction(()=>document.querySelector("#oe-org-emailAddress").value==="contact3@example.invalid"); assert.equal(await page.evaluate(()=>window.fixtureContactQueries.filter(q=>q==="Sam Rivera").length),queriesBefore+2); assert.equal(await button("Save chart").isDisabled(),true); await editor.getByRole("button",{name:"Cancel",exact:true}).click(); await page.evaluate(()=>{ window.fixtureContactOverrides={"2003":{PartyName:"Samuel Rivera",JobTitle:"Principal Architect"}}; window.fixtureContactDelay={"Sam Rivera":250}; }); await button("Edit Sam Rivera").click(); await editor.getByRole("textbox",{name:"Notes",exact:true}).fill("Pending note preserved"); await page.waitForFunction(()=>document.querySelector("#oe-org-role").value==="Principal Architect"); assert.equal(await editor.getByRole("combobox",{name:"Name",exact:true}).inputValue(),"Samuel Rivera"); assert.equal(await editor.getByRole("textbox",{name:"Notes",exact:true}).inputValue(),"Pending note preserved"); assert.equal(await button("Edit Samuel Rivera").count(),1); await save(); const updated=await page.evaluate(async()=>{ const chart=(await window.fixtureOrgStorage.load({principal:"example@example.invalid",customerId:"100000810210835"})).chart; return chart.nodes.find(node=>node.contactPartyId==="2003"); }); assert.equal(updated.name,"Samuel Rivera");assert.equal(updated.jobTitle,"Principal Architect"); assert.equal(updated.notes,"Pending note preserved");assert.equal("emailAddress" in updated,false); await button("Close organization chart").click(); assert.deepEqual(errors, []); console.log("PASS: contact autocomplete, chart creation, reparenting, editing, notes, collapse, reload, failed-save recovery, unsaved protection, deletion, account/user isolation and revision conflict."); console.log("Screenshots: " + output); } finally { await browser?.close(); server.kill(); } })().catch(error => { console.error(error); process.exitCode=1; });