feat: add mdtodo web-probe commands (#898)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -348,10 +348,25 @@ async function processCommand(command) {
|
||||
case "cancel": return withObserverSync(await cancelRunningTurn(), "cancel");
|
||||
case "selectProvider": return withObserverSync(await selectProvider(String(command.provider || command.value || command.text || "")), "selectProvider");
|
||||
case "clickSession": return withObserverSync(await clickSession(String(command.sessionId || command.value || "")), "clickSession");
|
||||
case "gotoProjectMdtodo": return withObserverSync(await gotoProjectMdtodo(), "gotoProjectMdtodo");
|
||||
case "openMdtodoSourceConfig": return openMdtodoSourceConfig(command);
|
||||
case "configureMdtodoHwpodSource": return configureMdtodoHwpodSource(command);
|
||||
case "probeMdtodoSource": return probeMdtodoSource(command);
|
||||
case "reindexMdtodoSource": return reindexMdtodoSource(command);
|
||||
case "selectProjectSource": return selectProjectSource(command);
|
||||
case "selectMdtodoSource": return selectMdtodoSource(command);
|
||||
case "selectMdtodoFile": return selectMdtodoFile(command);
|
||||
case "selectMdtodoTask": return selectMdtodoTask(command);
|
||||
case "expandMdtodoTask": return expandMdtodoTask(command);
|
||||
case "editMdtodoTaskTitle": return editMdtodoTaskTitle(command);
|
||||
case "editMdtodoTaskBody": return editMdtodoTaskBody(command);
|
||||
case "toggleMdtodoTaskStatus": return toggleMdtodoTaskStatus(command);
|
||||
case "addMdtodoRootTask": return addMdtodoRootTask(command);
|
||||
case "addMdtodoSubTask": return addMdtodoSubTask(command);
|
||||
case "continueMdtodoTask": return continueMdtodoTask(command);
|
||||
case "deleteMdtodoTask": return deleteMdtodoTask(command);
|
||||
case "launchWorkbenchFromTask": return withObserverSync(await launchWorkbenchFromTask(command), "launchWorkbenchFromTask");
|
||||
case "launchWorkbenchFromMdtodo": return withObserverSync(await launchWorkbenchFromMdtodo(command), "launchWorkbenchFromMdtodo");
|
||||
case "screenshot": return captureScreenshot(command.reason || "manual", command.imageType || "png");
|
||||
case "mark": return { mark: truncate(command.label || command.text || "mark", 200), currentUrl: currentPageUrl(), pageId };
|
||||
case "stop": stopping = true; return { stopping: true, currentUrl: currentPageUrl(), pageId };
|
||||
@@ -1505,11 +1520,69 @@ function ensureProjectManagementCommand(type) {
|
||||
if (!projectManagement.commandAllowlist.includes(type)) throw new Error(type + " is not in webProbe.projectManagement.commandAllowlist for the selected node/lane");
|
||||
}
|
||||
|
||||
async function clickProjectItemByAttr({ type, attr, value, fallbackSelector }) {
|
||||
async function gotoProjectMdtodo() {
|
||||
ensureProjectManagementCommand("gotoProjectMdtodo");
|
||||
return gotoTarget("/projects/mdtodo");
|
||||
}
|
||||
|
||||
function commandValue(command, keys) {
|
||||
for (const key of keys) {
|
||||
const value = command?.[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function visibleLocator(locator) {
|
||||
return await locator.count().catch(() => 0) > 0 && await locator.first().isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
async function selectHtmlOptionByValueOrLabel(locator, value) {
|
||||
const select = locator.first();
|
||||
const targetValue = typeof value === "string" && value.trim() ? value.trim() : "";
|
||||
if (targetValue) {
|
||||
const byValue = await select.selectOption({ value: targetValue }).then((selected) => ({ ok: true, selected })).catch(() => ({ ok: false, selected: [] }));
|
||||
if (byValue.ok && byValue.selected.length > 0) return { mode: "select-value", selectedValue: byValue.selected[0] || targetValue };
|
||||
const byLabel = await select.selectOption({ label: targetValue }).then((selected) => ({ ok: true, selected })).catch(() => ({ ok: false, selected: [] }));
|
||||
if (byLabel.ok && byLabel.selected.length > 0) return { mode: "select-label", selectedValue: byLabel.selected[0] || targetValue };
|
||||
}
|
||||
const selectedValue = await select.evaluate((element) => {
|
||||
const options = Array.from(element.options || []).filter((option) => !option.disabled && option.value);
|
||||
const chosen = options[0] || null;
|
||||
if (!chosen) return "";
|
||||
element.value = chosen.value;
|
||||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
element.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return chosen.value;
|
||||
});
|
||||
return { mode: "select-first", selectedValue };
|
||||
}
|
||||
|
||||
async function clickProjectItemByAttr({ type, attr, value, fallbackSelector, selectTestId }) {
|
||||
ensureProjectManagementCommand(type);
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const targetValue = typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
if (selectTestId) {
|
||||
const select = page.locator('[data-testid="' + cssEscape(selectTestId) + '"]');
|
||||
if (await visibleLocator(select)) {
|
||||
const selected = await selectHtmlOptionByValueOrLabel(select, targetValue || "");
|
||||
await page.waitForTimeout(700);
|
||||
const afterProject = await projectManagementCommandSnapshot();
|
||||
return {
|
||||
beforeUrl,
|
||||
afterUrl: currentPageUrl(),
|
||||
type,
|
||||
attr,
|
||||
mode: selected.mode,
|
||||
selected: opaqueIdSummary(selected.selectedValue || targetValue),
|
||||
beforeProject,
|
||||
afterProject,
|
||||
pageId,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
const selector = targetValue ? "[" + attr + "=\"" + cssEscape(targetValue) + "\"]" : fallbackSelector;
|
||||
const locator = page.locator(selector).first();
|
||||
await locator.waitFor({ state: "visible", timeout: 15000 });
|
||||
@@ -1535,7 +1608,18 @@ async function selectProjectSource(command) {
|
||||
type: "selectProjectSource",
|
||||
attr: "data-source-id",
|
||||
value: command.sourceId || command.value || command.text || "",
|
||||
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]'
|
||||
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]',
|
||||
selectTestId: "mdtodo-source-select"
|
||||
});
|
||||
}
|
||||
|
||||
async function selectMdtodoSource(command) {
|
||||
return clickProjectItemByAttr({
|
||||
type: "selectMdtodoSource",
|
||||
attr: "data-source-id",
|
||||
value: command.sourceId || command.value || command.text || "",
|
||||
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]',
|
||||
selectTestId: "mdtodo-source-select"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1544,25 +1628,276 @@ async function selectMdtodoFile(command) {
|
||||
type: "selectMdtodoFile",
|
||||
attr: "data-file-ref",
|
||||
value: command.fileRef || command.value || command.text || "",
|
||||
fallbackSelector: '[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]'
|
||||
fallbackSelector: '[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]',
|
||||
selectTestId: "mdtodo-file-select"
|
||||
});
|
||||
}
|
||||
|
||||
async function mdtodoTaskLocator(command) {
|
||||
const taskRef = commandValue(command, ["taskRef"]);
|
||||
const taskId = commandValue(command, ["taskId", "task", "value", "text"]);
|
||||
const selectors = [];
|
||||
if (taskRef) selectors.push('[data-task-ref="' + cssEscape(taskRef) + '"]');
|
||||
if (taskId) {
|
||||
selectors.push('[data-task-id="' + cssEscape(taskId) + '"]');
|
||||
selectors.push('[data-rxx-id="' + cssEscape(taskId) + '"]');
|
||||
}
|
||||
for (const selector of selectors) {
|
||||
const locator = page.locator(selector).first();
|
||||
if (await visibleLocator(locator)) return { locator, taskRef, taskId, selector };
|
||||
}
|
||||
if (taskId) {
|
||||
const textLocator = page.locator('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]').filter({ hasText: taskId }).first();
|
||||
if (await visibleLocator(textLocator)) return { locator: textLocator, taskRef, taskId, selector: "text:" + taskId };
|
||||
}
|
||||
const fallback = page.locator('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]').first();
|
||||
return { locator: fallback, taskRef, taskId, selector: "first-visible-task" };
|
||||
}
|
||||
|
||||
async function selectMdtodoTask(command) {
|
||||
return clickProjectItemByAttr({
|
||||
ensureProjectManagementCommand("selectMdtodoTask");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const target = await mdtodoTaskLocator(command);
|
||||
await target.locator.waitFor({ state: "visible", timeout: 15000 });
|
||||
const clicked = await target.locator.evaluate((element) => ({
|
||||
taskRef: element.getAttribute("data-task-ref") || null,
|
||||
taskId: element.getAttribute("data-task-id") || element.getAttribute("data-rxx-id") || null,
|
||||
status: element.getAttribute("data-task-status") || null
|
||||
})).catch(() => ({ taskRef: target.taskRef || null, taskId: target.taskId || null, status: null }));
|
||||
await target.locator.click();
|
||||
await page.waitForTimeout(700);
|
||||
const afterProject = await projectManagementCommandSnapshot();
|
||||
return {
|
||||
beforeUrl,
|
||||
afterUrl: currentPageUrl(),
|
||||
type: "selectMdtodoTask",
|
||||
attr: "data-task-ref",
|
||||
value: command.taskRef || command.value || command.text || "",
|
||||
fallbackSelector: '[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]'
|
||||
});
|
||||
selector: target.selector,
|
||||
selectedTask: opaqueIdSummary(clicked.taskRef || target.taskRef),
|
||||
selectedTaskId: clicked.taskId || target.taskId || null,
|
||||
selectedTaskStatus: clicked.status || null,
|
||||
beforeProject,
|
||||
afterProject,
|
||||
pageId,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function expandMdtodoTask(command) {
|
||||
ensureProjectManagementCommand("expandMdtodoTask");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const target = await mdtodoTaskLocator(command);
|
||||
await target.locator.waitFor({ state: "visible", timeout: 15000 });
|
||||
const toggle = target.locator.locator('[data-testid="mdtodo-task-toggle"], [data-testid="mdtodo-task-expand"], [data-action="toggle-task"], button[aria-expanded]').first();
|
||||
const toggleVisible = await visibleLocator(toggle);
|
||||
if (toggleVisible) await toggle.click();
|
||||
else await target.locator.click();
|
||||
await page.waitForTimeout(700);
|
||||
return {
|
||||
beforeUrl,
|
||||
afterUrl: currentPageUrl(),
|
||||
type: "expandMdtodoTask",
|
||||
selector: target.selector,
|
||||
toggleVisible,
|
||||
beforeProject,
|
||||
afterProject: await projectManagementCommandSnapshot(),
|
||||
pageId,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function openMdtodoSourceConfig(command) {
|
||||
ensureProjectManagementCommand("openMdtodoSourceConfig");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const button = page.locator('[data-testid="mdtodo-source-config-open"]').first();
|
||||
await button.waitFor({ state: "visible", timeout: 15000 });
|
||||
await button.click();
|
||||
await page.locator('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]').first().waitFor({ state: "visible", timeout: 10000 }).catch(() => null);
|
||||
return {
|
||||
beforeUrl,
|
||||
afterUrl: currentPageUrl(),
|
||||
type: "openMdtodoSourceConfig",
|
||||
beforeProject,
|
||||
afterProject: await projectManagementCommandSnapshot(),
|
||||
pageId,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureMdtodoSourceConfigOpen() {
|
||||
const form = page.locator('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-form-node"], [data-testid="mdtodo-source-form-root"]').first();
|
||||
if (await visibleLocator(form)) return { opened: false };
|
||||
await openMdtodoSourceConfig({ type: "openMdtodoSourceConfig" });
|
||||
return { opened: true };
|
||||
}
|
||||
|
||||
async function fillMdtodoField(testId, value) {
|
||||
if (typeof value !== "string" || !value.trim()) return { testId, filled: false };
|
||||
const locator = page.locator('[data-testid="' + cssEscape(testId) + '"]').first();
|
||||
await locator.waitFor({ state: "visible", timeout: 10000 });
|
||||
await locator.fill(value);
|
||||
return { testId, filled: true, value: opaqueIdSummary(value), valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function clickProjectButtonAndMaybeWait(testId, pathPattern) {
|
||||
const button = page.locator('[data-testid="' + cssEscape(testId) + '"]').first();
|
||||
await button.waitFor({ state: "visible", timeout: 15000 });
|
||||
const responsePromise = pathPattern ? page.waitForResponse((response) => {
|
||||
try {
|
||||
const pathname = new URL(response.url()).pathname;
|
||||
return pathPattern.test(pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 15000 }).then((response) => ({ observed: true, status: response.status(), path: new URL(response.url()).pathname })).catch((error) => ({ observed: false, waitError: errorSummary(error) })) : Promise.resolve(null);
|
||||
const buttonState = await button.evaluate((element) => ({ disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true", testId: element.getAttribute("data-testid") || null })).catch((error) => ({ disabled: null, error: errorSummary(error) }));
|
||||
if (buttonState.disabled === true) {
|
||||
const error = new Error(testId + " button is disabled");
|
||||
error.details = { buttonState, valuesRedacted: true };
|
||||
throw error;
|
||||
}
|
||||
await button.click();
|
||||
const response = await responsePromise;
|
||||
await page.waitForTimeout(900);
|
||||
return { buttonState, response, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function configureMdtodoHwpodSource(command) {
|
||||
ensureProjectManagementCommand("configureMdtodoHwpodSource");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const dialog = await ensureMdtodoSourceConfigOpen();
|
||||
const fields = [
|
||||
await fillMdtodoField("mdtodo-source-form-hwpod", commandValue(command, ["hwpodId", "hwpod", "value"])),
|
||||
await fillMdtodoField("mdtodo-source-form-node", commandValue(command, ["nodeId", "node"])),
|
||||
await fillMdtodoField("mdtodo-source-form-root", commandValue(command, ["root", "path"])),
|
||||
];
|
||||
const save = await clickProjectButtonAndMaybeWait("mdtodo-source-save", /^\/v1\/project-management\/mdtodo\/sources/u);
|
||||
return {
|
||||
beforeUrl,
|
||||
afterUrl: currentPageUrl(),
|
||||
type: "configureMdtodoHwpodSource",
|
||||
dialog,
|
||||
fields,
|
||||
save,
|
||||
beforeProject,
|
||||
afterProject: await projectManagementCommandSnapshot(),
|
||||
pageId,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
async function probeMdtodoSource(command) {
|
||||
ensureProjectManagementCommand("probeMdtodoSource");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
await ensureMdtodoSourceConfigOpen();
|
||||
const probe = await clickProjectButtonAndMaybeWait("mdtodo-source-probe", /^\/v1\/project-management\/mdtodo\/sources/u);
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), type: "probeMdtodoSource", probe, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function reindexMdtodoSource(command) {
|
||||
ensureProjectManagementCommand("reindexMdtodoSource");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
await ensureMdtodoSourceConfigOpen();
|
||||
const reindex = await clickProjectButtonAndMaybeWait("mdtodo-source-reindex", /^\/v1\/project-management\/mdtodo\/sources/u);
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), type: "reindexMdtodoSource", reindex, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function selectTaskIfCommandTargetsOne(command) {
|
||||
if (commandValue(command, ["taskRef", "taskId", "task"]).length === 0) return null;
|
||||
return selectMdtodoTask(command);
|
||||
}
|
||||
|
||||
async function saveMdtodoTaskWithButton(command, type, testId, fields) {
|
||||
ensureProjectManagementCommand(type);
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const selection = await selectTaskIfCommandTargetsOne(command);
|
||||
for (const field of fields) {
|
||||
if (field.kind === "fill") await fillMdtodoField(field.testId, field.value);
|
||||
if (field.kind === "select") {
|
||||
const locator = page.locator('[data-testid="' + cssEscape(field.testId) + '"]').first();
|
||||
await locator.waitFor({ state: "visible", timeout: 10000 });
|
||||
await selectHtmlOptionByValueOrLabel(locator, field.value);
|
||||
}
|
||||
}
|
||||
const save = await clickProjectButtonAndMaybeWait(testId, /^\/v1\/project-management\/mdtodo\/tasks/u);
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), type, selection, save, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function editMdtodoTaskTitle(command) {
|
||||
const title = commandValue(command, ["title", "text", "value"]);
|
||||
if (!title) throw new Error("editMdtodoTaskTitle requires --title or --text");
|
||||
return saveMdtodoTaskWithButton(command, "editMdtodoTaskTitle", "mdtodo-edit-save", [{ kind: "fill", testId: "mdtodo-edit-title", value: title }]);
|
||||
}
|
||||
|
||||
async function editMdtodoTaskBody(command) {
|
||||
const body = commandValue(command, ["text", "body", "value"]);
|
||||
if (!body) throw new Error("editMdtodoTaskBody requires --text or --text-stdin");
|
||||
return saveMdtodoTaskWithButton(command, "editMdtodoTaskBody", "mdtodo-edit-body-save", [{ kind: "fill", testId: "mdtodo-edit-body", value: body }]);
|
||||
}
|
||||
|
||||
async function toggleMdtodoTaskStatus(command) {
|
||||
const status = commandValue(command, ["status", "value", "text"]);
|
||||
if (!status) throw new Error("toggleMdtodoTaskStatus requires --status");
|
||||
return saveMdtodoTaskWithButton(command, "toggleMdtodoTaskStatus", "mdtodo-edit-save", [{ kind: "select", testId: "mdtodo-edit-status", value: status }]);
|
||||
}
|
||||
|
||||
async function fillNewTaskDraft(command) {
|
||||
const title = commandValue(command, ["title", "text", "value"]);
|
||||
if (!title) throw new Error(command.type + " requires --title or --text");
|
||||
const fields = [await fillMdtodoField("mdtodo-new-title", title)];
|
||||
const body = commandValue(command, ["body"]) || (command.title ? commandValue(command, ["text"]) : "");
|
||||
if (body) fields.push(await fillMdtodoField("mdtodo-new-body", body));
|
||||
return fields;
|
||||
}
|
||||
|
||||
async function addMdtodoTaskWithButton(command, type, testId) {
|
||||
ensureProjectManagementCommand(type);
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const selection = type === "addMdtodoRootTask" ? null : await selectTaskIfCommandTargetsOne(command);
|
||||
const fields = await fillNewTaskDraft(command);
|
||||
const save = await clickProjectButtonAndMaybeWait(testId, /^\/v1\/project-management\/mdtodo\/tasks/u);
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), type, selection, fields, save, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function addMdtodoRootTask(command) {
|
||||
return addMdtodoTaskWithButton(command, "addMdtodoRootTask", "mdtodo-add-root");
|
||||
}
|
||||
|
||||
async function addMdtodoSubTask(command) {
|
||||
return addMdtodoTaskWithButton(command, "addMdtodoSubTask", "mdtodo-add-subtask");
|
||||
}
|
||||
|
||||
async function continueMdtodoTask(command) {
|
||||
return addMdtodoTaskWithButton(command, "continueMdtodoTask", "mdtodo-continue-task");
|
||||
}
|
||||
|
||||
async function deleteMdtodoTask(command) {
|
||||
ensureProjectManagementCommand("deleteMdtodoTask");
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot();
|
||||
const selection = await selectTaskIfCommandTargetsOne(command);
|
||||
const firstClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", null);
|
||||
let confirmClick = null;
|
||||
const confirmVisible = await visibleLocator(page.locator('[data-testid="mdtodo-delete-cancel"]').first());
|
||||
if (confirmVisible) confirmClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", /^\/v1\/project-management\/mdtodo\/tasks/u);
|
||||
return { beforeUrl, afterUrl: currentPageUrl(), type: "deleteMdtodoTask", selection, firstClick, confirmClick, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function launchWorkbenchFromTask(command) {
|
||||
ensureProjectManagementCommand("launchWorkbenchFromTask");
|
||||
const commandType = command.type === "launchWorkbenchFromMdtodo" ? "launchWorkbenchFromMdtodo" : "launchWorkbenchFromTask";
|
||||
ensureProjectManagementCommand(commandType);
|
||||
const beforeUrl = currentPageUrl();
|
||||
const beforeProject = await projectManagementCommandSnapshot({ includeRaw: true });
|
||||
const requestedTaskRef = typeof command.taskRef === "string" && command.taskRef.trim() ? command.taskRef.trim() : null;
|
||||
if (requestedTaskRef && beforeProject.selectedTaskRefRaw !== requestedTaskRef) {
|
||||
const requestedTaskId = typeof command.taskId === "string" && command.taskId.trim() ? command.taskId.trim() : null;
|
||||
if ((requestedTaskRef && beforeProject.selectedTaskRefRaw !== requestedTaskRef) || requestedTaskId) {
|
||||
await selectMdtodoTask({ ...command, taskRef: requestedTaskRef });
|
||||
}
|
||||
const projectBeforeClick = await projectManagementCommandSnapshot({ includeRaw: true });
|
||||
@@ -1638,6 +1973,10 @@ async function launchWorkbenchFromTask(command) {
|
||||
};
|
||||
}
|
||||
|
||||
async function launchWorkbenchFromMdtodo(command) {
|
||||
return launchWorkbenchFromTask({ ...command, type: "launchWorkbenchFromMdtodo" });
|
||||
}
|
||||
|
||||
async function projectManagementCommandSnapshot(options = {}) {
|
||||
const raw = await page.evaluate(() => {
|
||||
const visible = (element) => {
|
||||
@@ -1650,17 +1989,27 @@ async function projectManagementCommandSnapshot(options = {}) {
|
||||
const selectedTask = document.querySelector('[data-task-ref][data-selected="true"], [data-task-ref][aria-selected="true"], [data-task-ref].selected, [data-task-ref].is-selected');
|
||||
const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
|
||||
const selectedFile = document.querySelector('[data-file-ref][data-selected="true"], [data-file-ref][aria-selected="true"], [data-file-ref].selected, [data-file-ref].is-selected');
|
||||
const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
|
||||
const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
|
||||
const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
|
||||
const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
|
||||
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]');
|
||||
return {
|
||||
path: window.location.pathname,
|
||||
pageKind: visible(document.querySelector('[data-testid="project-management-mdtodo"]')) ? "project-management-mdtodo" : visible(document.querySelector('[data-testid="project-management-root"]')) ? "project-management-root" : null,
|
||||
sourceCount: Array.from(document.querySelectorAll('[data-source-id]')).filter(visible).length,
|
||||
fileCount: Array.from(document.querySelectorAll('[data-file-ref]')).filter(visible).length,
|
||||
sourceCount: Math.max(Array.from(document.querySelectorAll('[data-source-id]')).filter(visible).length, sourceOptionCount),
|
||||
fileCount: Math.max(Array.from(document.querySelectorAll('[data-file-ref]')).filter(visible).length, fileOptionCount),
|
||||
taskCount: Array.from(document.querySelectorAll('[data-task-ref]')).filter(visible).length,
|
||||
selectedSourceIdRaw: selectedSource?.getAttribute("data-source-id") || null,
|
||||
selectedFileRefRaw: selectedFile?.getAttribute("data-file-ref") || null,
|
||||
selectedSourceIdRaw: selectedSource?.getAttribute("data-source-id") || sourceSelect?.value || null,
|
||||
selectedFileRefRaw: selectedFile?.getAttribute("data-file-ref") || fileSelect?.value || null,
|
||||
selectedTaskRefRaw: selectedTask?.getAttribute("data-task-ref") || null,
|
||||
selectedTaskId: selectedTask?.getAttribute("data-task-id") || selectedTask?.getAttribute("data-rxx-id") || null,
|
||||
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
|
||||
sourceSelectVisible: visible(sourceSelect),
|
||||
fileSelectVisible: visible(fileSelect),
|
||||
sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
|
||||
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')),
|
||||
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
|
||||
launchButtonVisible: visible(launch),
|
||||
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
|
||||
launchButtonText: text(launch),
|
||||
@@ -2077,6 +2426,10 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
if (!configuredPath && !rootVisible && !mdtodoVisible) return null;
|
||||
const sourceItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]')).filter(visible);
|
||||
const fileItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]')).filter(visible);
|
||||
const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
|
||||
const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
|
||||
const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
|
||||
const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
|
||||
const taskItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]')).filter(visible);
|
||||
const taskCandidates = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] li, [data-testid="mdtodo-task-tree"] [role="treeitem"], [data-testid="mdtodo-task-tree"] [role="listitem"]')).filter(visible);
|
||||
const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
|
||||
@@ -2100,14 +2453,19 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
configuredPath,
|
||||
rootVisible,
|
||||
mdtodoVisible,
|
||||
sourceCount: sourceItems.length,
|
||||
fileCount: fileItems.length,
|
||||
sourceCount: Math.max(sourceItems.length, sourceOptionCount),
|
||||
fileCount: Math.max(fileItems.length, fileOptionCount),
|
||||
taskCount: taskItems.length,
|
||||
taskRefMissingCount: Math.max(0, taskCandidates.length - taskItems.length),
|
||||
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id")),
|
||||
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref")),
|
||||
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value),
|
||||
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value),
|
||||
selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")),
|
||||
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
|
||||
sourceSelectVisible: visible(sourceSelect),
|
||||
fileSelectVisible: visible(fileSelect),
|
||||
sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
|
||||
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')),
|
||||
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
|
||||
taskStatusCounts: statusCounts,
|
||||
launchButtonVisible: visible(launch),
|
||||
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
|
||||
@@ -2292,6 +2650,11 @@ function digestProjectManagement(value) {
|
||||
selectedFileRef: opaque(value.selectedFileRef),
|
||||
selectedTaskRef: opaque(value.selectedTaskRef),
|
||||
selectedTaskStatus: value.selectedTaskStatus ?? null,
|
||||
sourceSelectVisible: value.sourceSelectVisible === true,
|
||||
fileSelectVisible: value.fileSelectVisible === true,
|
||||
sourceConfigVisible: value.sourceConfigVisible === true,
|
||||
taskEditorVisible: value.taskEditorVisible === true,
|
||||
newTaskDraftVisible: value.newTaskDraftVisible === true,
|
||||
taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
|
||||
launchButtonVisible: value.launchButtonVisible === true,
|
||||
launchButtonEnabled: value.launchButtonEnabled === true,
|
||||
@@ -2398,6 +2761,15 @@ function commandInputSummary(command) {
|
||||
sourceId: opaque(command.sourceId),
|
||||
fileRef: opaque(command.fileRef),
|
||||
taskRef: opaque(command.taskRef),
|
||||
taskId: command.taskId || null,
|
||||
titleHash: command.title ? sha256Text(command.title) : null,
|
||||
titleBytes: command.title ? Buffer.byteLength(command.title) : null,
|
||||
bodyHash: command.body ? sha256Text(command.body) : null,
|
||||
bodyBytes: command.body ? Buffer.byteLength(command.body) : null,
|
||||
status: command.status || null,
|
||||
hwpodId: opaque(command.hwpodId),
|
||||
nodeId: opaque(command.nodeId),
|
||||
root: opaque(command.root),
|
||||
label: command.label ? truncate(command.label, 200) : null,
|
||||
textHash: text === null ? null : sha256Text(text),
|
||||
textBytes: text === null ? null : Buffer.byteLength(text),
|
||||
|
||||
Reference in New Issue
Block a user