"license": "ISC",
"dependencies": {
"@squeep/api-dingus": "^2",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
"@squeep/logger-json-console": "^3",
"argon2": "^0.43.0",
"better-sqlite3": "^11",
"@squeep/eslint-config": "^1",
"@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v3.0.1",
"eslint": "^9",
+ "html-validate": "^9",
"husky": "^9",
"mocha": "^11",
"nyc": "^17",
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@html-validate/stylish": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-4.2.0.tgz",
+ "integrity": "sha512-Nl8HCv0hGRSLQ+n1OD4Hk3a+Urwk9HH0vQkAzzCarT4KlA7bRl+6xEiS5PZVwOmjtC7XiH/oNe3as9Fxcr2A1w==",
+ "dev": true,
+ "dependencies": {
+ "kleur": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"eslint": ">= 9"
}
},
+ "node_modules/@squeep/html-template-helper": {
+ "version": "1.6.1",
+ "resolved": "git+https://git.squeep.com/squeep-html-template-helper#93d1b030d6b3c6ea93c36a46f4940181a1acaca0",
+ "dependencies": {
+ "@squeep/lazy-property": "^1"
+ },
+ "engines": {
+ "node": ">=14.13.1"
+ }
+ },
+ "node_modules/@squeep/lazy-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@squeep/lazy-property/-/lazy-property-1.1.4.tgz",
+ "integrity": "sha512-UQ8Q+Znc4Ms0e+2K46DZH88S4f8xTwFIAwzM0rSSD2n6KCuAh38hDK2gyAE17sH9QekRYzUQdpLLdBwqM49Ihw==",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@squeep/log-helper": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@squeep/log-helper/-/log-helper-1.0.1.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ]
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
+ "node_modules/html-validate": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-9.6.1.tgz",
+ "integrity": "sha512-BmHr0dac/HKNxP3Iks7Nvos50ohl45ZeZ071OqDMsvF2Hi+didPsuveM3Wjdlx78TMkqA+S87syiER9TmKPrpw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/html-validate"
+ }
+ ],
+ "dependencies": {
+ "@html-validate/stylish": "^4.1.0",
+ "@sidvind/better-ajv-errors": "4.0.0",
+ "ajv": "^8.0.0",
+ "glob": "^10.0.0",
+ "kleur": "^4.1.0",
+ "minimist": "^1.2.0",
+ "prompts": "^2.0.0",
+ "semver": "^7.0.0"
+ },
+ "bin": {
+ "html-validate": "bin/html-validate.mjs"
+ },
+ "engines": {
+ "node": "^18.19.0 || >= 20.6.0"
+ },
+ "peerDependencies": {
+ "jest": "^27.1 || ^28.1.3 || ^29.0.3 || ^30.0.0",
+ "jest-diff": "^27.1 || ^28.1.3 || ^29.0.3 || ^30.0.0",
+ "jest-snapshot": "^27.1 || ^28.1.3 || ^29.0.3 || ^30.0.0",
+ "vitest": "^0.34.0 || ^1.0.0 || ^2.0.0 || ^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "jest": {
+ "optional": true
+ },
+ "jest-diff": {
+ "optional": true
+ },
+ "jest-snapshot": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/html-validate/node_modules/@sidvind/better-ajv-errors": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-4.0.0.tgz",
+ "integrity": "sha512-rLZQkN6IfNwG6iqZZwqFMcs7DvQX3ZrLVhsHmSO1LUA4EZAz+VZLpTBCIOFsC5Qu3xuwzVfRMZ+1rtk/mCRRZw==",
+ "dev": true,
+ "dependencies": {
+ "kleur": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "ajv": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/html-validate/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/html-validate/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"read": "^1.0.4"
}
},
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prompts/node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/proxy-agent": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-in-the-middle": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz",
"url": "https://opencollective.com/sinon"
}
},
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true
+ },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"license": "ISC",
"dependencies": {
"@squeep/api-dingus": "^2",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1",
"@squeep/logger-json-console": "^3",
"argon2": "^0.43.0",
"better-sqlite3": "^11",
"@squeep/test-helper": "git+https://git.squeep.com/squeep-test-helper#v3.0.1",
"eslint": "^9",
"husky": "^9",
+ "html-validate": "^9",
"mocha": "^11",
"nyc": "^17",
"sinon": "^21"
'use strict';
-const toDateString = (ts) => (ts && isFinite(ts)) ? new Date(ts * 1000).toISOString() : 'never';
+const { TemplateHelper: th } = require('@squeep/html-template-helper');
-module.exports = (ctx, details, pageTitle) => {
+const toDateString = (ts) => {
+ try {
+ return (ts && isFinite(ts)) ? (new Date(ts * 1000).toISOString()) : 'never';
+ } catch (e) { // eslint-disable-line no-unused-vars
+ return `(whatever '${ts.toString()}' is)`;
+ }
+};
+
+
+/**
+ *
+ * @param {object} details link details
+ * @returns {string} HTML details section
+ */
+function renderDetailsSection(details) {
const created = toDateString(details.created);
const lastAccess = toDateString(details.lastAccess);
const expires = details.expires ? toDateString(details.expires) : '';
- return `<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>${pageTitle} - ${details.id} Details</title>
- <link rel="stylesheet" href="../../static/theme.css">
- </head>
- <body>
- <header>
- <h1>Link Details</h1>
- </header>
- <main>
- <div class="identifier">id: <span><a href="/${details.id}">${details.id}</a></span></div>
- <div class="url">url: <span>${details.url}</span></div>
- <div class="created">created: <time datetime="${created}">${created}</time></div>
- <div class="accesses">accesses: <span>${details.accesses}</span></div>
- <div class="lastaccess">last access: <time datetime="${lastAccess}">${lastAccess}</time></div>` +
- (!details.expires ? '' : `
- <div class="expires">expires: <time datetime="${expires}">${expires}</time></div>`) + `
- </main>
- </body>
-</html>`;
+ return `<section>
+\t<div class="identifier">id: <span><a href="/${details.id}">${details.id}</a></span></div>
+\t<div class="url">url: <span>${details.url}</span></div>
+\t<div class="created">created: <time datetime="${created}">${created}</time></div>
+\t<div class="accesses">accesses: <span>${details.accesses}</span></div>
+\t<div class="lastaccess">last access: <time datetime="${lastAccess}">${lastAccess}</time></div>` +
+(!details.expires ? '' : `
+\t<div class="expires">expires: <time datetime="${expires}">${expires}</time></div>`) + `
+</section>`;
+}
+
+module.exports = (ctx, details, pageTitle) => {
+ const pagePathLevel = 1;
+ const htmlOptions = {
+ pageIdentifier: 'details',
+ pageTitle,
+ footerEntries: [],
+ headElements: [],
+ };
+
+ const main = [
+ renderDetailsSection(details),
+ ];
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, main);
};
\ No newline at end of file
'use strict';
+const { TemplateHelper: th } = require('@squeep/html-template-helper');
+
const toDateString = (ts) => {
try {
return (ts && isFinite(ts)) ? (new Date(ts * 1000).toISOString()) : 'never';
}
};
+
+/**
+ *
+ * @returns {string} HTML table header row
+ */
+function renderLinkHeaderRow() {
+ const headers = ['id', 'url', 'accesses', 'created', 'expires', 'last access'];
+ return '<tr>' +
+ (headers.map((h) => `\t<th scope="col">${h}</th>`).join('\n')) +
+ '</tr>';
+}
+
+
+/**
+ *
+ * @param {object} link link object
+ * @returns {string} HTML table row
+ */
+function renderLinkRow(link) {
+ return `<tr>
+\t<th scope="row"><a href="/${link.id}/info">${link.id}</a></th>
+\t<td>${link.url}</td>
+\t<td>${link.accesses}</td>
+\t<td>${toDateString(link.created)}</td>
+\t<td>${toDateString(link.expires)}</td>
+\t<td>${toDateString(link.lastAccess)}</td>
+</tr>`;
+}
+
+
+/**
+ *
+ * @param {object[]} links array of link objects
+ * @returns {string} link report section
+ */
+function linkReportSection(links) {
+ return `<section>
+\t<table>
+\t\t<thead>
+${renderLinkHeaderRow()}
+\t\t</thead>
+\t\t<tbody>
+${links.map((l) => renderLinkRow(l)).join('\n')}
+\t\t</tbody>
+\t</table>
+</section>`;
+}
+
+
module.exports = (ctx, links, pageTitle) => {
- return `<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>${pageTitle} - Link Report</title>
- <link rel="stylesheet" href="../../static/theme.css">
- </head>
- <body>
- <header>
- <h1>Link Report</h1>
- </header>
- <main>
- <table>
- <thead>
- <tr>
- <th scope="col">id</th>
- <th scope="col">url</th>
- <th scope="col">accesses</th>
- <th scope="col">created</th>
- <th scope="col">expires</th>
- <th scope="col">last access</th>
- </tr>
- </thead>
- <tbody>` +
- links.map((l) => `
- <tr>
- <th scope="row"><a href="/${l.id}/info">${l.id}</a></th>
- <td>${l.url}</td>
- <td>${l.accesses}</td>
- <td>${toDateString(l.created)}</td>
- <td>${toDateString(l.expires)}</td>
- <td>${toDateString(l.lastAccess)}</td>
- </tr>`).join('\n') + `
- </tbody>
- <table>
- </main>
- </body>
-</html>`;
+ const pagePathLevel = 1;
+ const htmlOptions = {
+ pageIdentifier: 'report',
+ pageTitle,
+ footerEntries: [],
+ headElements: [],
+ };
+
+ const main = [
+ linkReportSection(links),
+ ];
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, main);
};
'use strict';
+const { TemplateHelper: th } = require('@squeep/html-template-helper');
+
+/**
+ *
+ * @param {object} ctx context
+ * @returns {string} copy script or not
+ */
+function copyScript(ctx) {
+ return '<script>' + (!ctx.createdLink ? '0;' : `
+document.addEventListener('DOMContentLoaded', function () {
+\tlet timeout;
+\tdocument.querySelector('.copy-button').addEventListener('click', function (event) {
+\t\tconst linkHref = document.querySelector('.link').href;
+\t\tnavigator.clipboard.writeText(linkHref);
+\t\tconst copyButton = this;
+\t\tcopyButton.style.backgroundColor = 'lightgreen';
+\t\tif (timeout) {
+\t\t\tclearTimeout(timeout);
+\t\t}
+\t\ttimeout = setTimeout(function () {
+\t\t\tcopyButton.style.backgroundColor = 'initial';
+\t\t\ttimeout = undefined;
+\t\t}, 3000);
+\t});
+});`) + '</script>';
+}
+
+
+/**
+ *
+ * @param {object} ctx context
+ * @returns {string} link created section or not
+ */
+function linkCreatedSection(ctx) {
+ return (!ctx.createdLink ? '' : `<section class="created-link">
+\t<a class="src-link" href="${ctx.sourceLink}">${ctx.sourceLink}</a> is now liked to by <a class="link" href="${ctx.createdLink}">${ctx.createdLink}</a>
+\t<span class="copy">
+\t\t<button type="button" class="copy-button">Copy to Clipboard</button>
+\t</span>
+\t<div class="meta-link">
+\t\tcontrol token <span class="auth-token">${ctx.authToken}</span>
+\t</div>
+</section>`);
+}
+
+
+/**
+ *
+ * @returns {string} create link form section
+ */
+function createLinkSection() {
+ return `<section class="create-link">
+\t<form method="post" class="submit-url">
+\t\t<div class="submit-url">
+\t\t\t<label for="url">URL: </label>
+\t\t\t<input type="text" name="url" id="url" required>
+\t\t</div>
+\t\t<div class="submit-url">
+\t\t\t<button type="submit" value="Create Link">Create Link</button>
+\t\t</div>
+\t</form>
+</section>
+`;
+}
+
+
+/**
+ *
+ * @param {object} ctx context
+ * @returns {string} message section or not
+ */
+function messageSection(ctx) {
+ return (!ctx.message ? '' : `<section class="message">
+\t${ctx.message}
+</section>`);
+}
+
+/**
+ * @param {object} ctx context
+ * @param {string} ctx.createdLink created link url
+ * @param {string} ctx.sourceLink source link url
+ * @param {string} ctx.authToken auth token
+ * @param {string} ctx.message message
+ * @param {string} pageTitle page title
+ * @param {string} logoUrl logo url
+ * @returns {string} html content
+ */
module.exports = (ctx, pageTitle, logoUrl) => {
- const logoImg = logoUrl ? `<img src="${logoUrl}" class="logo">` : '';
- return `<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="robots" content="noindex">
- <title>${pageTitle}</title>
- <link rel="stylesheet" href="static/theme.css">
- </head>
- <body>
- <script type="text/javascript">` + (!ctx.createdLink ? '0;' : `
- document.addEventListener('DOMContentLoaded', function () {
- let timeout;
- document.querySelector('.copy-button').addEventListener('click', function (event) {
- const linkHref = document.querySelector('.link').href;
- navigator.clipboard.writeText(linkHref);
- const copyButton = this;
- copyButton.style.backgroundColor = 'lightgreen';
- if (timeout) {
- clearTimeout(timeout);
- }
- timeout = setTimeout(function () {
- copyButton.style.backgroundColor = 'initial';
- timeout = undefined;
- }, 3000);
- });
- });`) + `
- </script>
- <header>
- <h1>${logoImg}${pageTitle}</h1>
- </header>` +
- (!ctx.createdLink ? '' : `
- <section class="created-link">
- <a class="src-link" href="${ctx.sourceLink}">${ctx.sourceLink}</a>
- is now liked to by
- <a class="link" href="${ctx.createdLink}">${ctx.createdLink}</a>
- <span class="copy">
- <button class="copy-button">Copy to Clipboard</button>
- </span>
- <div class="meta-link">
- control token <span class="auth-token">${ctx.authToken}</span>
- </div>
- </section>`) +
- (!ctx.message ? '' : `
- <section class="message">
- ${ctx.message}
- </section>`) + `
- <section class="create-link">
- <form action="" method="post" class="submit-url">
- <div class="submit-url">
- <label for="url">URL: </label>
- <input type="text" name="url" id="url" required>
- </div>
- <div class="submit-url">
- <input type="submit" value="Create Link">
- </div>
- </form>
- </section>
- </body>
-</html>`;
+ const pagePathLevel = 0;
+ const htmlOptions = {
+ pageIdentifier: 'root',
+ pageTitle,
+ logoUrl,
+ footerEntries: [],
+ headElements: [
+ '<meta name="robots" content="noindex">',
+ ],
+ };
+ const main = [
+ copyScript(ctx),
+ linkCreatedSection(ctx),
+ messageSection(ctx),
+ createLinkSection(),
+ ];
+
+ return th.htmlPage(pagePathLevel, ctx, htmlOptions, main);
};
\ No newline at end of file
--- /dev/null
+'use strict';
+
+const assert = require('node:assert');
+const sinon = require('sinon');
+const template = require('../../../src/template/info-html');
+const { StubLogger } = require('@squeep/test-helper');
+const { makeHtmlLint } = require('@squeep/html-template-helper');
+const { HtmlValidate } = require('html-validate');
+
+const stubLogger = new StubLogger(sinon);
+const htmlValidate = new HtmlValidate();
+const lintHtml = makeHtmlLint(stubLogger, htmlValidate);
+
+describe('Info HTML Template', function () {
+ let ctx, details, pageTitle;
+ beforeEach(function () {
+ pageTitle = 'info page';
+ ctx = {};
+ details = {
+ id: 'abc123',
+ url: 'https://example.com',
+ accesses: 42,
+ created: 1672531200, // 2023-01-01T00:00:00.000Z
+ expires: 1704067200, // 2024-01-01T00:00:00.000Z
+ lastAccess: 1696118400, // 2023-10-01T00:00:00.000Z
+ };
+ });
+ it('renders details', async function () {
+ const result = template(ctx, details, pageTitle);
+ await lintHtml(result);
+ assert(result);
+ });
+ it('renders odd details', async function () {
+ details.expires = Infinity;
+ details.lastAccess = 1696118400n;
+ const result = template(ctx, details, pageTitle);
+ await lintHtml(result);
+ assert(result);
+ });
+
+}); // Report HTML Template
\ No newline at end of file
--- /dev/null
+'use strict';
+
+const assert = require('node:assert');
+const sinon = require('sinon');
+const template = require('../../../src/template/report-html');
+const { StubLogger } = require('@squeep/test-helper');
+const { makeHtmlLint } = require('@squeep/html-template-helper');
+const { HtmlValidate } = require('html-validate');
+
+const stubLogger = new StubLogger(sinon);
+const htmlValidate = new HtmlValidate();
+const lintHtml = makeHtmlLint(stubLogger, htmlValidate);
+
+describe('Report HTML Template', function () {
+ let ctx, pageTitle, links;
+ beforeEach(function () {
+ pageTitle = 'report page';
+ ctx = {};
+ links = [
+ {
+ id: 'abc123',
+ url: 'https://example.com',
+ accesses: 42,
+ created: 1672531200, // 2023-01-01T00:00:00.000Z
+ expires: 1704067200, // 2024-01-01T00:00:00.000Z
+ lastAccess: 1696118400, // 2023-10-01T00:00:00.000Z
+ },
+ {
+ id: 'def456',
+ url: 'https://example.org',
+ accesses: 0,
+ created: null,
+ expires: Infinity,
+ lastAccess: null,
+ },
+ {
+ id: 'ghi789',
+ url: 'https://example.net',
+ accesses: 100,
+ created: 1672531200, // 2023-01-01T00:00:00.000Z
+ expires: null,
+ lastAccess: 1672531200n,
+ },
+ ];
+ });
+ it('renders links', async function () {
+ const result = template(ctx, links, pageTitle);
+ await lintHtml(result);
+ assert(result);
+ });
+}); // Report HTML Template
\ No newline at end of file
--- /dev/null
+'use strict';
+
+const assert = require('node:assert');
+const sinon = require('sinon');
+const template = require('../../../src/template/root-html');
+const { StubLogger } = require('@squeep/test-helper');
+const { makeHtmlLint } = require('@squeep/html-template-helper');
+const { HtmlValidate } = require('html-validate');
+
+const stubLogger = new StubLogger(sinon);
+const htmlValidate = new HtmlValidate();
+const lintHtml = makeHtmlLint(stubLogger, htmlValidate);
+
+describe('Root HTML Template', function () {
+ let ctx, pageTitle, logoUrl;
+ beforeEach(function () {
+ pageTitle = 'root page';
+ ctx = {};
+ });
+ it('renders plain', async function () {
+ const result = template(ctx, pageTitle, logoUrl);
+ await lintHtml(result);
+ assert(result);
+ });
+ it('covers created link', async function () {
+ ctx.createdLink = 'https://example.com/created';
+ ctx.sourceLink = 'https://example.com/source';
+ ctx.authToken = 'auth-token';
+ ctx.message = 'message';
+ const result = template(ctx, pageTitle, logoUrl);
+ await lintHtml(result);
+ assert(result);
+ });
+}); // Root HTML Template
\ No newline at end of file