// @ts-check import * as assert from 'node:assert/strict'; import { after, before, beforeEach, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; import { loadFixture } from './test-utils.js'; function startOfHourISOString() { const date = new Date(); date.setMinutes(0, 0, 0); return date.toISOString(); } /** * @template {Record void>} T * @template {keyof T} K */ class MockFunction { /** @type {Parameters[]} */ calls = []; /** * @param {T} object * @param {K} property */ constructor(object, property) { this.object = object; this.property = property; this.original = object[property]; object[property] = /** @param {Parameters} args */ (...args) => { this.calls.push(args); }; } restore() { this.object[this.property] = this.original; } reset() { this.calls = []; } } describe('Web Vitals integration basics', () => { /** @type {import('./test-utils').Fixture} */ let fixture; /** @type {import('./test-utils').DevServer} */ let devServer; /** @type {MockFunction} */ let consoleErrorMock; before(async () => { consoleErrorMock = new MockFunction(console, 'error'); fixture = await loadFixture({ root: './fixtures/basics/' }); devServer = await fixture.startDevServer({}); }); after(async () => { consoleErrorMock.restore(); await devServer.stop(); }); beforeEach(() => { consoleErrorMock.reset(); }); it('adds a meta tag to the page', async () => { const html = await fixture.fetch('/', {}).then((res) => res.text()); const { document } = parseHTML(html); const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]'); assert.ok(meta); assert.equal(meta.getAttribute('content'), '/'); }); it('adds a meta tag using the route pattern to the page', async () => { const html = await fixture.fetch('/test', {}).then((res) => res.text()); const { document } = parseHTML(html); const meta = document.querySelector('head > meta[name="x-astro-vitals-route"]'); assert.ok(meta); assert.equal(meta.getAttribute('content'), '/[dynamic]'); }); it('returns a 200 response even when bad data is sent to the injected endpoint', async () => { { // bad data const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: 'garbage' }); assert.equal(res.status, 200); } { // no data const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[]' }); assert.equal(res.status, 200); } assert.equal(consoleErrorMock.calls.length, 2); }); it('validates data sent to the injected endpoint with Zod', async () => { const res = await fixture.fetch('/_web-vitals', { method: 'POST', body: '[{}]' }); assert.equal(res.status, 200); const call = consoleErrorMock.calls[0][0]; assert.ok(call instanceof Error); assert.equal(call.name, 'ZodError'); }); describe('inserting data via the injected endpoint', () => { /** @type {Response} */ let res; before(async () => { res = await fixture.fetch('/_web-vitals', { method: 'POST', body: JSON.stringify([ { pathname: '/', route: '/', name: 'CLS', id: 'v4-1711484350895-3748043125387', value: 0, rating: 'good', }, ]), }); }); it('inserting data does not error', () => { assert.equal(res.status, 200); assert.equal( consoleErrorMock.calls.length, 0, 'Endpoint logged errors:\n' + consoleErrorMock.calls[0]?.join(' '), ); }); it('inserted data can be retrieved from the database', async () => { const dbRows = await fixture.fetch('/rows.json', {}).then((r) => r.json()); assert.deepEqual(dbRows, [ { pathname: '/', route: '/', name: 'CLS', id: 'v4-17114843-3748043125387', value: 0, rating: 'good', timestamp: startOfHourISOString(), }, ]); }); }); it('inserted data uses a truncated timestamp in the ID', async () => { // The IDs generated by the `web-vitals` package include a high resolution timestamp as the second portion, // e.g. 'v4-1711484350895-3748043125387'. We reduce this data to an hourly resolution to lessen privacy concerns. const dbRows = await fixture.fetch('/rows.json', {}).then((r) => r.json()); assert.deepEqual(dbRows[0].id, 'v4-17114843-3748043125387'); }); });