Copies .dockerignore, .gitignore, .prettierignore, check.yml and REPO_POLICIES.md from sneak/prompts at dd4027b, keeping the repo's own entries (dist/, release/, yarn files) after the canonical content. The Dockerfile gets separate lint and test phases; its last stage depends on both, checks the git describe version and runs make build. script/lint, test, check, cibuild and docker are the canonical models. check-censored moves into the lint phase and test-verify-build into the test phase. fmt and fmt-check fall back to the nvm-installed node. The e2e image builds are uncached. Comments citing the old test caps or what runs a script are updated. Model: opus-5-5
245 lines
8.1 KiB
JavaScript
245 lines
8.1 KiB
JavaScript
// Tests for the dust threshold field in Settings (issue #233).
|
|
//
|
|
// Two halves: what the parse accepts, and what the settings view does with a
|
|
// rejection. The view half runs against the real change handler with the DOM
|
|
// helpers stubbed out, because the bug was not in the parse — it was that a
|
|
// rejection said nothing.
|
|
|
|
const {
|
|
DUST_THRESHOLD_MESSAGE,
|
|
parseDustThresholdGwei,
|
|
} = require("../src/popup/dustThreshold");
|
|
|
|
describe("parsing the dust threshold", () => {
|
|
test("accepts a whole number of gwei", () => {
|
|
expect(parseDustThresholdGwei("100000")).toBe(100000);
|
|
expect(parseDustThresholdGwei("1")).toBe(1);
|
|
});
|
|
|
|
// Zero is a real setting, not an empty field: it hides nothing.
|
|
test("accepts zero", () => {
|
|
expect(parseDustThresholdGwei("0")).toBe(0);
|
|
});
|
|
|
|
test("accepts surrounding whitespace", () => {
|
|
expect(parseDustThresholdGwei(" 250 ")).toBe(250);
|
|
});
|
|
|
|
test("rejects an empty field", () => {
|
|
expect(parseDustThresholdGwei("")).toBe(null);
|
|
expect(parseDustThresholdGwei(" ")).toBe(null);
|
|
});
|
|
|
|
test("rejects a negative threshold", () => {
|
|
expect(parseDustThresholdGwei("-1")).toBe(null);
|
|
});
|
|
|
|
// parseInt used to read this as 1, which is not what was typed.
|
|
test("rejects a fractional value", () => {
|
|
expect(parseDustThresholdGwei("1.5")).toBe(null);
|
|
expect(parseDustThresholdGwei("1.0")).toBe(null);
|
|
});
|
|
|
|
// parseInt used to read this as 100. The unit is printed beside the
|
|
// field already.
|
|
test("rejects a value carrying its unit", () => {
|
|
expect(parseDustThresholdGwei("100 gwei")).toBe(null);
|
|
});
|
|
|
|
// Number() reads this as 16. Storing 16 for a field that was told to
|
|
// want a whole number of gwei would be the same silent substitution the
|
|
// message exists to end.
|
|
test("rejects hex notation", () => {
|
|
expect(parseDustThresholdGwei("0x10")).toBe(null);
|
|
});
|
|
|
|
// Number() reads this as 1000.
|
|
test("rejects exponent notation", () => {
|
|
expect(parseDustThresholdGwei("1e3")).toBe(null);
|
|
});
|
|
|
|
test("rejects other non-numeric input", () => {
|
|
expect(parseDustThresholdGwei("lots")).toBe(null);
|
|
expect(parseDustThresholdGwei("+5")).toBe(null);
|
|
expect(parseDustThresholdGwei("Infinity")).toBe(null);
|
|
expect(parseDustThresholdGwei(undefined)).toBe(null);
|
|
expect(parseDustThresholdGwei(5)).toBe(null);
|
|
});
|
|
|
|
// Beyond 2^53 the digits would round on the way in, so the stored
|
|
// threshold would not be the one typed.
|
|
test("rejects a value too large to hold exactly", () => {
|
|
expect(parseDustThresholdGwei("9007199254740993")).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe("the rejection message", () => {
|
|
// README, Language & Labeling: error messages are full sentences.
|
|
test("is a full sentence naming the constraint", () => {
|
|
expect(DUST_THRESHOLD_MESSAGE).toMatch(/^[A-Z].*\.$/);
|
|
expect(DUST_THRESHOLD_MESSAGE).toContain("whole number of gwei");
|
|
expect(DUST_THRESHOLD_MESSAGE).toContain("zero or greater");
|
|
});
|
|
});
|
|
|
|
describe("the flash line the message is shown in", () => {
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const POPUP_HTML = fs.readFileSync(
|
|
path.join(__dirname, "..", "src", "popup", "index.html"),
|
|
"utf8",
|
|
);
|
|
|
|
// This asserts only that the reservation exists in the markup. It does
|
|
// NOT and CANNOT assert that the message fits inside it: jest runs on
|
|
// the node environment here, with no layout engine, so every rendered
|
|
// height is zero. An earlier version of this block claimed to pin the
|
|
// No Layout Shift policy with this regex, and it passed at any message
|
|
// length, including one that wrapped to two lines and pushed the
|
|
// settings view down 12px.
|
|
//
|
|
// The line cuts a message too long for it with an ellipsis (see
|
|
// showFlash() in src/popup/views/helpers.js). The assertions that
|
|
// measure that, in a real browser at the documented 360x600 popup, are
|
|
// "a rejected dust threshold shifts no layout (#233)" and "an over-long
|
|
// flash message keeps to one line (#252)" in tests/e2e/run.js, run by
|
|
// make test-e2e. They are not in make check because REPO_POLICIES.md
|
|
// caps make test at 60 seconds and a browser suite does not fit.
|
|
test("reserves its height in the markup", () => {
|
|
const flashLine = POPUP_HTML.match(
|
|
/<div\s+id="flash-msg"\s+class="([^"]*)"/,
|
|
);
|
|
expect(flashLine).not.toBeNull();
|
|
expect(flashLine[1]).toMatch(/min-h-\[/);
|
|
});
|
|
});
|
|
|
|
describe("the settings view on a change to the field", () => {
|
|
let elements;
|
|
let flashes;
|
|
let saves;
|
|
let state;
|
|
|
|
// A stand-in for one DOM node: enough of an element for init() to set
|
|
// properties on it and hang listeners off it.
|
|
function fakeElement() {
|
|
return {
|
|
value: "",
|
|
checked: false,
|
|
textContent: "",
|
|
href: "",
|
|
style: {},
|
|
dataset: {},
|
|
classList: { add() {}, remove() {} },
|
|
listeners: {},
|
|
addEventListener(event, handler) {
|
|
this.listeners[event] = handler;
|
|
},
|
|
querySelectorAll: () => [],
|
|
};
|
|
}
|
|
|
|
function loadSettingsView() {
|
|
elements = {};
|
|
flashes = [];
|
|
saves = 0;
|
|
|
|
jest.resetModules();
|
|
|
|
jest.doMock("../src/popup/views/helpers", () => ({
|
|
$: (id) => (elements[id] ||= fakeElement()),
|
|
showView: () => {},
|
|
updateDebugBanner: () => {},
|
|
showFlash: (msg) => flashes.push(msg),
|
|
escapeHtml: (s) => s,
|
|
flashCopyFeedback: () => {},
|
|
goBack: () => {},
|
|
pushCurrentView: () => {},
|
|
onViewLeave: () => {},
|
|
VIEWS: [],
|
|
}));
|
|
|
|
state = require("../src/shared/state").state;
|
|
state.dustThresholdGwei = 100000;
|
|
|
|
const settings = require("../src/popup/views/settings");
|
|
settings.init({});
|
|
return elements["settings-dust-threshold"];
|
|
}
|
|
|
|
beforeEach(() => {
|
|
globalThis.chrome = {
|
|
runtime: { sendMessage: () => {} },
|
|
storage: {
|
|
local: {
|
|
get: async () => ({}),
|
|
set: async () => {
|
|
saves++;
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.dontMock("../src/popup/views/helpers");
|
|
delete globalThis.chrome;
|
|
});
|
|
|
|
async function change(field, typed) {
|
|
field.value = typed;
|
|
await field.listeners.change();
|
|
}
|
|
|
|
test("a valid value is stored and says nothing", async () => {
|
|
const field = loadSettingsView();
|
|
|
|
await change(field, "250");
|
|
|
|
expect(state.dustThresholdGwei).toBe(250);
|
|
expect(field.value).toBe(250);
|
|
expect(flashes).toEqual([]);
|
|
expect(saves).toBe(1);
|
|
});
|
|
|
|
test("a rejected value shows the message and is not stored", async () => {
|
|
const field = loadSettingsView();
|
|
|
|
await change(field, "1.5");
|
|
|
|
expect(state.dustThresholdGwei).toBe(100000);
|
|
expect(flashes).toEqual([DUST_THRESHOLD_MESSAGE]);
|
|
expect(saves).toBe(0);
|
|
});
|
|
|
|
// The snap-back is the behaviour the message explains, so it stays.
|
|
test("a rejected value still resyncs the field to what is stored", async () => {
|
|
const field = loadSettingsView();
|
|
|
|
await change(field, "100 gwei");
|
|
|
|
expect(field.value).toBe(100000);
|
|
});
|
|
|
|
test("every rejected notation gets the same one message", async () => {
|
|
for (const typed of ["", "-1", "1.5", "100 gwei", "0x10", "1e3"]) {
|
|
const field = loadSettingsView();
|
|
|
|
await change(field, typed);
|
|
|
|
expect(flashes).toEqual([DUST_THRESHOLD_MESSAGE]);
|
|
expect(state.dustThresholdGwei).toBe(100000);
|
|
}
|
|
});
|
|
|
|
test("zero is accepted, not treated as an empty field", async () => {
|
|
const field = loadSettingsView();
|
|
|
|
await change(field, "0");
|
|
|
|
expect(state.dustThresholdGwei).toBe(0);
|
|
expect(flashes).toEqual([]);
|
|
});
|
|
});
|