@@ -1164,6 +1164,199 @@ void test_node_filter_tree_walker_focus_navigation(webscene_engine* engine)
11641164 "NodeFilter/TreeWalker focus navigation regressed: " + result);
11651165}
11661166
1167+ void test_node_iterator_dompurify_and_filter_contracts(webscene_engine* engine)
1168+ {
1169+ const auto result = evaluate(engine, R"JS(
1170+ (() => {
1171+ document.body.innerHTML = '<main id="root">before<!--comment--><section><span>inside</span></section>after</main>';
1172+ const root = document.getElementById('root');
1173+ const mask = NodeFilter.SHOW_ELEMENT
1174+ | NodeFilter.SHOW_TEXT
1175+ | NodeFilter.SHOW_COMMENT;
1176+ let filterCalls = 0;
1177+ const iterator = document.createNodeIterator(root, mask, {
1178+ acceptNode(node) {
1179+ ++filterCalls;
1180+ return node.nodeType === Node.ELEMENT_NODE
1181+ && node.localName === 'section'
1182+ ? NodeFilter.FILTER_REJECT
1183+ : NodeFilter.FILTER_ACCEPT;
1184+ }
1185+ });
1186+ const callsAfterConstruction = filterCalls;
1187+ const forward = [];
1188+ for (let node = iterator.nextNode(); node; node = iterator.nextNode()) {
1189+ forward.push(node.nodeType === Node.ELEMENT_NODE
1190+ ? node.localName
1191+ : node.nodeType === Node.COMMENT_NODE
1192+ ? `#comment:${node.data}`
1193+ : `#text:${node.data}`);
1194+ }
1195+ const forwardCalls = filterCalls;
1196+ const endState = [
1197+ iterator.referenceNode.nodeName,
1198+ iterator.pointerBeforeReferenceNode
1199+ ];
1200+ const backwards = [
1201+ iterator.previousNode()?.nodeName ?? null,
1202+ iterator.previousNode()?.nodeName ?? null,
1203+ iterator.nextNode()?.nodeName ?? null
1204+ ];
1205+
1206+ const thrown = { marker: 'filter failure' };
1207+ const throwing = document.createNodeIterator(
1208+ root, NodeFilter.SHOW_ELEMENT, () => { throw thrown; });
1209+ let constructorDeferredException = true;
1210+ let propagatedExactException = false;
1211+ try { throwing.nextNode(); } catch (error) {
1212+ propagatedExactException = error === thrown;
1213+ }
1214+
1215+ let recursive;
1216+ let depth = 0;
1217+ recursive = document.createNodeIterator(
1218+ root,
1219+ NodeFilter.SHOW_ELEMENT,
1220+ node => {
1221+ if (recursive.referenceNode !== root && depth === 0) {
1222+ ++depth;
1223+ recursive.nextNode();
1224+ }
1225+ return NodeFilter.FILTER_ACCEPT;
1226+ });
1227+ recursive.nextNode();
1228+ recursive.nextNode();
1229+ let recursiveNext = null;
1230+ try { recursive.nextNode(); } catch (error) {
1231+ recursiveNext = error.name;
1232+ }
1233+ --depth;
1234+ let recursivePrevious = null;
1235+ try { recursive.previousNode(); } catch (error) {
1236+ recursivePrevious = error.name;
1237+ }
1238+
1239+ const functionFilter = document.createNodeIterator(
1240+ root,
1241+ NodeFilter.SHOW_ELEMENT,
1242+ node => node.localName === 'span'
1243+ ? NodeFilter.FILTER_ACCEPT
1244+ : NodeFilter.FILTER_SKIP);
1245+ const functionFiltered = [];
1246+ for (let node = functionFilter.nextNode(); node;
1247+ node = functionFilter.nextNode()) {
1248+ functionFiltered.push(node.localName);
1249+ }
1250+
1251+ const fragment = document.createDocumentFragment();
1252+ const detached = document.createElement('strong');
1253+ fragment.appendChild(detached);
1254+ const detachedIterator = document.createNodeIterator(
1255+ fragment, NodeFilter.SHOW_ALL);
1256+
1257+ let invalidRoot = false;
1258+ let invalidFilter = false;
1259+ try { document.createNodeIterator(null); } catch (error) {
1260+ invalidRoot = error instanceof TypeError;
1261+ }
1262+ try { document.createNodeIterator(root, mask, {}); } catch (error) {
1263+ invalidFilter = error instanceof TypeError;
1264+ }
1265+
1266+ return {
1267+ instance: iterator instanceof NodeIterator,
1268+ brand: iterator.toString(),
1269+ root: iterator.root === root,
1270+ whatToShow: iterator.whatToShow,
1271+ callsAfterConstruction,
1272+ forwardCalls,
1273+ forward,
1274+ endState,
1275+ backwards,
1276+ constructorDeferredException,
1277+ propagatedExactException,
1278+ recursiveNext,
1279+ recursivePrevious,
1280+ functionFiltered,
1281+ detached: [
1282+ detachedIterator.nextNode() === fragment,
1283+ detachedIterator.nextNode() === detached,
1284+ detachedIterator.nextNode()
1285+ ],
1286+ invalidRoot,
1287+ invalidFilter
1288+ };
1289+ })()
1290+ )JS", "native-node-iterator-dompurify.js");
1291+ require(
1292+ result == R"JSON({"instance":true,"brand":"[object NodeIterator]","root":true,"whatToShow":133,"callsAfterConstruction":0,"forwardCalls":7,"forward":["main","#text:before","#comment:comment","span","#text:inside","#text:after"],"endState":["#text",false],"backwards":["#text","#text","#text"],"constructorDeferredException":true,"propagatedExactException":true,"recursiveNext":"InvalidStateError","recursivePrevious":"InvalidStateError","functionFiltered":["span"],"detached":[true,true,null],"invalidRoot":true,"invalidFilter":true})JSON",
1293+ "NodeIterator DOMPurify/filter contract regressed: " + result);
1294+ }
1295+
1296+ void test_node_iterator_isolated_scaling_and_memory_gate()
1297+ {
1298+ auto engine = std::unique_ptr<webscene_engine, decltype(&webscene_engine_destroy)>{
1299+ webscene_engine_create(0), &webscene_engine_destroy};
1300+ require(engine != nullptr, "NodeIterator performance engine creation failed");
1301+
1302+ const auto result = evaluate(engine.get(), R"JS(
1303+ (() => {
1304+ const run = count => {
1305+ document.body.innerHTML = `<div id="iterator-root">${'<i></i>'.repeat(count)}</div>`;
1306+ const root = document.getElementById('iterator-root');
1307+ let filterCalls = 0;
1308+ const started = performance.now();
1309+ const iterator = document.createNodeIterator(
1310+ root,
1311+ NodeFilter.SHOW_ELEMENT,
1312+ () => {
1313+ ++filterCalls;
1314+ return NodeFilter.FILTER_ACCEPT;
1315+ });
1316+ const callsAfterConstruction = filterCalls;
1317+ let visited = 0;
1318+ while (iterator.nextNode()) ++visited;
1319+ return {
1320+ count,
1321+ visited,
1322+ filterCalls,
1323+ callsAfterConstruction,
1324+ elapsed: performance.now() - started
1325+ };
1326+ };
1327+
1328+ const small = run(10000);
1329+ const large = run(50000);
1330+ return {
1331+ smallCounts: [small.visited, small.filterCalls, small.callsAfterConstruction],
1332+ largeCounts: [large.visited, large.filterCalls, large.callsAfterConstruction],
1333+ underFiveSeconds: large.elapsed < 5000,
1334+ scalesLinearly: large.elapsed <= small.elapsed * 8 + 250
1335+ };
1336+ })()
1337+ )JS", "native-node-iterator-isolated-performance.js");
1338+
1339+ webscene_engine_memory_metrics memory{
1340+ sizeof(webscene_engine_memory_metrics)};
1341+ require(
1342+ webscene_engine_get_memory_metrics(engine.get(), &memory) != 0,
1343+ "NodeIterator performance memory metrics were unavailable");
1344+ constexpr uint64_t maximum_fixture_nodes = 70'000U;
1345+ constexpr uint64_t maximum_fixture_heap_bytes = 128U * 1024U * 1024U;
1346+ const auto bounded_memory =
1347+ memory.native_dom_node_count <= maximum_fixture_nodes
1348+ && memory.native_wrapper_handle_count <= maximum_fixture_nodes
1349+ && memory.v8_used_heap_bytes <= maximum_fixture_heap_bytes;
1350+
1351+ engine.reset();
1352+ require(
1353+ result == R"JSON({"smallCounts":[10001,10001,0],"largeCounts":[50001,50001,0],"underFiveSeconds":true,"scalesLinearly":true})JSON",
1354+ "isolated NodeIterator scaling gate failed: " + result);
1355+ require(
1356+ bounded_memory,
1357+ "isolated NodeIterator memory gate exceeded its 70,000-node/handle or 128 MiB V8 bound");
1358+ }
1359+
11671360void test_animation_frame_dispatch_is_attributed()
11681361{
11691362 auto* engine = webscene_engine_create(0);
0 commit comments