Implemented the customer rate card view, including SKU search
This commit is contained in:
@@ -29,6 +29,7 @@ test("request categories cover every current family and unknown requests stay vi
|
||||
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
|
||||
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests",
|
||||
requestConsumptionPlanDetail: "consumption",
|
||||
requestRatecardByPlan: "consumption",
|
||||
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",
|
||||
requestConsumptionServiceWeekly: "consumption", requestConsumptionServiceDaily: "consumption", futureRequest: "other"
|
||||
})) assert.equal(h.app.debugContentView.getRequestCategory({ name }), category, name);
|
||||
@@ -95,7 +96,7 @@ test("tabs support arrows, Home and End with a single selected tab and labelled
|
||||
|
||||
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
||||
const root = path.join(__dirname, "..");
|
||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css"]
|
||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css"]
|
||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
||||
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
||||
|
||||
@@ -158,6 +158,6 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
||||
assert.ok(remaining > 0);
|
||||
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
|
||||
}
|
||||
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css"].map(read).join("\n");
|
||||
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css"].map(read).join("\n");
|
||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
||||
});
|
||||
|
||||
332
tests/ratecard.test.cjs
Normal file
332
tests/ratecard.test.cjs
Normal file
@@ -0,0 +1,332 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { loadBackground, response, plain } = require("./support/characterization.cjs");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
|
||||
const type = "opportunitiesExtension.requestRatecardByPlan";
|
||||
const item = {
|
||||
orderNumber: "SYNTH-ORDER-2", partNum: "B-SYNTH-1", productName: "Synthetic GPU compute",
|
||||
uom: "GPU PER HOUR", subLnStartSt: "30-Sep-2025", subLnEndDt: "27-May-2028",
|
||||
usageMax: "-", rateCardDiscountPerc: "12.5", listPriceUsd: "4.3585",
|
||||
netUnitPriceUsd: "0.9589", overagePriceUsd: "1.2501"
|
||||
};
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: rate card uses the active planNumber, OCICONS-TS and exact POST contract, with debug`, async () => {
|
||||
const calls = [];
|
||||
const app = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.endsWith("consumerTokenService")) return response([
|
||||
{ tokenService: "ACCP-TS", uiaasHeader: { value: "synthetic-account" } },
|
||||
{ tokenService: "OCICONS-TS", uiaasHeader: { label: "not-used", value: "synthetic-consumption" } }
|
||||
]);
|
||||
return response([item]);
|
||||
} });
|
||||
const auth = await app.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||
const result = await app.dispatch({ type, planNumber: "PLAN-123", subPlanNum: "WRONG-ID", authorization: auth.authorizations.accountPlans });
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(result.items, [item]);
|
||||
assert.equal(result.planNumber, "PLAN-123");
|
||||
assert.equal(calls[1].url, "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/ratecard/byPlan");
|
||||
assert.equal(calls[1].options.method, "POST");
|
||||
assert.equal(calls[1].options.credentials, "include");
|
||||
assert.equal(calls[1].options.cache, "no-store");
|
||||
assert.deepEqual(JSON.parse(calls[1].options.body), { planNumber: "PLAN-123", recordsSize: 10000 });
|
||||
assert.deepEqual(plain(calls[1].options.headers), {
|
||||
Accept: "application/json", "Content-Type": "application/json", "G2m-Authorization": "synthetic-consumption"
|
||||
});
|
||||
const entry = result.debugRequests[0];
|
||||
assert.equal(entry.name, "requestRatecardByPlan");
|
||||
assert.equal(entry.httpStatus, 200);
|
||||
assert.equal(entry.status, "success");
|
||||
assert.ok(entry.startedAt && entry.completedAt);
|
||||
assert.equal(typeof entry.durationMs, "number");
|
||||
assert.deepEqual(JSON.parse(entry.requestBody), { planNumber: "PLAN-123", recordsSize: 10000 });
|
||||
assert.match(entry.responseBody, /Synthetic GPU/);
|
||||
const content = loadApplication("content");
|
||||
assert.doesNotMatch(content.debugContentLog.formatHeaderBlock(entry.requestHeaders), /synthetic-consumption/);
|
||||
assert.equal(content.debugContentView.getRequestCategory(entry), "consumption");
|
||||
});
|
||||
}
|
||||
|
||||
test("rate card fails locally for missing identifiers/credentials, HTTP, network, JSON and collection errors without retries", async () => {
|
||||
for (const failure of ["id", "token", "401", "403", "network", "json", "shape", "item"]) {
|
||||
let calls = 0;
|
||||
const app = loadBackground({ fetch: async () => {
|
||||
calls += 1;
|
||||
if (["401", "403"].includes(failure)) return response({}, Number(failure));
|
||||
if (failure === "network") throw new Error("Synthetic network failure");
|
||||
if (failure === "json") return new Response("<html>fallback</html>");
|
||||
return response(failure === "shape" ? {} : [null]);
|
||||
} });
|
||||
const result = await app.dispatch({ type, planNumber: failure === "id" ? "" : "A",
|
||||
authorization: { value: failure === "token" ? "" : "synthetic" } });
|
||||
assert.equal(result.ok, false, failure);
|
||||
assert.ok(result.error, failure);
|
||||
assert.equal(calls, ["id", "token"].includes(failure) ? 0 : 1, failure);
|
||||
if (calls) assert.equal(result.debugRequests[0].status, "failed", failure);
|
||||
}
|
||||
});
|
||||
|
||||
test("all eleven columns are searchable, percentages and precise prices are formatted, and sorting is typed", () => {
|
||||
const model = loadApplication("content").ratecardModel;
|
||||
assert.equal(model.columns.length, 11);
|
||||
for (const column of model.columns) {
|
||||
assert.equal(model.selectItems([item], item[column.key]).length, 1, column.key);
|
||||
assert.equal(model.selectItems([item], model.formatValue(item, column)).length, 1, column.key);
|
||||
}
|
||||
const format = (key, value) => model.formatValue({ [key]: value }, model.columns.find(column => column.key === key));
|
||||
assert.equal(format("netUnitPriceUsd", "0.9589"), "0.9589");
|
||||
assert.equal(format("listPriceUsd", 0), "0.0000");
|
||||
assert.equal(format("rateCardDiscountPerc", 12.5), "12.5%");
|
||||
assert.equal(format("usageMax", 0), "0");
|
||||
assert.equal(format("subLnEndDt", null), "-");
|
||||
assert.equal(model.selectItems([{ ...item, productName: "Synthet\u00edc" }], "synthetic").length, 1);
|
||||
assert.equal(model.selectItems([{ ...item, hiddenMetadata: "secretmatch" }], "secretmatch").length, 0);
|
||||
const rows = [
|
||||
{ ...item, netUnitPriceUsd: "10", subLnStartSt: "01-Jan-2027" },
|
||||
{ ...item, netUnitPriceUsd: "2", subLnStartSt: "31-Dec-2026" },
|
||||
{ ...item, netUnitPriceUsd: null, subLnStartSt: null }
|
||||
];
|
||||
assert.deepEqual(plain(model.selectItems(rows, "", "netUnitPriceUsd").map(row => row.netUnitPriceUsd)), ["2", "10", null]);
|
||||
assert.deepEqual(plain(model.selectItems(rows, "", "netUnitPriceUsd", "desc").map(row => row.netUnitPriceUsd)), ["10", "2", null]);
|
||||
assert.deepEqual(plain(model.selectItems(rows, "", "subLnStartSt").map(row => row.subLnStartSt)), ["31-Dec-2026", "01-Jan-2027", null]);
|
||||
assert.deepEqual(rows.map(row => row.netUnitPriceUsd), ["10", "2", null], "sorting does not mutate responses");
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const document = { activeElement: null, documentElement: {} };
|
||||
const walk = node => [node, ...node.children.flatMap(walk)];
|
||||
class Element {
|
||||
constructor(tag) {
|
||||
this.tag = tag; this.children = []; this.attributes = {}; this.listeners = {}; this.style = {}; this.dataset = {};
|
||||
this.text = ""; this.value = ""; this.inert = false;
|
||||
this.classList = { add: name => { this.className = `${this.className || ""} ${name}`; } };
|
||||
}
|
||||
append(...nodes) { nodes.forEach(node => { node.parent = this; }); this.children.push(...nodes); }
|
||||
set textContent(value) { this.text = String(value); this.children.forEach(node => { node.parent = null; }); this.children = []; }
|
||||
get textContent() { return this.text; }
|
||||
setAttribute(key, value) { this.attributes[key] = value; }
|
||||
getAttribute(key) { return this.attributes[key]; }
|
||||
addEventListener(name, callback) { this.listeners[name] = callback; }
|
||||
focus(options) { document.activeElement = this; this.focusOptions = options; }
|
||||
remove() { if (this.parent) this.parent.children = this.parent.children.filter(node => node !== this); this.parent = null; }
|
||||
get isConnected() { return this === root || !!this.parent?.isConnected; }
|
||||
querySelectorAll(selector) {
|
||||
return walk(this).slice(1).filter(node => selector.split(",").some(part => {
|
||||
const value = part.trim();
|
||||
if (value.startsWith(".")) return (node.className || "").split(" ").includes(value.slice(1));
|
||||
const attr = value.match(/^\[([^=]+)='([^']+)'\]$/);
|
||||
return attr ? node.attributes[attr[1]] === attr[2] : node.tag === value;
|
||||
}));
|
||||
}
|
||||
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
|
||||
}
|
||||
const root = new Element("main");
|
||||
document.createElement = tag => new Element(tag);
|
||||
document.createElementNS = (_, tag) => new Element(tag);
|
||||
document.createTextNode = text => { const node = new Element("#text"); node.textContent = text; return node; };
|
||||
document.getElementById = id => walk(root).find(node => node.id === id) || null;
|
||||
const timers = new Map();
|
||||
const frames = new Map();
|
||||
let id = 0;
|
||||
const app = loadApplication("content", { document, window: { addEventListener() {} },
|
||||
requestAnimationFrame: fn => { frames.set(++id, fn); return id; }, cancelAnimationFrame: key => frames.delete(key),
|
||||
setTimeout: fn => { timers.set(++id, fn); return id; }, clearTimeout: key => timers.delete(key) });
|
||||
root.id = app.contracts.MODAL_ID;
|
||||
app.consumptionPlanDetailController.loadPlanDetailForPanel = () => {};
|
||||
app.consumptionTrendController.loadConsumptionTrendForPanel = () => {};
|
||||
const plans = [{ contractId: "CONTRACT-A", planNumber: "PLAN-A", subPlanNum: "SUB-A", bookingCustomer: "Booking A", soldToPartyName: "Sold A", hdrStatusCode: "ACTIVE", startDate: "23-Aug-2024", endDate: "22-Aug-2028" },
|
||||
{ contractId: "CONTRACT-B", planNumber: "PLAN-B", subPlanNum: "SUB-B", bookingCustomer: "Booking B", soldToPartyName: "Sold B", hdrStatusCode: "EXPIRED", startDate: "01-Jan-2020", endDate: "01-Jan-2024" }];
|
||||
const opportunity = { OptyNumber: "SYNTH", CustomerAccount: { PartyUniqueName: "Synthetic Customer" } };
|
||||
app.consumptionPlansView.openAccountPlansModal(opportunity, [], "loading");
|
||||
app.consumptionPlansView.openAccountPlansModal(opportunity, plans);
|
||||
timers.clear();
|
||||
frames.clear();
|
||||
const logs = [], requests = [];
|
||||
app.debugContentLog.appendBackgroundRequestLog = entries => logs.push(entries);
|
||||
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ accountPlans: { value: "synthetic-plan" } });
|
||||
app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => requests.push({ message, resolve }));
|
||||
const find = suffix => root.querySelector(`.opportunities-extension-ratecard-${suffix}`);
|
||||
const buttons = root.querySelectorAll(".opportunities-extension-ratecard-button");
|
||||
const nextFrame = () => { const callbacks = [...frames.values()]; frames.clear(); callbacks.forEach(fn => fn()); };
|
||||
return { app, root, document, walk, timers, frames, nextFrame, plans, requests, logs, buttons, find };
|
||||
}
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
test("Rate card sits beside Contract ID, opens a skeleton, searches and sorts without HTML injection", async () => {
|
||||
const { app, root, buttons, find, document, requests, logs } = setup();
|
||||
assert.equal(buttons.length, 2);
|
||||
assert.equal(buttons[0].textContent, "Rate Card");
|
||||
assert.equal(buttons[0].parent.parent.className, "opportunities-extension-contract-row");
|
||||
assert.equal(buttons[0].parent.children[0].textContent, "CONTRACT-A");
|
||||
const opening = buttons[0].listeners.click();
|
||||
assert.equal(find("drawer").attributes["aria-busy"], "true");
|
||||
assert.equal(root.querySelectorAll(".opportunities-extension-ratecard-skeleton").length, 77);
|
||||
assert.equal(document.activeElement, find("search"));
|
||||
await tick();
|
||||
assert.deepEqual(plain(requests[0].message), { type, planNumber: "SUB-A", authorization: { value: "synthetic-plan" } });
|
||||
const parentHeader = root.querySelector(".opportunities-extension-account-plans-header");
|
||||
assert.equal(find("header").querySelector("h2").textContent, "Rate Card");
|
||||
assert.equal(parentHeader.querySelector("h2").textContent, "Cloud Consumption");
|
||||
assert.equal(find("header").querySelector("p").textContent, parentHeader.querySelector("p").textContent);
|
||||
assert.equal(find("header").querySelector("p").textContent, "Synthetic Customer");
|
||||
assert.notEqual(find("header").querySelector("h2").id, parentHeader.querySelector("h2").id);
|
||||
assert.ok(find("header").className.includes("opportunities-extension-account-plans-header"));
|
||||
const details = find("plan-details");
|
||||
assert.deepEqual(details.querySelectorAll("dt").map(node => node.textContent), ["Booking Customer", "Sold To Party Name", "Contract ID", "Header Status", "Start Date", "End Date"]);
|
||||
assert.deepEqual(details.querySelectorAll("dd").map(node => node.textContent), ["Booking A", "Sold A", "CONTRACT-A", "ACTIVE", "23-Aug-2024", "22-Aug-2028"]);
|
||||
assert.equal(details.querySelectorAll("button").length, 0, "no recursive Rate Card action inside its own drawer");
|
||||
const unsafe = "<img src=x onerror=bad()>";
|
||||
requests[0].resolve({ ok: true, items: [{ ...item, netUnitPriceUsd: "10", productName: unsafe }, item], debugRequests: [{ name: "requestRatecardByPlan" }] });
|
||||
await opening;
|
||||
assert.equal(find("drawer").attributes["aria-busy"], "false");
|
||||
assert.equal(find("count").textContent, "2 of 2 items");
|
||||
assert.equal(root.querySelectorAll("img").length, 0);
|
||||
const table = find("table");
|
||||
assert.equal(table.querySelector("tbody").children[0].children[2].textContent, unsafe);
|
||||
assert.equal(logs[0][0].name, "requestRatecardByPlan");
|
||||
const priceSort = table.querySelectorAll("th")[9];
|
||||
priceSort.children[0].listeners.click();
|
||||
assert.equal(priceSort.attributes["aria-sort"], "ascending");
|
||||
assert.equal(table.querySelector("tbody").children[0].children[9].textContent, "0.9589");
|
||||
priceSort.children[0].listeners.click();
|
||||
assert.equal(priceSort.attributes["aria-sort"], "descending");
|
||||
assert.equal(table.querySelector("tbody").children[0].children[9].textContent, "10.0000");
|
||||
find("search").value = "12.5%";
|
||||
find("search").listeners.input();
|
||||
assert.equal(find("count").textContent, "2 of 2 items");
|
||||
find("search").value = "no match";
|
||||
find("search").listeners.input();
|
||||
assert.equal(find("count").textContent, "0 of 2 items");
|
||||
assert.equal(find("notice").children[0].textContent, "No matching rate card items.");
|
||||
find("header").querySelector("button").listeners.click();
|
||||
assert.equal(root.querySelector(".opportunities-extension-account-plans-modal").hidden, undefined, "drawer close keeps Cloud Consumption open");
|
||||
app.ratecardView.closeRatecardDrawer({ immediate: true });
|
||||
});
|
||||
|
||||
test("quick clicks share a request, tab changes discard stale results, closing restores focus and keyboard stays inside", async () => {
|
||||
const { app, root, buttons, requests, find, document, timers } = setup();
|
||||
const first = buttons[0].listeners.click();
|
||||
const repeat = buttons[0].listeners.click();
|
||||
await tick();
|
||||
assert.equal(requests.length, 1);
|
||||
root.querySelectorAll("[role='tab']")[1].listeners.click();
|
||||
assert.equal(find("overlay"), null);
|
||||
const second = buttons[1].listeners.click();
|
||||
await tick();
|
||||
assert.equal(requests[1].message.planNumber, "SUB-B");
|
||||
assert.deepEqual(find("plan-details").querySelectorAll("dd").map(node => node.textContent), ["Booking B", "Sold B", "CONTRACT-B", "EXPIRED", "01-Jan-2020", "01-Jan-2024"]);
|
||||
requests[1].resolve({ ok: true, items: [item] });
|
||||
await second;
|
||||
requests[0].resolve({ ok: true, items: [{ ...item, productName: "WRONG PLAN" }] });
|
||||
await Promise.all([first, repeat]);
|
||||
assert.equal(find("table").querySelector("tbody").children[0].children[2].textContent, item.productName);
|
||||
const background = root.querySelector(".opportunities-extension-account-plans-dialog");
|
||||
assert.equal(background.inert, true);
|
||||
const overlay = find("overlay");
|
||||
const close = find("header").querySelector("button");
|
||||
close.focus();
|
||||
let prevented = 0, stopped = 0;
|
||||
overlay.listeners.keydown({ key: "Tab", shiftKey: true, preventDefault() { prevented++; } });
|
||||
assert.equal(document.activeElement, find("table").querySelectorAll("button")[10]);
|
||||
overlay.listeners.keydown({ key: "Tab", preventDefault() { prevented++; } });
|
||||
assert.equal(document.activeElement, close);
|
||||
overlay.listeners.keydown({ key: "Escape", preventDefault() { prevented++; }, stopPropagation() { stopped++; } });
|
||||
assert.equal(prevented, 3);
|
||||
assert.equal(stopped, 1);
|
||||
assert.equal(overlay.dataset.closing, "true");
|
||||
assert.equal(document.activeElement, buttons[1]);
|
||||
assert.equal(background.inert, false);
|
||||
assert.equal(timers.size, 1);
|
||||
const reopening = buttons[1].listeners.click();
|
||||
assert.equal(timers.size, 0);
|
||||
assert.equal(overlay.isConnected, false);
|
||||
await tick();
|
||||
app.consumptionPlansView.closeAccountPlansModal();
|
||||
assert.equal(find("overlay"), null);
|
||||
requests[2].resolve({ ok: true, items: [item] });
|
||||
await reopening;
|
||||
assert.equal(find("overlay"), null, "late responses never reopen a closed drawer");
|
||||
});
|
||||
|
||||
test("missing subPlanNum does not use another identifier; errors are local, empty results and retry on reopening work", async () => {
|
||||
const { app, plans, buttons, requests, find } = setup();
|
||||
delete plans[0].subPlanNum;
|
||||
await buttons[0].listeners.click();
|
||||
assert.match(find("notice").children[0].textContent, /Plan number is unavailable/);
|
||||
assert.equal(requests.length, 0);
|
||||
const failed = buttons[1].listeners.click();
|
||||
await tick();
|
||||
requests[0].resolve({ ok: false, error: "Synthetic 401", debugRequests: [] });
|
||||
await failed;
|
||||
assert.equal(find("notice").children[0].attributes.role, "alert");
|
||||
assert.equal(find("notice").children[0].textContent, "Synthetic 401");
|
||||
const retry = buttons[1].listeners.click();
|
||||
await tick();
|
||||
requests[1].resolve({ ok: true, items: [] });
|
||||
await retry;
|
||||
assert.equal(find("notice").children[0].textContent, "No rate card items found for this plan.");
|
||||
find("overlay").listeners.click({ target: find("overlay") });
|
||||
app.ratecardView.closeRatecardDrawer({ immediate: true });
|
||||
assert.equal(find("overlay"), null);
|
||||
});
|
||||
|
||||
test("opening a plan without planNumber sends its subPlanNum as the payload planNumber", async () => {
|
||||
const { plans, buttons, requests } = setup();
|
||||
delete plans[0].planNumber;
|
||||
const opening = buttons[0].listeners.click();
|
||||
await tick();
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].message.planNumber, "SUB-A");
|
||||
requests[0].resolve({ ok: true, items: [] });
|
||||
await opening;
|
||||
});
|
||||
|
||||
test("drawer starts offscreen, opens after a painted frame and never scrolls when focused", () => {
|
||||
const { app, buttons, plans, find, frames, nextFrame } = setup();
|
||||
app.ratecardView.openRatecardDrawer(plans[0], buttons[0]);
|
||||
const overlay = find("overlay");
|
||||
assert.equal(overlay.dataset.open, "false");
|
||||
assert.equal(find("search").focusOptions.preventScroll, true);
|
||||
nextFrame();
|
||||
assert.equal(overlay.dataset.open, "false", "offscreen position gets one full paint");
|
||||
nextFrame();
|
||||
assert.equal(overlay.dataset.open, "true");
|
||||
assert.equal(frames.size, 0);
|
||||
app.ratecardView.closeRatecardDrawer({ immediate: true });
|
||||
assert.equal(buttons[0].focusOptions.preventScroll, true);
|
||||
for (const painted of [false, true]) {
|
||||
app.ratecardView.openRatecardDrawer(plans[0], buttons[0]);
|
||||
const cancelled = find("overlay");
|
||||
if (painted) nextFrame();
|
||||
app.ratecardView.closeRatecardDrawer({ immediate: true });
|
||||
assert.equal(frames.size, 0);
|
||||
nextFrame();
|
||||
assert.equal(cancelled.dataset.open, "false", "closing cancels pending entry frames");
|
||||
assert.equal(cancelled.isConnected, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("drawer CSS supports both themes, reduced motion, bounded scrolling and separate header icon space", () => {
|
||||
const css = fs.readFileSync(path.join(__dirname, "../src/features/ratecard/styles.css"), "utf8");
|
||||
assert.match(css, /data-theme="dark"/);
|
||||
const buttonStyle = css.match(/\.opportunities-extension-ratecard-button \{([^}]+)\}/)[1];
|
||||
assert.match(buttonStyle, /display: inline-flex/);
|
||||
assert.match(buttonStyle, /align-items: center/);
|
||||
assert.match(buttonStyle, /height: 28px/);
|
||||
assert.match(buttonStyle, /line-height: 20px/);
|
||||
assert.match(css, /\.opportunities-extension-contract-ratecard > span \{\s*line-height: 20px/);
|
||||
assert.match(css, /prefers-reduced-motion/);
|
||||
assert.match(css, /translateX\(100%\)/);
|
||||
assert.match(css, /transition: transform 320ms/);
|
||||
assert.match(css, /\[data-open="true"\] \.opportunities-extension-ratecard-drawer \{ transform: translateX\(0\); \}/);
|
||||
assert.match(css, /overflow: clip/, "the overlay cannot be scrolled by focus during entry");
|
||||
assert.doesNotMatch(css, /opportunities-ratecard-(?:fade|slide|backdrop)-in/);
|
||||
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
||||
assert.match(css, /min-width: 1100px/);
|
||||
assert.match(css, /grid-template-rows: auto auto auto minmax\(0, 1fr\)/);
|
||||
assert.doesNotMatch(css, /\.opportunities-extension-account-plans-dialog\s*\{/);
|
||||
});
|
||||
Reference in New Issue
Block a user