From 508da72c699af82eac562ed953a82b129bd750b9 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 12:44:00 +0900 Subject: [PATCH 1/8] [flutter_inappwebview] Fix onTitleChanged to fire after the initial page load The Tizen implementation only reported the page title once, right after a page finished loading. It never listened for the WebView's own title-changed notifications, so title updates made afterwards (for example by JavaScript setting document.title) were never reported to onTitleChanged. Register a "title,changed" listener on the underlying webview instance, matching the pattern already used for load and navigation events, so onTitleChanged fires whenever the title actually changes. --- .../flutter_inappwebview/tizen/src/webview.cc | 16 ++++++++++++++++ .../flutter_inappwebview/tizen/src/webview.h | 1 + 2 files changed, 17 insertions(+) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 323947569..def626cd9 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -354,6 +354,8 @@ void WebView::Dispose() { &WebView::OnNavigationPolicy); evas_object_smart_callback_del(webview_instance_, "url,changed", &WebView::OnUrlChange); + evas_object_smart_callback_del(webview_instance_, "title,changed", + &WebView::OnTitleChange); auto& ewk_view = EwkInternalApiBinding::GetInstance().view; if (ewk_view.OnJavaScriptAlert) { ewk_view.OnJavaScriptAlert(webview_instance_, nullptr, nullptr); @@ -620,6 +622,8 @@ bool WebView::InitWebView() { &WebView::OnNavigationPolicy, this); evas_object_smart_callback_add(webview_instance_, "url,changed", &WebView::OnUrlChange, this); + evas_object_smart_callback_add(webview_instance_, "title,changed", + &WebView::OnTitleChange, this); Resize(width_, height_); evas_object_show(webview_instance_); @@ -1112,6 +1116,18 @@ void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { std::make_unique(args)); } +void WebView::OnTitleChange(void* data, Evas_Object* obj, void* event_info) { + WebView* webview = static_cast(data); + const char* title = static_cast(event_info); + if (!title) { + return; + } + flutter::EncodableMap args = { + {flutter::EncodableValue("title"), flutter::EncodableValue(title)}}; + webview->webview_channel_->InvokeMethod( + "onTitleChanged", std::make_unique(args)); +} + void WebView::OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data) { FlMethodResult* result = static_cast(user_data); diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index cf42b38f0..b17925b8a 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -92,6 +92,7 @@ class WebView : public PlatformView { static void OnNavigationPolicy(void* data, Evas_Object* obj, void* event_info); static void OnUrlChange(void* data, Evas_Object* obj, void* event_info); + static void OnTitleChange(void* data, Evas_Object* obj, void* event_info); static void OnEvaluateJavaScript(Evas_Object* obj, const char* result_value, void* user_data); static Eina_Bool OnJavaScriptAlertDialog(Evas_Object* o, const char* message, From cd951fb544339999646299eabd55d713059e3e21 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:26:45 +0900 Subject: [PATCH 2/8] [flutter_inappwebview] Fix getUrl race and shouldOverrideUrlLoading round-trip on programmatic navigation OnNavigationPolicy always suspended the view and asked Dart's shouldOverrideUrlLoading whether to allow a navigation, even for navigations the app itself requested (loadUrl, goBack, reload, ...). That round-trip is meant for user/page-initiated navigation only. Also, when Dart calls stopLoading() to cancel a pending navigation, getUrl() had no way to know a cancellation happened: EWK's "url,changed" event can still fire for the cancelled URL (before or after ewk_view_stop() takes effect), so getUrl() could end up reporting a URL the app never actually finished navigating to. Fix both: - Every EWK call that starts an app-requested navigation now goes through NavigateProgrammatically(), which marks the navigation as programmatic. OnNavigationPolicy checks this flag and accepts immediately, skipping the shouldOverrideUrlLoading round-trip for it. - StopNavigation() records that the current navigation was cancelled and reverts committed_url_ to the URL snapshotted just before the navigation decision was accepted (pending_navigation_revert_url_). OnUrlChange ignores "url,changed" while a cancellation is pending, and getUrl() returns committed_url_ instead of asking EWK directly in that window. --- .../flutter_inappwebview/tizen/src/webview.cc | 130 +++++++++++++++--- .../flutter_inappwebview/tizen/src/webview.h | 13 ++ 2 files changed, 121 insertions(+), 22 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index def626cd9..2c4b7da5b 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -307,9 +307,23 @@ void WebView::StopNavigation() { if (disposed_ || !webview_instance_) { return; } + is_navigation_cancelled_ = true; + if (!pending_navigation_revert_url_.empty()) { + committed_url_ = pending_navigation_revert_url_; + } + ewk_view_resume(webview_instance_); ewk_view_stop(webview_instance_); } +bool WebView::NavigateProgrammatically(const std::function& ewk_call) { + is_programmatic_navigation_ = true; + const bool started = ewk_call(); + if (!started) { + is_programmatic_navigation_ = false; + } + return started; +} + void WebView::Dispose() { if (disposed_) { return; @@ -491,7 +505,10 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose, if (strcmp(key, "XF86Back") == 0 && !is_down) { if (ewk_view_back_possible(webview_instance_)) { - ewk_view_back(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_back(webview_instance_); + return true; + }); return true; } return false; @@ -689,7 +706,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { std::string url = std::string("file://") + res_path + "flutter_assets/" + initial_file; free(res_path); - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); return; } } @@ -701,8 +721,11 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { std::string base_url = "about:blank"; if (GetValueFromEncodableMap(initial_data, "data", &data)) { GetValueFromEncodableMap(initial_data, "baseUrl", &base_url); - ewk_view_html_string_load(webview_instance_, data.c_str(), - base_url.c_str(), nullptr); + NavigateProgrammatically([this, &data, &base_url] { + ewk_view_html_string_load(webview_instance_, data.c_str(), + base_url.c_str(), nullptr); + return true; + }); return; } } @@ -712,7 +735,10 @@ void WebView::ApplyInitialParams(const flutter::EncodableValue& params) { &url_request)) { std::string url; if (GetValueFromEncodableMap(url_request, "url", &url) && !url.empty()) { - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); } } } @@ -772,16 +798,22 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } const auto ewk_method = method == "POST" ? EWK_HTTP_METHOD_POST : EWK_HTTP_METHOD_GET; - bool ret = ewk_view_url_request_set( - webview_instance_, url.c_str(), ewk_method, ewk_headers, - body.empty() ? nullptr : reinterpret_cast(body.data())); + const bool ret = NavigateProgrammatically([&] { + return ewk_view_url_request_set( + webview_instance_, url.c_str(), ewk_method, ewk_headers, + body.empty() ? nullptr + : reinterpret_cast(body.data())); + }); eina_hash_free(ewk_headers); if (!ret) { result->Error("Operation failed", "Failed to load URL request."); return; } } else { - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); } result->Success(); } else if (method_name == "postUrl") { @@ -795,9 +827,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, if (!body.empty()) { body.push_back('\0'); } - const bool ret = ewk_view_url_request_set( - webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, - body.empty() ? nullptr : reinterpret_cast(body.data())); + const bool ret = NavigateProgrammatically([&] { + return ewk_view_url_request_set( + webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, + body.empty() ? nullptr + : reinterpret_cast(body.data())); + }); if (ret) { result->Success(); } else { @@ -810,8 +845,11 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return; } GetValueFromEncodableMap(arguments, "baseUrl", &base_url); - ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(), - nullptr); + NavigateProgrammatically([this, &data, &base_url] { + ewk_view_html_string_load(webview_instance_, data.c_str(), + base_url.c_str(), nullptr); + return true; + }); result->Success(); } else if (method_name == "loadFile") { std::string file_path; @@ -831,7 +869,10 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, url = std::string("file://") + res_path + "flutter_assets/" + file_path; free(res_path); } - ewk_view_url_set(webview_instance_, url.c_str()); + NavigateProgrammatically([this, &url] { + ewk_view_url_set(webview_instance_, url.c_str()); + return true; + }); result->Success(); } else if (method_name == "canGoBack") { result->Success(flutter::EncodableValue( @@ -840,18 +881,31 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, result->Success(flutter::EncodableValue( static_cast(ewk_view_forward_possible(webview_instance_)))); } else if (method_name == "goBack") { - ewk_view_back(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_back(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "goForward") { - ewk_view_forward(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_forward(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "reload") { - ewk_view_reload(webview_instance_); + NavigateProgrammatically([this] { + ewk_view_reload(webview_instance_); + return true; + }); result->Success(); } else if (method_name == "getUrl") { - const char* url = ewk_view_url_get(webview_instance_); - result->Success(url ? flutter::EncodableValue(url) - : flutter::EncodableValue()); + if (is_navigation_cancelled_ && !committed_url_.empty()) { + result->Success(flutter::EncodableValue(committed_url_)); + } else { + const char* url = ewk_view_url_get(webview_instance_); + result->Success(url ? flutter::EncodableValue(url) + : flutter::EncodableValue()); + } } else if (method_name == "getTitle") { const char* title = ewk_view_title_get(webview_instance_); result->Success(title ? flutter::EncodableValue(std::string(title)) @@ -1007,6 +1061,7 @@ void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1016,6 +1071,7 @@ void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadFinished(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1044,6 +1100,7 @@ void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + webview->is_programmatic_navigation_ = false; Ewk_Error* error = static_cast(event_info); std::string url = ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : ""; @@ -1084,6 +1141,26 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, WebView* webview = static_cast(data); Ewk_Policy_Decision* policy_decision = static_cast(event_info); + + // A new navigation decision means any previous cancellation is stale: + // getUrl() should stop overriding with the old committed_url_ snapshot. + webview->is_navigation_cancelled_ = false; + + if (webview->is_programmatic_navigation_) { + webview->is_programmatic_navigation_ = false; + ewk_policy_decision_use(policy_decision); + return; + } + + // Snapshot the URL EWK is displaying before accepting the navigation + // below. EWK can fire "url,changed" for the new (possibly-to-be-cancelled) + // URL as soon as ewk_policy_decision_use() runs, racing with the async + // shouldOverrideUrlLoading round-trip. StopNavigation() reverts getUrl() + // using this snapshot rather than whatever "url,changed" reported last, so + // that race can't leave getUrl() stuck on a cancelled URL. + const std::string url_before_navigation = + GetViewUrl(webview->webview_instance_); + // Always accept the navigation on its original frame so iframe loads stay // in their iframe. The view is then suspended while we wait for the Dart // shouldOverrideUrlLoading response and either resumed (allow) or stopped @@ -1093,6 +1170,8 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, return; } + webview->pending_navigation_revert_url_ = url_before_navigation; + const char* url_cstr = ewk_policy_decision_url_get(policy_decision); const std::string url = url_cstr ? std::string(url_cstr) : std::string(); ewk_view_suspend(webview->webview_instance_); @@ -1107,9 +1186,16 @@ void WebView::OnNavigationPolicy(void* data, Evas_Object* obj, void WebView::OnUrlChange(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); + if (webview->is_navigation_cancelled_) { + // Stale "url,changed" for the navigation we just cancelled (EWK can fire + // it before or after ewk_view_stop() takes effect); getUrl() is already + // pinned to committed_url_ and must not be overwritten with this URL. + return; + } + webview->committed_url_ = GetViewUrl(webview->webview_instance_); flutter::EncodableMap args = { {flutter::EncodableValue("url"), - flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}, + flutter::EncodableValue(webview->committed_url_)}, {flutter::EncodableValue("isReload"), flutter::EncodableValue(false)}}; webview->webview_channel_->InvokeMethod( "onUpdateVisitedHistory", diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index b17925b8a..96f913389 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -83,6 +84,14 @@ class WebView : public PlatformView { bool InitWebView(); + // Runs an EWK call that starts a navigation the app itself requested + // (loadUrl, goBack, reload, ...), marking it as programmatic first so the + // next OnNavigationPolicy skips the shouldOverrideUrlLoading round-trip for + // it. If `ewk_call` reports the navigation never started, the flag is + // cleared immediately instead of leaking into some later, unrelated + // navigation. Returns whatever `ewk_call` returned. + bool NavigateProgrammatically(const std::function& ewk_call); + static void OnFrameRendered(void* data, Evas_Object* obj, void* event_info); static void OnLoadStarted(void* data, Evas_Object* obj, void* event_info); static void OnLoadFinished(void* data, Evas_Object* obj, void* event_info); @@ -128,6 +137,10 @@ class WebView : public PlatformView { bool texture_registered_ = false; bool disposed_ = false; Ewk_Mouse_Button_Type mouse_button_type_ = (Ewk_Mouse_Button_Type)0; + bool is_programmatic_navigation_ = false; + bool is_navigation_cancelled_ = false; + std::string committed_url_; + std::string pending_navigation_revert_url_; static std::set instances_; static std::mutex instances_mutex_; From 2338d9b9ca660808c891a46a7d7e70e62052d36e Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:30:54 +0900 Subject: [PATCH 3/8] [flutter_inappwebview] Fix scrollBy/getScrollX/getScrollY returning stale position ewk_view_scroll_pos_get() right after ewk_view_scroll_set() can return the pre-scroll position because EWK applies the scroll asynchronously, so scrollBy's delta and getScrollX/getScrollY's return value were sometimes stale by one frame. Track the last requested scroll position in target_scroll_x_/y_ and use it as the source of truth until EWK's reported position catches up with it, then fall back to querying EWK directly. Reset both to -1 on navigation start/error since a new page invalidates any pending scroll target. --- .../flutter_inappwebview/tizen/src/webview.cc | 32 +++++++++++++++++-- .../flutter_inappwebview/tizen/src/webview.h | 2 ++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 2c4b7da5b..4430baa34 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -944,11 +944,19 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } if (method_name == "scrollTo") { ewk_view_scroll_set(webview_instance_, x, y); + target_scroll_x_ = x; + target_scroll_y_ = y; } else { - ewk_view_scroll_by(webview_instance_, x, y); + int32_t current_x = 0, current_y = 0; + ewk_view_scroll_pos_get(webview_instance_, ¤t_x, ¤t_y); + int32_t base_x = (target_scroll_x_ >= 0) ? target_scroll_x_ : current_x; + int32_t base_y = (target_scroll_y_ >= 0) ? target_scroll_y_ : current_y; + target_scroll_x_ = base_x + x; + target_scroll_y_ = base_y + y; + ewk_view_scroll_set(webview_instance_, target_scroll_x_, target_scroll_y_); } - int32_t new_x = 0, new_y = 0; - ewk_view_scroll_pos_get(webview_instance_, &new_x, &new_y); + int32_t new_x = target_scroll_x_; + int32_t new_y = target_scroll_y_; flutter::EncodableMap args = { {flutter::EncodableValue("x"), flutter::EncodableValue(new_x)}, {flutter::EncodableValue("y"), flutter::EncodableValue(new_y)}, @@ -959,6 +967,20 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, } else if (method_name == "getScrollX" || method_name == "getScrollY") { int32_t x = 0, y = 0; ewk_view_scroll_pos_get(webview_instance_, &x, &y); + if (target_scroll_x_ >= 0) { + if (x == target_scroll_x_) { + target_scroll_x_ = -1; + } else { + x = target_scroll_x_; + } + } + if (target_scroll_y_ >= 0) { + if (y == target_scroll_y_) { + target_scroll_y_ = -1; + } else { + y = target_scroll_y_; + } + } result->Success( flutter::EncodableValue(method_name == "getScrollX" ? x : y)); } else if (method_name == "zoomBy") { @@ -1062,6 +1084,8 @@ void WebView::OnFrameRendered(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadStarted(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); webview->is_programmatic_navigation_ = false; + webview->target_scroll_x_ = -1; + webview->target_scroll_y_ = -1; flutter::EncodableMap args = { {flutter::EncodableValue("url"), flutter::EncodableValue(GetViewUrl(webview->webview_instance_))}}; @@ -1101,6 +1125,8 @@ void WebView::OnProgress(void* data, Evas_Object* obj, void* event_info) { void WebView::OnLoadError(void* data, Evas_Object* obj, void* event_info) { WebView* webview = static_cast(data); webview->is_programmatic_navigation_ = false; + webview->target_scroll_x_ = -1; + webview->target_scroll_y_ = -1; Ewk_Error* error = static_cast(event_info); std::string url = ewk_error_url_get(error) ? std::string(ewk_error_url_get(error)) : ""; diff --git a/packages/flutter_inappwebview/tizen/src/webview.h b/packages/flutter_inappwebview/tizen/src/webview.h index 96f913389..6bfe23c59 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.h +++ b/packages/flutter_inappwebview/tizen/src/webview.h @@ -141,6 +141,8 @@ class WebView : public PlatformView { bool is_navigation_cancelled_ = false; std::string committed_url_; std::string pending_navigation_revert_url_; + int32_t target_scroll_x_ = -1; + int32_t target_scroll_y_ = -1; static std::set instances_; static std::mutex instances_mutex_; From b6158328d309b9f44f47d1cf0b8d78bca8cb992f Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 18:31:52 +0900 Subject: [PATCH 4/8] [flutter_inappwebview] Bump flutter_inappwebview_tizen to 0.2.0 --- packages/flutter_inappwebview/CHANGELOG.md | 12 ++++++++++++ packages/flutter_inappwebview/README.md | 2 +- packages/flutter_inappwebview/pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/flutter_inappwebview/CHANGELOG.md b/packages/flutter_inappwebview/CHANGELOG.md index 39d9ed658..e2de3f121 100644 --- a/packages/flutter_inappwebview/CHANGELOG.md +++ b/packages/flutter_inappwebview/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.2.0 + +- Fix `onTitleChanged` to also fire when the page's title changes after the + initial load (e.g. when JavaScript updates `document.title`), instead of + only once when loading finishes. +- Fix a race where `getUrl()` could return the URL of a navigation that was + cancelled via `shouldOverrideUrlLoading`, and skip the + `shouldOverrideUrlLoading` round-trip for app-initiated navigations + (`loadUrl`, `goBack`, `reload`, etc.). +- Fix `scrollBy`/`getScrollX`/`getScrollY` occasionally returning a stale + scroll position right after `scrollTo`/`scrollBy`. + ## 0.1.1 - Fix a crash when a webview is disposed. diff --git a/packages/flutter_inappwebview/README.md b/packages/flutter_inappwebview/README.md index 456d45fb9..6ecfa54d3 100644 --- a/packages/flutter_inappwebview/README.md +++ b/packages/flutter_inappwebview/README.md @@ -26,7 +26,7 @@ Add the internet privilege to the app manifest: ```yaml dependencies: flutter_inappwebview: ^6.1.5 - flutter_inappwebview_tizen: ^0.1.1 + flutter_inappwebview_tizen: ^0.2.0 ``` ```dart diff --git a/packages/flutter_inappwebview/pubspec.yaml b/packages/flutter_inappwebview/pubspec.yaml index b9e860a4c..88f9d2090 100644 --- a/packages/flutter_inappwebview/pubspec.yaml +++ b/packages/flutter_inappwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_inappwebview_tizen description: Tizen implementation of the flutter_inappwebview plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/flutter_inappwebview -version: 0.1.1 +version: 0.2.0 environment: sdk: ">=3.8.0 <4.0.0" From 6bb2e06498d3faed3baddbc8f6c2d492361de3f1 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 12:44:23 +0900 Subject: [PATCH 5/8] [flutter_inappwebview] Add integration tests based on upstream v6.1.5 Add Tizen-compatible test cases derived from the upstream flutter_inappwebview v6.1.5 integration test suite, covering the parts of the API that the Tizen implementation actually supports (the InAppWebView widget/controller and CookieManager.deleteAllCookies). Most of upstream's suite exercises features this plugin does not implement (in-app browser, Chrome Custom Tabs, headless webview, find interaction, service worker, proxy, tracing, process-global config, the local asset-loader server, and most Android/iOS-only settings and callbacks), so those tests don't apply here and were left out. New test cases, alongside the 4 already in the file: - getProgress reports 100 once the page finishes loading - reload reloads the currently displayed page - loadUrl navigates to a new URL - postUrl and loadUrl submit an HTTP POST request body - loadFile loads a bundled asset file - programmatic scroll updates and reports the scroll position - onScrollChanged fires when the scroll position changes - onTitleChanged fires when document.title changes - stopLoading interrupts an in-flight page load - clearAllCache completes without throwing - zoomBy triggers onZoomScaleChanged - onReceivedError reports a host lookup failure / is not raised for a successful load - setSettings applies updated webview settings The new tests reuse the file's existing local HTTP server fixture instead of upstream's live external URLs, so they stay reliable on a TV emulator or device without depending on outside network resources. A small bundled HTML asset was added for the loadFile case. Making the onTitleChanged test pass required fixing a gap in the plugin itself (separate commit): it only reported the title once, right after a page finished loading, and never listened for later title changes such as JavaScript setting document.title. Validated with `flutter-tizen drive` on a Raspberry Pi device (all 17 cases pass). flutter_inappwebview is currently marked disabled for the TV emulator profile in .github/recipe.yaml because of a separate, unrelated crash on WebView disposal there; that is out of scope for this change. The postUrl/loadUrl body assertions poll for the expected text via _waitForCondition instead of reading document.querySelector('p') immediately, since the page's DOM update after a POST/navigation isn't synchronous with the awaited call and the immediate read was occasionally flaky. --- .../assets/test_assets/load_file_test.html | 10 + .../flutter_inappwebview_test.dart | 402 +++++++++++++++++- .../flutter_inappwebview/example/pubspec.yaml | 2 + 3 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html diff --git a/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html new file mode 100644 index 000000000..bb80a8033 --- /dev/null +++ b/packages/flutter_inappwebview/example/assets/test_assets/load_file_test.html @@ -0,0 +1,10 @@ + + + + + Load file test + + +

