Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cb61d1a10 |
No files matched your search
+4
-5
@@ -18,9 +18,8 @@ COPY . .
|
|||||||
RUN yarn run lint
|
RUN yarn run lint
|
||||||
RUN script/check-censored
|
RUN script/check-censored
|
||||||
|
|
||||||
# Test phase, same shape and for the same reason: the jest suite (its worker
|
# Test phase, same shape and for the same reason: the jest suite, rerun
|
||||||
# cap is in package.json), rerun verbose on failure, then
|
# verbose on failure, then script/test-verify-build.
|
||||||
# script/test-verify-build.
|
|
||||||
#
|
#
|
||||||
# node:22-slim (22.x LTS), 2026-02-24
|
# node:22-slim (22.x LTS), 2026-02-24
|
||||||
FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36 AS test
|
FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36 AS test
|
||||||
@@ -33,9 +32,9 @@ RUN script/bootstrap
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN timeout 90 yarn run test || \
|
RUN yarn run test || \
|
||||||
{ echo "--- Rerunning with --verbose for details ---"; \
|
{ echo "--- Rerunning with --verbose for details ---"; \
|
||||||
timeout 90 yarn run test:verbose; exit 1; }
|
yarn run test:verbose; exit 1; }
|
||||||
RUN script/test-verify-build
|
RUN script/test-verify-build
|
||||||
|
|
||||||
# Development environment with the extension built, and the last stage: a
|
# Development environment with the extension built, and the last stage: a
|
||||||
|
|||||||
@@ -1274,8 +1274,9 @@ A native amount is labelled with the native currency of the transaction's chain:
|
|||||||
`src/shared/networks.js`). The balance lists and Send use the active network's.
|
`src/shared/networks.js`). The balance lists and Send use the active network's.
|
||||||
The decoded swap lines call the native currency `ETH` on both networks.
|
The decoded swap lines call the native currency `ETH` on both networks.
|
||||||
|
|
||||||
TransactionDetail and every history row show the time as an ISO datetime and
|
TransactionDetail shows the time as an ISO datetime and a relative age. A
|
||||||
then a relative age. With "UTC Timestamps" on, the datetime is in UTC.
|
history row shows the age, with the datetime as its tooltip. With "UTC
|
||||||
|
Timestamps" on, the datetime is in UTC.
|
||||||
|
|
||||||
Every nonzero holding is stored exactly. A token holding below 0.000001 is left
|
Every nonzero holding is stored exactly. A token holding below 0.000001 is left
|
||||||
out of:
|
out of:
|
||||||
@@ -1760,8 +1761,8 @@ The `Makefile` targets are thin shims over `script/`:
|
|||||||
`package.json`.
|
`package.json`.
|
||||||
- `script/setup`: `script/bootstrap`, then the git pre-commit hook.
|
- `script/setup`: `script/bootstrap`, then the git pre-commit hook.
|
||||||
- `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`.
|
- `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`.
|
||||||
The stage runs the jest suite, stopped at 90 seconds and rerun verbose on
|
The stage runs the jest suite, rerun verbose on failure, then
|
||||||
failure, then `script/test-verify-build`.
|
`script/test-verify-build`.
|
||||||
- `script/lint`: build the `lint` stage with `--no-cache`. The stage runs
|
- `script/lint`: build the `lint` stage with `--no-cache`. The stage runs
|
||||||
ESLint, the prettier check and `script/check-censored`. Linting runs only in
|
ESLint, the prettier check and `script/check-censored`. Linting runs only in
|
||||||
Docker.
|
Docker.
|
||||||
@@ -1860,9 +1861,10 @@ have left a complete, loadable debug bundle behind. `make build-debug` keeps its
|
|||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
`make test` runs the jest suite in Docker. The suite must finish within 60
|
`make test` runs the jest suite in Docker, in one process. The suite must finish
|
||||||
seconds; the test stage stops it at 90. `make check` runs the tests, the lint
|
within 60 seconds. A test that leaves a timer or another open handle running
|
||||||
stage and the formatting check.
|
fails the run. `make check` runs the tests, the lint stage and the formatting
|
||||||
|
check.
|
||||||
|
|
||||||
Some unit tests pin facts stated elsewhere in this file:
|
Some unit tests pin facts stated elsewhere in this file:
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -135,9 +135,14 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unit tests, and the helpers they require: jest on node.
|
// Unit tests, the helpers they require, and the test files they hand to
|
||||||
|
// jest: jest on node.
|
||||||
{
|
{
|
||||||
files: ["tests/**/*.test.js", "tests/support/**/*.js"],
|
files: [
|
||||||
|
"tests/**/*.test.js",
|
||||||
|
"tests/support/**/*.js",
|
||||||
|
"tests/fixtures/**/*.js",
|
||||||
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...commonjs,
|
...commonjs,
|
||||||
globals: { ...globals.node, ...globals.jest },
|
globals: { ...globals.node, ...globals.jest },
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node script/lib/jest.js --maxWorkers=3",
|
"test": "node script/lib/jest.js --runInBand",
|
||||||
"test:verbose": "node script/lib/jest.js --maxWorkers=3 --verbose",
|
"test:verbose": "node script/lib/jest.js --runInBand --verbose",
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fmt": "prettier --write .",
|
"fmt": "prettier --write .",
|
||||||
|
|||||||
+17
-25
@@ -1,8 +1,8 @@
|
|||||||
// Runs jest with the arguments it is given, and fails the run when jest had to
|
// Runs jest with the arguments it is given, and fails the run at once when a
|
||||||
// force-stop the worker process that ran a test file. Jest itself only prints a
|
// test left a timer or another open handle running. Such a handle keeps this
|
||||||
// warning when that happens, and its run still passes. The warning means a test
|
// process alive after the last test, so the run would hang. Jest itself only
|
||||||
// left a timer or another open handle running. The test scripts in package.json
|
// prints a warning one second after the run, and then keeps waiting. The test
|
||||||
// run jest through this file.
|
// scripts in package.json run jest through this file, in this one process.
|
||||||
//
|
//
|
||||||
// Jest runs inside this process and writes to the terminal directly, so its
|
// Jest runs inside this process and writes to the terminal directly, so its
|
||||||
// output looks the same as a plain jest run. This file only checks each write
|
// output looks the same as a plain jest run. This file only checks each write
|
||||||
@@ -10,31 +10,23 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// The start of the warning that jest prints on stderr after it force-stops a
|
// The start of the warning that jest prints on stderr when the process has not
|
||||||
// worker process.
|
// exited one second after the test run.
|
||||||
const WORKER_WARNING = "A worker process has failed to exit gracefully";
|
const OPEN_HANDLE_WARNING =
|
||||||
|
"Jest did not exit one second after the test run has completed.";
|
||||||
let forceStopped = false;
|
|
||||||
|
|
||||||
const writeStderr = process.stderr.write.bind(process.stderr);
|
const writeStderr = process.stderr.write.bind(process.stderr);
|
||||||
process.stderr.write = (chunk, ...rest) => {
|
process.stderr.write = (chunk, ...rest) => {
|
||||||
if (String(chunk).includes(WORKER_WARNING)) {
|
const written = writeStderr(chunk, ...rest);
|
||||||
forceStopped = true;
|
if (String(chunk).includes(OPEN_HANDLE_WARNING)) {
|
||||||
}
|
writeStderr(
|
||||||
return writeStderr(chunk, ...rest);
|
"script/lib/jest.js: failing the run, because a test left a " +
|
||||||
};
|
"timer or another open handle running.\n",
|
||||||
|
|
||||||
// Runs after jest has finished, as the process exits.
|
|
||||||
process.on("exit", () => {
|
|
||||||
if (forceStopped) {
|
|
||||||
console.error(
|
|
||||||
"script/lib/jest.js: failing the run, because jest force-stopped " +
|
|
||||||
"a worker process. A test left a timer or another open " +
|
|
||||||
"handle running.",
|
|
||||||
);
|
);
|
||||||
process.exitCode = 1;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
return written;
|
||||||
|
};
|
||||||
|
|
||||||
// Jest's own command-line program. It reads its arguments from process.argv.
|
// Jest's own command-line program. It reads its arguments from process.argv.
|
||||||
require("jest/bin/jest");
|
require("jest/bin/jest");
|
||||||
@@ -12,7 +12,8 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
transactionTimeHtml,
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
||||||
@@ -235,11 +236,10 @@ function renderTransactions(txs) {
|
|||||||
const nameStr = escapeHtml(title || ensName || "");
|
const nameStr = escapeHtml(title || ensName || "");
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity-50" : "";
|
||||||
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
||||||
// The time has a line of its own, so a long time or direction label
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
// never pushes the other onto a second line.
|
|
||||||
html += `<div class="text-muted">${transactionTimeHtml(tx.timestamp)}</div>`;
|
|
||||||
html += `<div class="text-right">${dirLabel}${err}</div>`;
|
|
||||||
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
transactionTimeHtml,
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { bundledToken, resolveSymbol } = require("../../shared/tokenList");
|
const { bundledToken, resolveSymbol } = require("../../shared/tokenList");
|
||||||
@@ -328,11 +329,10 @@ function renderTransactions(txs) {
|
|||||||
const nameStr = escapeHtml(title || ensName || "");
|
const nameStr = escapeHtml(title || ensName || "");
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity-50" : "";
|
||||||
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
||||||
// The time has a line of its own, so a long time or direction label
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
// never pushes the other onto a second line.
|
|
||||||
html += `<div class="text-muted">${transactionTimeHtml(tx.timestamp)}</div>`;
|
|
||||||
html += `<div class="text-right">${dirLabel}${err}</div>`;
|
|
||||||
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -615,8 +615,9 @@ function formatAddressHtml(address, ensName, maxLen, title) {
|
|||||||
return renderAddressHtml(address, { title, ensName, maxLen });
|
return renderAddressHtml(address, { title, ensName, maxLen });
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ISO datetime of a transaction's time, in UTC when the UTC Timestamps
|
// A transaction's time as every screen shows it (README, Amounts): the ISO
|
||||||
// setting is on. Views show it through transactionTimeHtml().
|
// datetime, in UTC when the UTC Timestamps setting is on, and the relative age.
|
||||||
|
// Views import these two; they keep no copies.
|
||||||
function isoDate(timestamp) {
|
function isoDate(timestamp) {
|
||||||
const d = new Date(timestamp * 1000);
|
const d = new Date(timestamp * 1000);
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
@@ -673,15 +674,6 @@ function timeAgo(timestamp) {
|
|||||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
||||||
}
|
}
|
||||||
|
|
||||||
// A transaction's time as every screen shows it (README, Amounts): the ISO
|
|
||||||
// datetime, then the relative age in parentheses. With copyable set, the
|
|
||||||
// datetime is click-to-copy, and the caller attaches the copy handlers.
|
|
||||||
function transactionTimeHtml(timestamp, { copyable = false } = {}) {
|
|
||||||
const iso = isoDate(timestamp);
|
|
||||||
const isoHtml = copyable ? copyableHtml(iso) : escapeHtml(iso);
|
|
||||||
return isoHtml + " (" + escapeHtml(timeAgo(timestamp)) + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared external-link icon SVG used across all views.
|
// Shared external-link icon SVG used across all views.
|
||||||
const EXT_ICON =
|
const EXT_ICON =
|
||||||
`<span class="inline-block w-[10px] h-[10px] ml-[4px] align-middle">` +
|
`<span class="inline-block w-[10px] h-[10px] ml-[4px] align-middle">` +
|
||||||
@@ -877,5 +869,6 @@ module.exports = {
|
|||||||
reviewedNetworkProvider,
|
reviewedNetworkProvider,
|
||||||
EXT_ICON,
|
EXT_ICON,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
transactionTimeHtml,
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
};
|
};
|
||||||
@@ -5,7 +5,8 @@ const {
|
|||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
markTrackedTokensNotFetched,
|
markTrackedTokensNotFetched,
|
||||||
transactionTimeHtml,
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
txCounterpartyHtml,
|
txCounterpartyHtml,
|
||||||
HISTORY_OFF_TEXT,
|
HISTORY_OFF_TEXT,
|
||||||
@@ -149,11 +150,10 @@ function renderHomeTxList(ctx) {
|
|||||||
const titleStr = title ? escapeHtml(title) : "";
|
const titleStr = title ? escapeHtml(title) : "";
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity-50" : "";
|
||||||
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
||||||
// The time has a line of its own, so a long time or direction label
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
// never pushes the other onto a second line.
|
|
||||||
html += `<div class="text-muted">${transactionTimeHtml(tx.timestamp)}</div>`;
|
|
||||||
html += `<div class="text-right">${dirLabel}${err}</div>`;
|
|
||||||
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
|
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const {
|
|||||||
CONTRACT_CREATION_TEXT,
|
CONTRACT_CREATION_TEXT,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
transactionTimeHtml,
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
blockieHtml,
|
blockieHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
@@ -203,9 +204,9 @@ function render() {
|
|||||||
);
|
);
|
||||||
loadFullTxDetails(tx.hash, tx.chainId);
|
loadFullTxDetails(tx.hash, tx.chainId);
|
||||||
|
|
||||||
$("tx-detail-time").innerHTML = transactionTimeHtml(tx.timestamp, {
|
const isoStr = isoDate(tx.timestamp);
|
||||||
copyable: true,
|
$("tx-detail-time").innerHTML =
|
||||||
});
|
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
|
||||||
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
||||||
showView("transaction");
|
showView("transaction");
|
||||||
attachCopyHandlers("view-transaction");
|
attachCopyHandlers("view-transaction");
|
||||||
|
|||||||
@@ -951,71 +951,6 @@ test("tap-to-copy on the transaction detail screen copies the address (#151)", a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------ history row time (#760)
|
|
||||||
|
|
||||||
// A Home history row shows the ISO datetime and then the relative age, on a
|
|
||||||
// line of its own. Measured at the popup's size: the time line is one line
|
|
||||||
// high, and no line of a row is wider than the row or the popup.
|
|
||||||
test("a Home history row shows the datetime and the age, one line high, without overflow (#760)", async (env) => {
|
|
||||||
await goHome(env.page);
|
|
||||||
env.routeOpts.seedNativeTransfer = true;
|
|
||||||
const page = await openPopup(env.ctx, env.popupUrl);
|
|
||||||
try {
|
|
||||||
await page.setViewportSize(POPUP_VIEWPORT);
|
|
||||||
await visible(page, "#view-main");
|
|
||||||
await page.waitForFunction(
|
|
||||||
() =>
|
|
||||||
document.querySelectorAll("#home-tx-list .home-tx-row")
|
|
||||||
.length === 2,
|
|
||||||
null,
|
|
||||||
{ timeout: 30000 },
|
|
||||||
);
|
|
||||||
const rows = await page.evaluate(() => {
|
|
||||||
const popupRight = document.body.getBoundingClientRect().right;
|
|
||||||
return [
|
|
||||||
...document.querySelectorAll("#home-tx-list .home-tx-row"),
|
|
||||||
].map((row) => {
|
|
||||||
const time = row.firstElementChild;
|
|
||||||
return {
|
|
||||||
time: time.textContent,
|
|
||||||
timeHeight: time.getBoundingClientRect().height,
|
|
||||||
lineHeight: parseFloat(getComputedStyle(time).lineHeight),
|
|
||||||
overflowingLines: [...row.children].filter(
|
|
||||||
(line) =>
|
|
||||||
line.scrollWidth > line.clientWidth ||
|
|
||||||
line.getBoundingClientRect().right > popupRight,
|
|
||||||
).length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log("# Home history rows: " + JSON.stringify(rows));
|
|
||||||
for (const row of rows) {
|
|
||||||
assert(
|
|
||||||
/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(Z|[+-]\d\d:\d\d) \(\d+ \w+ ago\)$/.test(
|
|
||||||
row.time,
|
|
||||||
),
|
|
||||||
"the time line is not the ISO datetime and then the age: " +
|
|
||||||
JSON.stringify(row.time),
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
row.timeHeight === row.lineHeight,
|
|
||||||
"the time line is " +
|
|
||||||
row.timeHeight +
|
|
||||||
"px high, not one line of " +
|
|
||||||
row.lineHeight +
|
|
||||||
"px",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
row.overflowingLines === 0,
|
|
||||||
row.overflowingLines + " line(s) of a row overflow it",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
env.routeOpts.seedNativeTransfer = false;
|
|
||||||
await page.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ------------------------- the last of the #150 and #151 items (#295)
|
// ------------------------- the last of the #150 and #151 items (#295)
|
||||||
//
|
//
|
||||||
// Add Token's confirm button, TransactionDetail opened from the token screen
|
// Add Token's confirm button, TransactionDetail opened from the token screen
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// A test file that starts an interval and never clears it.
|
||||||
|
// tests/jestRunner.test.js runs script/lib/jest.js on this file alone. The
|
||||||
|
// name does not end in .test.js, so the normal run never picks it up.
|
||||||
|
|
||||||
|
test("starts an interval and never clears it", () => {
|
||||||
|
setInterval(() => {}, 1000);
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// script/lib/jest.js, which package.json's test scripts run, fails a run in
|
||||||
|
// which a test left an open handle, instead of hanging on it.
|
||||||
|
|
||||||
|
const { spawnSync } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = path.join(__dirname, "..");
|
||||||
|
|
||||||
|
test("a test that leaves an interval running fails the run", () => {
|
||||||
|
// The run ends about one second after its last test. Without the check in
|
||||||
|
// script/lib/jest.js it would never end, and the timeout stops it.
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
"script/lib/jest.js",
|
||||||
|
"--runInBand",
|
||||||
|
"--testMatch",
|
||||||
|
"<rootDir>/tests/fixtures/leavesIntervalRunning.js",
|
||||||
|
],
|
||||||
|
{ cwd: root, encoding: "utf8", timeout: 20000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.signal).toBeNull();
|
||||||
|
expect(result.status).toBe(1);
|
||||||
|
expect(result.stderr).toContain("1 passed");
|
||||||
|
expect(result.stderr).toContain(
|
||||||
|
"a test left a timer or another open handle running",
|
||||||
|
);
|
||||||
|
});
|
||||||
+62
-101
@@ -1,14 +1,15 @@
|
|||||||
// A transaction's time shows the ISO datetime and the relative age on every
|
// A transaction's time is written by isoDate() and timeAgo() in
|
||||||
// screen that shows one (README, Amounts). Home, AddressDetail and AddressToken
|
// src/popup/views/helpers.js on every screen that shows one (README, Amounts;
|
||||||
// used to show only the age, with the datetime as a tooltip
|
// https://git.eeqj.de/sneak/AutistMask/issues/168). AddressDetail and
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/760).
|
// AddressToken used to define their own copies, so a fix to the shared pair
|
||||||
|
// would not have reached them.
|
||||||
//
|
//
|
||||||
// The views run against a minimal DOM stub, as in
|
// The pair is replaced before the views are loaded, because a view takes it
|
||||||
// tests/historyAfterNetworkSwitch.test.js, and over
|
// when it loads. A view that writes the time with a copy of its own shows the
|
||||||
// tests/support/storageStub.js.
|
// real time instead of the replacement.
|
||||||
|
//
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
// Driven against a minimal DOM stub in the shape
|
||||||
const { STATE_SCHEMA_VERSION } = require("../src/shared/stateSchema");
|
// tests/contractCreation.test.js uses.
|
||||||
|
|
||||||
jest.mock("../src/shared/log", () => ({
|
jest.mock("../src/shared/log", () => ({
|
||||||
log: {
|
log: {
|
||||||
@@ -25,78 +26,33 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// The history lists ask the explorer for their transactions and resolve ENS
|
// The history lists ask the explorer for their transactions and resolve ENS
|
||||||
// names for them; here the explorer answers with mockHistory() and no name
|
// names for them; here the explorer answers with mockHistory and no name
|
||||||
// resolves.
|
// resolves.
|
||||||
|
let mockHistory = [];
|
||||||
jest.mock("../src/shared/transactions", () => ({
|
jest.mock("../src/shared/transactions", () => ({
|
||||||
...jest.requireActual("../src/shared/transactions"),
|
...jest.requireActual("../src/shared/transactions"),
|
||||||
fetchRecentTransactions: async () => mockHistory(),
|
fetchRecentTransactions: async () => mockHistory,
|
||||||
}));
|
}));
|
||||||
jest.mock("../src/shared/ens", () => ({
|
jest.mock("../src/shared/ens", () => ({
|
||||||
...jest.requireActual("../src/shared/ens"),
|
...jest.requireActual("../src/shared/ens"),
|
||||||
resolveEnsNames: async () => new Map(),
|
resolveEnsNames: async () => new Map(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { state, loadState } = require("../src/shared/state");
|
globalThis.chrome = {
|
||||||
const home = require("../src/popup/views/home");
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpers = require("../src/popup/views/helpers");
|
||||||
|
jest.spyOn(helpers, "isoDate").mockReturnValue("SHARED-ISO-DATE");
|
||||||
|
jest.spyOn(helpers, "timeAgo").mockReturnValue("SHARED-TIME-AGO");
|
||||||
|
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
const addressDetail = require("../src/popup/views/addressDetail");
|
const addressDetail = require("../src/popup/views/addressDetail");
|
||||||
const addressToken = require("../src/popup/views/addressToken");
|
const addressToken = require("../src/popup/views/addressToken");
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
const FROM = "0x0000000000000000000000000000000000000a11";
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
||||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
const TIMESTAMP = 1790000000;
|
|
||||||
// TIMESTAMP as shown with "UTC Timestamps" on, three hours after it.
|
|
||||||
const SHOWN_TIME = "2026-09-21T14:13:20Z (3 hours ago)";
|
|
||||||
|
|
||||||
// A profile on Ethereum Mainnet with one address, "Use Blockscout" and "UTC
|
|
||||||
// Timestamps" on.
|
|
||||||
function profile() {
|
|
||||||
return {
|
|
||||||
schemaVersion: STATE_SCHEMA_VERSION,
|
|
||||||
hasWallet: true,
|
|
||||||
wallets: [
|
|
||||||
{
|
|
||||||
type: "hd",
|
|
||||||
name: "Main",
|
|
||||||
xpub: "xpub-main",
|
|
||||||
encryptedSecret: "encrypted-secret",
|
|
||||||
nextIndex: 1,
|
|
||||||
addresses: [
|
|
||||||
{ address: FROM, balance: "1.0", tokenBalances: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
activeAddress: FROM,
|
|
||||||
networkId: "mainnet",
|
|
||||||
rpcUrl: "https://ethereum-rpc.publicnode.com",
|
|
||||||
blockscoutUrl: "https://eth.blockscout.com/api/v2",
|
|
||||||
useBlockscout: true,
|
|
||||||
utcTimestamps: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A transfer of ETH from FROM, as Blockscout answers it.
|
|
||||||
function mockHistory() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
|
|
||||||
blockNumber: 1,
|
|
||||||
from: FROM,
|
|
||||||
to: RECIPIENT,
|
|
||||||
value: "0.0000",
|
|
||||||
exactValue: "0.0",
|
|
||||||
rawAmount: "0",
|
|
||||||
rawUnit: "wei",
|
|
||||||
symbol: "ETH",
|
|
||||||
timestamp: TIMESTAMP,
|
|
||||||
isError: false,
|
|
||||||
directionLabel: "Sent",
|
|
||||||
direction: "sent",
|
|
||||||
contractAddress: null,
|
|
||||||
chainId: "0x1",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeElement(id) {
|
function makeElement(id) {
|
||||||
const el = {
|
const el = {
|
||||||
@@ -134,7 +90,6 @@ function makeDocument() {
|
|||||||
return els.get(id);
|
return els.get(id);
|
||||||
},
|
},
|
||||||
createElement: () => makeElement("created"),
|
createElement: () => makeElement("created"),
|
||||||
addEventListener: () => {},
|
|
||||||
body: { prepend: () => {} },
|
body: { prepend: () => {} },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -143,58 +98,64 @@ function node(id) {
|
|||||||
return globalThis.document.getElementById(id);
|
return globalThis.document.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lets every promise that is not held open settle.
|
// A transaction FROM sent, as the history lists hold it.
|
||||||
function settle() {
|
function historyTx() {
|
||||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
return {
|
||||||
|
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
|
||||||
|
from: FROM,
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0.0000",
|
||||||
|
exactValue: "0.0",
|
||||||
|
rawAmount: "0",
|
||||||
|
rawUnit: "wei",
|
||||||
|
symbol: "ETH",
|
||||||
|
timestamp: 1790000000,
|
||||||
|
isError: false,
|
||||||
|
directionLabel: "Sent",
|
||||||
|
direction: "sent",
|
||||||
|
contractAddress: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
const storage = makeStorageStub({ autistmask: profile() });
|
|
||||||
globalThis.chrome = {
|
|
||||||
storage: { local: storage.local, onChanged: storage.onChanged },
|
|
||||||
};
|
|
||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
jest.spyOn(Date, "now").mockReturnValue((TIMESTAMP + 3 * 3600) * 1000);
|
state.wallets = [
|
||||||
await loadState();
|
{
|
||||||
|
name: "Main",
|
||||||
|
type: "key",
|
||||||
|
addresses: [{ address: FROM, balance: "0.0000" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
|
state.currentView = null;
|
||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
state.selectedToken = "ETH";
|
state.selectedToken = "ETH";
|
||||||
const ctx = { pageClosed: new AbortController().signal };
|
// The history is Blockscout's.
|
||||||
addressDetail.init(ctx);
|
state.useBlockscout = true;
|
||||||
addressToken.init(ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
delete globalThis.chrome;
|
|
||||||
delete globalThis.document;
|
|
||||||
delete globalThis.window;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
[
|
|
||||||
"Home",
|
|
||||||
"home-tx-list",
|
|
||||||
() => home.render({ pageClosed: new AbortController().signal }),
|
|
||||||
],
|
|
||||||
["AddressDetail", "tx-list", () => addressDetail.show()],
|
["AddressDetail", "tx-list", () => addressDetail.show()],
|
||||||
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
||||||
])("a transaction history row on %s", (_name, listId, open) => {
|
])("a transaction history row on %s", (_name, listId, open) => {
|
||||||
test("shows the ISO datetime and then the relative age", async () => {
|
test("shows the time written by the shared isoDate() and timeAgo()", async () => {
|
||||||
|
mockHistory = [historyTx()];
|
||||||
open();
|
open();
|
||||||
// The list is drawn once the history has been fetched.
|
// The list is drawn once the history has been fetched.
|
||||||
for (let i = 0; i < 10; i++) await settle();
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
const html = node(listId).innerHTML;
|
const html = node(listId).innerHTML;
|
||||||
expect(html).toContain(">" + SHOWN_TIME + "<");
|
expect(html).toContain('title="SHARED-ISO-DATE"');
|
||||||
expect(html).not.toContain("title=");
|
expect(html).toContain(">SHARED-TIME-AGO<");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the transaction detail view shows the ISO datetime, copyable, and then the relative age", () => {
|
test("the transaction detail view shows the time written by the shared isoDate() and timeAgo()", () => {
|
||||||
transactionDetail.show(mockHistory()[0]);
|
transactionDetail.show(historyTx());
|
||||||
const html = node("tx-detail-time").innerHTML;
|
const html = node("tx-detail-time").innerHTML;
|
||||||
expect(html).toContain('data-copy="2026-09-21T14:13:20Z"');
|
expect(html).toContain("SHARED-ISO-DATE");
|
||||||
expect(html).toContain(">2026-09-21T14:13:20Z</span> (3 hours ago)");
|
expect(html).toContain("(SHARED-TIME-AGO)");
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user