assistant/tests/navigation_dom_fixture_d38.cjs

89 lines
5.6 KiB
JavaScript

// In-memory read-only DOM primitives for executing the actual observer strings.
// No browser, network, module imports in the observer context or platform state.
const vm = require('node:vm');
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', d => input += d);
process.stdin.on('end', () => {
const { code, arg, scenario = 'note', observer = 'source' } = JSON.parse(input);
class El {
constructor(tag, attrs = {}, text = '', children = [], top = 10) {
this.tagName = tag.toUpperCase(); this.attrs = attrs; this.id = attrs.id || '';
this.ownText = text; this.children = children; this.top = top;
this.scrollTop = 0; this.scrollHeight = this.clientHeight = 600;
for (const child of children) child.parentElement = this;
}
get href() { return this.attrs.href ? new URL(this.attrs.href, location.href).href : ''; }
get innerText() { return [this.ownText, ...this.children.map(c => c.innerText)].filter(Boolean).join('\n'); }
getBoundingClientRect() { return { left: 0, right: 300, top: this.top, bottom: this.top + 35, width: 300, height: 35 }; }
getAttribute(k) { return this.attrs[k] ?? null; }
matches(query) { return query.split(',').some(q => match(this, q.trim())); }
closest(query) { let node = this; while (node) { if (node.matches(query)) return node; node = node.parentElement; } return null; }
contains(node) { return node === this || this.children.some(c => c.contains(node)); }
querySelectorAll(query) { return flat(this).slice(1).filter(n => n.matches(query)); }
querySelector(query) { return this.querySelectorAll(query)[0] || null; }
}
function flat(n) { return [n, ...n.children.flatMap(flat)]; }
function atom(n, raw) {
let q = raw;
const attrs = [...q.matchAll(/\[([\w-]+)(\*=|=)?["']?([^\]"']*)["']?\]/g)];
for (const [, k, op, value] of attrs) {
const actual = n.attrs[k];
if (actual === undefined || op === '=' && actual !== value || op === '*=' && !actual.includes(value)) return false;
}
q = q.replace(/\[[^\]]+\]/g, '');
const id = q.match(/#([\w-]+)/); if (id && n.id !== id[1]) return false;
const classes = [...q.matchAll(/\.([\w-]+)/g)].map(x => x[1]);
if (classes.some(c => !(n.attrs.class || '').split(/\s+/).includes(c))) return false;
const tag = q.match(/^[A-Za-z]+/); return !tag || tag[0].toUpperCase() === n.tagName;
}
function match(n, raw) {
const parts = raw.trim().split(/\s+/); let cur = n;
if (!atom(cur, parts.pop())) return false;
while (parts.length) {
const part = parts.pop(); cur = cur.parentElement;
while (cur && !atom(cur, part)) cur = cur.parentElement;
if (!cur) return false;
}
return true;
}
const location = { href: 'https://www.xiaohongshu.com/explore/' + 'a'.repeat(24) };
const pa = 'https://www.xiaohongshu.com/user/profile/' + 'b'.repeat(24);
const pb = 'https://www.xiaohongshu.com/user/profile/' + 'c'.repeat(24);
const author = new El('a', {id:'author-avatar',href:pa,class:'avatar'}, '', [new El('img')]);
const authorName = new El('a', {id:'author-name',href:pa,class:'name'}, '正文作者');
const header = new El('div', {class:'note-header'}, '', [author, authorName]);
const desc = new El('div', {id:'detail-desc'}, '本人正在找装修公司');
const replyAuthor = new El('a', {id:'reply-author',href:pa,class:'name'}, '回复者');
const replyContent = new El('div', {id:'reply-content',class:'content'}, '回复本人需求');
const reply = new El('div', {id:'reply',class:'comment-item-sub'}, '', [new El('div',{class:'author-wrapper'},'', [replyAuthor]),replyContent]);
const commentAuthor = new El('a', {id:'comment-author',href:pb,class:'name'}, '评论作者');
const commentContent = new El('div', {id:'comment-content',class:'content'}, '评论本人的需求');
const comment = new El('div', {id:'comment',class:'comment-item','data-comment-id':'actual-comment'}, '', [new El('div',{class:'author-wrapper'},'', [commentAuthor]),commentContent, reply]);
const commentArea = new El('div', {id:'discussion',class:'comments-container'}, '', [comment]);
const counter = new El('span',{id:'comment-entry',class:'chat-wrapper'},'101');
const root = new El('div',{id:'noteContainer'},'',[header,desc,commentArea,counter]);
root.getBoundingClientRect = () => ({left:0,right:350,top:0,bottom:900,width:350,height:900});
if (scenario === 'ambiguous') {
const other = new El('a',{id:'other-avatar',href:pb,class:'avatar'},'', [new El('img')]); other.parentElement=header;header.children.push(other);
}
if (scenario === 'below_viewport') for (const el of flat(commentArea)) el.top=800;
const body = new El('body',{},'',[root]);
const html = new El('html',{},'',[body]); html.scrollHeight=900;
const document = {
body, documentElement:html, scrollingElement:html,
querySelectorAll(q) { return flat(html).filter(n => n.matches(q)); },
querySelector(q) { return this.querySelectorAll(q)[0] || null; },
createTreeWalker(el) {
const nodes=flat(el).filter(n=>n.ownText).map(n=>({nodeValue:n.ownText,parentElement:n}));let i=0;
return {nextNode(){return nodes[i++] || null;}};
},
createRange() { let node; return {selectNodeContents(n){node=n;},getClientRects(){return [node.parentElement.getBoundingClientRect()];}}; }
};
const context={document,location,URL,Set,Array,Math,JSON,NodeFilter:{SHOW_TEXT:4},innerHeight:600,innerWidth:400,
getComputedStyle(){return {display:'block',visibility:'visible',opacity:'1',overflowY:'visible'};}};
const fn=vm.runInNewContext('(' + code + ')',context,{timeout:1000});
const result=fn(arg);
process.stdout.write(JSON.stringify(result));
});