Trong lần chạy thật của MONA trên Chrome 156 và macOS 14.4.1 ngày 29/10/2026, visibilitychange báo “hidden” khi trang thi bị chuyển sang tab khác, thu nhỏ cửa sổ hoặc bị một cửa sổ khác che kín. Sự kiện blur của cửa sổ kêu trong cả ba trường hợp đó, kêu cả khi tiêu điểm sang một ứng dụng khác, và kêu nhầm khi thí sinh chỉ bấm vào một iframe nằm trong chính trang thi. Khi một cửa sổ khác chỉ che nửa trang, visibilitychange im lặng và chỉ còn blur; một chiếc điện thoại đặt cạnh không làm sự kiện nào kêu (suy ra từ cách hai sự kiện gắn với cửa sổ trình duyệt, MONA chưa thử bằng điện thoại thật). Vì vậy Page Visibility API chỉ là một tín hiệu để ghi lại, không đủ để kết luận gian lận.
Trang thử ghi bốn loại sự kiện kèm trạng thái của trang
Trang thử gồm một nút vào toàn màn hình, một ô nhập, một iframe và một khung nhật ký. Mỗi sự kiện in ra một dòng có giây kể từ lúc tải, tên sự kiện, document.visibilityState, document.hasFocus() và trạng thái toàn màn hình. Đây là toàn bộ file lab.html đã chạy trong các lần thử bên dưới:
<!doctype html>
<html lang="vi"><meta charset="utf-8"><title>Lab chuyển tab</title>
<body style="font:16px system-ui;margin:20px">
<button id="fs">Vào toàn màn hình</button>
<input id="ans" placeholder="Ô trả lời">
<iframe id="fr" srcdoc="<input id=x placeholder='ô trong iframe'>" style="width:260px;height:50px"></iframe>
<pre id="log" style="background:#eef;padding:8px;min-height:200px"></pre>
<script>
const t0 = performance.now();
const logEl = document.getElementById("log");
window.__log = [];
function write(name) {
const line = [
((performance.now() - t0) / 1000).toFixed(2).padStart(7) + "s",
name.padEnd(17),
"state=" + document.visibilityState,
"hasFocus=" + document.hasFocus(),
"fullscreen=" + (document.fullscreenElement ? "yes" : "no"),
].join(" ");
window.__log.push(line);
logEl.textContent += line + "\n";
}
document.addEventListener("visibilitychange", () => write("visibilitychange"));
window.addEventListener("blur", () => write("window.blur"));
window.addEventListener("focus", () => write("window.focus"));
document.addEventListener("fullscreenchange", () => write("fullscreenchange"));
document.getElementById("fs").onclick = () => document.documentElement.requestFullscreen();
write("load");
</script></body></html>
Muốn chạy lại, làm theo các bước sau:
- Lưu đoạn mã trên thành
lab.html. - Chạy
python3 -m http.server 5181trong thư mục chứa file. - Mở
http://localhost:5181/lab.htmltrong Chrome. - Làm lần lượt từng thao tác trong bảng ở phần sau, rồi đọc khung nhật ký.
Hai trường visibilityState và hasFocus() cho biết trang đang ở trạng thái nào trong ba trạng thái mà tài liệu Page Lifecycle của Chrome định nghĩa: active (đang hiện và có tiêu điểm), passive (đang hiện nhưng không có tiêu điểm) và hidden (không hiện). Hai sự kiện focus và blur chuyển trang giữa active và passive, còn visibilitychange chuyển giữa passive và hidden.
Nhật ký thật khi chuyển tab, che cửa sổ và thu nhỏ
MONA mở Chrome 156.0.8078.48 bằng hồ sơ riêng và điều khiển nó qua giao thức CDP (Chrome DevTools Protocol) thô: kích hoạt tab, mở cửa sổ mới ở vị trí và kích thước định sẵn, thu nhỏ cửa sổ. Các đoạn sau là đầu ra nguyên văn của script điều khiển, cho thao tác 1 (chuyển tab), 2 (cửa sổ đặt cạnh), 4 (che kín), 5 (Finder) và 8 (thu nhỏ):
### 1. open a new tab in the same window (it takes the foreground)
2.91s window.blur state=visible hasFocus=false fullscreen=no
2.92s visibilitychange state=hidden hasFocus=false fullscreen=no
### 2. open a second Chrome window beside the exam window (no overlap)
6.12s window.blur state=visible hasFocus=false fullscreen=no
### 4. second window covers the exam window completely
12.41s window.blur state=visible hasFocus=false fullscreen=no
13.52s visibilitychange state=hidden hasFocus=false fullscreen=no
### 5. switch to another application (Finder activated)
15.83s window.blur state=visible hasFocus=false fullscreen=no
### 8. minimize the window (CDP Browser.setWindowBounds)
46.61s window.blur state=visible hasFocus=false fullscreen=no
46.61s visibilitychange state=hidden hasFocus=false fullscreen=no
Nhật ký cho thấy ba điều. Khi chuyển tab, blur (2,91 giây) và visibilitychange sang hidden (2,92 giây) đến cách nhau 0,01 giây. Khi một cửa sổ khác che kín trang, blur đến lúc 12,41 giây nhưng hidden chỉ đến lúc 13,52 giây, chậm khoảng 1,1 giây vì Chrome phải nhận ra cửa sổ bị che. Khi khôi phục cửa sổ, focus đến lúc 14,03 giây khi visibilityState vẫn là hidden, rồi 0,09 giây sau mới có visibilitychange sang visible. Trang thi tính giờ rời tab từ visibilitychange sẽ chậm hơn mốc blur đúng khoảng đó.

