| 1 | import { readdirSync, readFileSync } from "node:fs"; |
| 2 | import { join } from "node:path"; |
| 3 | import { fileURLToPath } from "node:url"; |
| 4 | import { describe, expect, it } from "vitest"; |
| 5 | import { USAGE_COUNTING_COPY } from "./content/usage-counting"; |
| 6 | import { WEBSITE_USAGE_DISCLOSURE } from "./legal-copy"; |
| 7 | import { PRODUCT_COUNTER_FIELDS, type ProductCounter } from "./telemetry/product-usage"; |
| 8 | |
| 9 | // How the public copy names each counter the website can send. |
| 10 | const WEBSITE_COUNTER_WORDING: Partial<Record<ProductCounter, { en: string; zh: string }>> = { |
| 11 | page_view: { en: "page views", zh: "页面浏览" }, |
| 12 | docs_view: { en: "documentation views", zh: "文档浏览" }, |
| 13 | install_copy: { en: "install-command copies", zh: "安装命令复制" }, |
| 14 | download: { en: "downloads", zh: "下载" }, |
| 15 | login: { en: "sign-in", zh: "登录" }, |
| 16 | signup: { en: "sign-up", zh: "注册" }, |
| 17 | error_shown: { en: "error pages shown", zh: "错误页面" }, |
| 18 | }; |
| 19 | |
| 20 | function sources(dir: string): string[] { |
| 21 | return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { |
| 22 | const path = join(dir, entry.name); |
| 23 | if (entry.isDirectory()) return sources(path); |
| 24 | return /\.tsx?$/.test(entry.name) && !/\.test\.tsx?$/.test(entry.name) ? [path] : []; |
| 25 | }); |
| 26 | } |
| 27 | |
| 28 | /** Counters the website records: route views plus every call site and marker. */ |
| 29 | function recordedCounters(): Set<string> { |
| 30 | const recorded = new Set<string>(["page_view", "docs_view"]); |
| 31 | for (const dir of ["../app", "../components"]) { |
| 32 | for (const file of sources(fileURLToPath(new URL(dir, import.meta.url)))) { |
| 33 | const text = readFileSync(file, "utf8"); |
| 34 | for (const m of text.matchAll(/recordUsage\("(\w+)"\)|data-usage="(\w+)"/g)) { |
| 35 | recorded.add(m[1] ?? m[2]); |
| 36 | } |
| 37 | for (const m of text.matchAll(/data-usage=\{[^}]*\}/g)) { |
| 38 | for (const q of m[0].matchAll(/"(\w+)"/g)) recorded.add(q[1]); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | return recorded; |
| 43 | } |
| 44 | |
| 45 | describe("usage-counting copy", () => { |
| 46 | const recorded = recordedCounters(); |
| 47 | const privacy = WEBSITE_USAGE_DISCLOSURE.body; |
| 48 | |
| 49 | it("only names website counters the copy knows how to describe", () => { |
| 50 | for (const counter of recorded) { |
| 51 | expect(PRODUCT_COUNTER_FIELDS as readonly string[]).toContain(counter); |
| 52 | expect(WEBSITE_COUNTER_WORDING).toHaveProperty(counter); |
| 53 | } |
| 54 | }); |
| 55 | |
| 56 | it.each(Object.entries(WEBSITE_COUNTER_WORDING))( |
| 57 | "names %s exactly when the site records it", |
| 58 | (counter, wording) => { |
| 59 | const sent = recorded.has(counter); |
| 60 | expect(USAGE_COUNTING_COPY.summary.en.includes(wording.en)).toBe(sent); |
| 61 | expect(USAGE_COUNTING_COPY.summary.zh.includes(wording.zh)).toBe(sent); |
| 62 | expect(privacy.includes(wording.en)).toBe(sent); |
| 63 | }, |
| 64 | ); |
| 65 | }); |
| 66 |