fix(web-probe): 对齐新版 MDTODO DOM 合同
This commit is contained in:
@@ -7,10 +7,32 @@ import {
|
||||
} from "./hwlab-node-web-observe-runner-sampling-source";
|
||||
|
||||
test("MDTODO task tree pane gap treats the pinned facts footer as content", async () => {
|
||||
assert.match(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR, /\[data-testid="mdtodo-task-tree-facts"\]/u);
|
||||
assert.match(
|
||||
MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR,
|
||||
/\[data-testid="mdtodo-task-tree-facts"\]/u,
|
||||
);
|
||||
assert.match(
|
||||
MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR,
|
||||
/\[data-testid="mdtodo-task-page-fact"\]/u,
|
||||
);
|
||||
|
||||
const source = nodeWebObserveRunnerSamplingSource();
|
||||
assert.ok(source.includes(`measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)})`));
|
||||
assert.ok(
|
||||
source.includes(
|
||||
`measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)})`,
|
||||
),
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/semanticTaskCandidates\.length > 0 \? semanticTaskCandidates : fallbackTaskCandidates/u,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/taskCandidates\.filter\(\(candidate\) => !taskRefElement\(candidate\)\)\.length/u,
|
||||
);
|
||||
assert.doesNotMatch(source, /taskCandidates\.length - taskItems\.length/u);
|
||||
assert.match(source, /mdtodo-body-read/u);
|
||||
assert.match(source, /mdtodo-launch-workbench/u);
|
||||
|
||||
const child = Bun.spawn(["node", "--input-type=module", "--check", "-"], {
|
||||
stdin: "pipe",
|
||||
@@ -19,6 +41,9 @@ test("MDTODO task tree pane gap treats the pinned facts footer as content", asyn
|
||||
});
|
||||
child.stdin.write(source);
|
||||
child.stdin.end();
|
||||
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
||||
const [stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
]);
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export const MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR = [
|
||||
'[data-testid="mdtodo-task-tree-facts"]',
|
||||
'[data-testid="mdtodo-task-page-fact"]',
|
||||
"[data-task-ref]",
|
||||
'[role="treeitem"]',
|
||||
'[role="listitem"]',
|
||||
@@ -433,22 +434,32 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
selected: option.selected === true,
|
||||
})) : [];
|
||||
const selectedFileOption = fileOptions.find((option) => option.selected) || null;
|
||||
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 taskTree = document.querySelector('[data-testid="mdtodo-task-tree"]');
|
||||
const semanticTaskCandidates = taskTree
|
||||
? Array.from(taskTree.querySelectorAll('[role="treeitem"], [role="listitem"]')).filter(visible)
|
||||
: [];
|
||||
const fallbackTaskCandidates = taskTree
|
||||
? Array.from(taskTree.querySelectorAll("li")).filter(visible)
|
||||
: [];
|
||||
const taskCandidates = semanticTaskCandidates.length > 0 ? semanticTaskCandidates : fallbackTaskCandidates;
|
||||
const taskRefElement = (candidate) => candidate.matches("[data-task-ref]")
|
||||
? candidate
|
||||
: candidate.querySelector("[data-task-ref]");
|
||||
const taskItems = Array.from(new Set(taskCandidates.map(taskRefElement).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');
|
||||
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 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 statusCounts = {};
|
||||
for (const task of taskItems) {
|
||||
const status = task.getAttribute("data-task-status") || "unknown";
|
||||
const status = task.getAttribute("data-task-status") || task.querySelector("[data-state]")?.getAttribute("data-state") || "unknown";
|
||||
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
||||
}
|
||||
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]');
|
||||
const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]');
|
||||
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]');
|
||||
const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"], [data-testid="mdtodo-body-read"]');
|
||||
const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]');
|
||||
const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]');
|
||||
const reportLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-report-link"]')).filter(visible);
|
||||
const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({
|
||||
const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [data-testid="mdtodo-create-blocker"], .launch-blocker, [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({
|
||||
index,
|
||||
testId: element.getAttribute("data-testid"),
|
||||
role: element.getAttribute("role"),
|
||||
@@ -475,7 +486,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
};
|
||||
const paneGaps = [
|
||||
measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)}),
|
||||
measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-document-footer'),
|
||||
measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-body-read"], [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .document-header, .task-status-stack > *, .task-document-footer, .document-footer'),
|
||||
measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'),
|
||||
];
|
||||
return {
|
||||
@@ -486,17 +497,17 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
|
||||
sourceCount: Math.max(sourceItems.length, sourceOptionCount),
|
||||
fileCount: Math.max(fileItems.length, fileOptionCount),
|
||||
taskCount: taskItems.length,
|
||||
taskRefMissingCount: Math.max(0, taskCandidates.length - taskItems.length),
|
||||
taskRefMissingCount: taskCandidates.filter((candidate) => !taskRefElement(candidate)).length,
|
||||
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value),
|
||||
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value),
|
||||
selectedFileLabel: selectedFile ? trim(selectedFile.textContent || "", 180) : selectedFileOption?.label || null,
|
||||
fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 24),
|
||||
selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")),
|
||||
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
|
||||
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || selectedTask?.querySelector("[data-state]")?.getAttribute("data-state") || 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"]')),
|
||||
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"], [data-testid="mdtodo-body-editor"]')),
|
||||
taskBodyVisible: visible(bodyRendered),
|
||||
taskBodyText: visible(bodyRendered) ? trim(bodyRendered.textContent || "", 500) : "",
|
||||
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { nodeWebObserveRunnerWorkbenchSource } from "./hwlab-node-web-observe-runner-workbench-source";
|
||||
|
||||
test("MDTODO observe commands recognize the rewritten workspace contracts", async () => {
|
||||
const source = nodeWebObserveRunnerWorkbenchSource();
|
||||
|
||||
for (const testId of [
|
||||
"mdtodo-body-read",
|
||||
"mdtodo-body-editor",
|
||||
"mdtodo-body-save",
|
||||
"mdtodo-launch-workbench",
|
||||
]) {
|
||||
assert.match(source, new RegExp(testId, "u"));
|
||||
}
|
||||
for (const legacyTestId of [
|
||||
"mdtodo-body-rendered",
|
||||
"mdtodo-edit-body",
|
||||
"mdtodo-edit-body-save",
|
||||
"mdtodo-workbench-launch",
|
||||
]) {
|
||||
assert.match(source, new RegExp(legacyTestId, "u"));
|
||||
}
|
||||
|
||||
const child = Bun.spawn(["node", "--input-type=module", "--check", "-"], {
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
child.stdin.write(source);
|
||||
child.stdin.end();
|
||||
const [stderr, exitCode] = await Promise.all([
|
||||
new Response(child.stderr).text(),
|
||||
child.exited,
|
||||
]);
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
});
|
||||
@@ -764,7 +764,7 @@ async function selectMdtodoTask(command) {
|
||||
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,
|
||||
status: element.getAttribute("data-task-status") || element.querySelector("[data-state]")?.getAttribute("data-state") || null,
|
||||
selected: element.getAttribute("data-selected") === "true" || element.getAttribute("aria-selected") === "true"
|
||||
})).catch(() => ({ taskRef: target.taskRef || null, taskId: target.taskId || null, status: null }));
|
||||
if (!clicked.selected) {
|
||||
@@ -895,12 +895,22 @@ async function closeMdtodoSourceConfigIfOpen(options = {}) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function mdtodoTestIdCandidates(value) {
|
||||
return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" && item.length > 0);
|
||||
}
|
||||
|
||||
function mdtodoTestIdLocator(value) {
|
||||
const candidates = mdtodoTestIdCandidates(value);
|
||||
return page.locator(candidates.map((testId) => '[data-testid="' + cssEscape(testId) + '"]').join(", ")).first();
|
||||
}
|
||||
|
||||
async function fillMdtodoField(testId, value) {
|
||||
if (typeof value !== "string" || !value.trim()) return { testId, filled: false };
|
||||
const locator = page.locator('[data-testid="' + cssEscape(testId) + '"]').first();
|
||||
const locator = mdtodoTestIdLocator(testId);
|
||||
await locator.waitFor({ state: "visible", timeout: 10000 });
|
||||
await locator.fill(value);
|
||||
return { testId, filled: true, value: opaqueIdSummary(value), valuesRedacted: true };
|
||||
const resolvedTestId = await locator.getAttribute("data-testid").catch(() => null);
|
||||
return { testId: resolvedTestId || mdtodoTestIdCandidates(testId)[0] || null, filled: true, value: opaqueIdSummary(value), valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function clickProjectButtonAndMaybeWait(testId, pathPattern) {
|
||||
@@ -1003,7 +1013,7 @@ async function saveMdtodoTaskWithButton(command, type, testId, fields) {
|
||||
if (field.openByDblClickTestId) inlineEditors.push(await ensureMdtodoInlineEditor(field.testId, field.openByDblClickTestId));
|
||||
if (field.kind === "fill") await fillMdtodoField(field.testId, field.value);
|
||||
if (field.kind === "select") {
|
||||
const locator = page.locator('[data-testid="' + cssEscape(field.testId) + '"]').first();
|
||||
const locator = mdtodoTestIdLocator(field.testId);
|
||||
await locator.waitFor({ state: "visible", timeout: 10000 });
|
||||
await selectHtmlOptionByValueOrLabel(locator, field.value);
|
||||
}
|
||||
@@ -1023,9 +1033,9 @@ async function clickProjectButtonAndMaybeWaitAny(testIds, pathPattern) {
|
||||
}
|
||||
|
||||
async function ensureMdtodoInlineEditor(editorTestId, readTestId) {
|
||||
const editor = page.locator('[data-testid="' + cssEscape(editorTestId) + '"]').first();
|
||||
const editor = mdtodoTestIdLocator(editorTestId);
|
||||
if (await visibleLocator(editor)) return { editorTestId, readTestId, opened: false };
|
||||
const read = page.locator('[data-testid="' + cssEscape(readTestId) + '"]').first();
|
||||
const read = mdtodoTestIdLocator(readTestId);
|
||||
await read.waitFor({ state: "visible", timeout: 10000 });
|
||||
await read.dblclick();
|
||||
await editor.waitFor({ state: "visible", timeout: 10000 });
|
||||
@@ -1041,7 +1051,7 @@ async function editMdtodoTaskTitle(command) {
|
||||
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, openByDblClickTestId: "mdtodo-body-rendered" }]);
|
||||
return saveMdtodoTaskWithButton(command, "editMdtodoTaskBody", ["mdtodo-body-save", "mdtodo-edit-body-save"], [{ kind: "fill", testId: ["mdtodo-body-editor", "mdtodo-edit-body"], value: body, openByDblClickTestId: ["mdtodo-body-read", "mdtodo-body-rendered"] }]);
|
||||
}
|
||||
|
||||
async function toggleMdtodoTaskStatus(command) {
|
||||
@@ -1060,7 +1070,7 @@ async function editMdtodoTaskInline(command) {
|
||||
if (field === "body" || field === "content") {
|
||||
const body = commandValue(command, ["body", "text"]);
|
||||
if (!body) throw new Error("editMdtodoTaskInline --field body requires --body, --text, or --text-stdin");
|
||||
return saveMdtodoTaskWithButton(command, "editMdtodoTaskInline", "mdtodo-edit-body-save", [{ kind: "fill", testId: "mdtodo-edit-body", value: body, openByDblClickTestId: "mdtodo-body-rendered" }]);
|
||||
return saveMdtodoTaskWithButton(command, "editMdtodoTaskInline", ["mdtodo-body-save", "mdtodo-edit-body-save"], [{ kind: "fill", testId: ["mdtodo-body-editor", "mdtodo-edit-body"], value: body, openByDblClickTestId: ["mdtodo-body-read", "mdtodo-body-rendered"] }]);
|
||||
}
|
||||
if (field === "status") {
|
||||
const status = commandValue(command, ["status", "text"]);
|
||||
@@ -1219,7 +1229,9 @@ async function deleteMdtodoTask(command) {
|
||||
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());
|
||||
const deleteButton = page.locator('[data-testid="mdtodo-delete-task"]').first();
|
||||
const confirmVisible = await visibleLocator(page.locator('[data-testid="mdtodo-delete-cancel"]').first())
|
||||
|| await deleteButton.evaluate((element) => /确认|confirm/iu.test(String(element.textContent || ""))).catch(() => false);
|
||||
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 };
|
||||
}
|
||||
@@ -1250,7 +1262,7 @@ async function launchWorkbenchFromTask(command) {
|
||||
: null;
|
||||
const providerSelection = await selectMdtodoProviderProfileForLaunch(command);
|
||||
const projectBeforeClick = await projectManagementCommandSnapshot({ includeRaw: true });
|
||||
const button = page.locator('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]').first();
|
||||
const button = page.locator('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]').first();
|
||||
await button.waitFor({ state: "visible", timeout: 15000 });
|
||||
const buttonState = await button.evaluate((element) => ({
|
||||
disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true",
|
||||
@@ -1392,8 +1404,8 @@ async function projectManagementCommandSnapshot(options = {}) {
|
||||
selected: option.selected === true
|
||||
})) : [];
|
||||
const selectedFileOption = fileOptions.find((option) => option.selected) || null;
|
||||
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]');
|
||||
const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]');
|
||||
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]');
|
||||
const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"], [data-testid="mdtodo-body-read"]');
|
||||
const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]');
|
||||
const reportError = document.querySelector('[data-testid="mdtodo-report-error"]');
|
||||
const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]');
|
||||
@@ -1410,11 +1422,11 @@ async function projectManagementCommandSnapshot(options = {}) {
|
||||
fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 20),
|
||||
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,
|
||||
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || selectedTask?.querySelector("[data-state]")?.getAttribute("data-state") || 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"]')),
|
||||
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"], [data-testid="mdtodo-body-editor"]')),
|
||||
taskBodyVisible: visible(bodyRendered),
|
||||
taskBodyText: visible(bodyRendered) ? text(bodyRendered) : "",
|
||||
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
|
||||
@@ -1427,7 +1439,7 @@ async function projectManagementCommandSnapshot(options = {}) {
|
||||
launchButtonVisible: visible(launch),
|
||||
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
|
||||
launchButtonText: text(launch),
|
||||
blockerTexts: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [role="alert"]')).filter(visible).map(text).filter(Boolean).slice(0, 8),
|
||||
blockerTexts: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [data-testid="mdtodo-create-blocker"], .launch-blocker, [role="alert"]')).filter(visible).map(text).filter(Boolean).slice(0, 8),
|
||||
workbenchLinkCount: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible).length,
|
||||
valuesRedacted: true
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user