Author SHA1 Message Date
sneak cd2a1ef825 fix: the first wallet's first address is active as soon as it is added (closes #512)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The popup picked an active address only when it opened, so after the first
wallet was created nothing was active until a reopen: Home had no headline,
and Send and Receive refused. Each of the three import paths on AddWallet now
makes the new wallet's first address active when no address is. The
background already answered sites with that address through its own
fallback, so nothing is broadcast. The address scan stand-in in the popup
test boot now answers in the real scan's shape, which the phrase and xprv
imports read.

Model: opus-5-5
2026-10-14 19:42:37 +00:00
6 changed files with 120 additions and 61 deletions
+4 -10
View File
@@ -362,14 +362,6 @@ are outside `make check`. Neither opens the popup from the toolbar button: both
load its page in an ordinary tab, so what the toolbar popup itself adds, its
size and its closing when it loses focus, is covered by neither.
Each run builds its image under a tag of its own, `autistmask-e2e-chrome:` or
`autistmask-e2e-firefox:` followed by twelve random hex digits, so two clones
can run the same suite at once without one build taking the other's image away.
The run removes that image, and its container if one is still running, after the
suite passes or fails, on Ctrl-C, on `kill`, and when the terminal or SSH
session it runs in closes. A run killed outright (`kill -9`) cannot clean up:
its image stays under its tag until removed by hand with `docker image rm`.
### Chrome (`make test-e2e`)
`make test-e2e` builds `dist/chrome/` and drives the **real popup in a real
@@ -1496,8 +1488,10 @@ view would leave a wallet one click from deletion.
- **Transitions**:
- "Import" with a valid entry and a matching password of at least 12
characters → creates the wallet, clears the navigation stack, and →
**Home**. The phrase and xprv modes then scan for further used addresses
and report the count as a flash message.
**Home**. When no address is active yet, as on the first wallet, the new
wallet's first address becomes the active one. The phrase and xprv modes
then scan for further used addresses and report the count as a flash
message.
- "Import" with a missing, short or mismatched password → full-sentence
error on the error line, no screen change
- "Import" with an invalid entry or a duplicate wallet or address → flash
+16 -28
View File
@@ -22,29 +22,13 @@ set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# Each run builds, runs and then removes an image under a tag of its own.
# Under one tag shared by every run, a second clone's build moves the tag
# off this run's image, and the image store can then delete that untagged
# image before or while this run uses it. The tag is random rather than
# $$ because CI jobs share one docker daemon, but each job's container
# numbers its processes from scratch.
SUFFIX="$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
NAME="$("$SCRIPT_DIR/projectname")-e2e-chrome"
IMAGE="$NAME:$SUFFIX"
# Both docker runs below use this container name: docker run --rm returns
# only once its container is removed, so the second never meets the first.
CONTAINER="$NAME-$SUFFIX"
IMAGE="$("$SCRIPT_DIR/projectname")-e2e-chrome"
IIDFILE=""
cleanup() {
# After a third Ctrl-C docker's client exits whether or not the
# container has stopped, and a container still running keeps the image
# in use.
if docker container inspect "$CONTAINER" >/dev/null 2>&1; then
docker container rm -f "$CONTAINER" >/dev/null
fi
# No image to remove when the build failed or was interrupted.
if docker image inspect "$IMAGE" >/dev/null 2>&1; then
docker image rm "$IMAGE" >/dev/null
if [ -n "$IIDFILE" ]; then
rm -f "$IIDFILE"
fi
}
@@ -56,16 +40,22 @@ main() {
exit 1
fi
IIDFILE="$(mktemp)"
trap cleanup EXIT
# HUP: the terminal or SSH session running the suite closed.
trap 'cleanup; exit 130' HUP INT TERM
trap 'cleanup; exit 130' INT TERM
echo "Building the Chrome e2e image (extension included)..."
# --no-cache: the image build runs make build and its checks, and a
# cached layer is a check that did not run.
docker build --no-cache -t "$IMAGE" -f tests/e2e/Dockerfile .
docker build --no-cache --iidfile "$IIDFILE" -t "$IMAGE" \
-f tests/e2e/Dockerfile .
echo "Running e2e suite in the pinned Playwright container..."
# The image is run by ID, not by tag: where two clones of this repo run
# the suite at once, the other build can move the tag between this
# build and this run, and the suite would then silently test the other
# checkout.
#
# --ipc=host: Chromium's shared-memory needs more than the default
# 64MB /dev/shm or renderers crash.
# HOME=/tmp: the image's root home is not a reliable place for the
@@ -85,12 +75,11 @@ main() {
# probe. The image is pinned by digest, so this can only ever bite
# on a deliberate bump.
docker run --rm \
--name "$CONTAINER" \
--ipc=host \
-e HOME=/tmp \
-e PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 \
-e "E2E_TRACE_NETWORK=${E2E_TRACE_NETWORK:-0}" \
"$IMAGE" \
"$(cat "$IIDFILE")" \
node tests/e2e/run.js
# Where chrome.storage.local lives, and what moves it: two unpacked loads
@@ -104,10 +93,9 @@ main() {
# --host-resolver-rules=MAP * ~NOTFOUND so nothing it does can leave.
echo "Running the extension-id and storage-partition observations..."
docker run --rm \
--name "$CONTAINER" \
--ipc=host \
-e HOME=/tmp \
"$IMAGE" \
"$(cat "$IIDFILE")" \
node tests/e2e/storagePartition.js
}
+16 -22
View File
@@ -21,23 +21,13 @@ set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# A tag and a container name of this run's own, as in script/test-e2e,
# which says why.
SUFFIX="$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
NAME="$("$SCRIPT_DIR/projectname")-e2e-firefox"
IMAGE="$NAME:$SUFFIX"
CONTAINER="$NAME-$SUFFIX"
IMAGE="$("$SCRIPT_DIR/projectname")-e2e-firefox"
IIDFILE=""
cleanup() {
# After a third Ctrl-C docker's client exits whether or not the
# container has stopped, and a container still running keeps the image
# in use.
if docker container inspect "$CONTAINER" >/dev/null 2>&1; then
docker container rm -f "$CONTAINER" >/dev/null
fi
# No image to remove when the build failed or was interrupted.
if docker image inspect "$IMAGE" >/dev/null 2>&1; then
docker image rm "$IMAGE" >/dev/null
if [ -n "$IIDFILE" ]; then
rm -f "$IIDFILE"
fi
}
@@ -49,16 +39,22 @@ main() {
exit 1
fi
IIDFILE="$(mktemp)"
trap cleanup EXIT
# HUP: the terminal or SSH session running the suite closed.
trap 'cleanup; exit 130' HUP INT TERM
trap 'cleanup; exit 130' INT TERM
echo "Building the pinned Firefox e2e image (extension included)..."
# --no-cache: the image build runs make build and its checks, and a
# cached layer is a check that did not run.
docker build --no-cache -t "$IMAGE" -f tests/e2e/firefox/Dockerfile .
docker build --no-cache --iidfile "$IIDFILE" -t "$IMAGE" \
-f tests/e2e/firefox/Dockerfile .
echo "Running the Firefox e2e suite..."
# The image is run by ID, not by tag: where two clones of this repo run
# the suite at once, the other build can move the tag between this
# build and this run, and the suite would then silently test the other
# checkout.
#
# --shm-size=1g: Firefox needs more than the default 64MB /dev/shm.
# --network none: the suite stubs nothing, so this is what keeps the
# run offline and deterministic. The extension swallows its own
@@ -73,11 +69,10 @@ main() {
# "CanCreateUserNamespace() clone() failure: EPERM" on startup here;
# it is cosmetic and headless Firefox runs fine without it.
docker run --rm \
--name "$CONTAINER" \
--shm-size=1g \
--network none \
-e HOME=/tmp \
"$IMAGE" \
"$(cat "$IIDFILE")" \
node tests/e2e/firefox/run.js dist/firefox
# The install/uninstall/re-install property, against the packaged XPI
@@ -87,11 +82,10 @@ main() {
# and the suite above shares one session throughout.
echo "Running the Firefox re-install suite against the packaged XPI..."
docker run --rm \
--name "$CONTAINER" \
--shm-size=1g \
--network none \
-e HOME=/tmp \
"$IMAGE" \
"$(cat "$IIDFILE")" \
node tests/e2e/firefox/reinstall.js
}
+9
View File
@@ -194,6 +194,9 @@ async function importMnemonic(ctx) {
};
state.wallets.push(wallet);
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = firstAddress;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
@@ -254,6 +257,9 @@ async function importPrivateKey(ctx) {
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
});
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = addr;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
@@ -315,6 +321,9 @@ async function importXprvKey(ctx) {
};
state.wallets.push(wallet);
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = firstAddress;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
+69
View File
@@ -0,0 +1,69 @@
// Adding the first wallet left no address active until the popup was reopened,
// so Send and Receive on Home only said "No active address selected."
// (https://git.eeqj.de/sneak/AutistMask/issues/512). Each test boots a fresh
// install, adds the first wallet through one of the three import tabs, and
// opens Receive from Home in the same popup.
const { HDNodeWallet, Mnemonic, Wallet } = require("ethers");
const { bootPopup, cleanupPopup } = require("./support/popupBoot");
const PHRASE =
"abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about";
const PRIVATE_KEY = "0x" + "11".repeat(32);
// The master key of PHRASE, so its first address is the phrase's.
const XPRV = HDNodeWallet.fromSeed(
Mnemonic.fromPhrase(PHRASE).computeSeed(),
).extendedKey;
const PASSWORD = "correct horse battery staple";
const PHRASE_ADDRESS = HDNodeWallet.fromPhrase(PHRASE).address;
const TABS = [
{
tab: "tab-mnemonic",
field: "wallet-mnemonic",
secret: PHRASE,
address: PHRASE_ADDRESS,
},
{
tab: "tab-privkey",
field: "import-private-key",
secret: PRIVATE_KEY,
address: new Wallet(PRIVATE_KEY).address,
},
{
tab: "tab-xprv",
field: "import-xprv-key",
secret: XPRV,
address: PHRASE_ADDRESS,
},
];
afterEach(() => {
cleanupPopup();
});
describe("the first wallet's first address is active right away", () => {
test.each(TABS)(
"added from $tab, Receive opens on it",
async ({ tab, field, secret, address }) => {
const page = await bootPopup(undefined);
expect(page.pageErrors).toEqual([]);
await page.click("btn-welcome-add");
await page.click(tab);
page.node(field).value = secret;
page.node("add-wallet-password").value = PASSWORD;
page.node("add-wallet-password-confirm").value = PASSWORD;
await page.click("btn-add-wallet-confirm");
expect(page.visibleViews()).toEqual(["main"]);
await page.click("btn-main-receive");
expect(page.visibleViews()).toEqual(["receive"]);
expect(page.node("receive-address-block").dataset.full).toBe(
address,
);
},
);
});
+6 -1
View File
@@ -258,12 +258,17 @@ async function bootPopup(stored, options) {
formatAddressTotal: () => "",
getAddressValue: () => ({ usd: null, partial: false }),
}));
// scanForAddresses() answers in the real one's shape, a scan that found
// no used address: AddWallet reads `addresses` from it.
jest.doMock("../../src/shared/balances", () => ({
fetchTokenBalances: jest.fn(async () => []),
refreshBalances: jest.fn(async () => {}),
lookupTokenInfo: jest.fn(async () => null),
getProvider: () => ({}),
scanForAddresses: jest.fn(async () => []),
scanForAddresses: jest.fn(async () => ({
addresses: [],
nextIndex: 1,
})),
}));
// filterTransactions() answers in the real one's shape: Home,
// AddressDetail and AddressToken read both fields, and a bare list makes