Skip to content

Commit c312bcf

Browse files
HeikoKlareclaude
andcommitted
[Win32][Edge] Register every BrowserFunction via
AddScriptToExecuteOnDocumentCreated Use AddScriptToExecuteOnDocumentCreated to persistently register every BrowserFunction so it is automatically injected before page scripts run on every navigation. When createFunction() is called inside a WebView2 callback, the persistent registration is deferred via asyncExec. This removes the need for the NavigationStarting fallback that manually re-injected non-persistent functions on each navigation. See #20 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4897047 commit c312bcf

3 files changed

Lines changed: 140 additions & 8 deletions

File tree

bundles/org.eclipse.swt/Eclipse SWT Browser/win32/org/eclipse/swt/browser/Edge.java

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public WebViewEnvironment(ICoreWebView2Environment environment) {
8585
boolean inNewWindow;
8686
private boolean inEvaluate;
8787
HashMap<Long, LocationEvent> navigations = new HashMap<>();
88+
/** Maps BrowserFunction index to the script ID from AddScriptToExecuteOnDocumentCreated. */
89+
private final Map<Integer, String> functionScriptIds = new HashMap<>();
8890
private boolean ignoreGotFocus;
8991
private boolean ignoreFocusIn;
9092
private String lastCustomText;
@@ -1146,14 +1148,6 @@ int handleNavigationStarting(long pView, long pArgs, boolean top) {
11461148
navigations.put(pNavId[0], event);
11471149
if (event.doit) {
11481150
settings.put_IsScriptEnabled(jsEnabledOnNextPage);
1149-
// Register browser functions in the new document.
1150-
if (!functions.isEmpty()) {
1151-
StringBuilder sb = new StringBuilder();
1152-
for (BrowserFunction function : functions.values()) {
1153-
sb.append(function.functionString);
1154-
}
1155-
execute(sb.toString());
1156-
}
11571151
} else {
11581152
args.put_Cancel(true);
11591153
}
@@ -1836,4 +1830,49 @@ public boolean setUrl(String url, String postData, String[] headers) {
18361830
return setWebpageData(url, postData, headers, null);
18371831
}
18381832

1833+
/**
1834+
* Registers the function script persistently via AddScriptToExecuteOnDocumentCreated so it is
1835+
* injected on every future document creation before any page scripts run, avoiding the race
1836+
* condition between async function injection and navigation completion.
1837+
* If called while inside a WebView2 callback, the persistent registration is deferred via
1838+
* {@link Display#asyncExec(Runnable)} so it completes once the callback returns.
1839+
* See <a href="https://github.com/eclipse-platform/eclipse.platform.swt/issues/20">issue #20</a>.
1840+
*/
1841+
@Override
1842+
public void createFunction(BrowserFunction function) {
1843+
super.createFunction(function);
1844+
int functionIndex = function.index;
1845+
String functionString = function.functionString;
1846+
if (inCallback > 0) {
1847+
// Cannot wait for a callback result while already inside a WebView2 callback;
1848+
// defer the persistent registration to after the callback completes.
1849+
browser.getDisplay().asyncExec(() -> {
1850+
if (browser.isDisposed() || !functions.containsKey(functionIndex)) return;
1851+
registerFunctionScript(functionIndex, functionString);
1852+
});
1853+
return;
1854+
}
1855+
registerFunctionScript(functionIndex, functionString);
1856+
}
1857+
1858+
private void registerFunctionScript(int functionIndex, String functionString) {
1859+
String[] scriptId = new String[1];
1860+
callAndWait(scriptId, completion ->
1861+
webViewProvider.getWebView(true).AddScriptToExecuteOnDocumentCreated(
1862+
stringToWstr(functionString), completion.getAddress()));
1863+
if (scriptId[0] != null) {
1864+
functionScriptIds.put(functionIndex, scriptId[0]);
1865+
}
1866+
}
1867+
1868+
@Override
1869+
void deregisterFunction(BrowserFunction function) {
1870+
super.deregisterFunction(function);
1871+
String scriptId = functionScriptIds.remove(function.index);
1872+
if (scriptId != null) {
1873+
webViewProvider.getWebView(true).RemoveScriptToExecuteOnDocumentCreated(
1874+
stringToWstr(scriptId));
1875+
}
1876+
}
1877+
18391878
}

bundles/org.eclipse.swt/Eclipse SWT PI/win32/org/eclipse/swt/internal/ole/win32/ICoreWebView2.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ public int AddScriptToExecuteOnDocumentCreated(char[] javaScript, long handler)
6767
return COM.VtblCall(27, address, javaScript, handler);
6868
}
6969

70+
public int RemoveScriptToExecuteOnDocumentCreated(char[] id) {
71+
return COM.VtblCall(28, address, id);
72+
}
73+
7074
public int ExecuteScript(char[] javaScript, IUnknown handler) {
7175
return COM.VtblCall(29, address, javaScript, handler.address);
7276
}

tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_browser_Browser.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,95 @@ public void completed(ProgressEvent event) {
29432943
browser2.dispose();
29442944
}
29452945

2946+
/**
2947+
* Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20
2948+
*
2949+
* <p>A BrowserFunction registered before a navigation must be available when the new page's
2950+
* inline scripts execute (not just after the page finishes loading). In Edge/WebView2,
2951+
* function injection via {@code execute()} is asynchronous and can race with navigation
2952+
* completion. The fix uses {@code AddScriptToExecuteOnDocumentCreated} which guarantees
2953+
* injection before any page script runs.
2954+
*/
2955+
@Test
2956+
public void test_BrowserFunction_availableBeforePageScripts_issue20() {
2957+
assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation");
2958+
AtomicBoolean functionCalled = new AtomicBoolean(false);
2959+
AtomicBoolean pageLoadCompleted = new AtomicBoolean(false);
2960+
2961+
new BrowserFunction(browser, "options") {
2962+
@Override
2963+
public Object function(Object[] arguments) {
2964+
functionCalled.set(true);
2965+
return null;
2966+
}
2967+
};
2968+
2969+
// Navigate to a page whose inline <script> calls options() immediately.
2970+
// With the fix, options() is injected before any page scripts via
2971+
// AddScriptToExecuteOnDocumentCreated and is therefore guaranteed to be defined.
2972+
browser.addProgressListener(completedAdapter(e -> pageLoadCompleted.set(true)));
2973+
browser.setText("<html><body><script>options();</script></body></html>");
2974+
2975+
shell.open();
2976+
assertTrue(waitForPassCondition(pageLoadCompleted::get), "Page did not finish loading");
2977+
assertTrue(functionCalled.get(),
2978+
"BrowserFunction 'options' was not called by the page script — it was not available "
2979+
+ "before page scripts ran (regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)");
2980+
}
2981+
2982+
/**
2983+
* Regression test for https://github.com/eclipse-platform/eclipse.platform.swt/issues/20
2984+
*
2985+
* <p>BrowserFunctions must survive page navigations. When a second Browser instance is being
2986+
* initialized concurrently, event-loop processing for the first browser can cause its async
2987+
* function-injection script to race with navigation completion, making the function undefined
2988+
* on the newly loaded page.
2989+
*/
2990+
@Test
2991+
public void test_BrowserFunction_availableOnLoad_concurrentInstances_issue20() {
2992+
assumeTrue(isEdge, "Race condition is specific to async Edge/WebView2 implementation");
2993+
AtomicBoolean browser1FuncAvailable = new AtomicBoolean(false);
2994+
AtomicBoolean browser2FuncAvailable = new AtomicBoolean(false);
2995+
2996+
// Use new Browser() directly (not the createBrowser() helper that waits for
2997+
// initialization) so that both browsers are initializing concurrently, replicating
2998+
// the timing described in the bug report.
2999+
Browser b1 = new Browser(shell, SWT.NONE);
3000+
b1.setUrl("about:blank");
3001+
new BrowserFunction(b1, "options") {
3002+
@Override
3003+
public Object function(Object[] arguments) { return null; }
3004+
};
3005+
b1.addProgressListener(completedAdapter(e -> {
3006+
try {
3007+
b1.evaluate("options();");
3008+
browser1FuncAvailable.set(true);
3009+
} catch (SWTException ignored) {}
3010+
}));
3011+
createdBroswers.add(b1);
3012+
3013+
// Creating a second browser forces event-loop processing that can reveal the race.
3014+
Browser b2 = new Browser(shell, SWT.NONE);
3015+
b2.setUrl("about:blank");
3016+
new BrowserFunction(b2, "options") {
3017+
@Override
3018+
public Object function(Object[] arguments) { return null; }
3019+
};
3020+
b2.addProgressListener(completedAdapter(e -> {
3021+
try {
3022+
b2.evaluate("options();");
3023+
browser2FuncAvailable.set(true);
3024+
} catch (SWTException ignored) {}
3025+
}));
3026+
createdBroswers.add(b2);
3027+
3028+
shell.open();
3029+
assertTrue(
3030+
waitForPassCondition(() -> browser1FuncAvailable.get() && browser2FuncAvailable.get()),
3031+
"BrowserFunction must be available when page load completes on both browsers "
3032+
+ "(regression of https://github.com/eclipse-platform/eclipse.platform.swt/issues/20)");
3033+
}
3034+
29463035
@Test
29473036
@Disabled("Too fragile on CI, Display.getDefault().post(event) does not work reliably")
29483037
public void test_TabTraversalOutOfBrowser() {

0 commit comments

Comments
 (0)