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
90 lines
2.7 KiB
JavaScript
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,
|
|
},
|
|
};
|