265 lines
20 KiB
JavaScript
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; });
|