2024-05-03 10:40:53 -05:00
|
|
|
// @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';
|
|
|
|
|
2024-05-18 09:33:14 -05:00
|
|
|
function startOfHourISOString() {
|
|
|
|
const date = new Date();
|
|
|
|
date.setMinutes(0, 0, 0);
|
|
|
|
return date.toISOString();
|
|
|
|
}
|
|
|
|
|
2024-05-03 10:40:53 -05:00
|
|
|
/**
|
|
|
|
* @template {Record<K, (...args: any[]) => void>} T
|
|
|
|
* @template {keyof T} K
|
|
|
|
*/
|
|
|
|
class MockFunction {
|
|
|
|
/** @type {Parameters<T[K]>[]} */
|
|
|
|
calls = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {T} object
|
|
|
|
* @param {K} property
|
|
|
|
*/
|
|
|
|
constructor(object, property) {
|
|
|
|
this.object = object;
|
|
|
|
this.property = property;
|
|
|
|
this.original = object[property];
|
|
|
|
object[property] = /** @param {Parameters<T[K]>} 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<Console, 'error'>} */
|
|
|
|
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');
|
|
|
|
});
|
|
|
|
|
2024-05-18 09:33:14 -05:00
|
|
|
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, [
|
2024-05-03 10:40:53 -05:00
|
|
|
{
|
|
|
|
pathname: '/',
|
|
|
|
route: '/',
|
|
|
|
name: 'CLS',
|
2024-05-18 09:33:14 -05:00
|
|
|
id: 'v4-17114843-3748043125387',
|
2024-05-03 10:40:53 -05:00
|
|
|
value: 0,
|
|
|
|
rating: 'good',
|
2024-05-18 09:33:14 -05:00
|
|
|
timestamp: startOfHourISOString(),
|
2024-05-03 10:40:53 -05:00
|
|
|
},
|
2024-05-18 09:33:14 -05:00
|
|
|
]);
|
2024-05-03 10:40:53 -05:00
|
|
|
});
|
2024-05-18 09:33:14 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
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');
|
2024-05-03 10:40:53 -05:00
|
|
|
});
|
|
|
|
});
|