Mở đầu — vì sao bài này quan trọng
Nếu bạn từng mở một ứng dụng ngân hàng, ví điện tử, hay app thương mại điện tử ở Việt Nam và nhấn vào nút "Thanh toán", rồi thấy màn hình chuyển sang một giao diện trông hơi khác biệt — chữ nhỏ hơn một chút, thanh cuộn hơi lạ, đôi khi có cả thanh địa chỉ URL thấp thoáng — thì rất có thể bạn vừa bước từ phần native của ứng dụng sang một WebView. WebView là một "trình duyệt thu nhỏ" được nhúng vào bên trong app native, cho phép hiển thị nội dung HTML/CSS/JavaScript ngay trong ứng dụng mà không cần mở trình duyệt ngoài.
Đây là một trong những khu vực dễ phát sinh bug nhất và cũng dễ bị QA bỏ sót nhất. Lý do là vì WebView nằm ở ranh giới giữa hai thế giới: thế giới native (Kotlin/Java trên Android, Swift/Objective-C trên iOS) và thế giới web. Công cụ test native như Appium hay Espresso "nhìn" WebView như một hộp đen, còn công cụ test web lại không biết gì về phần native bao quanh nó. Chính cái ranh giới mờ nhòe đó là nơi các lỗi nghiêm trọng ẩn nấp: mất session khi chuyển context, thanh toán treo giữa chừng, link mở sai chỗ, hay lỗ hổng bảo mật khiến JavaScript truy cập được vào dữ liệu native.
Với QA mobile ở Việt Nam, hiểu WebView không phải là kiến thức "nice-to-have" mà là bắt buộc — bởi rất nhiều luồng quan trọng nhất (thanh toán qua cổng, KYC/eKYC, điều khoản dịch vụ, khảo sát, chương trình khuyến mãi) đều được triển khai bằng WebView để team backend/marketing cập nhật nhanh mà không cần release app mới. Bài này sẽ giúp bạn nắm được cách một app hybrid vận hành, cách phát hiện WebView, cách test luồng lai giữa native và web, và những cạm bẫy đặc trưng mà bạn phải chủ động săn tìm.
Khái niệm cốt lõi
App native, web và hybrid khác nhau thế nào
Để định vị đúng chủ đề bài này, cần phân biệt ba loại kiến trúc. App native thuần viết bằng ngôn ngữ và UI toolkit của nền tảng (Jetpack Compose/UIKit), toàn bộ màn hình là native. App web thuần (mobile web/PWA) chạy hoàn toàn trong trình duyệt. App hybrid là dạng ở giữa: vỏ ngoài là app native cài từ store, nhưng một phần hoặc toàn bộ giao diện được render bằng WebView.
Trong bài này ta tập trung riêng vào phần WebView và hybrid, không lấn sang phần so sánh tổng thể Native/Web/PWA (đó là chủ đề của một bài khác).
Các dạng hybrid app thường gặp
Có ba dạng chính bạn sẽ gặp trong thực tế:
Dạng 1 — WebView container (Cordova, Ionic, legacy). Gần như toàn bộ app là một WebView khổng lồ. App chỉ là cái vỏ native mỏng, còn mọi màn hình đều là trang web đóng gói bên trong. Nhiều app doanh nghiệp Việt Nam thời kỳ 2015–2019 xây theo kiểu này để tiết kiệm chi phí — viết một lần bằng web, chạy cả iOS lẫn Android. Cầu nối giữa web và native gọi là JavaScript bridge — cho phép JavaScript gọi camera, GPS, lưu file... của thiết bị.
Dạng 2 — Native app với WebView cục bộ. App chủ yếu là native, nhưng nhúng WebView cho một vài luồng cụ thể: trang thanh toán của cổng thanh toán, trang điều khoản, banner khuyến mãi, hoặc trang hỗ trợ/FAQ. Đây là dạng phổ biến nhất hiện nay ở các app fintech và e-commerce.
Dạng 3 — React Native / Flutter với WebView. UI chính là native (do RN/Flutter render), nhưng vẫn dùng component WebView (react-native-webview, webview_flutter) cho những phần như cổng thanh toán 3D Secure, eKYC của bên thứ ba, hay nội dung do CMS quản lý. Lưu ý: chi tiết test đặc thù của RN và Flutter là chủ đề riêng của các bài sau — ở đây ta chỉ quan tâm phần WebView bên trong chúng.
Context: native context và web context
Khái niệm quan trọng nhất khi test WebView là context. Một app hybrid tồn tại nhiều context song song: một context native (thường tên là NATIVE_APP) và một hoặc nhiều context web (tên dạng WEBVIEW_com.tenapp.package trên Android, WEBVIEW_xxxx trên iOS). Công cụ tự động hóa như Appium chỉ "nhìn thấy" và tương tác được với đúng context mà nó đang đứng. Khi đứng ở native context, bạn thấy các element native nhưng bên trong WebView chỉ là một hộp trống. Khi chuyển sang web context, bạn thấy DOM HTML nhưng không thấy nút native nào.
Việc chuyển context (context switch) chính là kỹ năng cốt lõi. Nếu không chuyển đúng, test của bạn sẽ báo "không tìm thấy element" dù element hiển thị rõ ràng trên màn hình.
WebView trên Android và iOS
Trên Android, WebView dựa trên engine Chromium và có thể debug qua chrome://inspect của Chrome trên máy tính. Trên iOS, có hai loại: WKWebView (hiện đại, hiệu năng tốt, an toàn) và UIWebView (đã lỗi thời, bị Apple từ chối từ 2020). iOS WebView debug qua Safari Web Inspector. Sự khác biệt engine này khiến cùng một trang web có thể hiển thị và hành xử khác nhau giữa hai nền tảng — đó là điều bạn phải luôn kiểm tra chéo.
Tình huống thực tế
Ví dụ 1 — Ví điện tử và cú "mất session" ở cổng thanh toán
Một ví điện tử giả định tên PayViet tích hợp thanh toán thẻ quốc tế qua cổng của một ngân hàng. Luồng như sau: người dùng ở màn hình native chọn "Nạp tiền bằng thẻ", app mở WebView chứa trang 3D Secure của ngân hàng, người dùng nhập OTP, rồi ngân hàng redirect về một URL callback để app đóng WebView và hiển thị màn hình "Nạp thành công".
QA test tay thấy mọi thứ ổn. Nhưng khi lên production, tỉ lệ giao dịch "treo" tăng vọt vào giờ cao điểm. Điều tra ra: trên một số máy Android đời thấp, khi WebView tải trang OTP, hệ điều hành thu hồi bộ nhớ và hủy WebView khi người dùng chuyển sang app SMS để copy mã OTP rồi quay lại. Session trong WebView mất, callback không bao giờ được gọi, tiền đã trừ ở ngân hàng nhưng app không biết.
Bài học: WebView không bền vững khi app bị đẩy xuống background. Phải test kịch bản "rời app giữa chừng rồi quay lại" (đặc biệt trong luồng thanh toán) và kiểm tra cơ chế đối soát/xác minh trạng thái giao dịch phía server chứ không chỉ dựa vào callback trong WebView. Đây là loại bug chỉ lộ ra khi bạn cố tình phá vòng đời của app.
Ví dụ 2 — App thương mại điện tử và link "mở nhầm cửa"
Một sàn TMĐT giả định ChợNhanh có trang khuyến mãi Tết là WebView. Trong trang này có các link: link tới sản phẩm (phải mở màn hình native chi tiết sản phẩm), link tới điều khoản (mở trong chính WebView), và link tới fanpage Facebook (phải mở app Facebook hoặc trình duyệt ngoài).
Bản build mới vô tình cấu hình sai shouldOverrideUrlLoading: mọi link đều mở trong chính WebView. Kết quả: bấm vào sản phẩm thì trang chi tiết web hiện ra thay vì màn hình native (mất luôn nút "Thêm vào giỏ" native), bấm fanpage thì Facebook bị nhồi vào cái WebView bé xíu không có nút back rõ ràng, người dùng kẹt cứng phải tắt app. Đội chăm sóc khách hàng nhận hàng loạt phản ánh "app bị đơ ở trang khuyến mãi", tỉ lệ chuyển đổi của chiến dịch giảm khoảng 18% trong hai ngày đầu.
Bài học: QA phải có một ma trận điều hướng link cho mỗi WebView: mỗi loại link đi đâu, mở ở đâu (in-WebView, native screen, hay external browser/app). Đừng cho rằng "bấm link nào cũng như nhau". Đặc biệt chú ý link tel:, mailto:, intent://, deep link nội bộ và link ngoài miền.
Ví dụ 3 — App KYC và bàn phím che ô nhập trên iOS
Một app cho vay giả định VayNhanh dùng WebView của bên thứ ba cho bước eKYC (chụp CMND, nhập thông tin). Trên Android mọi thứ mượt. Trên iOS (WKWebView), khi người dùng chạm vào ô nhập số điện thoại ở nửa dưới màn hình, bàn phím bật lên che mất ô nhập và trang không tự cuộn lên, khiến người dùng gõ mù. Trên iPhone màn nhỏ (SE), nút "Tiếp tục" cũng bị bàn phím che hoàn toàn, người dùng tưởng app lỗi và bỏ ngang — tỉ lệ hoàn tất eKYC trên iOS thấp hơn Android tới 25%.
Bài học: WebView xử lý bàn phím, cuộn (scroll), và viewport khác nhau giữa iOS và Android. Luôn test nhập liệu trong WebView trên nhiều kích thước màn hình iOS, đặc biệt các ô ở nửa dưới. Đây là dạng lỗi "chỉ trên một nền tảng" rất kinh điển của hybrid app.
Hướng dẫn từng bước
Dưới đây là quy trình test một luồng WebView/hybrid một cách bài bản.
Bước 1 — Xác định đâu là WebView. Trước tiên phải biết màn hình nào là native, màn hình nào là WebView. Cách nhanh nhất: bật debug. Với Android, kết nối máy qua USB, mở Chrome trên máy tính gõ chrome://inspect, nếu màn hình app hiện lên như một trang inspect được thì đó là WebView. Với iOS, bật "Web Inspector" trong Settings > Safari > Advanced, kết nối máy, mở Safari > Develop trên Mac, thấy app trong danh sách nghĩa là có WebView. Nếu dùng Appium, gọi lệnh lấy danh sách context và xem có context nào bắt đầu bằng WEBVIEW_ không.
Bước 2 — Lập bản đồ context và luồng. Vẽ ra sơ đồ: người dùng bắt đầu ở native context nào, tại điểm nào WebView xuất hiện, bên trong WebView có những màn hình web nào, và tại điểm nào quay lại native. Ghi rõ mỗi lần chuyển context (native → web và web → native).
Bước 3 — Test tương tác cơ bản trong WebView. Kiểm tra hiển thị (layout, font, ảnh, dark mode nếu có), nhập liệu (ô text, dropdown, ngày tháng), cuộn, và các nút bấm. Test cả tiếng Việt có dấu trong ô nhập của WebView vì đây là điểm hay lỗi.
Bước 4 — Test điều hướng và JavaScript bridge. Đi qua toàn bộ ma trận link như ví dụ 2. Nếu app có bridge cho phép web gọi tính năng native (mở camera, chia sẻ, đóng WebView), test từng lệnh bridge: gọi đúng chưa, trả kết quả về web đúng chưa, xử lý khi người dùng từ chối quyền chưa.
Bước 5 — Test vòng đời và mạng. Ép các kịch bản khó: đẩy app xuống background giữa lúc WebView đang tải rồi quay lại; xoay màn hình; mất mạng giữa chừng; mạng chậm (3G). Quan sát WebView có tải lại, có mất dữ liệu form, có mất session không.
Bước 6 — Test nút Back và thoát. Trên Android, nút Back cứng phải hành xử hợp lý: đang ở trang con của WebView thì Back nên lùi trong lịch sử web, ở trang đầu thì Back nên đóng WebView về native. Kiểm tra không bị "kẹt" trong WebView.
Bước 7 — Với tự động hóa Appium: viết test chuyển context rõ ràng — lấy danh sách context, chuyển sang context web đúng, tương tác bằng selector web (CSS/XPath của DOM), rồi chuyển lại NATIVE_APP để tương tác phần native. Luôn thêm chờ (wait) cho WebView tải xong DOM trước khi tìm element web.
Lỗi thường gặp & mẹo
Quên chuyển context. Đây là lỗi số một khi tự động hóa hybrid app. Test báo "element not found" trong khi mắt thường thấy element. Nguyên nhân: bạn đang ở native context nhưng tìm element web (hoặc ngược lại). Mẹo: luôn log ra context hiện tại và danh sách context khả dụng khi debug.
WebView chưa tải xong đã tương tác. WebView tải bất đồng bộ. Nếu test thao tác ngay, DOM chưa render. Thêm chờ tường minh cho một element đặc trưng của trang web xuất hiện.
Chỉ test trên Android hoặc chỉ trên iOS. Vì hai engine WebView khác nhau, bug về layout, bàn phím, cuộn, cookie thường chỉ xuất hiện trên một nền tảng. Bắt buộc test chéo cả hai.
Bỏ qua cookie và session. WebView có kho cookie riêng, đôi khi không đồng bộ với phần native. Người dùng đã đăng nhập ở native nhưng WebView lại yêu cầu đăng nhập lại, hoặc đăng xuất ở native mà WebView vẫn giữ session cũ. Luôn test tính nhất quán đăng nhập/đăng xuất giữa hai context.
Bỏ quên bảo mật của JavaScript bridge. Nếu app bật setJavaScriptEnabled(true) và expose bridge cho mọi URL, một trang web độc hại tải trong WebView có thể gọi tính năng native. Mẹo QA: kiểm tra WebView chỉ tải các URL trong danh sách miền được phép (allowlist), không cho tải URL tùy ý, và bridge chỉ phản hồi cho miền tin cậy. (Chi tiết chuẩn bảo mật là chủ đề riêng của bài về OWASP MASVS.)
Không test khi mạng kém. WebView phụ thuộc mạng nặng hơn native. Trang trắng, spinner quay vô tận, timeout — hãy chủ động test ở điều kiện mạng chậm và mất kết nối, kiểm tra app có màn hình lỗi/thử lại tử tế không.
Mẹo cuối: Luôn hỏi đội dev "màn hình này là native hay WebView?" ngay từ đầu sprint. Biết trước sẽ giúp bạn thiết kế test đúng, chọn công cụ đúng, và không phí thời gian.
Bài tập thực hành
- Săn WebView: Chọn một app ví điện tử hoặc TMĐT bạn đang dùng (MoMo, Shopee, một app ngân hàng...). Đi qua các luồng thanh toán, khuyến mãi, điều khoản, hỗ trợ. Ghi lại màn hình nào bạn nghi là WebView và dấu hiệu nhận biết (font khác, có thanh URL thoáng qua, cuộn lạ). Nếu có máy Android, dùng
chrome://inspectđể xác nhận.
- Ma trận điều hướng link: Với một trang WebView bạn tìm được ở bài 1, liệt kê mọi link/nút trong đó và điền vào bảng: link này mở ở đâu (trong WebView / màn hình native / trình duyệt ngoài / app khác), hành vi thực tế có đúng kỳ vọng không.
- Phá vòng đời: Trong một luồng WebView (ưu tiên luồng thanh toán sandbox nếu có), thực hiện: đẩy app xuống background 30 giây rồi quay lại; xoay màn hình; bật chế độ máy bay giữa chừng rồi tắt. Ghi lại app có mất dữ liệu form, mất session, hay hiển thị lỗi tử tế không.
- (Nâng cao) Chuyển context bằng Appium: Viết một kịch bản Appium đơn giản mở app, điều hướng tới một WebView, in ra danh sách context, chuyển sang context web, đọc tiêu đề trang web, rồi chuyển về
NATIVE_APP. Mục tiêu là làm quen với thao tác context switch.
Tóm tắt
WebView là "trình duyệt nhúng" bên trong app native, và app hybrid là sự pha trộn giữa native và web ở ba dạng chính: WebView container (Cordova/Ionic/legacy), native app nhúng WebView cục bộ, và RN/Flutter dùng WebView cho các luồng như thanh toán. Khái niệm sống còn khi test là context — native context và web context tồn tại song song, và bạn phải chuyển đúng context thì mới tương tác được, đặc biệt khi tự động hóa bằng Appium.
Những rủi ro đặc trưng của WebView gồm: mất session khi app xuống background giữa luồng thanh toán, link điều hướng mở sai chỗ, bàn phím che ô nhập trên iOS, khác biệt engine Chromium (Android) và WKWebView (iOS), cookie/session không đồng bộ giữa native và web, và lỗ hổng từ JavaScript bridge. Quy trình test bài bản là: xác định WebView, lập bản đồ context, test tương tác cơ bản, test điều hướng và bridge, ép các kịch bản vòng đời và mạng, kiểm tra nút Back/thoát, và tự động hóa với context switch tường minh. Nắm vững những điều này, bạn sẽ tự tin xử lý phần ranh giới khó nhằn nhất của app mobile — nơi tiền bạc và trải nghiệm người dùng Việt Nam thường xuyên đi qua.