DEV Community

On Pounch Man
On Pounch Man

Posted on

My Landing Page Has 50 Tests and None of Them Need a Browser

The landing page said the tool helps you prepare messages. Two edits later it said the tool reliably sends them. One more and it promised reliable delivery.

No check failed. No reviewer objected. The product simply started claiming something the code cannot guarantee, and nothing in the pipeline had an opinion about it.

That is the failure mode I actually have. Not a crash. A sentence that drifts away from what the code does, one reasonable commit at a time.

The suite that grew out of that worry is 50 tests across 14 files, and it finishes in 568ms:

$ node --test "tests/**/*.test.mjs"
# tests 50
# pass 50
# fail 0
# duration_ms 568.668334
Enter fullscreen mode Exit fullscreen mode

No browser. No dev server. No build step. Here is what it covers, and where I think it is the wrong tool.

Browser APIs are injectable if you stop reaching for globals

Everything about storage in this app goes through one factory:

export function createWorkspaceStorage({
  indexedDB = globalThis.indexedDB,
  localStorage = globalThis.localStorage,
  now = Date.now,
  uuid = () => globalThis.crypto.randomUUID()
} = {}) {
Enter fullscreen mode Exit fullscreen mode

Those defaults are the entire reason the storage layer is testable in Node. In tests I hand it fake-indexeddb, a Map pretending to be localStorage, and a frozen clock:

function fixture(initial = {}) {
  const entries = new Map(Object.entries(initial));
  const localStorage = {
    getItem: key => entries.get(key) ?? null,
    removeItem: key => entries.delete(key),
    setItem: (key, value) => entries.set(key, value)
  };
  const options = { indexedDB: new IDBFactory(), localStorage, now: () => clock };
  return { entries, localStorage, options, storage: createWorkspaceStorage(options) };
}
Enter fullscreen mode Exit fullscreen mode

The Map is returned rather than hidden so a test can assert on raw keys, which is what makes the migration tests further down possible. sql.js is WebAssembly, so the SQLite session parser runs in Node with no shims at all.

One caveat: no DOM. Anything that only exists in a rendered component is not covered here and I am not pretending otherwise.

Test the shape of prose, never the sentences

I do not assert that my copy is good. I assert the properties that survive a rewrite.

const home = source('app/pages/index.vue');
assert.match(home, /class="faq-item"><h3><button/);
assert.match(home, /<th scope="col">/);
assert.match(home, /<th scope="row">/);
assert.match(home, /<cite><a/);
assert.match(home, /<time datetime="2026-10-08">/);
Enter fullscreen mode Exit fullscreen mode

Four things are pinned here and none of them is a word choice.

<h3><button> means the FAQ is a real disclosure widget. A div with a click handler looks identical on screen and is invisible to a keyboard, a screen reader, and most crawlers. That mistake is easy to make and easy to miss in review.

scope on the headers means the comparison table parses as a table.

<cite> next to an https://telegram.org/faq_spam link means claims about Telegram's own policy point at Telegram rather than at a blog post that paraphrased it. One test is stricter than that:

assert.ok(locale.sources.every(source => source.url.startsWith('https://telegram.org/')));
Enter fullscreen mode Exit fullscreen mode

Every reference in every article has to be a primary source. Wanting to cite a third party therefore requires changing the test, which is the conversation I want to be forced into.

<time datetime="2026-10-08"> means the page carries the date it was last verified. That date decides whether the page is trustworthy in a year, and it is also the easiest thing in the world to forget.

Bilingual parity is a contract you can execute

English is the source language. Chinese gets written after, usually days later, sometimes without me rereading the English.

assert.equal(detail.en.sections.length, detail.zh.sections.length);
assert.ok(locale.sections.length >= 5);
assert.ok(locale.sections.some(section => section.table));
assert.ok(locale.sections.some(section => section.list));
assert.ok(locale.coverAlt.length > 15);
Enter fullscreen mode Exit fullscreen mode

I cannot proofread Chinese at the speed I write English, so I stopped verifying words and started verifying shape. Section counts match. Every article has a table and a list, which stops one language from getting an article that reads as a wall of text. Cover alt text is long enough to describe something.

There is also a test whose only job is keeping one bad phrase from coming back:

assert.ok(!home.toLowerCase().includes('telegram message send,'));
Enter fullscreen mode Exit fullscreen mode

Ugly. It has been in the repo for months.

Assert document order, not just presence

The homepage makes an argument in sequence. What the tool is, how a campaign runs, what to do when it breaks, then the questions people ask before they trust it. Reorder those blocks and the page still renders, still looks fine, and still passes any test that only checks for presence.

assert.ok(home.indexOf('workflow-section') < home.indexOf('<FeatureStories'));
assert.ok(home.indexOf('<FeatureStories') < home.indexOf('class="faq section'));
Enter fullscreen mode Exit fullscreen mode

Ordering assertions are cheap and they encode intent that no linter and no type system will ever know about.

I also count data entries with regex, which I recommend with a caveat:

assert.equal((articles.match(/sections: \[\s*\n/g) || []).length, 6);
assert.equal(stories.split("kind: 'compose'").length - 1, 2);
Enter fullscreen mode Exit fullscreen mode

The second line says each of three stories appears exactly twice, once per language. It works, and it will break the day someone reformats that file.

The analytics boundary is the most tested thing in the repo

Seven of the fifty tests exist because analytics is the only path by which user data can leave the machine.

The whole boundary is one function and one allowlist:

const schema = {
  account_login: { outcome: ['success', 'failed', 'password_required'] },
  session_import: { outcome: ['success', 'partial', 'failed'] },
  campaign_complete: { outcome: ['success', 'partial', 'failed'] }
  // ...
};

export function safeAnalyticsEvent(name, parameters = {}) {
  if (!Object.hasOwn(schema, name)) return null;
  const result = {};
  for (const [key, allowed] of Object.entries(schema[name])) {
    if (allowed.includes(parameters[key])) result[key] = parameters[key];
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Event names that are not in the schema are dropped entirely, and so is every parameter that is not listed for that event. A parameter whose value is not one of the enumerated strings never makes it out either, which is what makes free text structurally unable to be recorded.

The test that carries the weight asserts on the entire output rather than the absence of one field:

assert.deepEqual(
  safeAnalyticsEvent('account_login', {
    outcome: 'success',
    phone: '+123',
    password: 'private',
    session: 'private',
    error: 'private'
  }),
  { outcome: 'success' }
);
Enter fullscreen mode Exit fullscreen mode

Written this way it fails if anyone adds a field later, including a field I might approve of. Blocklists leak things nobody thought of. A snapshot of the allowed output does not.

The same shape applies to page paths and clicks:

analyticsPage('/zh/guide?phone=123#code')                    // { page_path: '/zh/guide', ... }
analyticsPage('/user/private-phone')                         // { page_path: '/404', ... }
classifyAnalyticsLink('javascript:alert(1)', origin)         // null
classifyAnalyticsLink('https://t.me/private-user', origin)   // null
Enter fullscreen mode Exit fullscreen mode

A phone number that ends up in a URL cannot reach the analytics provider, because the path is matched against a fixed list and everything else becomes /404. Click events never carry an href. They carry a destination label assigned by code, and a link whose target is not one of the known ones reports nothing at all.

The test that lints my future code

Unit tests check code that exists. The failure I was worried about lives in code I have not written yet, at a call site that goes around the safe function.

const workspace = readFileSync(new URL('../app/components/OnlineWorkspace.vue', import.meta.url), 'utf8');
for (const call of workspace.matchAll(/track\([^;\n]+/g)) {
  assert.ok(!/phone\.value|message\.value|file\.name|account\.id|account\.phone|cause\.message/.test(call[0]));
}
Enter fullscreen mode Exit fullscreen mode

It walks every track() call and fails if any of them mentions message content, a phone number, an account id, a filename, or a raw error message. Crude regex, and it has already caught one call site I wrote on autopilot.

The analytics plugin gets checked for the settings that are easy to lose:

assert.match(plugin, /send_page_view: false/);
assert.match(plugin, /allow_google_signals: false/);
assert.match(plugin, /ad_user_data: 'denied'/);
assert.ok(!plugin.includes('document.referrer'));
Enter fullscreen mode Exit fullscreen mode

send_page_view: false is deliberate. This is a client-routed app, so the built-in page view fires at the wrong moment and I fire it manually after navigation.

One assertion exists purely because of a race I hit while building the consent flow:

assert.ok(plugin.indexOf('localStorage.getItem(storageKey)') < plugin.indexOf('consentReady.value = true'));
Enter fullscreen mode Exit fullscreen mode

Saved consent has to be read from storage before the app declares consent resolved. Get that order wrong and a returning visitor either sees the banner again, or gets measured before their stored choice is applied. No type catches this. Now a test does.

Migration tests where the interesting part is failure

Moving accounts and logs out of localStorage produced the three tests I would have written last if I had written them from the code. I wrote them from the failure modes instead.

The rule is commit first, delete second.

// initialize() writes to IndexedDB, then removes the legacy keys.
// Any failed write means nothing is removed and everything rolls back.
await assert.rejects(storage.initialize());
assert.equal(entries.size, 2);              // both legacy keys still present
localStorage.setItem('legacy.logs.v1', '[]');
const result = await storage.initialize();  // clean retry
Enter fullscreen mode Exit fullscreen mode

Second, the case where the delete never succeeds. I replace removeItem with something that throws and run initialization twice:

localStorage.removeItem = () => { throw new Error('Storage denied'); };
assert.equal((await storage.initialize()).logs.length, 1);
assert.equal((await storage.initialize()).logs.length, 1);  // no duplicate
Enter fullscreen mode Exit fullscreen mode

A migration that is not idempotent duplicates every record on each retry, so the user opens the page and finds their log twice. That bug ships quietly.

Third, boundaries, using the injected clock:

assert.equal(retainLogs([
  { time: clock - LOG_MAX_AGE },
  { time: clock - LOG_MAX_AGE - 1 },
  { time: NaN }
], clock).length, 1);
Enter fullscreen mode Exit fullscreen mode

Exactly thirty days old is kept. One millisecond older is dropped. A record with an unparseable timestamp is dropped rather than sorted into some unpredictable position. Testing that without an injectable clock means waiting a month.

There is an atomicity test as well: saving an account that is missing its id rejects and leaves the previously stored accounts untouched. Accounts hold sessions, so a partial write there is not cosmetic.

Security regressions deserve names

The session importer reads a SQLite file the user downloaded from somewhere else, and that file contains the Telegram server address to connect to.

db.run('CREATE TABLE sessions (dc_id INTEGER, server_address TEXT, port INTEGER, auth_key BLOB)');
db.run('INSERT INTO sessions VALUES (?, ?, ?, ?)', [2, 'attacker.example', 80, key]);

assert.equal(parseSessionBytes(db.export(), SQL).serverAddress, '149.154.167.51');
Enter fullscreen mode Exit fullscreen mode

The test builds a real database, plants a hostile endpoint in it, and asserts the parser throws the address away and dials the official one. If someone later simplifies the parser into trusting the file, this test fails and its name explains why.

The key in that test is a synthetic 256-byte array and the test file says so in a comment. A test that ships a real credential into a CI log is its own kind of bug.

The credential masking pair gets similar treatment:

const masked = maskTelegramApiHash(hash);
assert.equal(restoreTelegramApiHash(masked), hash);
assert.ok(!masked.includes(hash));
assert.notEqual(masked, maskTelegramApiHash(hash));  // fresh mask every call

for (const value of [undefined, null, '', 'bad', {}, 123]) {
  assert.equal(restoreTelegramApiHash(value), '');
}
Enter fullscreen mode Exit fullscreen mode

Round trip works, the plaintext never appears as a substring of the output, and garbage input returns an empty string instead of throwing or echoing back what it received.

What this costs

String assertions against source files are brittle, and I want to name the specific places mine are.

This one slices a Vue component between two markers and asserts where a log reset lands relative to the calls around it:

const campaign = source.slice(source.indexOf('async function sendCampaign()'), source.indexOf('onMounted(async'));
const reset = campaign.indexOf('await storage.clearLogs(); logs.value = [];');
assert.ok(reset > campaign.indexOf('await client.connect()'));
assert.ok(reset < campaign.indexOf('await client.sendMessage'));
Enter fullscreen mode Exit fullscreen mode

It breaks on a rename. It is also the only test I have that catches a bug class no unit test can see, because the bug is not in the storage layer and not in the send loop. It is in the order they are wired together, and that order is what a user experiences when the previous campaign's log appears at the top of the new one.

Same story for the indexOf ordering checks and for counting entries with regex. Tests that read .vue files can pass while the page is broken in a real browser. I accept that trade, because the failure I need to catch here is content that slowly stops being true, and that failure is invisible to a unit test no matter how well the layer underneath is factored.

My rule when writing these: assert what a reader, a crawler, or a keyboard can observe. Markup semantics, document order, cross-language parity, timestamp validity, and where user data stops moving. Skip anything I plan to refactor.

Where this code lives

Every assertion above belongs to TGSender, a free Telegram bulk message sender that runs entirely in the browser: TGSender. If you would rather not keep a tab open, there is a Chrome extension that runs the same code in the background.

One claim I am not going to write, and the suite is the reason: free does not mean unlimited sending. Telegram's own rate limits, permissions, and privacy settings all still apply, and the 30-second gap between messages is a number this tool chose rather than one Telegram endorses. Every sentence in that paragraph has something in the suite holding it up, so if I ever drift, something goes red.

The line I keep coming back to

If a claim is load-bearing for user safety, legal exposure, or accessibility, promote it from prose into a failing test.

Prose rots on its own. A failing test rots loudly.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.