Compare commits
2
Commits
eafc9e4fe3
...
d14523f260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d14523f260 | ||
|
|
e865099c5b |
@@ -218,7 +218,9 @@ development workflow, and the Makefile targets are thin shims that call them. We
|
|||||||
provide:
|
provide:
|
||||||
|
|
||||||
- `script/bootstrap` — install all dependencies (pinned node via nvm if needed,
|
- `script/bootstrap` — install all dependencies (pinned node via nvm if needed,
|
||||||
yarn via corepack, `yarn install --frozen-lockfile`)
|
yarn via corepack, `yarn install --frozen-lockfile`), then fail, naming the
|
||||||
|
package, if node cannot find a package listed in `dependencies` or
|
||||||
|
`devDependencies` of `package.json`
|
||||||
- `script/setup` — make a fresh clone ready for development: bootstrap plus the
|
- `script/setup` — make a fresh clone ready for development: bootstrap plus the
|
||||||
git pre-commit hook
|
git pre-commit hook
|
||||||
- `script/projectname` — print the project name (used for the Docker image tag)
|
- `script/projectname` — print the project name (used for the Docker image tag)
|
||||||
@@ -1468,6 +1470,9 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Reveal" (wrong password) → full-sentence error on the error line, nothing
|
- "Reveal" (wrong password) → full-sentence error on the error line, nothing
|
||||||
revealed (no screen change)
|
revealed (no screen change)
|
||||||
- "Back" → previous screen (AddressDetail)
|
- "Back" → previous screen (AddressDetail)
|
||||||
|
- Settings gear → **Settings**, whose "Back" goes to AddressDetail: leaving
|
||||||
|
drops the address the screen was showing, so it also takes the screen off
|
||||||
|
the Back stack
|
||||||
- **Secret handling**: nothing is decrypted, no key is derived, and nothing is
|
- **Secret handling**: nothing is decrypted, no key is derived, and nothing is
|
||||||
written into the page until the password is accepted; the key is never stored
|
written into the page until the password is accepted; the key is never stored
|
||||||
in state, and it is wiped from the page whenever the screen is left by any
|
in state, and it is wiped from the page whenever the screen is left by any
|
||||||
@@ -1795,6 +1800,9 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Reveal" (wrong password) → full-sentence error, nothing revealed (no
|
- "Reveal" (wrong password) → full-sentence error, nothing revealed (no
|
||||||
screen change)
|
screen change)
|
||||||
- "Back" → previous screen (Settings)
|
- "Back" → previous screen (Settings)
|
||||||
|
- Settings gear → **Settings**, whose "Back" never lands back on this
|
||||||
|
screen: leaving drops the wallet the screen was showing, so it also takes
|
||||||
|
the screen off the Back stack
|
||||||
- **Secret handling**: nothing is decrypted or written into the page until the
|
- **Secret handling**: nothing is decrypted or written into the page until the
|
||||||
password is accepted; the phrase is never stored in state, and it is wiped
|
password is accepted; the phrase is never stored in state, and it is wiped
|
||||||
from the page whenever the screen is left by any route, including the Settings
|
from the page whenever the screen is left by any route, including the Settings
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-06: `script/bootstrap` no longer reports success while node cannot
|
||||||
|
find a package listed in `dependencies` or `devDependencies` of `package.json`
|
||||||
|
([#263](https://git.eeqj.de/sneak/AutistMask/issues/263)). After the install
|
||||||
|
it asks node for each one's `package.json`, and fails naming the missing
|
||||||
|
package and the fix. yarn skips the install whenever
|
||||||
|
`node_modules/.yarn-integrity` matches `yarn.lock`, so a package deleted from
|
||||||
|
`node_modules` stayed deleted while bootstrap said it was complete. The
|
||||||
|
fresh-clone failure the issue reports did not reproduce.
|
||||||
|
|
||||||
|
- 2026-10-06: Back from Settings no longer lands on the private key export or
|
||||||
|
recovery phrase screen after either was left by the settings gear
|
||||||
|
([#461](https://git.eeqj.de/sneak/AutistMask/issues/461)). Leaving drops the
|
||||||
|
screen's selection, so its leave handler now also takes it off the Back stack,
|
||||||
|
as a reopened popup already does. `tests/exportPrivkey.test.js` and
|
||||||
|
`tests/showPhrase.test.js` drive the gear and then Back, and
|
||||||
|
`leavePrivkeyScreen()` in `tests/e2e/run.js` expects the address screen after
|
||||||
|
Settings.
|
||||||
|
|
||||||
- 2026-10-06: A token symbol or name read off a contract is no longer stored cut
|
- 2026-10-06: A token symbol or name read off a contract is no longer stored cut
|
||||||
between the two halves of an emoji
|
between the two halves of an emoji
|
||||||
([#458](https://git.eeqj.de/sneak/AutistMask/issues/458)). `lookupTokenInfo()`
|
([#458](https://git.eeqj.de/sneak/AutistMask/issues/458)). `lookupTokenInfo()`
|
||||||
|
|||||||
@@ -127,6 +127,40 @@ install_js_deps() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# run_node: run node from the repo root, through nvm when node is not on PATH;
|
||||||
|
# the script comes on stdin
|
||||||
|
run_node() {
|
||||||
|
if missing node && [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && node"
|
||||||
|
else
|
||||||
|
node
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# yarn install exits 0 without touching node_modules once
|
||||||
|
# node_modules/.yarn-integrity matches yarn.lock, so a package deleted from
|
||||||
|
# node_modules stays deleted. Fail unless node finds every package listed in
|
||||||
|
# dependencies and devDependencies of package.json, by its package.json. When a
|
||||||
|
# package's exports does not list that file, node throws
|
||||||
|
# ERR_PACKAGE_PATH_NOT_EXPORTED, which it can only do once it has found the
|
||||||
|
# package, so that error counts as found.
|
||||||
|
check_js_deps() {
|
||||||
|
run_node <<'EOF'
|
||||||
|
const { dependencies, devDependencies } = require("./package.json");
|
||||||
|
for (const name of Object.keys({ ...dependencies, ...devDependencies })) {
|
||||||
|
try {
|
||||||
|
require.resolve(name + "/package.json");
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") {
|
||||||
|
console.error(`bootstrap: node cannot find ${name} after yarn install`);
|
||||||
|
console.error(" fix: rm -rf node_modules && make bootstrap");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
@@ -136,6 +170,7 @@ main() {
|
|||||||
ensure_node
|
ensure_node
|
||||||
ensure_yarn
|
ensure_yarn
|
||||||
install_js_deps
|
install_js_deps
|
||||||
|
check_js_deps
|
||||||
|
|
||||||
echo "bootstrap complete"
|
echo "bootstrap complete"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,16 @@ async function reveal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
onViewLeave(VIEW, clear);
|
// Leaving drops the address selection, so the screen also comes off the
|
||||||
|
// Back stack, where the settings gear has just put it: Back from Settings
|
||||||
|
// must not land on a password prompt that can only fail. A reopened popup
|
||||||
|
// drops it from the stack the same way
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/461).
|
||||||
|
onViewLeave(VIEW, () => {
|
||||||
|
clear();
|
||||||
|
const stack = state.viewStack;
|
||||||
|
if (stack[stack.length - 1] === VIEW) stack.pop();
|
||||||
|
});
|
||||||
|
|
||||||
// No wipe here: goBack() routes through showView(), which runs the
|
// No wipe here: goBack() routes through showView(), which runs the
|
||||||
// leave hook. A per-button wipe would only cover this one path.
|
// leave hook. A per-button wipe would only cover this one path.
|
||||||
|
|||||||
@@ -134,7 +134,16 @@ async function reveal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
onViewLeave(VIEW, clear);
|
// Leaving drops the wallet selection, so the screen also comes off the
|
||||||
|
// Back stack, where the settings gear has just put it: Back from Settings
|
||||||
|
// must not land on a password prompt that can only fail. A reopened popup
|
||||||
|
// drops it from the stack the same way
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/461).
|
||||||
|
onViewLeave(VIEW, () => {
|
||||||
|
clear();
|
||||||
|
const stack = state.viewStack;
|
||||||
|
if (stack[stack.length - 1] === VIEW) stack.pop();
|
||||||
|
});
|
||||||
|
|
||||||
$("btn-show-phrase-back").addEventListener("click", () => {
|
$("btn-show-phrase-back").addEventListener("click", () => {
|
||||||
goBack();
|
goBack();
|
||||||
|
|||||||
+3
-3
@@ -1075,13 +1075,13 @@ async function revealPrivkey(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Leave the export screen, or the Settings screen the gear left it for, for
|
// Leave the export screen, or the Settings screen the gear left it for, for
|
||||||
// Home. The gear put the export screen on the Back stack, so from Settings the
|
// Home. Leaving takes the export screen off the Back stack, so from Settings
|
||||||
// way home passes through it, already emptied
|
// Back goes to the address screen it was opened from
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/461).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/461).
|
||||||
async function leavePrivkeyScreen(page) {
|
async function leavePrivkeyScreen(page) {
|
||||||
if (await page.isVisible("#view-settings")) {
|
if (await page.isVisible("#view-settings")) {
|
||||||
await page.click("#btn-settings-back");
|
await page.click("#btn-settings-back");
|
||||||
await visible(page, "#view-export-privkey");
|
await visible(page, "#view-address");
|
||||||
}
|
}
|
||||||
if (await page.isVisible("#view-export-privkey")) {
|
if (await page.isVisible("#view-export-privkey")) {
|
||||||
await page.click("#btn-export-privkey-back");
|
await page.click("#btn-export-privkey-back");
|
||||||
|
|||||||
@@ -336,6 +336,36 @@ describe("opening the screen again in the same popup session", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Back from Settings after leaving by the settings gear", () => {
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/461: leaving drops the
|
||||||
|
// address selection, so Back onto this screen showed a password prompt
|
||||||
|
// that could only answer "No address is selected."
|
||||||
|
test("goes to the address screen it was opened from", () => {
|
||||||
|
const { helpers, state, exportPrivkey } = load();
|
||||||
|
state.viewStack = ["main"];
|
||||||
|
exportPrivkey.show(0, 0);
|
||||||
|
// The settings gear: push the current view, then show Settings.
|
||||||
|
helpers.pushCurrentView();
|
||||||
|
helpers.showView("settings");
|
||||||
|
|
||||||
|
helpers.goBack();
|
||||||
|
|
||||||
|
expect(state.currentView).toBe("address");
|
||||||
|
expect(state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("its own Back button leaves the rest of the stack alone", async () => {
|
||||||
|
const { state, exportPrivkey } = load();
|
||||||
|
state.viewStack = ["main"];
|
||||||
|
exportPrivkey.show(0, 0);
|
||||||
|
|
||||||
|
await click("btn-export-privkey-back");
|
||||||
|
|
||||||
|
expect(state.currentView).toBe("address");
|
||||||
|
expect(state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("views the popup may reopen onto", () => {
|
describe("views the popup may reopen onto", () => {
|
||||||
// Restoring onto this screen would put a private key on display with no
|
// Restoring onto this screen would put a private key on display with no
|
||||||
// password prompt in front of it, on a popup reopened by accident.
|
// password prompt in front of it, on a popup reopened by accident.
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
// Tests for the recovery phrase display (issue #161).
|
// Tests for the recovery phrase display (issue #161).
|
||||||
//
|
//
|
||||||
// These cover the parts that do not need a DOM: which wallet types may be
|
// These cover which wallet types may be offered the action at all, the
|
||||||
// offered the action at all, the exclusion of the screen from the set of
|
// exclusion of the screen from the set of views the popup may reopen onto,
|
||||||
// views the popup may reopen onto, and the absence of any path from this
|
// the absence of any path from this module to the logger, and, against a
|
||||||
// module to the logger. The DOM behaviour it guards — nothing rendered
|
// minimal DOM stub, where Back goes after the screen is left by the settings
|
||||||
// before the password is accepted, a wrong password revealing nothing, and
|
// gear or by its own Back. The rest of the DOM behaviour it guards — nothing rendered before the
|
||||||
// the wipe on leaving — is driven against the real popup in a real browser
|
// password is accepted, a wrong password revealing nothing, and the wipe on
|
||||||
// by tests/e2e/run.js, which is where every other view behaviour is tested.
|
// leaving — is driven against the real popup in a real browser by
|
||||||
|
// tests/e2e/run.js, which is where every other view behaviour is tested.
|
||||||
|
|
||||||
|
jest.mock("../src/shared/vault", () => ({
|
||||||
|
decryptWithPassword: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
@@ -74,6 +79,86 @@ describe("views the popup may reopen onto", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Just enough document for helpers.showView() and this view: every element
|
||||||
|
// is made on first lookup and keeps what the view writes to it, its click
|
||||||
|
// handler included.
|
||||||
|
function makeDocument() {
|
||||||
|
const els = new Map();
|
||||||
|
function makeElement() {
|
||||||
|
const classes = new Set();
|
||||||
|
const el = {
|
||||||
|
textContent: "",
|
||||||
|
value: "",
|
||||||
|
style: {},
|
||||||
|
classList: {
|
||||||
|
add: (name) => classes.add(name),
|
||||||
|
remove: (name) => classes.delete(name),
|
||||||
|
contains: (name) => classes.has(name),
|
||||||
|
toggle: (name, on) =>
|
||||||
|
on ? classes.add(name) : classes.delete(name),
|
||||||
|
},
|
||||||
|
addEventListener: (name, fn) => {
|
||||||
|
if (name === "click") el.onClick = fn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getElementById(id) {
|
||||||
|
// Created on demand by helpers.js; absent on a mainnet popup
|
||||||
|
// that is not a debug build.
|
||||||
|
if (id === "debug-banner") return null;
|
||||||
|
if (!els.has(id)) els.set(id, makeElement());
|
||||||
|
return els.get(id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Back from Settings after leaving by the settings gear", () => {
|
||||||
|
// On Settings, opened from Home.
|
||||||
|
function load() {
|
||||||
|
jest.resetModules();
|
||||||
|
globalThis.document = makeDocument();
|
||||||
|
const helpers = loadHelpers();
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const showPhrase = require("../src/popup/views/showPhrase");
|
||||||
|
showPhrase.init();
|
||||||
|
state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [] }];
|
||||||
|
state.currentView = "settings";
|
||||||
|
state.viewStack = ["main"];
|
||||||
|
return { helpers, state, showPhrase };
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/461: leaving drops the
|
||||||
|
// wallet selection, so Back onto this screen showed a password prompt
|
||||||
|
// that could only answer "No wallet is selected."
|
||||||
|
test("does not land on the recovery phrase screen", () => {
|
||||||
|
const { helpers, state, showPhrase } = load();
|
||||||
|
|
||||||
|
// Opened from the wallet list in Settings, then left by the gear:
|
||||||
|
// push the current view, then show Settings.
|
||||||
|
showPhrase.show(0);
|
||||||
|
helpers.pushCurrentView();
|
||||||
|
helpers.showView("settings");
|
||||||
|
|
||||||
|
expect(state.viewStack).toEqual(["main", "settings"]);
|
||||||
|
helpers.goBack();
|
||||||
|
expect(state.currentView).not.toBe(SHOW_PHRASE_VIEW);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This Back takes Settings off the stack before the screen is left, so
|
||||||
|
// the stack's top is then the entry Back from Settings will need.
|
||||||
|
test("its own Back button leaves the rest of the stack alone", () => {
|
||||||
|
const { state, showPhrase } = load();
|
||||||
|
showPhrase.show(0);
|
||||||
|
|
||||||
|
globalThis.document.getElementById("btn-show-phrase-back").onClick();
|
||||||
|
|
||||||
|
expect(state.currentView).toBe("settings");
|
||||||
|
expect(state.viewStack).toEqual(["main"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("the phrase cannot reach the logger", () => {
|
describe("the phrase cannot reach the logger", () => {
|
||||||
const source = fs.readFileSync(
|
const source = fs.readFileSync(
|
||||||
path.join(__dirname, "..", "src", "popup", "views", "showPhrase.js"),
|
path.join(__dirname, "..", "src", "popup", "views", "showPhrase.js"),
|
||||||
|
|||||||
Reference in New Issue
Block a user