# Phát hiện chuyển tab khi thi online: Page Visibility API làm được gì và không làm được gì

> 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

URL: https://mona.software/phat-hien-chuyen-tab-thi-online-page-visibility-api/
Ngày đăng: 2026-11-02T10:02:51+07:00
Cập nhật: 2026-11-02T10:03:03+07:00
Tác giả: Lê Minh Nhựt

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:

1. Lưu đoạn mã trên thành `lab.html`.
2. Chạy `python3 -m http.server 5181` trong thư mục chứa file.
3. Mở `http://localhost:5181/lab.html` trong Chrome.
4. 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](https://developer.chrome.com/docs/web-platform/page-lifecycle-api) đị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 đó.

![Nhật ký sự kiện thật của trang thử khi chuyển tab, đổi cửa sổ và vào toàn màn hình](https://mona.software/wp-content/uploads/2026/11/nhat-ky-su-kien-chuyen-tab-chay-that-mona.webp)

Toàn bộ 27 dòng nhật ký của hai đợt thử trên Chrome 156, chụp từ chính trang `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ụ.

![Màn hình thống kê trong phần mềm MONA làm cho The IELTS Workshop](https://mona.software/wp-content/themes/monatheme/home-v2/assets/img/cs/ielts-thongke.webp)

Phần mềm cho việc này

Quy trình đang chạy mà phần mềm có sẵn không làm được?

MONA khảo sát nghiệp vụ trước dòng code đầu tiên.

[Viết phần mềm theo yêu cầu](https://mona.software/viet-phan-mem-theo-yeu-cau/)[Xem case study](https://mona.software/#case-study)

## 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](https://html.spec.whatwg.org/multipage/interaction.html#page-visibility) (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.

![Mục 6.2 Page visibility trong chuẩn HTML của WHATWG, định nghĩa trạng thái hidden và visible](https://mona.software/wp-content/uploads/2026/11/whatwg-html-page-visibility-mona.webp)

Mục 6.2 Page visibility trong chuẩn HTML của WHATWG: hai giá trị, giá trị ban đầu là hidden, và bước bắn `visibilitychange`. WHATWG HTML Standard, chụp ngày 29/10/2026.

Trang [MDN (Mozilla Developer Network) về Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/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ử.

![Trang MDN về Page Visibility API nêu ba tình huống làm visibilitychange kích hoạt](https://mona.software/wp-content/uploads/2026/11/mdn-page-visibility-api-mona.webp)

Đoạn “Concepts and usage” của MDN nêu ba tình huống kích hoạt `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:

![Bảng trạng thái active, passive, hidden của Page Lifecycle API trên Chrome for Developers](https://mona.software/wp-content/uploads/2026/11/chrome-page-lifecycle-states-mona.webp)

Bảng trạng thái active, passive và hidden kèm sự kiện chuyển trạng thái. Chrome for Developers, chụp ngày 29/10/2026.

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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://mona.software/phan-mem-chong-gian-lan-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](https://mona.software/phan-mem-thi-trac-nghiem-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](https://mona.software/phan-mem-cham-thi-trac-nghiem/) 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](https://mona.software/mona-lms/) (LMS là hệ thống quản lý học tập, xem [LMS là gì](https://mona.software/lms-la-gi/)) 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](https://mona.software/viet-phan-mem-theo-yeu-cau/) của MONA.

## Câu hỏi thường gặ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.
