diff --git a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java index edf1de646f..cf5acab04d 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java @@ -85,6 +85,8 @@ public WebViewEnvironment(ICoreWebView2Environment environment) { boolean inNewWindow; private boolean inEvaluate; HashMap navigations = new HashMap<>(); + /** Maps BrowserFunction index to the script ID from AddScriptToExecuteOnDocumentCreated. */ + private final Map functionScriptIds = new HashMap<>(); private boolean ignoreGotFocus; private boolean ignoreFocusIn; private String lastCustomText; @@ -1836,4 +1838,37 @@ public boolean setUrl(String url, String postData, String[] headers) { return setWebpageData(url, postData, headers, null); } +/** + * Registers the function script persistently via AddScriptToExecuteOnDocumentCreated so it is + * injected on every future document creation before any page scripts run, avoiding the race + * condition between async function injection and navigation completion. + * See issue #20. + */ +@Override +public void createFunction(BrowserFunction function) { + super.createFunction(function); + if (inCallback > 0) { + // Cannot wait for a callback result while already inside a WebView2 callback; + // the existing NavigationStarting re-injection will handle future navigations. + return; + } + String[] scriptId = new String[1]; + callAndWait(scriptId, completion -> + webViewProvider.getWebView(true).AddScriptToExecuteOnDocumentCreated( + stringToWstr(function.functionString), completion.getAddress())); + if (scriptId[0] != null) { + functionScriptIds.put(function.index, scriptId[0]); + } +} + +@Override +void deregisterFunction(BrowserFunction function) { + super.deregisterFunction(function); + String scriptId = functionScriptIds.remove(function.index); + if (scriptId != null) { + webViewProvider.getWebView(true).RemoveScriptToExecuteOnDocumentCreated( + stringToWstr(scriptId)); + } +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java index 473a3d981d..842d585f4b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java +++ b/bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java @@ -67,6 +67,10 @@ public int AddScriptToExecuteOnDocumentCreated(char[] javaScript, long handler) return COM.VtblCall(27, address, javaScript, handler); } +public int RemoveScriptToExecuteOnDocumentCreated(char[] id) { + return COM.VtblCall(28, address, id); +} + public int ExecuteScript(char[] javaScript, IUnknown handler) { return COM.VtblCall(29, address, javaScript, handler.address); } diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java index 595b49efc1..dbe78ed1da 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java @@ -2943,6 +2943,95 @@ public void completed(ProgressEvent event) { browser2.dispose(); } +/** + * Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20 + * + *

A BrowserFunction registered before a navigation must be available when the new page's + * inline scripts execute (not just after the page finishes loading). In Edge/WebView2, + * function injection via {@code execute()} is asynchronous and can race with navigation + * completion. The fix uses {@code AddScriptToExecuteOnDocumentCreated} which guarantees + * injection before any page script runs. + */ +@Test +public void test_BrowserFunction_availableBeforePageScripts_issue20() { + assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation"); + AtomicBoolean functionCalled = new AtomicBoolean(false); + AtomicBoolean pageLoadCompleted = new AtomicBoolean(false); + + new BrowserFunction(browser, "options") { + @Override + public Object function(Object[] arguments) { + functionCalled.set(true); + return null; + } + }; + + // Navigate to a page whose inline "); + + shell.open(); + assertTrue(waitForPassCondition(pageLoadCompleted::get), "Page did not finish loading"); + assertTrue(functionCalled.get(), + "BrowserFunction 'options' was not called by the page script — it was not available " + + "before page scripts ran (regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)"); +} + +/** + * Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20 + * + *

BrowserFunctions must survive page navigations. When a second Browser instance is being + * initialized concurrently, event-loop processing for the first browser can cause its async + * function-injection script to race with navigation completion, making the function undefined + * on the newly loaded page. + */ +@Test +public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() { + assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation"); + AtomicBoolean browser1FuncAvailable = new AtomicBoolean(false); + AtomicBoolean browser2FuncAvailable = new AtomicBoolean(false); + + // Use new Browser() directly (not the createBrowser() helper that waits for + // initialization) so that both browsers are initializing concurrently, replicating + // the timing described in the bug report. + Browser b1 = new Browser(shell, SWT.NONE); + b1.setUrl("about:blank"); + new BrowserFunction(b1, "options") { + @Override + public Object function(Object[] arguments) { return null; } + }; + b1.addProgressListener(completedAdapter(e -> { + try { + b1.evaluate("options();"); + browser1FuncAvailable.set(true); + } catch (SWTException ignored) {} + })); + createdBroswers.add(b1); + + // Creating a second browser forces event-loop processing that can reveal the race. + Browser b2 = new Browser(shell, SWT.NONE); + b2.setUrl("about:blank"); + new BrowserFunction(b2, "options") { + @Override + public Object function(Object[] arguments) { return null; } + }; + b2.addProgressListener(completedAdapter(e -> { + try { + b2.evaluate("options();"); + browser2FuncAvailable.set(true); + } catch (SWTException ignored) {} + })); + createdBroswers.add(b2); + + shell.open(); + assertTrue( + waitForPassCondition(() -> browser1FuncAvailable.get() && browser2FuncAvailable.get()), + "BrowserFunction must be available when page load completes on both browsers " + + "(regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)"); +} + @Test @Disabled("Too fragile on CI, Display.getDefault().post(event) does not work reliably") public void test_TabTraversalOutOfBrowser() {