diff --git a/Source code/tests/README.md b/Source code/tests/README.md
index 2148feb..2418a3e 100644
--- a/Source code/tests/README.md
+++ b/Source code/tests/README.md
@@ -22,6 +22,7 @@ websites (`fake-web.js`) on spare ports, so nothing else needs to be running.
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-bad-messages.js malformed network messages, printer lists and cookies from anyone on the network don't crash the main computer
+ 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('