Fully implemented the Manage Time functionality

This commit is contained in:
2026-10-19 10:30:09 -03:00
parent d392e5ddd7
commit 9eef03e5f4
17 changed files with 1429 additions and 139 deletions

View File

@@ -30,6 +30,7 @@
onMessage: { addListener: listener => listeners.push(listener) },
async sendMessage(message) {
const operation = message.type.split(".").at(-1);
if (operation === "requestTimeEntries") return { ok: true, items: [], debugRequests: [] };
if (operation === "requestLookupTaskType") return { ok: true, items: [{ lookUpCode_c: "SCTA_SERVICE_CONFIG", lookUpMeaning_c: "Config", tags_c: "SERVICE" }, { lookUpCode_c: "SCTA_SERVICE_STANDARD", lookUpMeaning_c: "Standard", tags_c: "SERVICE" }, { lookUpCode_c: "SCTA_NON_SERVICE_TRAINING", lookUpMeaning_c: "Training", tags_c: "NON_SERVICE" }], debugRequests: [] };
if (operation === "requestServiceRequestExpand") return { ok: true, items: [...message.srNumbers, "non-service-sr"].map(SrNumber => ({ SrNumber, SRActivities_c: "Account Plan;Customer Research, Assessment and Recommendation;Success Plan" })), debugRequests: [] };
if (operation === "requestResourceUser") return { ok: true, resource: { resourcePartyId: 123 }, debugRequests: [] };

View File

@@ -34,7 +34,7 @@ test("failed results are never cached and identity changes discard previous resp
h.user(""); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 5);
});
test("writes and token requests always execute; successful saves and Refresh all invalidate queries", async () => {
for (const name of ["requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "invalidateProductsCache"]) {
for (const name of ["requestTimeEntriesSave", "requestTimeEntriesDelete", "requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "invalidateProductsCache"]) {
const h = setup(); let queries = 0, writes = 0;
const query = message("requestActionFindLookup");
const queryTransport = async () => { queries++; return { ok: true }; };

View File

@@ -37,7 +37,7 @@ test("week errors return status and debug logs without masking failure", async (
const result = await app.dispatch({ type: "opportunitiesExtension.requestWeekDetails", dateReference: "2026-10-16" });
assert.equal(result.ok, false); assert.equal(result.httpStatus, 401); assert.equal(result.debugRequests.length, 1);
});
function setup(requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => []) {
function setup(requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [], requestEntries = async () => [], localStorage, requestSave, requestDelete) {
let document;
class Element {
constructor(tag) { this.tag = tag; this.children = []; this.attributes = {}; this.listeners = {}; this.text = ""; this.isConnected = true; }
@@ -53,16 +53,314 @@ function setup(requestServices = async () => [], requestExpand = async () => [],
}
const host = new Element("host");
document = { createElement: tag => new Element(tag), createElementNS: (_, tag) => new Element(tag), getElementById: () => host };
const calls = [];
const calls = [], notifications = [];
const searchableSelect = factory("src/features/opportunities/searchable-select.js", "createSearchableSelect")({ environment: { document } });
const app = factory("src/features/manage-time/view.js", "createManageTime")({ MODAL_ID: "host", environment: { document }, searchableSelect, dates, requestServices, requestExpand, requestTaskTypes,
const app = factory("src/features/manage-time/view.js", "createManageTime")({ MODAL_ID: "host", environment: { document, localStorage }, searchableSelect, dates, requestServices, requestExpand, requestTaskTypes, requestEntries, requestSave, requestDelete,
notify: (message, isError = false) => notifications.push({ message, isError }),
requestWeek: date => new Promise((resolve, reject) => calls.push({ date, resolve, reject })) });
app.setReady(true);
const walk = el => [el, ...el.children.flatMap(walk)];
const nodes = () => walk(host);
const find = label => nodes().find(el => el.attributes["aria-label"] === label);
return { app, calls, nodes, find, host };
return { app, calls, nodes, find, host, notifications };
}
test("persisted deletions wait for Save, stay scoped to their week and allow a deletion-only save", async () => {
const deleted = [];
const h = setup(undefined, undefined, undefined, async () => [{ id: 7, srNumber: "SR001", activityCode: "Design", taskType: "PREP", mondayTaskHours: 2, tag: "SERVICE" }], undefined, undefined, async id => deleted.push(id));
h.app.open(); h.calls[0].resolve(week()); await flush();
h.find("Delete row 1").click(); assert.deepEqual(deleted, []); assert.equal(h.find("Save").disabled, false);
h.find("Next week").click();
const otherWeek = week().map(day => ({ ...day, Date: dates.toApi(dates.shift(dates.fromApi(day.Date).toISOString().slice(0, 10), 7)), EndDate: "23-OCT-2026", WeekId: 420 }));
h.calls[1].resolve(otherWeek); await flush(); assert.equal(h.find("Save").disabled, true);
h.find("Previous week").click(); h.calls[2].resolve(week()); await flush();
assert.equal(h.find("Service Request, row 1"), undefined);
h.find("Save").click(); await flush();
assert.deepEqual(deleted, ["7"]); assert.equal(h.find("Save").disabled, true);
assert.deepEqual(h.notifications, [{ message: "1 time entry deleted.", isError: false }]);
});
test("deleting new rows is local; failed deletions retain only unfinished IDs", async () => {
const deleted = []; let fail = true;
const h = setup(undefined, undefined, undefined, async () => [7, 8].map(id => ({ id, srNumber: "SR001", activityCode: "Design", taskType: "PREP", mondayTaskHours: 2, tag: "SERVICE" })), undefined, undefined, async id => {
deleted.push(id); if (id === "8" && fail) throw new Error("HTTP 403");
});
h.app.open(); h.calls[0].resolve(week()); await flush();
h.find("Add").click(); h.find("Delete row 3").click();
assert.equal(h.find("Save").disabled, true); assert.deepEqual(deleted, []);
h.find("Delete row 1").click(); h.find("Delete row 1").click();
h.find("Save").click(); h.find("Save").click(); await flush();
assert.deepEqual(deleted, ["7", "8"]); assert.equal(h.find("Save").disabled, false);
assert.deepEqual(h.notifications, [{ message: "1 deleted. HTTP 403", isError: true }]);
fail = false; h.find("Save").click(); await flush();
assert.deepEqual(deleted, ["7", "8", "8"]); assert.equal(h.find("Save").disabled, true);
});
for (const family of ["chrome", "firefox"]) test(`${family}: DELETE uses the saved ID, identical write headers and a narrow Origin rule`, async () => {
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
const calls = [];
const app = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options }); return new Response(null, { status: 204 });
} });
app.responseHeaderListeners[0].listener({ url: `${origin}/ic/builder/rt/oalset_timeentrymobile/live/test`, incognito: false,
responseHeaders: [{ name: "x-appbuilder-client-id", value: "delete-client" }] });
const result = await app.dispatch({ type: "opportunitiesExtension.requestTimeEntriesDelete", id: 9339403 });
assert.equal(result.ok, true); assert.equal(calls.length, 1);
assert.match(calls[0].url, /\/mgmt\/timeEntries\/9339403$/);
assert.equal(calls[0].options.method, "DELETE"); assert.equal(calls[0].options.body, undefined);
assert.deepEqual({ ...calls[0].options.headers }, { Accept: "application/json", "Content-Type": "application/json", Authorization: "Session", Origin: origin, "x-appbuilder-client-id": "delete-client" });
const rule = app.headerRules[0].addRules[0];
assert.equal(rule.id, 7205); assert.deepEqual(rule.condition.requestMethods, ["delete"]);
assert.equal(new RegExp(rule.condition.regexFilter).test(calls[0].url), true);
assert.equal(new RegExp(rule.condition.regexFilter).test(calls[0].url + "0"), false);
const invalid = await app.dispatch({ type: "opportunitiesExtension.requestTimeEntriesDelete", id: "../other" });
assert.equal(invalid.ok, false); assert.equal(calls.length, 1);
});
test("Save validates service rows, posts once, then puts changes with the returned id", async () => {
const calls = []; let finish;
const h = setup(undefined, undefined, undefined, undefined, undefined, (method, items) => {
calls.push({ method, items }); return new Promise(resolve => { finish = () => resolve(items.map(item => ({ ...item, id: 123 }))); });
});
h.app.open(); h.calls[0].resolve(week()); await flush();
assert.equal(h.find("Save").disabled, true);
for (const [label, value] of [["Service Request", "SR001"], ["Activity", "Solution Design"], ["Task Type", "PREP"]]) {
const input = h.find(`${label}, row 1`); input.value = value; input.listeners.change();
assert.equal(h.find("Save").disabled, true);
}
const hours = h.find("Hours 12-OCT-2026, row 1"); hours.value = "2"; hours.valueAsNumber = 2; hours.listeners.input();
assert.equal(h.find("Save").disabled, false);
h.find("Save").click(); h.find("Save").click();
assert.equal(calls.length, 1); assert.equal(calls[0].method, "POST");
assert.equal(calls[0].items[0].mondayTaskHours, 2); assert.equal(calls[0].items[0].saturdayTaskHours, 0);
assert.equal(calls[0].items[0].id, undefined); assert.equal(calls[0].items[0].weekId, 419);
finish(); await flush(); assert.equal(h.find("Save").disabled, true);
assert.deepEqual(h.notifications, [{ message: "1 time entry saved.", isError: false }]);
const notes = h.nodes().find(el => el.tag === "textarea" && el.attributes["aria-label"] === "Notes, row 1");
notes.value = "Updated"; notes.listeners.input(); h.find("Save").click();
assert.equal(calls[1].method, "PUT"); assert.equal(calls[1].items[0].id, 123);
finish(); await flush(); assert.equal(h.find("Save").disabled, true);
assert.equal(h.notifications.length, 2); assert.equal(h.notifications[1].isError, false);
});
test("mixed saves retain successful PUTs when POST fails and never repost a saved row without an id", async () => {
const calls = [];
const h = setup(undefined, undefined, undefined, async () => [{ id: 7, srNumber: "SR001", activityCode: "Design", taskType: "PREP", tag: "SERVICE", mondayTaskHours: 2 }], undefined,
async (method, items) => { calls.push({ method, items }); if (method === "POST" && calls.length === 2) throw new Error("Post failed"); return null; });
h.app.open(); h.calls[0].resolve(week()); await flush();
const notes = h.nodes().find(el => el.tag === "textarea" && el.attributes["aria-label"] === "Notes, row 1");
notes.value = "Changed"; notes.listeners.input();
h.find("Copy row 1").click();
const copiedSr = h.find("Service Request, row 2"); copiedSr.value = "SR002"; copiedSr.listeners.change();
const copiedActivity = h.find("Activity, row 2"); copiedActivity.value = "Design"; copiedActivity.listeners.change();
h.find("Save").click(); await flush();
assert.deepEqual(calls.map(call => call.method), ["PUT", "POST"]);
assert.equal(h.find("Save").disabled, false);
h.find("Save").click(); await flush();
assert.deepEqual(calls.map(call => call.method), ["PUT", "POST", "POST"]);
const copyNotes = h.nodes().find(el => el.tag === "textarea" && el.attributes["aria-label"] === "Notes, row 2");
copyNotes.value = "Further edit"; copyNotes.listeners.input();
assert.equal(h.find("Save").disabled, true);
assert.ok(h.nodes().some(node => node.attributes.role === "alert" && node.textContent.includes("IDs could not be confirmed")));
});
test("Non Service saves without activity and failed saves keep edited values", async () => {
const calls = [];
const h = setup(undefined, undefined, undefined, undefined, undefined, async (method, items) => { calls.push({ method, items }); throw new Error("HTTP 403"); });
h.app.open(); h.calls[0].resolve(week()); await flush(); h.find("Add (Non service)").click();
const type = h.find("Task Type, row 4"); type.value = "TRAIN"; type.listeners.change();
const hours = h.find("Hours 10-OCT-2026, row 4"); hours.value = "3"; hours.valueAsNumber = 3; hours.listeners.input();
assert.equal(h.find("Save").disabled, false);
hours.value = "25"; hours.valueAsNumber = 25; hours.listeners.input();
assert.equal(hours.value, "3"); assert.equal(h.find("Save").disabled, false);
assert.deepEqual(h.notifications.at(-1), { message: "Enter hours between 0 and 24.", isError: true });
hours.value = "3"; hours.valueAsNumber = 3; hours.listeners.input(); h.find("Save").click(); await flush();
assert.equal(calls[0].items[0].tag, "NON_SERVICE"); assert.equal(calls[0].items[0].srNumber, "non-service-sr");
assert.equal(calls[0].items[0].activityCode, "");
assert.equal(h.find("Hours 10-OCT-2026, row 4").value, "3");
assert.equal(h.find("Save").disabled, false);
assert.ok(h.nodes().some(node => node.attributes.role === "alert" && node.textContent.includes("403")));
});
for (const method of ["POST", "PUT"]) test(`time entry ${method} uses Session and the current resource with a JSON array`, async () => {
let posted;
const app = loadBackground({ fetch: async (url, options) => {
if (url.includes("resourceUsers?")) return new Response(JSON.stringify({ resourcePartyId: 123 }), { headers: { "x-appbuilder-client-id": "time-save-client" } });
assert.match(url, /\/mgmt\/timeEntries$/); assert.equal(options.method, method);
assert.equal(options.headers.Authorization, "Session"); assert.equal(options.headers["Content-Type"], "application/json");
assert.equal(options.headers.Origin, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com");
assert.equal(options.headers["x-appbuilder-client-id"], "time-save-client");
posted = JSON.parse(options.body); return new Response(null, { status: 204 });
} });
const item = { id: 7, srNumber: "SR001", activityCode: "Design", taskType: "PREP", weekId: 419, tag: "SERVICE", mondayTaskHours: 2,
tuesdayTaskHours: 0, wednesdayTaskHours: 0, thursdayTaskHours: 0, fridayTaskHours: 0, saturdayTaskHours: 0, sundayTaskHours: 0 };
const result = await app.dispatch({ type: "opportunitiesExtension.requestTimeEntriesSave", method, items: [item], emailAddress: "test@oracle.com" });
assert.equal(result.ok, true); assert.equal(posted[0].resourceId, "123"); assert.equal(posted[0].id, method === "PUT" ? 7 : undefined);
const rule = app.headerRules.find(update => update.addRules.some(rule => rule.id === 7204)).addRules[0];
assert.deepEqual(rule.condition.requestMethods, ["post", "put"]);
assert.equal(rule.condition.initiatorDomains.length, 1);
assert.equal(rule.action.requestHeaders[0].value, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com");
assert.equal(new RegExp(rule.condition.regexFilter).test("https://example.com/mgmt/timeEntries"), false);
});
test("time entry headers reuse IDs captured from Time Entry responses and session storage", async () => {
const stored = {};
const runtimeApi = { runtime: { getURL: () => "chrome-extension://test/" },
storage: { session: { set: async values => Object.assign(stored, values), get: async () => stored } },
declarativeNetRequest: { updateSessionRules: async () => {} } };
const createHeaders = new vm.Script(fs.readFileSync("src/auth/sr-request-headers.js", "utf8") + "\ncreateSrRequestHeaders;").runInNewContext({ URL });
const headers = createHeaders({ runtimeApi });
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries";
headers.observeResponseHeaders(`${url}/weekDetails?dateValue=16-OCT-2026`, [{ name: "x-appbuilder-client-id", value: "time-entry-client" }]);
assert.equal((await headers.timeEntriesHeaders(url))["x-appbuilder-client-id"], "time-entry-client");
const restarted = createHeaders({ runtimeApi });
assert.equal((await restarted.timeEntriesHeaders(url))["x-appbuilder-client-id"], "time-entry-client");
});
test("hour inputs accept inclusive 0..24 and reject out-of-range or nonnumeric values", async () => {
const h = setup(); h.app.open(); h.calls[0].resolve(week()); await flush();
const input = h.find("Hours 10-OCT-2026, row 1");
for (const value of [0, 24]) {
input.value = String(value); input.valueAsNumber = value; input.listeners.input();
assert.equal(input.value, String(value));
}
for (const value of [-1, 25, 100, Infinity, NaN]) {
input.value = String(value); input.valueAsNumber = value; input.listeners.input();
assert.equal(input.value, "24"); assert.equal(h.notifications.at(-1).isError, true);
}
input.value = ""; input.valueAsNumber = NaN; input.listeners.input();
assert.equal(input.value, "");
});
test("time entry writes reject out-of-range hours before making a network request", async () => {
let calls = 0;
const app = loadBackground({ fetch: async () => { calls++; throw new Error("Unexpected fetch"); } });
for (const value of [-1, 25]) {
const result = await app.dispatch({ type: "opportunitiesExtension.requestTimeEntriesSave", method: "POST", items: [{
srNumber: "SR001", activityCode: "Design", taskType: "PREP", tag: "SERVICE", weekId: 419,
saturdayTaskHours: value, sundayTaskHours: 0, mondayTaskHours: 1, tuesdayTaskHours: 0, wednesdayTaskHours: 0, thursdayTaskHours: 0, fridayTaskHours: 0
}] });
assert.equal(result.ok, false);
}
assert.equal(calls, 0);
});
test("duplicate SR selection restores the previous selection and highlights the existing row", async () => {
const h = setup(async () => [{ srNumber: "SR001", srTitle: "One" }, { srNumber: "SR002", srTitle: "Two" }]);
h.app.open(); h.calls[0].resolve(week()); await flush();
function select(row, value) { const input = h.find(`Service Request, row ${row}`); input.value = value; input.listeners.change(); }
select(1, "SR001"); select(2, "SR002"); select(2, "SR001");
assert.equal(h.find("Service Request, row 2").value, "SR002");
const highlighted = h.nodes().filter(el => el.className === "oe-time-duplicate-row");
assert.equal(highlighted.length, 1);
assert.equal(h.find("Service Request, row 1").parent.parent, highlighted[0]);
assert.equal(h.notifications.at(-1).isError, true);
highlighted[0].listeners.animationend();
assert.equal(highlighted[0].className, "");
h.find("Delete row 1").click(); select(1, "SR001");
assert.equal(h.find("Service Request, row 1").value, "SR001");
const css = fs.readFileSync("src/features/manage-time/styles.css", "utf8");
assert.match(css, /oe-time-duplicate-fade 5\.5s/);
assert.match(css, /90\.909%/);
});
test("reference date persists manual and weekly changes across modal and page reopening", () => {
const saved = new Map();
const storage = { getItem: key => saved.get(key), setItem: (key, value) => saved.set(key, value) };
const h = setup(undefined, undefined, undefined, undefined, storage);
h.app.open();
const date = h.find("Reference date"); date.value = "2026-10-09"; date.listeners.change();
h.app.close(); h.app.open();
assert.equal(h.find("Reference date").value, "2026-10-09");
h.find("Next week").click();
const next = setup(undefined, undefined, undefined, undefined, storage); next.app.open();
assert.equal(next.calls[0].date, "2026-10-16");
const invalid = next.find("Reference date"); invalid.value = ""; invalid.listeners.change();
next.app.close(); next.app.open();
assert.equal(next.find("Reference date").value, "2026-10-16");
});
test("invalid stored date falls back to today and unavailable storage retains the in-memory date", () => {
const h = setup(undefined, undefined, undefined, undefined, { getItem: () => "invalid", setItem() { throw new Error("Blocked"); } });
h.app.open(); assert.equal(h.calls[0].date, dates.today());
const date = h.find("Reference date"); date.value = "2026-10-09"; date.listeners.change();
h.app.close(); h.app.open();
assert.equal(h.find("Reference date").value, "2026-10-09");
});
test("loading renders three skeleton rows until entries resolve, then restores empty input rows", async () => {
let resolveEntries;
const h = setup(undefined, undefined, undefined, () => new Promise(resolve => { resolveEntries = resolve; }));
h.app.open();
const skeletons = () => h.nodes().filter(el => el.className === "oe-time-skeleton-row");
assert.equal(skeletons().length, 3);
assert.ok(skeletons().every(row => row.children.length === 13));
assert.equal(h.find("Weekly time entries").attributes["aria-hidden"], "true");
assert.equal(h.find("Add").disabled, true);
h.calls[0].resolve(week()); await flush();
assert.equal(skeletons().length, 3);
resolveEntries([]); await flush();
assert.equal(skeletons().length, 0);
assert.equal(h.find("Weekly time entries").attributes["aria-hidden"], "false");
assert.ok(h.find("Service Request, row 3"));
assert.equal(h.find("Add").disabled, false);
});
test("time entry errors allow retry and stale weeks cannot replace current entries", async () => {
const pending = [];
const h = setup(undefined, undefined, undefined, weekId => new Promise((resolve, reject) => pending.push({ weekId, resolve, reject })));
h.app.open(); h.calls[0].resolve(week()); await flush();
pending[0].reject(new Error("Time entries unavailable")); await flush();
assert.equal(h.find("Retry").hidden, false);
assert.equal(h.find("Add").disabled, true);
h.find("Retry").click(); h.calls[1].resolve(week()); await flush();
h.find("Next week").click(); h.calls[2].resolve(week().map(day => ({ ...day, WeekId: 420 }))); await flush();
pending[2].resolve([{ srNumber: "SR002", activityCode: "Current", tag: "SERVICE" }]); await flush();
pending[1].resolve([{ srNumber: "SR001", activityCode: "Stale", tag: "SERVICE" }]); await flush();
assert.equal(h.find("Service Request, row 1").value, "SR002");
assert.equal(h.find("Activity, row 1").value, "Current");
});
test("saved entries hydrate selections, weekday hours and notes, preserving drafts", async () => {
const calls = [];
const h = setup(undefined, undefined, async () => [{ lookUpCode_c: "PREP", lookUpMeaning_c: "Preparation", tags_c: "SERVICE" }], async weekId => {
calls.push(weekId);
return [{ id: 9339403, srNumber: "SR0001502709", taskType: "PREP", activityCode: "Solution Design", tag: "SERVICE", mondayTaskHours: 2, saturdayTaskHours: 3, sundayTaskHours: 0, notes: "Saved notes" },
{ id: 2, srNumber: "non-service-sr", taskType: "TRAIN", activityCode: "Training", tag: "NON_SERVICE", fridayTaskHours: 4 }];
});
h.app.open(); h.calls[0].resolve(week()); await flush();
assert.deepEqual(calls, [419]);
assert.equal(h.find("Service Request, row 1").value, "SR0001502709");
assert.equal(h.find("Activity, row 1").value, "Solution Design");
assert.equal(h.find("Task Type, row 1").value, "PREP");
assert.equal(h.find("Hours 12-OCT-2026, row 1").value, "2");
assert.equal(h.find("Hours 10-OCT-2026, row 1").value, "3");
assert.equal(h.find("Hours 11-OCT-2026, row 1").value, "0");
assert.equal(h.find("Hours 16-OCT-2026, row 2").value, "4");
assert.ok(h.find("Non Service, row 2"));
const notes = h.nodes().find(el => el.tag === "textarea" && el.attributes["aria-label"] === "Notes, row 1");
assert.equal(notes.value, "Saved notes");
notes.value = "Edited notes"; notes.listeners.input();
h.find("Next week").click(); h.calls[1].resolve(week()); await flush();
assert.equal(calls.length, 1);
assert.equal(h.nodes().find(el => el.tag === "textarea" && el.attributes["aria-label"] === "Notes, row 1").value, "Edited notes");
});
for (const family of ["chrome", "firefox"]) test(`${family}: time entries use the resource party, selected week, Session and ascending created date`, async () => {
const calls = [];
const app = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
return new Response(JSON.stringify(url.includes("resourceUsers?") ? { resourcePartyId: 123 } : [{ id: 7, srNumber: "SR001" }]));
} });
const result = await app.dispatch({ type: "opportunitiesExtension.requestTimeEntries", weekId: 418, emailAddress: "test@oracle.com" });
assert.equal(result.ok, true);
const call = calls.find(call => call.url.includes("timeEntries?"));
assert.match(call.url, /timeEntries\?resourceId=123&sortBy=createdDate&sortOrder=ASC&weekId=418$/);
assert.equal(call.options.headers.Authorization, "Session");
assert.equal(call.options.credentials, "include");
assert.equal(result.items[0].id, 7);
});
test("autocomplete reopening highlights its saved option and filtering does not select another value", async () => {
const h = setup(undefined, undefined, async () => [
{ lookUpCode_c: "CONFIG", lookUpMeaning_c: "Config", tags_c: "SERVICE" },
{ lookUpCode_c: "PREP", lookUpMeaning_c: "Preparation", tags_c: "SERVICE" }
], async () => [{ srNumber: "SR001", taskType: "PREP", tag: "SERVICE" }]);
h.app.open(); h.calls[0].resolve(week()); await flush();
const input = h.nodes().find(el => el.attributes.role === "combobox" && el.attributes["aria-label"] === "Task Type, row 1");
input.click();
const list = input.parent.children[1];
assert.equal(list.children[0].attributes["aria-selected"], "false");
assert.equal(list.children[1].attributes["aria-selected"], "true");
assert.equal(input.attributes["aria-activedescendant"], list.children[1].id);
input.value = "Config"; input.listeners.input();
assert.equal(list.children[0].attributes["aria-selected"], "false");
assert.equal(h.find("Task Type, row 1").value, "PREP");
input.listeners.blur(); input.click();
assert.equal(list.children[1].attributes["aria-selected"], "true");
});
test("Service Request tooltips use expanded fields in order and keep description separate", async () => {
const keys = ["SrNumber", "AccountPartyUniqueName", "OptyName_c", "OptyNumber_c", "OptyOwner_c", "OptyStatus_c", "ServiceNew_c", "ReportedByPartyUniqueName", "CountryText_c", "StatusCdMeaning", "AssigneePersonName", "ProblemDescription"];
const record = Object.fromEntries(keys.map(key => [key, key === "SrNumber" ? "SR001" : `Value ${key}`]));
@@ -108,7 +406,8 @@ test("Manage Time autocompletes search and select SR, dependent Activity and fil
choose("Task Type, row 1", "config");
assert.equal(h.find("Task Type, row 1").value, "CONFIG");
h.find("Copy row 1").click();
assert.equal(combo("Activity, row 4").value, "Technical Support");
assert.equal(combo("Activity, row 4").value, "");
assert.equal(combo("Service Request, row 4").value, "");
h.find("Add (Non service)").click();
choose("Task Type, row 5", "train");
const ids = h.nodes().filter(el => el.attributes.role === "combobox").map(el => el.id);
@@ -181,7 +480,7 @@ test("modal loads today, navigates seven days, ignores stale results and preserv
assert.equal(h.find("Save").disabled, true);
const hours = h.find("Hours 10-OCT-2026, row 1"); hours.value = "2"; hours.valueAsNumber = 2; hours.listeners.input();
assert.equal(hours.step, "1");
assert.ok(h.nodes().some(el => el.tag === "strong" && el.textContent === "2 hrs"));
assert.ok(h.nodes().some(el => el.tag === "strong" && el.textContent === "2h"));
h.find("Copy row 1").click(); assert.equal(h.nodes().filter(el => el.type === "number").length, 28);
h.find("Next week").click(); assert.equal(h.calls[2].date, "2026-10-23");
h.find("Previous week").click(); assert.equal(h.calls[3].date, "2026-10-16");
@@ -246,7 +545,7 @@ test("active SRs load once per modal and notes preserve text, character count an
assert.ok(h.host.textContent.includes("1995 characters left"));
h.find("Collapse notes, row 1").click();
assert.equal(h.find("Notes, row 1").attributes["aria-expanded"], "false");
h.find("Copy row 1").click(); assert.equal(h.find("Service Request, row 4").value, "SR1");
h.find("Copy row 1").click(); assert.equal(h.find("Service Request, row 4").value, "");
h.find("Next week").click(); h.calls[1].resolve(week()); await flush();
assert.deepEqual(calls, [419]);
h.app.close(); h.app.open(); h.calls[2].resolve(week()); await flush();