POPUP_VIEWPORT in tests/e2e/run.js said 360x600, while body in src/popup/styles/main.css is 396px wide and the background opens an approval window at 396x600. It is now 396x600, defined once near the top of the file, and the hostile token symbol's frame takes its size from it. assertAddressesFit() now measures the page at that size, then puts back the size it found. Every page it measured, the popup's own screens and the two approval prompts, was 1280x720, where a page never scrolls sideways, so its sideways-scroll check could not fail. README's list of what the Chrome suite leaves out says what sizing the page does not cover. Comments in src/popup/index.html and tests/dustThreshold.test.js that said 360x600 now say 396x600. 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 396x600 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([]);
|
|
});
|
|
});
|