@@ -1132,6 +1132,140 @@ void test_nested_pointer_coordinate_projection() {
11321132 webscene_engine_destroy (engine);
11331133}
11341134
1135+ void test_nested_context_menu_handoff () {
1136+ constexpr std::string_view address{
1137+ " https://iframe-context-menu.test/index.html" };
1138+ resource_server server{
1139+ .content = {{std::string{address},
1140+ " <!doctype html><title>frame context menu</title><body></body>" },
1141+ {" https://iframe-context-menu.test/frame.html" , R"HTML(
1142+ <!doctype html><style>html,body{margin:0}#ancestor{position:absolute;left:10px;top:8px;width:40px;height:30px}#target{display:block;width:40px;height:30px}</style>
1143+ <body data-vscode-context='{"scope":"body","shared":"body","bodyOnly":true}'>
1144+ <div id="ancestor" data-vscode-context='{"scope":"ancestor","shared":"ancestor","ancestorOnly":true}'>
1145+ <span id="target" data-vscode-context='{"shared":"target","targetOnly":true}'>target</span>
1146+ </div>
1147+ <script>
1148+ const state = parent.__nestedContextMenu;
1149+ const target = document.querySelector('#target');
1150+ target.addEventListener('contextmenu', event => event.preventDefault(), {once:true});
1151+ addEventListener('contextmenu', event => {
1152+ if (event.defaultPrevented) {
1153+ ++state.cancelled;
1154+ return;
1155+ }
1156+ event.preventDefault();
1157+ let context = {};
1158+ let element = event.target;
1159+ while (true) {
1160+ if (!element) break;
1161+ element = element.closest('[data-vscode-context]');
1162+ if (!element) break;
1163+ context = {...JSON.parse(element.dataset.vscodeContext), ...context};
1164+ element = element.parentElement;
1165+ }
1166+ state.pathCrossedDocument = event.composedPath().includes(frameElement);
1167+ parent.__receiveContextMenu({
1168+ clientX:event.clientX,
1169+ clientY:event.clientY,
1170+ context,
1171+ defaultPrevented:event.defaultPrevented
1172+ });
1173+ });
1174+ state.ready = true;
1175+ </script>
1176+ </body>)HTML" }};
1177+ webscene_engine_options options{};
1178+ options.struct_size = sizeof (options);
1179+ options.resource_load_callback = load_test_resource;
1180+ options.resource_load_user_data = &server;
1181+ auto * engine = webscene_engine_create_with_options (&options);
1182+ require (engine != nullptr , " nested context menu engine creation failed" );
1183+ require (webscene_engine_load_url (engine, address.data (), address.size ()) != 0U ,
1184+ " nested context menu outer document failed" );
1185+ require (evaluate_until_equals (
1186+ engine, " location.href" , " nested-context-menu-ready.js" ,
1187+ R"( "https://iframe-context-menu.test/index.html")" , 2'500 )
1188+ == R"( "https://iframe-context-menu.test/index.html")" ,
1189+ " nested context menu outer document did not become ready" );
1190+
1191+ execute (engine, R"JS(
1192+ globalThis.__nestedContextMenu = {
1193+ ready:false, cancelled:0, requests:[], visibleDuringHandoff:false,
1194+ pathCrossedDocument:null
1195+ };
1196+ globalThis.__receiveContextMenu = data => {
1197+ __nestedContextMenu.requests.push(data);
1198+ const body = document.querySelector('#webview').contentDocument.body;
1199+ body.classList.toggle('vscode-context-menu-visible', true);
1200+ __nestedContextMenu.visibleDuringHandoff =
1201+ body.classList.contains('vscode-context-menu-visible');
1202+ };
1203+ document.body.style.margin = '0';
1204+ const frame = document.createElement('iframe');
1205+ frame.id = 'webview';
1206+ frame.dataset.vscodeContext = JSON.stringify({
1207+ scope:'outer', outerOnly:true
1208+ });
1209+ frame.setAttribute('sandbox', 'allow-same-origin allow-scripts');
1210+ frame.style.cssText =
1211+ 'position:absolute;left:80px;top:60px;width:200px;height:120px;border:4px solid black';
1212+ frame.src = './frame.html';
1213+ document.body.appendChild(frame);
1214+ )JS" , " nested-context-menu-setup.js" );
1215+ require (evaluate_until_equals (
1216+ engine, " __nestedContextMenu.ready" ,
1217+ " nested-context-menu-frame-ready.js" , " true" , 3'000 ) == " true" ,
1218+ " nested context menu frame did not become ready: "
1219+ + evaluate (engine, " JSON.stringify(__nestedContextMenu)" ,
1220+ " nested-context-menu-timeout.js" ));
1221+
1222+ const auto secondary_click = [&](uint64_t sequence) {
1223+ const webscene_input_event down{
1224+ WEBSCENE_INPUT_POINTER_DOWN , 2U | (3U << 8U ), sequence,
1225+ 99 , 77 , 0 , 0 };
1226+ const webscene_input_event up{
1227+ WEBSCENE_INPUT_POINTER_UP , (3U << 8U ), sequence + 1U ,
1228+ 99 , 77 , 0 , 0 };
1229+ const auto consumed_before = consumed_input_count (engine);
1230+ require (webscene_engine_enqueue (engine, &down) != 0 ,
1231+ " nested context menu pointer down was rejected" );
1232+ require (webscene_engine_enqueue (engine, &up) != 0 ,
1233+ " nested context menu pointer up was rejected" );
1234+ wait_for_consumed_inputs (
1235+ engine, consumed_before + 2U ,
1236+ " nested context menu pointer inputs were not consumed" );
1237+ };
1238+
1239+ secondary_click (9501U );
1240+ require (
1241+ evaluate (engine, R"JS( JSON.stringify({
1242+ cancelled:__nestedContextMenu.cancelled,
1243+ requestCount:__nestedContextMenu.requests.length,
1244+ visible:document.querySelector('#webview').contentDocument.body.classList
1245+ .contains('vscode-context-menu-visible')
1246+ }))JS" , " nested-context-menu-cancelled.js" )
1247+ == R"( {"cancelled":1,"requestCount":0,"visible":false})" ,
1248+ " a child-cancelled context menu escaped to the Code-shaped handler" );
1249+
1250+ secondary_click (9503U );
1251+ const auto result = evaluate (engine, R"JS( JSON.stringify({
1252+ request:__nestedContextMenu.requests[0],
1253+ visibleDuringHandoff:__nestedContextMenu.visibleDuringHandoff,
1254+ pathCrossedDocument:__nestedContextMenu.pathCrossedDocument
1255+ }))JS" , " nested-context-menu-result.js" );
1256+ require (
1257+ result == R"JSON( {"request":{"clientX":15,"clientY":13,"context":{"scope":"ancestor","shared":"target","bodyOnly":true,"ancestorOnly":true,"targetOnly":true},"defaultPrevented":true},"visibleDuringHandoff":true,"pathCrossedDocument":false})JSON" ,
1258+ " nested Code-shaped context menu handoff diverged: " + result);
1259+
1260+ require (evaluate (engine, R"JS( (() => {
1261+ const body=document.querySelector('#webview').contentDocument.body;
1262+ body.classList.toggle('vscode-context-menu-visible', false);
1263+ return !body.classList.contains('vscode-context-menu-visible');
1264+ })())JS" , " nested-context-menu-hidden.js" ) == " true" ,
1265+ " context menu visibility did not clear in the child document" );
1266+ webscene_engine_destroy (engine);
1267+ }
1268+
11351269void test_nested_keyboard_handoff_and_focus_traversal () {
11361270 constexpr std::string_view address{
11371271 " https://iframe-keyboard.test/index.html" };
0 commit comments