diff --git a/common/report-schema.js b/common/report-schema.js
index 75ccbff2..2c3e6a56 100644
--- a/common/report-schema.js
+++ b/common/report-schema.js
@@ -268,6 +268,19 @@ const SECTION_SPECS = {
rtt: opt(int(0, 600000)),
}))),
}),
+ // Ad & Tracker Blocking Test: per-category request counts only. Group
+ // ids stay capped strings — the probe list lives in the front end and
+ // changes with it.
+ adblocktest: obj({
+ testedAt: isoDate(),
+ groups: arr(16, obj({
+ id: str(16),
+ total: int(0, 1024),
+ blocked: int(0, 1024),
+ open: int(0, 1024),
+ unknown: int(0, 1024),
+ })),
+ }),
invisibility: obj({
testedAt: isoDate(),
ip: opt(ipValue()),
diff --git a/frontend/components/advanced-tools/AdblockTest.vue b/frontend/components/advanced-tools/AdblockTest.vue
index a64ccda5..1dfe2d1d 100644
--- a/frontend/components/advanced-tools/AdblockTest.vue
+++ b/frontend/components/advanced-tools/AdblockTest.vue
@@ -189,6 +189,7 @@
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
+import { emitAppEvent } from '@/utils/app-events.js';
import { ADBLOCK_PROBE_GROUPS, ADBLOCK_PROBES_VERSION } from '@/data/adblock-probes.js';
import {
COSMETIC_BAITS, COSMETIC_POLL_MS, COSMETIC_WAIT_MS,
@@ -364,5 +365,9 @@ const startTest = async () => {
runWithConcurrency(tasks, concurrency),
]);
status.value = 'finished';
+ // Domain event: per-address outcome for the report collector.
+ emitAppEvent('adblocktest:finished', {
+ items: plan.map((p) => ({ groupId: p.groupId, kind: p.kind, status: statusOf(p) })),
+ });
};
diff --git a/frontend/components/report/ReportPage.vue b/frontend/components/report/ReportPage.vue
index e8ab13eb..f15b14df 100644
--- a/frontend/components/report/ReportPage.vue
+++ b/frontend/components/report/ReportPage.vue
@@ -85,6 +85,7 @@ import ReportPingtest from './sections/ReportPingtest.vue';
import ReportMtrtest from './sections/ReportMtrtest.vue';
import ReportRuletest from './sections/ReportRuletest.vue';
import ReportBrowserinfo from './sections/ReportBrowserinfo.vue';
+import ReportAdblocktest from './sections/ReportAdblocktest.vue';
import ReportInvisibility from './sections/ReportInvisibility.vue';
import ReportEnhanceddnsleak from './sections/ReportEnhanceddnsleak.vue';
import ReportPersona from './sections/ReportPersona.vue';
@@ -100,6 +101,7 @@ const SECTION_COMPONENTS = {
mtrtest: ReportMtrtest,
ruletest: ReportRuletest,
browserinfo: ReportBrowserinfo,
+ adblocktest: ReportAdblocktest,
invisibility: ReportInvisibility,
enhanceddnsleak: ReportEnhanceddnsleak,
persona: ReportPersona,
diff --git a/frontend/components/report/sections/ReportAdblocktest.vue b/frontend/components/report/sections/ReportAdblocktest.vue
new file mode 100644
index 00000000..9e8cd42e
--- /dev/null
+++ b/frontend/components/report/sections/ReportAdblocktest.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
{{ t('adblocktest.report.Category') }}
+
+ {{ t(`adblocktest.count.${key}`) }}
+
+
{{ t('adblocktest.report.Total') }}
+
+
+
+
+
{{ groupTitle(group.id) }}
+
+ {{ group[key] }}
+
+
{{ group.total }}
+
+
+
+
+
+
+
diff --git a/frontend/components/report/sections/table-classes.js b/frontend/components/report/sections/table-classes.js
index 154278cd..85be2951 100644
--- a/frontend/components/report/sections/table-classes.js
+++ b/frontend/components/report/sections/table-classes.js
@@ -1,6 +1,6 @@
// Shared Tailwind class strings for the report tables — same visual language
// as the ping-test result table (GlobalLatencyTest.vue). Kept as constants so
-// the eleven renderers stay literally consistent.
+// the renderers stay literally consistent.
export const TH_LEFT = 'px-3 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wide text-left text-nowrap';
export const TH_RIGHT = 'px-3 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wide text-right text-nowrap';
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index 22afff37..c9e38cc4 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -1060,6 +1060,10 @@
"browser": "Bait elements were hidden: a blocker is working inside your browser, either an extension or the browser's own.",
"network": "Requests were blocked but the bait elements were not hidden: the blocking most likely happens outside the page, at a DNS service, VPN or router, or in the browser's tracking protection.",
"none": "Nothing was blocked: no ad or tracker blocking was found in this browser or on this network."
+ },
+ "report": {
+ "Category": "Category",
+ "Total": "Total"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "Per-hop MTR traceroutes toward the user's IP from probes around the world; loss at intermediate hops can be mere ICMP rate-limiting, sustained loss from one hop onward indicates a real problem.",
"ruletest": "Egress IPs observed when fetching 8 test endpoints — more than one unique IP means traffic is split across different routes (e.g. proxy rules).",
"browserinfo": "Browser, OS and display environment of the tested device.",
+ "adblocktest": "Ad & tracker blocking check run in the browser: per category (ad networks, analytics, social trackers, error monitoring, device-maker telemetry, tracking scripts), how many requests to well-known endpoints were blocked, got through, or timed out (noAnswer). Not blocking everything can be a deliberate choice, not a fault.",
"invisibility": "Proxy/VPN detection scores (0-100, higher = more likely detected) with the individual detection signals that flagged.",
"enhanceddnsleak": "Resolver-level DNS capture: every resolver IP that queried the test domain, with transport, ECS exposure and DNSSEC flags (do/cd).",
"persona": "How closely the browser matches a local resident of the chosen country: an overall grade plus each check's verdict. Verdicts only — the underlying values are deliberately not stored in a shared report."
diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json
index 9cad8f13..0a9b9a55 100644
--- a/frontend/locales/fr.json
+++ b/frontend/locales/fr.json
@@ -1060,6 +1060,10 @@
"browser": "Les éléments leurres ont été masqués : un bloqueur agit dans votre navigateur, qu'il s'agisse d'une extension ou d'une fonction intégrée.",
"network": "Des requêtes ont été bloquées mais les éléments leurres n'ont pas été masqués : le blocage a très probablement lieu hors de la page, au niveau d'un service DNS, d'un VPN, d'un routeur ou de la protection contre le pistage du navigateur.",
"none": "Rien n'a été bloqué : aucun blocage de publicités ou de traceurs n'a été détecté dans ce navigateur ni sur ce réseau."
+ },
+ "report": {
+ "Category": "Catégorie",
+ "Total": "Total"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "Traceroutes MTR saut par saut vers l'IP de l'utilisateur depuis des sondes mondiales ; la perte sur des sauts intermédiaires peut n'être que du rate-limiting ICMP, une perte continue à partir d'un saut indique un vrai problème.",
"ruletest": "IP de sortie observées en accédant à 8 points de test — plusieurs IP distinctes signifient que le trafic est réparti sur différentes routes (règles de proxy, par ex.).",
"browserinfo": "Navigateur, système et environnement d'affichage de l'appareil testé.",
+ "adblocktest": "Test de blocage des publicités et traceurs exécuté dans le navigateur : par catégorie (régies publicitaires, statistiques, traceurs sociaux, suivi des erreurs, télémétrie des fabricants d'appareils, scripts de pistage), le nombre de requêtes vers des adresses connues qui ont été bloquées, sont passées ou ont expiré (noAnswer). Ne pas tout bloquer peut être un choix délibéré, pas une panne.",
"invisibility": "Scores de détection proxy/VPN (0-100, plus haut = plus probablement détecté) avec les signaux de détection déclenchés.",
"enhanceddnsleak": "Capture DNS au niveau résolveur : chaque IP de résolveur ayant interrogé le domaine de test, avec transport, exposition ECS et indicateurs DNSSEC (do/cd).",
"persona": "À quel point le navigateur correspond à un habitant du pays choisi : une note globale et le verdict de chaque vérification. Verdicts uniquement — les valeurs sous-jacentes ne sont volontairement pas stockées dans un rapport partagé."
diff --git a/frontend/locales/pt-BR.json b/frontend/locales/pt-BR.json
index 1eecde75..11060148 100644
--- a/frontend/locales/pt-BR.json
+++ b/frontend/locales/pt-BR.json
@@ -1060,6 +1060,10 @@
"browser": "Os elementos-isca foram ocultados: há um bloqueador funcionando dentro do seu navegador, seja uma extensão ou o próprio navegador.",
"network": "Requisições foram bloqueadas, mas os elementos-isca não foram ocultados: o bloqueio provavelmente acontece fora da página, em um serviço de DNS, VPN, roteador ou na proteção contra rastreamento do navegador.",
"none": "Nada foi bloqueado: nenhum bloqueio de anúncios ou rastreadores foi encontrado neste navegador ou nesta rede."
+ },
+ "report": {
+ "Category": "Categoria",
+ "Total": "Total"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "Traceroutes MTR por salto até o IP do usuário a partir de sondas pelo mundo; perda em saltos intermediários pode ser apenas limitação de ICMP, mas perda sustentada a partir de um salto indica problema real.",
"ruletest": "IPs de saída observados ao buscar 8 endpoints de teste — mais de um IP único significa que o tráfego está dividido entre rotas diferentes (ex.: regras de proxy).",
"browserinfo": "Ambiente de navegador, sistema operacional e tela do dispositivo testado.",
+ "adblocktest": "Teste de bloqueio de anúncios e rastreadores executado no navegador: por categoria (redes de anúncios, análise, rastreadores sociais, monitoramento de erros, telemetria de fabricantes de dispositivos, scripts de rastreamento), quantas requisições a endereços conhecidos foram bloqueadas, passaram ou esgotaram o tempo (noAnswer). Não bloquear tudo pode ser uma escolha deliberada, não uma falha.",
"invisibility": "Pontuações de detecção de proxy/VPN (0–100, maior = mais provável de ser detectado) com os sinais individuais de detecção que foram sinalizados.",
"enhanceddnsleak": "Captura DNS em nível de resolvedor: cada IP de resolvedor que consultou o domínio de teste, com transporte, exposição ECS e flags DNSSEC (do/cd).",
"persona": "O quão próximo o navegador está de uma pessoa local do país escolhido: uma nota geral mais o veredito de cada verificação. Apenas vereditos — os valores subjacentes deliberadamente não são armazenados em relatórios compartilhados."
diff --git a/frontend/locales/ru.json b/frontend/locales/ru.json
index f1f6fb7f..c685ffe9 100644
--- a/frontend/locales/ru.json
+++ b/frontend/locales/ru.json
@@ -1060,6 +1060,10 @@
"browser": "Элементы-приманки скрыты: внутри браузера работает блокировщик — расширение или встроенная функция браузера.",
"network": "Запросы блокируются, но элементы-приманки не скрыты: скорее всего, блокировка происходит вне страницы — на DNS-сервисе, VPN, роутере или во встроенной защите браузера от отслеживания.",
"none": "Ничего не заблокировано: в этом браузере и в этой сети блокировка рекламы и трекеров не обнаружена."
+ },
+ "report": {
+ "Category": "Категория",
+ "Total": "Всего"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "Пошаговые трассировки MTR до IP пользователя с точек по всему миру. Потеря на промежуточных узлах может быть лишь ограничением частоты ICMP; постоянная потеря, продолжающаяся от одного узла дальше, указывает на реальную проблему.",
"ruletest": "Выходные IP, обнаруженные при обращении к 8 тестовым адресам. Несколько уникальных IP означают, что трафик разделён между маршрутами, например правилами прокси.",
"browserinfo": "Браузер, ОС и параметры экрана проверяемого устройства.",
+ "adblocktest": "Проверка блокировки рекламы и трекеров в браузере: по категориям (рекламные сети, аналитика, трекеры соцсетей, мониторинг ошибок, телеметрия производителей устройств, скрипты отслеживания) — сколько запросов к известным адресам было заблокировано, прошло или завершилось тайм-аутом (noAnswer). Неполная блокировка может быть осознанным выбором, а не неисправностью.",
"invisibility": "Оценки обнаружения прокси/VPN (0–100; чем выше, тем вероятнее обнаружение) и отдельные сработавшие признаки.",
"enhanceddnsleak": "Перехват DNS на уровне резолверов: каждый IP резолвера, запросившего тестовый домен, с данными о транспорте, раскрытии ECS и флагах DNSSEC (do/cd).",
"persona": "Насколько браузер похож на местного жителя выбранной страны: общая оценка и вердикт каждой проверки. Только вердикты — стоящие за ними значения намеренно не сохраняются в общем отчёте."
diff --git a/frontend/locales/zh-TW.json b/frontend/locales/zh-TW.json
index a472f2dd..92c2f556 100644
--- a/frontend/locales/zh-TW.json
+++ b/frontend/locales/zh-TW.json
@@ -1060,6 +1060,10 @@
"browser": "誘餌元素被隱藏了:瀏覽器內有阻擋器在運作,可能是擴充功能,也可能是瀏覽器內建的功能。",
"network": "請求被阻擋,但誘餌元素沒有被隱藏:阻擋很可能發生在頁面之外,例如 DNS 服務、VPN、路由器,或瀏覽器的防追蹤功能。",
"none": "沒有任何阻擋:在這個瀏覽器和網路中沒有發現廣告或追蹤封鎖。"
+ },
+ "report": {
+ "Category": "類別",
+ "Total": "總數"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "全球探針針對使用者 IP 進行 MTR 路由追蹤。途中某些節點發生封包遺失,可能只是 ICMP 速率限制;如果從某個節點開始,後續都持續出現封包遺失,才較可能代表實際問題。",
"ruletest": "存取 8 個測試端點時觀測到的出口 IP——出現多個不同 IP 表示流量被分流到不同線路(如代理規則)。",
"browserinfo": "被測裝置的瀏覽器、作業系統與顯示環境。",
+ "adblocktest": "在瀏覽器中執行的廣告與追蹤封鎖檢測:依類別(廣告網路、統計分析、社群平台追蹤、錯誤監控、裝置廠商遙測、追蹤腳本)統計送往常見端點的請求中,被阻擋、未被阻擋和逾時(noAnswer)的數量。沒有全部阻擋可能是使用者的主動選擇,並不代表故障。",
"invisibility": "代理/VPN 檢測得分(0-100,越高越可能被識別)及各項命中的檢測訊號。",
"enhanceddnsleak": "解析器層級的 DNS 查詢紀錄:包含向測試網域名稱發出查詢的每個解析器 IP、傳輸協定、ECS 暴露情況與 DNSSEC 標記(DO/CD)。",
"persona": "瀏覽器與所選國家本地居民的相符程度:一個總體評級,加上每項檢查的結論。只有結論——各項檢查依據的具體數值刻意不寫入可分享的報告。"
diff --git a/frontend/locales/zh.json b/frontend/locales/zh.json
index 3e27c608..8c9d91c5 100644
--- a/frontend/locales/zh.json
+++ b/frontend/locales/zh.json
@@ -1060,6 +1060,10 @@
"browser": "诱饵元素被隐藏了:浏览器内有拦截器在工作,可能是扩展,也可能是浏览器自带的功能。",
"network": "请求被拦截,但诱饵元素没有被隐藏:拦截很可能发生在页面之外,例如 DNS 服务、VPN、路由器,或浏览器的防跟踪功能。",
"none": "没有任何拦截:在这个浏览器和网络中没有发现广告或跟踪屏蔽。"
+ },
+ "report": {
+ "Category": "类别",
+ "Total": "总数"
}
},
"advancedtools": {
@@ -1530,6 +1534,7 @@
"mtrtest": "全球探针到用户 IP 的逐跳 MTR 路由追踪;中间跳的丢包可能只是 ICMP 限速,从某一跳起持续丢包才代表真实问题。",
"ruletest": "访问 8 个测试端点时观测到的出口 IP——出现多个不同 IP 说明流量被分流到不同线路(如代理规则)。",
"browserinfo": "被测设备的浏览器、操作系统与显示环境。",
+ "adblocktest": "在浏览器中运行的广告与跟踪屏蔽检测:按类别(广告网络、统计分析、社交平台跟踪、错误监控、设备厂商遥测、跟踪脚本)统计发往常见端点的请求中,被拦截、未被拦截和超时(noAnswer)的数量。没有全部拦截可能是用户的主动选择,并不代表故障。",
"invisibility": "代理/VPN 检测得分(0-100,越高越可能被识别)及各项命中的检测信号。",
"enhanceddnsleak": "解析器级 DNS 捕获:向测试域名发起查询的每个解析器 IP,含传输协议、ECS 暴露与 DNSSEC 标志(do/cd)。",
"persona": "浏览器与所选国家本地居民的相符程度:一个总体评级,加上每项检查的结论。只有结论——各项检查依据的具体数值刻意不写入可分享的报告。"
diff --git a/frontend/utils/report/report-builders.js b/frontend/utils/report/report-builders.js
index e356c3bd..8ae7623a 100644
--- a/frontend/utils/report/report-builders.js
+++ b/frontend/utils/report/report-builders.js
@@ -283,6 +283,28 @@ const buildBrowserinfo = (payload) => {
return Object.keys(section).length ? section : null;
};
+// adblocktest:finished — { items: [{ groupId, kind, status }] }, one item
+// per probed address. Only the per-category request counts travel; cosmetic
+// baits and unanswered (pending) items are dropped.
+const ADBLOCK_STATUSES = new Set(['blocked', 'open', 'unknown']);
+const buildAdblocktest = (payload) => {
+ const groups = new Map(); // first-appearance order = the probe list's
+ for (const item of Array.isArray(payload?.items) ? payload.items : []) {
+ if (typeof item?.groupId !== 'string' || item.kind === 'cosmetic'
+ || !ADBLOCK_STATUSES.has(item?.status)) continue;
+ const id = clip(item.groupId, 16);
+ if (!groups.has(id)) {
+ if (groups.size >= 16) continue;
+ groups.set(id, { id, total: 0, blocked: 0, open: 0, unknown: 0 });
+ }
+ const counts = groups.get(id);
+ if (counts.total >= 1024) continue;
+ counts.total += 1;
+ counts[item.status] += 1;
+ }
+ return groups.size ? { groups: [...groups.values()] } : null;
+};
+
// invisibility:result — { proxyScore, vpnScore, ip, flags: [{ key, flagged }] }
const buildInvisibility = (payload) => {
const proxy = finiteNum(payload?.proxyScore, 0, 100);
@@ -381,6 +403,7 @@ export const REPORT_EVENT_BUILDERS = {
'mtrtest:finished': { section: 'mtrtest', build: buildMtrtest },
'ruletest:finished': { section: 'ruletest', build: buildRuletest },
'browserinfo:finished': { section: 'browserinfo', build: buildBrowserinfo },
+ 'adblocktest:finished': { section: 'adblocktest', build: buildAdblocktest },
'invisibility:result': { section: 'invisibility', build: buildInvisibility },
'enhanceddnsleak:finished': { section: 'enhanceddnsleak', build: buildEnhanceddnsleak },
'persona:finished': { section: 'persona', build: buildPersona },
diff --git a/frontend/utils/report/report-export.js b/frontend/utils/report/report-export.js
index 997a7217..1b942393 100644
--- a/frontend/utils/report/report-export.js
+++ b/frontend/utils/report/report-export.js
@@ -19,6 +19,7 @@ export const SECTION_TITLE_KEYS = {
mtrtest: 'mtrtest.Title',
ruletest: 'ruletest.Title',
browserinfo: 'browserinfo.Title',
+ adblocktest: 'adblocktest.Title',
invisibility: 'invisibilitytest.Title',
enhanceddnsleak: 'enhanceddnsleaktest.Title',
persona: 'personacheck.Title',
@@ -157,6 +158,8 @@ const SECTION_RENDERERS = {
['display', section.display && `${section.display.width}x${section.display.height} @${section.display.pixelRatio}x`],
['connection', section.connection && `${section.connection.effectiveType ?? ''} downlink=${section.connection.downlink ?? '—'}Mbps rtt=${section.connection.rtt ?? '—'}ms`.trim()],
]),
+ adblocktest: (section) => mdTable(['group', 'total', 'blocked', 'notBlocked', 'noAnswer'],
+ section.groups.map((g) => [g.id, g.total, g.blocked, g.open, g.unknown])),
invisibility: (section) => [
kvLines([
['ip', section.ip],
diff --git a/tests/report-builders.test.js b/tests/report-builders.test.js
index e57158ed..4b3dea0b 100644
--- a/tests/report-builders.test.js
+++ b/tests/report-builders.test.js
@@ -81,6 +81,17 @@ const PAYLOADS = {
{ id: 3, ip: '检测失败', country_code: '', org: '检测失败' },
],
},
+ 'adblocktest:finished': {
+ items: [
+ { groupId: 'ads', kind: 'domain', status: 'blocked' },
+ { groupId: 'ads', kind: 'domain', status: 'open' },
+ { groupId: 'ads', kind: 'domain', status: 'blocked' },
+ { groupId: 'social', kind: 'domain', status: 'unknown' },
+ { groupId: 'scripts', kind: 'script', status: 'open' },
+ { groupId: 'scripts', kind: 'script', status: 'pending' },
+ { groupId: 'cosmetic', kind: 'cosmetic', status: 'blocked' },
+ ],
+ },
'browserinfo:finished': {
userAgent: {
ua: 'Mozilla/5.0 …',
@@ -160,6 +171,26 @@ describe('report builders produce schema-valid sections', () => {
});
describe('cleaning rules', () => {
+ it('adblocktest keeps per-category request counts only', () => {
+ const { build } = REPORT_EVENT_BUILDERS['adblocktest:finished'];
+ // Pending items and cosmetic baits never reach the counts.
+ assert.deepEqual(build(PAYLOADS['adblocktest:finished']), {
+ groups: [
+ { id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 },
+ { id: 'social', total: 1, blocked: 0, open: 0, unknown: 1 },
+ { id: 'scripts', total: 1, blocked: 0, open: 1, unknown: 0 },
+ ],
+ });
+ });
+
+ it('adblocktest reports nothing when only baits or unanswered items came back', () => {
+ const { build } = REPORT_EVENT_BUILDERS['adblocktest:finished'];
+ assert.equal(build({ items: [
+ { groupId: 'cosmetic', kind: 'cosmetic', status: 'blocked' },
+ { groupId: 'ads', kind: 'domain', status: 'pending' },
+ ] }), null);
+ });
+
it('ipinfo drops cards whose ip slot holds a placeholder', () => {
const { build } = REPORT_EVENT_BUILDERS['ipinfo:finished'];
const section = build(PAYLOADS['ipinfo:finished']);
diff --git a/tests/report-export.test.js b/tests/report-export.test.js
index f660c246..26cab75c 100644
--- a/tests/report-export.test.js
+++ b/tests/report-export.test.js
@@ -32,6 +32,10 @@ const makeSections = () => ({
{ id: 'gps-location', axis: 'leak', verdict: 'leak' },
],
},
+ adblocktest: {
+ testedAt: '2026-07-14T08:25:00.000Z',
+ groups: [{ id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 }],
+ },
ipinfo: {
testedAt: '2026-07-14T08:00:00.000Z',
cards: [{ source: 'IPCheck.ing IPv4', ip: '1.2.3.4', countryCode: 'US', city: 'LA', timezone: 'America/Los_Angeles', asn: 'AS15169', isp: 'Google' }],
@@ -144,6 +148,20 @@ describe('reportToMarkdown', () => {
assert.ok(md.includes('score: 72/100'));
});
+ it('renders the adblocktest section as its per-category table', () => {
+ const report = buildShareReport({
+ sections: makeSections(),
+ selectedIds: ['adblocktest'],
+ maskTail: false,
+ locale: 'en',
+ origin: 'ipcheck.ing',
+ });
+ assert.deepEqual(validateReport(report), { ok: true, errors: [] });
+ const md = reportToMarkdown(report, t);
+ assert.ok(md.includes('| group | total | blocked | notBlocked | noAnswer |'));
+ assert.ok(md.includes('| ads | 3 | 2 | 1 | 0 |'));
+ });
+
it('renders heading, intro, per-section blocks and the closing instruction', () => {
const md = reportToMarkdown(makeReport(), t);
assert.ok(md.startsWith('# report.ai.Heading'));
diff --git a/tests/report-schema.test.js b/tests/report-schema.test.js
index c5fc9c64..4637aee9 100644
--- a/tests/report-schema.test.js
+++ b/tests/report-schema.test.js
@@ -95,6 +95,10 @@ const makeValidReport = () => ({
display: { width: 1512, height: 982, pixelRatio: 2 },
connection: null,
},
+ adblocktest: {
+ testedAt: '2026-07-14T08:00:00.000Z',
+ groups: [{ id: 'ads', total: 3, blocked: 2, open: 1, unknown: 0 }],
+ },
invisibility: {
testedAt: '2026-07-14T08:00:00.000Z',
ip: '1.2.3.4',
@@ -299,7 +303,7 @@ describe('constants', () => {
assert.deepEqual(REPORT_TTL_DAYS, [1, 3, 7]);
assert.ok(REPORT_SECTION_IDS.includes('ipinfo'));
assert.ok(REPORT_SECTION_IDS.includes('enhanceddnsleak'));
- assert.equal(REPORT_SECTION_IDS.length, 12);
+ assert.equal(REPORT_SECTION_IDS.length, 13);
});
});