diff --git a/Source code/tests/README.md b/Source code/tests/README.md
index ed6faf4..b5e90d7 100644
--- a/Source code/tests/README.md
+++ b/Source code/tests/README.md
@@ -21,6 +21,7 @@ websites (`fake-web.js`) on spare ports, so nothing else needs to be running.
node t15-page-view.js closing the large page preview frees its image; reopening the same page works
node t16-other-printer.js a job only moves to the same kind (inkjet/laser to each other if ticked), never because a computer dropped off
node t17-update-now.js "Update PrintBridge now" shows a message window saying what it found, never a terminal
+ node t18-dialogs.js Every pop-up on the printing pages tells screen readers that it is a pop-up, and what it is called, using role="dialog" and aria-labelledby attributes.
These run on Linux/macOS. The Windows-only parts (the Task Scheduler task,
the installer) are covered by manual checks on Windows and Mac.
diff --git a/Source code/tests/t18-dialogs.js b/Source code/tests/t18-dialogs.js
new file mode 100644
index 0000000..7a62e3b
--- /dev/null
+++ b/Source code/tests/t18-dialogs.js
@@ -0,0 +1,17 @@
+//Every pop-up on the printing pages tells screen readers that it is a pop-up, and what it is called, using role="dialog" and aria-labelledby attributes.
+
+const fs = require("fs"), path = require("path"), assert = require("assert");
+let n = 0; const ok = (c, m) => { assert(c, m); n++; console.log("ok -", m); }
+
+const page = fs.readFileSync(path.join(__dirname, "..", "server", "public", "index.html"), "utf8");
+const overlays = page.match(/class="modal-overlay/g) || [];
+const boxes = page.match(/
]*>/g) || [];
+ok(boxes.length > 0 && boxes.length === overlays.length, "each of the" + overlays.length + " pop-ups has one box");
+for (const box of boxes) {
+ const title = (box.match(/aria-labelledby="([^"]+)"/) || [])[1];
+ ok(/role="dialog"/.test(box) && /aria-modal="true"/.test(box) && title, "pop-up is marked as a dialog with title: " + title);
+ ok(new RegExp('