diff --git a/README.md b/README.md index 956769b..448b451 100644 --- a/README.md +++ b/README.md @@ -711,8 +711,8 @@ screen, including ExportPrivKey, falls back to Home. `[x]` delete button, plus a "+ Add wallet" button - Tracked Tokens: one row per tracked token with an `[x]` remove button, plus a "+ Add token" button - - Display: "Show tracked tokens with zero balance" checkbox and a Theme - selector (System / Light / Dark) + - Display: "Show tracked tokens with zero balance" checkbox, "UTC + Timestamps" checkbox, and a Theme selector (System / Light / Dark) - Network: network selector (Ethereum Mainnet / Sepolia Testnet); switching resets the RPC and Blockscout endpoints to that network's defaults - Ethereum RPC: endpoint URL input + "Save" button (validated against @@ -723,7 +723,6 @@ screen, including ExportPrivKey, falls back to Home. - "Hide tokens with fewer than 1,000 holders" checkbox - "Hide transactions from detected fraud contracts" checkbox - "Hide dust transactions below N gwei" checkbox + threshold input - - "UTC Timestamps" checkbox - Allowed Sites: list with remove buttons - Denied Sites: list with remove buttons - About: project link, license, author, version, release date, and the diff --git a/TODO.md b/TODO.md index 08910e2..946db98 100644 --- a/TODO.md +++ b/TODO.md @@ -44,6 +44,9 @@ undefined identifiers, which is how # Completed Steps +- 2026-08-11: UTC Timestamps checkbox moved from the Token Spam Protection well + into Display, next to the theme selector + ([#212](https://git.eeqj.de/sneak/AutistMask/issues/212)). - 2026-08-11: Policy compliance sweep — conditional verbose test rerun, local Tailwind binary instead of `npx`, `--frozen-lockfile` on `make install`, and the Makefile-only targets documented in the README diff --git a/docs/README.md b/docs/README.md index 7244b8e..08f244a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -359,16 +359,16 @@ Click the gear icon on the home screen to access settings: - **Wallets**: Your wallets, and "+ Add wallet". - **Tracked Tokens**: The ERC-20 tokens tracked across all addresses, and "+ Add token". -- **Display**: Toggle whether tracked tokens with zero balance are shown, and - choose the theme (System, Light, or Dark). +- **Display**: Toggle whether tracked tokens with zero balance are shown, switch + timestamps to UTC, and choose the theme (System, Light, or Dark). - **Network**: Switch between Ethereum Mainnet and Sepolia Testnet. Switching resets the RPC and Blockscout endpoints to that network's defaults. - **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC. You can use your own node for maximum privacy. - **Blockscout API**: Change the Blockscout instance used for token balances and transaction history. You can use a self-hosted instance. -- **Token Spam Protection**: Toggle individual scam filters, set the dust - transaction threshold, and switch timestamps to UTC. +- **Token Spam Protection**: Toggle individual scam filters and set the dust + transaction threshold. - **Allowed Sites / Denied Sites**: View and manage web3 site permissions. - **About**: License, author, version, release date, and a link to the commit this build came from. diff --git a/src/popup/index.html b/src/popup/index.html index a38e901..d05f91e 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -869,6 +869,12 @@ /> Show tracked tokens with zero balance +
- UTC Timestamps -
diff --git a/tests/settingsUtcTimestamps.test.js b/tests/settingsUtcTimestamps.test.js new file mode 100644 index 0000000..91526db --- /dev/null +++ b/tests/settingsUtcTimestamps.test.js @@ -0,0 +1,111 @@ +// Tests for the UTC Timestamps setting. +// +// The checkbox was moved out of the Token Spam Protection well and into the +// Display well next to the theme selector. It is wired by id through the $() +// helper, so the move cannot break the handler — but nothing in the suite said +// so. These tests pin both halves down: the markup lives in Display and +// nowhere else, and the value still round-trips through storage. + +const fs = require("fs"); +const path = require("path"); + +const POPUP_HTML = fs.readFileSync( + path.join(__dirname, "..", "src", "popup", "index.html"), + "utf8", +); + +// The body of one `
` well, selected by its heading. +function wellWithHeading(html, heading) { + const headingIndex = html.indexOf( + '

' + heading + "

", + ); + expect(headingIndex).toBeGreaterThan(-1); + const start = html.lastIndexOf('
{ + test("the checkbox appears exactly once in the popup markup", () => { + const matches = POPUP_HTML.match(/id="settings-utc-timestamps"/g); + expect(matches).toHaveLength(1); + }); + + test("it renders in the Display well, alongside the theme selector", () => { + const display = wellWithHeading(POPUP_HTML, "Display"); + + expect(display).toContain('id="settings-utc-timestamps"'); + expect(display).toContain('id="settings-theme"'); + }); + + test("it does not render in the Token Spam Protection well", () => { + const spam = wellWithHeading(POPUP_HTML, "Token Spam Protection"); + + expect(spam).not.toContain('id="settings-utc-timestamps"'); + // The filters that do belong there are untouched. + expect(spam).toContain('id="settings-hide-low-holders"'); + expect(spam).toContain('id="settings-hide-fraud-contracts"'); + expect(spam).toContain('id="settings-hide-dust"'); + expect(spam).toContain('id="settings-dust-threshold"'); + }); +}); + +describe("the UTC Timestamps setting round-trips through storage", () => { + let store; + + function loadStateModule() { + store = {}; + global.chrome = { + storage: { + local: { + get: async (key) => + key in store ? { [key]: store[key] } : {}, + set: async (obj) => Object.assign(store, obj), + }, + }, + }; + jest.resetModules(); + return require("../src/shared/state"); + } + + afterEach(() => { + delete global.chrome; + }); + + test("defaults to off with nothing persisted", async () => { + const { state, loadState } = loadStateModule(); + + await loadState(); + + expect(state.utcTimestamps).toBe(false); + }); + + test("an enabled checkbox is persisted and read back", async () => { + const first = loadStateModule(); + + // What the change handler in views/settings.js does. + first.state.utcTimestamps = true; + await first.saveState(); + expect(store.autistmask.utcTimestamps).toBe(true); + + // A fresh popup load sees it. + jest.resetModules(); + const second = require("../src/shared/state"); + expect(second.state.utcTimestamps).toBe(false); + await second.loadState(); + expect(second.state.utcTimestamps).toBe(true); + }); + + test("turning it back off is persisted too", async () => { + const { state, saveState, loadState } = loadStateModule(); + + state.utcTimestamps = true; + await saveState(); + state.utcTimestamps = false; + await saveState(); + + state.utcTimestamps = true; + await loadState(); + expect(state.utcTimestamps).toBe(false); + }); +});