Loaded from asset

+ + diff --git a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart index 58232c234..517cbbc71 100644 --- a/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart +++ b/packages/flutter_inappwebview/example/integration_test/flutter_inappwebview_test.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -44,11 +46,13 @@ void main() { late String firstUrl; late String secondUrl; late String blockedUrl; + late String echoPostUrl; + late String slowUrl; setUpAll(() async { server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); unawaited( - server.forEach((HttpRequest request) { + server.forEach((HttpRequest request) async { request.response.headers.contentType = ContentType.html; switch (request.uri.path) { case '/first': @@ -57,18 +61,26 @@ void main() { request.response.write(_htmlPage('Second page')); case '/blocked': request.response.write(_htmlPage('Blocked page')); + case '/echo-post': + final String body = await utf8.decoder.bind(request).join(); + request.response.write('

$body

'); + case '/slow': + await Future.delayed(const Duration(seconds: 5)); + request.response.write(_htmlPage('Slow page')); case '/favicon.ico': request.response.statusCode = HttpStatus.notFound; default: fail('unexpected request: ${request.method} ${request.uri}'); } - request.response.close(); + await request.response.close(); }), ); final String baseUrl = 'http://${server.address.address}:${server.port}'; firstUrl = '$baseUrl/first'; secondUrl = '$baseUrl/second'; blockedUrl = '$baseUrl/blocked'; + echoPostUrl = '$baseUrl/echo-post'; + slowUrl = '$baseUrl/slow'; }); tearDownAll(() => server.close(force: true)); @@ -286,16 +298,378 @@ document.cookie; ); expect(cookieAfter.toString(), isNot(contains('tizen_inappwebview=1'))); }); + + testWidgets('getProgress reports 100 once the page finishes loading', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + expect(await controller.getProgress(), 100); + }); + + testWidgets('reload reloads the currently displayed page', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + final Future reloaded = loadStops.stream.first.timeout( + const Duration(seconds: 10), + ); + await controller.reload(); + expect(await reloaded, firstUrl); + }); + + testWidgets('loadUrl navigates to a new URL', (WidgetTester tester) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + final Future secondLoad = _waitForValue( + loadStops.stream, + secondUrl, + ); + await controller.loadUrl(urlRequest: URLRequest(url: WebUri(secondUrl))); + expect(await secondLoad, secondUrl); + expect((await controller.getUrl()).toString(), secondUrl); + }); + + testWidgets('postUrl and loadUrl submit an HTTP POST request body', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future firstPost = _waitForValue( + loadStops.stream, + echoPostUrl, + ); + await controller.postUrl( + url: WebUri(echoPostUrl), + postData: Uint8List.fromList(utf8.encode('name=postUrl')), + ); + await firstPost; + expect( + await _waitForCondition( + () => controller.evaluateJavascript( + source: "document.querySelector('p')?.textContent", + ), + (Object? value) => value == 'name=postUrl', + ), + 'name=postUrl', + ); + + final Future secondPost = loadStops.stream.first.timeout( + const Duration(seconds: 10), + ); + await controller.loadUrl( + urlRequest: URLRequest( + url: WebUri(echoPostUrl), + method: 'POST', + body: Uint8List.fromList(utf8.encode('name=loadUrl')), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ), + ); + expect(await secondPost, echoPostUrl); + expect( + await _waitForCondition( + () => controller.evaluateJavascript( + source: "document.querySelector('p')?.textContent", + ), + (Object? value) => value == 'name=loadUrl', + ), + 'name=loadUrl', + ); + }); + + testWidgets('loadFile loads a bundled asset file', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + final InAppWebViewController controller = await _pumpWebView( + tester, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future fileLoaded = loadStops.stream.firstWhere( + (String url) => url.endsWith('load_file_test.html'), + ); + await controller.loadFile( + assetFilePath: 'assets/test_assets/load_file_test.html', + ); + await fileLoaded.timeout(const Duration(seconds: 10)); + + expect( + await _waitForCondition( + () => controller.evaluateJavascript(source: "document.title"), + (Object? value) => value == 'Load file test', + ), + 'Load file test', + ); + expect( + await controller.evaluateJavascript( + source: "document.querySelector('h1').textContent", + ), + 'Loaded from asset', + ); + }); + + testWidgets('programmatic scroll updates and reports the scroll position', ( + WidgetTester tester, + ) async { + final InAppWebViewController controller = await _pumpWebView(tester); + await _loadFixture(controller); + + await controller.scrollTo(x: 0, y: 0); + + const int scrollX = 30; + const int scrollY = 40; + await controller.scrollTo(x: scrollX, y: scrollY); + expect(await controller.getScrollX(), scrollX); + expect(await controller.getScrollY(), scrollY); + + await controller.scrollBy(x: scrollX, y: scrollY); + expect(await controller.getScrollX(), scrollX * 2); + expect(await controller.getScrollY(), scrollY * 2); + }); + + testWidgets('onScrollChanged fires when the scroll position changes', ( + WidgetTester tester, + ) async { + final Completer scrollChanged = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onScrollChanged: (_, int x, int y) { + if (x == 50 && y == 60 && !scrollChanged.isCompleted) { + scrollChanged.complete(); + } + }, + ); + await _loadFixture(controller); + + await controller.scrollTo(x: 50, y: 60); + await scrollChanged.future.timeout(const Duration(seconds: 10)); + }); + + testWidgets('onTitleChanged fires when document.title changes', ( + WidgetTester tester, + ) async { + final Completer titleChanged = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onTitleChanged: (_, String? title) { + if (title == 'updated title' && !titleChanged.isCompleted) { + titleChanged.complete(); + } + }, + ); + await _loadFixture(controller); + + await controller.evaluateJavascript( + source: "document.title = 'updated title';", + ); + await titleChanged.future.timeout(const Duration(seconds: 10)); + }); + + testWidgets('stopLoading interrupts an in-flight page load', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + addTearDown(loadStops.close); + + await _pumpWebView( + tester, + initialUrl: slowUrl, + onLoadStart: (InAppWebViewController controller, WebUri? url) { + controller.stopLoading(); + }, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + ); + + final Future slowLoad = _waitForValue( + loadStops.stream, + slowUrl, + timeout: const Duration(seconds: 2), + ); + await expectLater(slowLoad, throwsA(isA())); + }); + + testWidgets('clearAllCache completes without throwing', ( + WidgetTester tester, + ) async { + await expectLater( + InAppWebViewController.clearAllCache(includeDiskFiles: true), + completes, + ); + }); + + testWidgets('zoomBy triggers onZoomScaleChanged', ( + WidgetTester tester, + ) async { + final Completer zoomRatio = Completer(); + final InAppWebViewController controller = await _pumpWebView( + tester, + onZoomScaleChanged: (_, double oldScale, double newScale) { + if (!zoomRatio.isCompleted) { + zoomRatio.complete(newScale / oldScale); + } + }, + ); + await _loadFixture(controller); + + await controller.zoomBy(zoomFactor: 2); + expect(await zoomRatio.future.timeout(const Duration(seconds: 10)), 2); + }); + + testWidgets( + 'onReceivedError reports a host lookup failure for an unresolvable URL', + (WidgetTester tester) async { + final Completer receivedError = + Completer(); + + await _pumpWebView( + tester, + initialUrl: 'http://this-domain-does-not-exist.invalid/', + onReceivedError: (_, WebResourceRequest __, WebResourceError error) { + if (!receivedError.isCompleted) { + receivedError.complete(error); + } + }, + ); + + final WebResourceError error = await receivedError.future.timeout( + const Duration(seconds: 10), + ); + expect(error.type, WebResourceErrorType.HOST_LOOKUP); + }, + ); + + testWidgets('onReceivedError is not raised for a successful page load', ( + WidgetTester tester, + ) async { + final StreamController loadStops = + StreamController.broadcast(); + final Completer receivedError = Completer(); + addTearDown(loadStops.close); + + await _pumpWebView( + tester, + initialUrl: firstUrl, + onLoadStop: (_, WebUri? url) { + if (url != null) { + loadStops.add(url.toString()); + } + }, + onReceivedError: (_, WebResourceRequest __, WebResourceError ___) { + receivedError.complete(); + }, + ); + await _waitForValue(loadStops.stream, firstUrl); + + await expectLater( + receivedError.future.timeout(const Duration(seconds: 1)), + throwsA(isA()), + ); + }); + + testWidgets('setSettings applies updated webview settings', ( + WidgetTester tester, + ) async { + final InAppWebViewController controller = await _pumpWebView(tester); + await _loadFixture(controller); + + await expectLater( + controller.setSettings( + settings: InAppWebViewSettings( + javaScriptEnabled: true, + supportZoom: true, + ), + ), + completes, + ); + expect( + await controller.evaluateJavascript( + source: "document.querySelector('h1').textContent", + ), + 'Fixture Page', + ); + }); } Future _pumpWebView( WidgetTester tester, { String initialUrl = 'about:blank', InAppWebViewSettings? initialSettings, + void Function(InAppWebViewController, WebUri?)? onLoadStart, void Function(InAppWebViewController, WebUri?)? onLoadStop, void Function(InAppWebViewController, int)? onProgressChanged, void Function(InAppWebViewController, ConsoleMessage)? onConsoleMessage, void Function(InAppWebViewController, WebUri?, bool?)? onUpdateVisitedHistory, + void Function(InAppWebViewController, int, int)? onScrollChanged, + void Function(InAppWebViewController, String?)? onTitleChanged, + void Function(InAppWebViewController, double, double)? onZoomScaleChanged, + void Function(InAppWebViewController, WebResourceRequest, WebResourceError)? + onReceivedError, Future Function(InAppWebViewController, JsAlertRequest)? onJsAlert, Future Function(InAppWebViewController, JsConfirmRequest)? @@ -319,10 +693,15 @@ Future _pumpWebView( initialSettings: initialSettings, initialUrlRequest: URLRequest(url: WebUri(initialUrl)), onWebViewCreated: controllerCompleter.complete, + onLoadStart: onLoadStart, onLoadStop: onLoadStop, onProgressChanged: onProgressChanged, onConsoleMessage: onConsoleMessage, onUpdateVisitedHistory: onUpdateVisitedHistory, + onScrollChanged: onScrollChanged, + onTitleChanged: onTitleChanged, + onZoomScaleChanged: onZoomScaleChanged, + onReceivedError: onReceivedError, onJsAlert: onJsAlert, onJsConfirm: onJsConfirm, onJsPrompt: onJsPrompt, @@ -383,6 +762,25 @@ Future _waitForValue( return stream.firstWhere((T event) => event == value).timeout(timeout); } +Future _waitForCondition( + Future Function() poll, + bool Function(Object? value) isReady, { + Duration timeout = const Duration(seconds: 10), +}) async { + Object? lastResult; + final DateTime end = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(end)) { + lastResult = await poll(); + if (isReady(lastResult)) { + return lastResult; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + + throw TimeoutException('Condition not met. Last result: $lastResult'); +} + String _htmlPage(String title) { return ''' diff --git a/packages/flutter_inappwebview/example/pubspec.yaml b/packages/flutter_inappwebview/example/pubspec.yaml index ccb0a802b..0bf1815d4 100644 --- a/packages/flutter_inappwebview/example/pubspec.yaml +++ b/packages/flutter_inappwebview/example/pubspec.yaml @@ -31,3 +31,5 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/test_assets/ From ad7dd6b0c30128154f8292a1dde052c845f9b27e Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 12 Aug 2026 20:03:43 +0900 Subject: [PATCH 6/8] [flutter_inappwebview] Fix format --- packages/flutter_inappwebview/tizen/src/webview.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 4430baa34..5a79920d7 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -802,7 +802,7 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return ewk_view_url_request_set( webview_instance_, url.c_str(), ewk_method, ewk_headers, body.empty() ? nullptr - : reinterpret_cast(body.data())); + : reinterpret_cast(body.data())); }); eina_hash_free(ewk_headers); if (!ret) { @@ -830,8 +830,7 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, const bool ret = NavigateProgrammatically([&] { return ewk_view_url_request_set( webview_instance_, url.c_str(), EWK_HTTP_METHOD_POST, nullptr, - body.empty() ? nullptr - : reinterpret_cast(body.data())); + body.empty() ? nullptr : reinterpret_cast(body.data())); }); if (ret) { result->Success(); @@ -953,7 +952,8 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, int32_t base_y = (target_scroll_y_ >= 0) ? target_scroll_y_ : current_y; target_scroll_x_ = base_x + x; target_scroll_y_ = base_y + y; - ewk_view_scroll_set(webview_instance_, target_scroll_x_, target_scroll_y_); + ewk_view_scroll_set(webview_instance_, target_scroll_x_, + target_scroll_y_); } int32_t new_x = target_scroll_x_; int32_t new_y = target_scroll_y_; From e7dcbb1442fd09eae98dd65d9967377bb7264844 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 13 Aug 2026 10:32:19 +0900 Subject: [PATCH 7/8] [flutter_inappwebview] Fix stale programmatic-nav flag and scroll target goBack/goForward always reported success to NavigateProgrammatically regardless of whether ewk_view_back()/ewk_view_forward() actually had history to navigate. When called with no history, no navigation policy callback ever fires to clear is_programmatic_navigation_, so the flag leaks into the next user-initiated navigation and incorrectly skips shouldOverrideUrlLoading. Use the EWK calls' own return value instead. getScrollX/getScrollY kept substituting the requested scrollTo/scrollBy target for the actual position until they matched, to mask EWK applying scroll asynchronously. If the requested position is beyond the page's max scroll extent, EWK clamps it and the actual position never matches the target, so out-of-range coordinates were reported indefinitely. Mask only the single read immediately following a scroll instead. Found by chatgpt-codex-connector's review on PR #1083. --- .../flutter_inappwebview/tizen/src/webview.cc | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index 5a79920d7..f5d169b58 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -880,15 +880,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, result->Success(flutter::EncodableValue( static_cast(ewk_view_forward_possible(webview_instance_)))); } else if (method_name == "goBack") { - NavigateProgrammatically([this] { - ewk_view_back(webview_instance_); - return true; - }); + NavigateProgrammatically( + [this] { return static_cast(ewk_view_back(webview_instance_)); }); result->Success(); } else if (method_name == "goForward") { NavigateProgrammatically([this] { - ewk_view_forward(webview_instance_); - return true; + return static_cast(ewk_view_forward(webview_instance_)); }); result->Success(); } else if (method_name == "reload") { @@ -968,18 +965,12 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, int32_t x = 0, y = 0; ewk_view_scroll_pos_get(webview_instance_, &x, &y); if (target_scroll_x_ >= 0) { - if (x == target_scroll_x_) { - target_scroll_x_ = -1; - } else { - x = target_scroll_x_; - } + x = target_scroll_x_; + target_scroll_x_ = -1; } if (target_scroll_y_ >= 0) { - if (y == target_scroll_y_) { - target_scroll_y_ = -1; - } else { - y = target_scroll_y_; - } + y = target_scroll_y_; + target_scroll_y_ = -1; } result->Success( flutter::EncodableValue(method_name == "getScrollX" ? x : y)); From d1b459ad4643f91e5ddad856f2bca4f2191e3f5a Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 13 Aug 2026 14:31:25 +0900 Subject: [PATCH 8/8] [flutter_inappwebview] Let remote Back reach the delegate; fix stale getUrl after loadData The XF86Back (remote/hardware Back key) handler wrapped ewk_view_back() in NavigateProgrammatically, marking it as a programmatic navigation. OnNavigationPolicy takes the early-accept path for programmatic navigations and never calls shouldOverrideUrlLoading, so apps could not intercept or block a user-initiated Back-key navigation even with useShouldOverrideUrlLoading enabled. Call ewk_view_back() directly so it goes through the normal navigation-policy path, matching goBack() being the only case that should bypass the delegate. is_navigation_cancelled_ (set by StopNavigation() when a delegate cancels a navigation) is only cleared by OnNavigationPolicy. loadData() calls ewk_view_html_string_load(), which never triggers OnNavigationPolicy, so calling loadData() after a cancelled navigation left the flag stuck and getUrl() kept returning the pre-cancellation URL even though new content had loaded. Clear the flag before the html_string_load call. Found by chatgpt-codex-connector's review on PR #1083. --- packages/flutter_inappwebview/tizen/src/webview.cc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/flutter_inappwebview/tizen/src/webview.cc b/packages/flutter_inappwebview/tizen/src/webview.cc index f5d169b58..baca23f55 100644 --- a/packages/flutter_inappwebview/tizen/src/webview.cc +++ b/packages/flutter_inappwebview/tizen/src/webview.cc @@ -505,10 +505,10 @@ bool WebView::SendKey(const char* key, const char* string, const char* compose, if (strcmp(key, "XF86Back") == 0 && !is_down) { if (ewk_view_back_possible(webview_instance_)) { - NavigateProgrammatically([this] { - ewk_view_back(webview_instance_); - return true; - }); + // Not wrapped in NavigateProgrammatically: this is a user-initiated + // navigation (remote Back key), so it must still reach + // shouldOverrideUrlLoading via OnNavigationPolicy. + ewk_view_back(webview_instance_); return true; } return false; @@ -844,6 +844,10 @@ void WebView::HandleWebViewMethodCall(const FlMethodCall& method_call, return; } GetValueFromEncodableMap(arguments, "baseUrl", &base_url); + // ewk_view_html_string_load() doesn't go through OnNavigationPolicy, so + // a stale cancellation from an earlier navigation would otherwise never + // clear and getUrl() would keep returning the pre-cancellation URL. + is_navigation_cancelled_ = false; NavigateProgrammatically([this, &data, &base_url] { ewk_view_html_string_load(webview_instance_, data.c_str(), base_url.c_str(), nullptr);