lab.html ngày 29/10/2026.Nếu kiểm thử trang thi bằng Playwright, hãy kiểm lại trước khi tin kết quả. MONA chạy cùng kịch bản (chuyển tab, mở cửa sổ khác, che kín, kích hoạt Finder) qua Playwright trên Chrome có giao diện, và nhật ký không ghi một dòng blur hay visibilitychange nào, trong khi cùng thao tác qua CDP thô thì có. Nguyên nhân MONA chưa xác minh, vì vậy các số đo trong bài lấy từ lần chạy bằng CDP thô. Khi kiểm thử trang thi tự động, hãy so một lần chạy bằng tay hoặc CDP thô với lần chạy bằng công cụ.
Chuẩn chỉ cho phép hai trạng thái, và Chrome coi che nửa cửa sổ là vẫn hiện
Mục 6.2 của chuẩn HTML của WHATWG (nhóm duy trì chuẩn HTML) giao cho trình duyệt quyết định trạng thái hiển thị của hệ thống. Ví dụ trong chuẩn là cửa sổ bị thu nhỏ, tab nằm nền hoặc bộ chuyển tác vụ che trang. Tài liệu chỉ có hai giá trị, “hidden” và “visible”, và visibilitychange chỉ bắn khi giá trị thật sự đổi. Chuẩn không viết quy tắc bắt buộc nào cho cửa sổ bị che, do đó “che bao nhiêu thì tính là hidden” do từng trình duyệt chọn.

visibilitychange. WHATWG HTML Standard, chụp ngày 29/10/2026.Trang MDN (Mozilla Developer Network) về Page Visibility API liệt kê ba tình huống làm visibilitychange kích hoạt: thu nhỏ cửa sổ, chuyển sang tab khác, và tài liệu bị cửa sổ khác che hoàn toàn. Số đo của MONA khớp ba tình huống này và thêm một mốc cụ thể: trên Chrome 156 và macOS 14.4.1, cửa sổ che nửa trang thi không làm trang thành hidden (bước 3 của nhật ký, chỉ có blur). Firefox, Safari, Edge và Windows chưa được thử.

visibilitychange. MDN Web Docs, chụp ngày 29/10/2026.Blur báo nhiều hơn visibilitychange, kể cả những lần thí sinh không làm gì sai
MDN gọi blur và focus là “imperfect proxy” (đại diện chưa hoàn hảo) cho việc trang bị ẩn: chúng cho biết cửa sổ mất tiêu điểm, chưa chắc trang đã bị ẩn. Bảng trạng thái của Chrome mô tả đúng khoảng trống này, vì trang passive đang hiện nhưng không có tiêu điểm:

