Files
opportunities-extension/scripts/verify-org-chart.cjs

265 lines
20 KiB
JavaScript

// 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);
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<boxes.length;a++) for(let b=a+1;b<boxes.length;b++) {
const p=boxes[a],q=boxes[b]; assert.ok(p.x+p.w<=q.x || q.x+q.w<=p.x || p.y+p.h<=q.y || q.y+q.h<=p.y, "Cards must not overlap");
}
await page.mouse.move(5,5);
await page.screenshot({ path: path.join(output, "light.png") });
await page.locator(".opportunities-extension-modal").first().evaluate(node => 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; });