summaryrefslogtreecommitdiff
path: root/packages/integrations/web-vitals/test/basics.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/web-vitals/test/basics.test.js')
-rw-r--r--packages/integrations/web-vitals/test/basics.test.js153
1 files changed, 153 insertions, 0 deletions
diff --git a/packages/integrations/web-vitals/test/basics.test.js b/packages/integrations/web-vitals/test/basics.test.js
new file mode 100644
index 000000000..7d5afa9df
--- /dev/null
+++ b/packages/integrations/web-vitals/test/basics.test.js
@@ -0,0 +1,153 @@
+// @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<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');
+ });
+
+ 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');
+ });
+});