Trường hợp báo nhầm MONA đo được là bấm vào iframe nằm trong trang thi, chẳng hạn khung nhúng video hoặc khung chấm điểm. Dòng nhật ký có hasFocus=true và state=visible nhưng vẫn có blur:
### 6. move keyboard focus into an iframe on the same page
35.24s window.blur state=visible hasFocus=true fullscreen=no
Nếu trang thi có iframe, đừng tính mỗi blur là một lần rời trang. Kiểm document.hasFocus() ngay trong xử lý blur cũng chưa đủ, vì như dòng trên cho thấy, giá trị này vẫn là true khi tiêu điểm nằm trong iframe cùng trang. Ghi cả hai trường vào nhật ký, như trang thử đã làm, để người chấm tự phân biệt hai trường hợp.
Toàn màn hình cho biết lúc nào thoát, không cho biết thoát vì lý do gì
Nút trong lab.html gọi document.documentElement.requestFullscreen(), và fullscreenchange bắn cả lúc vào lẫn lúc ra. MDN về Fullscreen API cho biết người dùng thoát được bất cứ lúc nào bằng Esc hoặc F11, và chuyển tab hay Alt-Tab cũng thoát toàn màn hình. Nhật ký khi MONA mở một tab mới lúc trang đang toàn màn hình:
### 7. requestFullscreen() with a user gesture
38.28s fullscreenchange state=visible hasFocus=true fullscreen=yes
### 7c. new tab while in fullscreen
41.40s fullscreenchange state=visible hasFocus=true fullscreen=no
41.40s window.blur state=visible hasFocus=false fullscreen=no
41.40s visibilitychange state=hidden hasFocus=false fullscreen=no
Ba sự kiện đến cùng một mốc 41,40 giây, theo thứ tự fullscreenchange, blur, visibilitychange, trang thi nhận được cả ba và có thể ghép chúng thành một lần rời tab. Phím Esc thật thì MONA chưa thử: macOS từ chối cho script gửi phím, và lệnh Escape gửi qua CDP không thoát được toàn màn hình (bước 7b: sau lệnh vẫn là fs=True). Vì vậy chưa có số đo cho việc fullscreenchange đến một mình, không kèm blur hay visibilitychange.
Bảng các tình huống: sự kiện nào bắt được, sự kiện nào bỏ sót
Bảng dưới gộp mọi thao tác đã chạy và những tình huống chưa chạy. Cột cuối nói rõ mỗi ô lấy từ lần chạy thật, từ tài liệu hay chưa có số đo:
| Tình huống | visibilitychange |
blur |
Mức chắc chắn |
|---|---|---|---|
| Chuyển sang tab khác trong cùng cửa sổ | hidden | Có | Chạy thật, Chrome 156 |
| Thu nhỏ cửa sổ trình duyệt | hidden | Có | Chạy thật, Chrome 156 |
| Cửa sổ khác che kín trang thi | hidden, chậm khoảng 1,1 giây | Có | Chạy thật, Chrome 156 |
| Cửa sổ khác che nửa trang thi | Không | Có | Chạy thật, Chrome 156 |
| Cửa sổ khác đặt cạnh, không che | Không | Có, lúc tiêu điểm sang cửa sổ đó | Chạy thật, Chrome 156 |
| Chuyển sang ứng dụng khác (Finder) | Không, khi cửa sổ ứng dụng đó không che trang | Có | Chạy thật, Chrome 156 |
| Bấm vào iframe trong chính trang thi | Không | Có (báo nhầm, hasFocus() vẫn là true) |
Chạy thật, Chrome 156 |
| Điện thoại hay máy thứ hai đặt cạnh | Không có sự kiện nào gắn với thiết bị đó | Không có sự kiện nào gắn với thiết bị đó | Chưa thử bằng thiết bị thật; suy ra từ việc hai sự kiện chỉ gắn với cửa sổ trình duyệt |
| Chia đôi màn hình (Split View, Snap) | Chưa có số đo | Chưa có số đo | Chưa thử; gần nhất là dòng “che nửa trang” |
| Chuyển ứng dụng trên điện thoại hay máy tính bảng | hidden theo tài liệu Chrome | Chưa có số đo | Tài liệu Page Lifecycle của Chrome; chưa thử trên máy thật |
Hai dòng để lại ít dấu nhất là thiết bị thứ hai, không có sự kiện nào, và cửa sổ che nửa trang, chỉ có một blur lẻ. Mọi con số trong bảng đo trên một máy, một phiên bản Chrome và một hệ điều hành; đổi trình duyệt hoặc hệ điều hành thì phải đo lại. Với thiết bị thứ hai, nhật ký của trang thi trông giống hệt một thí sinh ngồi yên làm bài.
Đọc nhật ký thế nào để quyết định ghi hay chặn
Nếu cần một quy tắc dựa trên các số đo trên, MONA khuyên ghi mọi sự kiện kèm giờ và cho người chấm xem, thay vì tự động hủy bài. Đây là khuyến nghị của bài, rút ra từ nhật ký phía trên, chưa phải kết quả đo trên bài thi thật. Các mẫu sau xuất hiện trong nhật ký đã chạy và cách đọc từng mẫu:
| Mẫu trong nhật ký | Cách đọc |
|---|---|
blur rồi visibilitychange sang hidden cách nhau dưới 0,1 giây (2,91 và 2,92 giây) |
Rời tab hoặc thu nhỏ cửa sổ. Tín hiệu mạnh. |
blur rồi hidden cách nhau khoảng 1 giây (12,41 và 13,52 giây) |
Cửa sổ khác che kín trang thi. Tín hiệu mạnh. |
fullscreenchange sang “no”, blur, hidden cùng một mốc (41,40 giây) |
Rời tab khi đang toàn màn hình. Tín hiệu mạnh. |
blur đơn lẻ, state=visible, hasFocus=false (15,83 giây) |
Tiêu điểm sang ứng dụng hoặc cửa sổ khác mà trang vẫn hiện. Tín hiệu yếu, ghi lại để người chấm xem. |
blur đơn lẻ, state=visible, hasFocus=true (35,24 giây) |
Tiêu điểm vào iframe trong trang. Không tính là vi phạm. |
Điện thoại bên cạnh và cửa sổ che nửa trang không để lại mẫu nào ở bảng trên, vì vậy cần biện pháp khác ngoài trình duyệt, ví dụ đề thi xáo trộn theo thí sinh hoặc người coi thi. Trang phần mềm chống gian lận thi online giới thiệu các phần mềm làm việc đó, danh sách phần mềm thi trắc nghiệm online cho biết mỗi phần mềm có những tính năng nào, và bài phần mềm chấm thi trắc nghiệm nói về khâu chấm sau khi thi xong.
Hỏi đáp
Page Visibility API chạy được trên những trình duyệt nào?
MDN ghi API này ở mức “Baseline widely available” khi MONA xem trang ngày 29/10/2026, tức chạy trên các trình duyệt chính. Số đo trong bài chỉ có Chrome 156; Firefox, Safari và Edge chưa được thử.
Trên điện thoại, chuyển sang ứng dụng khác có làm trang thành hidden không?
Tài liệu Page Lifecycle của Chrome ghi chuyển ứng dụng trên di động là một lý do làm visibilitychange kích hoạt, và nói hidden là trạng thái cuối cùng mà trang còn quan sát chắc chắn được, nhất là trên di động. MONA chưa thử trên điện thoại thật, vì vậy bảng ở trên để trống ô đó.
Có cần tự động nộp bài khi blur kêu không?
Đừng dựa riêng vào blur, vì nhật ký trên cho thấy bấm vào iframe trong trang cũng làm blur kêu với hasFocus=true. Ghi sự kiện kèm giờ, đếm riêng số lần hidden, và để người chấm quyết định.
Trang thi ép thí sinh ở lại toàn màn hình được không?
Không ép được. Trong lần thử, requestFullscreen() chạy với cử chỉ người dùng giả lập qua CDP và vào được toàn màn hình, còn MDN ghi người dùng thoát bất cứ lúc nào bằng Esc hoặc F11. Trang chỉ nhận fullscreenchange sau khi việc thoát đã xảy ra.
fullscreenchange có cho biết thí sinh bấm Esc không?
Không có số đo cho Esc thật. Sự kiện chỉ nói trạng thái toàn màn hình đã đổi: lần thoát do mở tab mới trong nhật ký cũng gọi đúng sự kiện này, kèm blur và visibilitychange.
MONA LMS (LMS là hệ thống quản lý học tập, xem LMS là gì) là phần mềm quản lý trường học và trung tâm giáo dục của MONA. Trang thi đang có ô nhập hay khung nhúng khiến blur báo nhầm thì gọi hotline 1900 636 648 kèm nhật ký sự kiện của một lượt thi, để xem mẫu nào cần đếm và mẫu nào bỏ. Trang thi viết riêng cho một trường hoặc một doanh nghiệp thuộc nhóm phần mềm viết theo yêu cầu của MONA.




















































