Files
crawler-plugin/app/amazon/scripts/patrol_delete/delete_confirm_modal_click.js
koko 1be28f10e9 Stabilize patrol delete automation against delayed Amazon UI
The patrol-delete flow now waits for country and listing-status controls before acting, uses the dedicated PatrolDeleteTask entry point, and includes the browser helper scripts required by the bulk-delete path. Tests cover delayed dropdown readiness, payload rejection/retry behavior, complete-draft normalization, filtered bulk deletion, and asset fallback lookup.

Constraint: Amazon listing UI exposes status controls through dynamic KAT components and delayed option rendering

Rejected: Keep row-by-row delete flow | bulk selection is the current implemented path and is covered by the added helper scripts

Confidence: high

Scope-risk: moderate

Directive: Do not remove the patrol_delete JS helper files without checking app/amazon/patrol_delete.py script loading

Tested: uv run --group dev pytest tests\\test_amazon_base.py tests\\test_patrol_delete.py

Tested: uv run python -m py_compile amazon\\base.py amazon\\main.py blueprints\\main.py

Not-tested: Real Amazon Seller Central browser session
2026-04-28 20:46:50 +08:00

90 lines
2.7 KiB
JavaScript

function textOf(node) {
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
}
function attrsText(el) {
if (!el || !el.attributes) return '';
return Array.from(el.attributes).map((attr) => `${attr.name}=${attr.value}`).join(' ');
}
function isVisible(el) {
if (!el || !el.getBoundingClientRect) return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
}
function walkRoots(root, rows, seen) {
if (!root || seen.has(root)) return;
seen.add(root);
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let node = walker.currentNode;
while (node) {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
for (const assigned of node.assignedElements({ flatten: true })) {
walkRoots(assigned, rows, seen);
}
}
node = walker.nextNode();
}
}
function allElements(root) {
const rows = [];
walkRoots(root || document, rows, new Set());
return rows;
}
function clickElement(el) {
el.scrollIntoView({ block: 'center', inline: 'nearest' });
if (el.focus) el.focus();
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
}
el.click();
}
function clickTargetFor(el) {
if (el && el.shadowRoot) {
const button = el.shadowRoot.querySelector('button:not([disabled]), [role="button"]:not([disabled])');
if (button) return button;
}
return el;
}
const modal = document.querySelector('kat-modal[data-testid="action-modal"]') ||
allElements(document).find((el) => /删除|Delete/i.test(textOf(el)) && (el.tagName || '').toLowerCase() === 'kat-modal');
if (!modal) return { ok: false, reason: 'delete modal not found' };
const buttons = allElements(modal).filter((el) => {
const haystack = `${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`;
return isVisible(el) && /kat-button|button/i.test(el.tagName || '') && /variant=primary|删除|确认|Delete|Confirm/i.test(haystack);
});
const button = buttons[0];
if (!button) {
return {
ok: false,
reason: 'confirm button not found',
modalText: textOf(modal),
modalHTML: (modal.outerHTML || '').slice(0, 2000),
};
}
const target = clickTargetFor(button);
clickElement(target);
return {
ok: true,
clicked: {
tag: button.tagName,
text: textOf(button),
attrs: attrsText(button),
targetTag: target.tagName,
},
};