Compare commits
4
Commits
7748e49aa8
..
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e48308c5b6 | ||
|
|
aa10a2a5f5 | ||
|
|
2c0888e112 | ||
|
|
f857bb0a3f |
@@ -362,6 +362,14 @@ 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
|
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.
|
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`)
|
### Chrome (`make test-e2e`)
|
||||||
|
|
||||||
`make test-e2e` builds `dist/chrome/` and drives the **real popup in a real
|
`make test-e2e` builds `dist/chrome/` and drives the **real popup in a real
|
||||||
@@ -479,6 +487,9 @@ papered over:
|
|||||||
forced by clicking Reveal and the settings gear in one page task, which a
|
forced by clicking Reveal and the settings gear in one page task, which a
|
||||||
person cannot do; the case a person can hit, the first decrypt after the popup
|
person cannot do; the case a person can hit, the first decrypt after the popup
|
||||||
opens while libsodium is still loading, is not driven.
|
opens while libsodium is still loading, is not driven.
|
||||||
|
- Approval windows open at 1280x720 whatever size the extension asks for, so
|
||||||
|
whether an approval screen fits its window is checked in the Firefox suite
|
||||||
|
only.
|
||||||
|
|
||||||
Any test that drives a failure path on purpose declares the `console.error` it
|
Any test that drives a failure path on purpose declares the `console.error` it
|
||||||
is about to provoke, via `errors.expect()`. That is not a mute: the declaration
|
is about to provoke, via `errors.expect()`. That is not a mute: the declaration
|
||||||
@@ -573,7 +584,9 @@ suite), wallet creation through the UI, the Add Token screen, and the dApp round
|
|||||||
trips — `eth_requestAccounts`, `personal_sign`, `wallet_switchEthereumChain`
|
trips — `eth_requestAccounts`, `personal_sign`, `wallet_switchEthereumChain`
|
||||||
rejected and then approved, `eth_sendTransaction`, and a closed approval window
|
rejected and then approved, `eth_sendTransaction`, and a closed approval window
|
||||||
rejecting with EIP-1193 4001 — driven through the real content script,
|
rejecting with EIP-1193 4001 — driven through the real content script,
|
||||||
background page and approval windows.
|
background page and approval windows. The `personal_sign` prompt is also
|
||||||
|
required to fit its window without scrolling sideways, with the extension's
|
||||||
|
pages at 170% zoom.
|
||||||
|
|
||||||
The suite lives in `tests/e2e/firefox/`. Its WebDriver client (`driver.js`) has
|
The suite lives in `tests/e2e/firefox/`. Its WebDriver client (`driver.js`) has
|
||||||
**no npm dependencies at all**: it is built on global `fetch` and
|
**no npm dependencies at all**: it is built on global `fetch` and
|
||||||
@@ -947,8 +960,16 @@ discoverable.
|
|||||||
enough that vanilla JS with simple view switching is sufficient. A framework
|
enough that vanilla JS with simple view switching is sufficient. A framework
|
||||||
would add bundle size, build complexity, and attack surface for no benefit at
|
would add bundle size, build complexity, and attack surface for no benefit at
|
||||||
this scale.
|
this scale.
|
||||||
- **360x600 popup**: Standard browser extension popup dimensions. The UI is
|
- **396x600 popup**: The page is 396 pixels wide (`body` in
|
||||||
designed for this fixed viewport.
|
`src/popup/styles/main.css`), and 600 pixels is as tall as the browser lets
|
||||||
|
the toolbar popup grow. The UI is designed for this fixed viewport. The window
|
||||||
|
an approval opens in is given the same size. The browser counts the window's
|
||||||
|
frame in that width, and a screen taller than the window gives some of it to a
|
||||||
|
vertical scrollbar, so once the approval's screen is up the page widens its
|
||||||
|
window by however far it would scroll sideways, scaled by the browser's zoom.
|
||||||
|
Firefox draws some zoom steps a little larger than the zoom it reports, so the
|
||||||
|
page measures again once the window has grown, and widens it again while it
|
||||||
|
still falls short.
|
||||||
|
|
||||||
#### No Layout Shift
|
#### No Layout Shift
|
||||||
|
|
||||||
@@ -1531,8 +1552,14 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Import" with a valid entry and a matching password of at least 12
|
- "Import" with a valid entry and a matching password of at least 12
|
||||||
characters → creates the wallet, clears the navigation stack, and →
|
characters → creates the wallet, clears the navigation stack, and →
|
||||||
**Home**. The phrase and xprv modes then scan for further used addresses
|
**Home**. When no address is active yet, as on the first wallet, the new
|
||||||
and report the count as a flash message.
|
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. When the scan finds two or more used addresses, they replace the
|
||||||
|
wallet's list, which then holds the first address only if it was used. If
|
||||||
|
the first address was active and is not in the new list, the new list's
|
||||||
|
first address becomes the active one and `AUTISTMASK_ACTIVE_CHANGED` is
|
||||||
|
broadcast.
|
||||||
- "Import" with a missing, short or mismatched password → full-sentence
|
- "Import" with a missing, short or mismatched password → full-sentence
|
||||||
error on the error line, no screen change
|
error on the error line, no screen change
|
||||||
- "Import" with an invalid entry or a duplicate wallet or address → flash
|
- "Import" with an invalid entry or a duplicate wallet or address → flash
|
||||||
|
|||||||
+30
-17
@@ -22,13 +22,29 @@ set -eu
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
IMAGE="$("$SCRIPT_DIR/projectname")-e2e-chrome"
|
# 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
|
||||||
IIDFILE=""
|
# 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"
|
||||||
|
# Each docker run below uses this container name: docker run --rm returns
|
||||||
|
# only once its container is removed, so no run meets the one before it.
|
||||||
|
CONTAINER="$NAME-$SUFFIX"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [ -n "$IIDFILE" ]; then
|
# After a third Ctrl-C docker's client exits whether or not the
|
||||||
rm -f "$IIDFILE"
|
# 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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,22 +56,16 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IIDFILE="$(mktemp)"
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
trap 'cleanup; exit 130' INT TERM
|
# HUP: the terminal or SSH session running the suite closed.
|
||||||
|
trap 'cleanup; exit 130' HUP INT TERM
|
||||||
|
|
||||||
echo "Building the Chrome e2e image (extension included)..."
|
echo "Building the Chrome e2e image (extension included)..."
|
||||||
# --no-cache: the image build runs make build and its checks, and a
|
# --no-cache: the image build runs make build and its checks, and a
|
||||||
# cached layer is a check that did not run.
|
# cached layer is a check that did not run.
|
||||||
docker build --no-cache --iidfile "$IIDFILE" -t "$IMAGE" \
|
docker build --no-cache -t "$IMAGE" -f tests/e2e/Dockerfile .
|
||||||
-f tests/e2e/Dockerfile .
|
|
||||||
|
|
||||||
echo "Running e2e suite in the pinned Playwright container..."
|
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
|
# --ipc=host: Chromium's shared-memory needs more than the default
|
||||||
# 64MB /dev/shm or renderers crash.
|
# 64MB /dev/shm or renderers crash.
|
||||||
# HOME=/tmp: the image's root home is not a reliable place for the
|
# HOME=/tmp: the image's root home is not a reliable place for the
|
||||||
@@ -75,11 +85,12 @@ main() {
|
|||||||
# probe. The image is pinned by digest, so this can only ever bite
|
# probe. The image is pinned by digest, so this can only ever bite
|
||||||
# on a deliberate bump.
|
# on a deliberate bump.
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--name "$CONTAINER" \
|
||||||
--ipc=host \
|
--ipc=host \
|
||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
-e PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 \
|
-e PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 \
|
||||||
-e "E2E_TRACE_NETWORK=${E2E_TRACE_NETWORK:-0}" \
|
-e "E2E_TRACE_NETWORK=${E2E_TRACE_NETWORK:-0}" \
|
||||||
"$(cat "$IIDFILE")" \
|
"$IMAGE" \
|
||||||
node tests/e2e/run.js
|
node tests/e2e/run.js
|
||||||
|
|
||||||
# Where chrome.storage.local lives, and what moves it: two unpacked loads
|
# Where chrome.storage.local lives, and what moves it: two unpacked loads
|
||||||
@@ -93,9 +104,10 @@ main() {
|
|||||||
# --host-resolver-rules=MAP * ~NOTFOUND so nothing it does can leave.
|
# --host-resolver-rules=MAP * ~NOTFOUND so nothing it does can leave.
|
||||||
echo "Running the extension-id and storage-partition observations..."
|
echo "Running the extension-id and storage-partition observations..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--name "$CONTAINER" \
|
||||||
--ipc=host \
|
--ipc=host \
|
||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
"$(cat "$IIDFILE")" \
|
"$IMAGE" \
|
||||||
node tests/e2e/storagePartition.js
|
node tests/e2e/storagePartition.js
|
||||||
|
|
||||||
# Approvals left open for seven minutes in minimized windows, past the
|
# Approvals left open for seven minutes in minimized windows, past the
|
||||||
@@ -105,9 +117,10 @@ main() {
|
|||||||
# attached to. It reaches nothing but its own loopback server.
|
# attached to. It reaches nothing but its own loopback server.
|
||||||
echo "Running the approvals left open for seven minutes..."
|
echo "Running the approvals left open for seven minutes..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--name "$CONTAINER" \
|
||||||
--ipc=host \
|
--ipc=host \
|
||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
"$(cat "$IIDFILE")" \
|
"$IMAGE" \
|
||||||
node tests/e2e/slowApproval.js
|
node tests/e2e/slowApproval.js
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-16
@@ -21,13 +21,23 @@ set -eu
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
IMAGE="$("$SCRIPT_DIR/projectname")-e2e-firefox"
|
# A tag and a container name of this run's own, as in script/test-e2e,
|
||||||
|
# which says why.
|
||||||
IIDFILE=""
|
SUFFIX="$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')"
|
||||||
|
NAME="$("$SCRIPT_DIR/projectname")-e2e-firefox"
|
||||||
|
IMAGE="$NAME:$SUFFIX"
|
||||||
|
CONTAINER="$NAME-$SUFFIX"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [ -n "$IIDFILE" ]; then
|
# After a third Ctrl-C docker's client exits whether or not the
|
||||||
rm -f "$IIDFILE"
|
# 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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,22 +49,16 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IIDFILE="$(mktemp)"
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
trap 'cleanup; exit 130' INT TERM
|
# HUP: the terminal or SSH session running the suite closed.
|
||||||
|
trap 'cleanup; exit 130' HUP INT TERM
|
||||||
|
|
||||||
echo "Building the pinned Firefox e2e image (extension included)..."
|
echo "Building the pinned Firefox e2e image (extension included)..."
|
||||||
# --no-cache: the image build runs make build and its checks, and a
|
# --no-cache: the image build runs make build and its checks, and a
|
||||||
# cached layer is a check that did not run.
|
# cached layer is a check that did not run.
|
||||||
docker build --no-cache --iidfile "$IIDFILE" -t "$IMAGE" \
|
docker build --no-cache -t "$IMAGE" -f tests/e2e/firefox/Dockerfile .
|
||||||
-f tests/e2e/firefox/Dockerfile .
|
|
||||||
|
|
||||||
echo "Running the Firefox e2e suite..."
|
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.
|
# --shm-size=1g: Firefox needs more than the default 64MB /dev/shm.
|
||||||
# --network none: the suite stubs nothing, so this is what keeps the
|
# --network none: the suite stubs nothing, so this is what keeps the
|
||||||
# run offline and deterministic. The extension swallows its own
|
# run offline and deterministic. The extension swallows its own
|
||||||
@@ -69,10 +73,11 @@ main() {
|
|||||||
# "CanCreateUserNamespace() clone() failure: EPERM" on startup here;
|
# "CanCreateUserNamespace() clone() failure: EPERM" on startup here;
|
||||||
# it is cosmetic and headless Firefox runs fine without it.
|
# it is cosmetic and headless Firefox runs fine without it.
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--name "$CONTAINER" \
|
||||||
--shm-size=1g \
|
--shm-size=1g \
|
||||||
--network none \
|
--network none \
|
||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
"$(cat "$IIDFILE")" \
|
"$IMAGE" \
|
||||||
node tests/e2e/firefox/run.js dist/firefox
|
node tests/e2e/firefox/run.js dist/firefox
|
||||||
|
|
||||||
# The install/uninstall/re-install property, against the packaged XPI
|
# The install/uninstall/re-install property, against the packaged XPI
|
||||||
@@ -82,10 +87,11 @@ main() {
|
|||||||
# and the suite above shares one session throughout.
|
# and the suite above shares one session throughout.
|
||||||
echo "Running the Firefox re-install suite against the packaged XPI..."
|
echo "Running the Firefox re-install suite against the packaged XPI..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
|
--name "$CONTAINER" \
|
||||||
--shm-size=1g \
|
--shm-size=1g \
|
||||||
--network none \
|
--network none \
|
||||||
-e HOME=/tmp \
|
-e HOME=/tmp \
|
||||||
"$(cat "$IIDFILE")" \
|
"$IMAGE" \
|
||||||
node tests/e2e/firefox/reinstall.js
|
node tests/e2e/firefox/reinstall.js
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -453,7 +453,10 @@ function releaseApproval(approval) {
|
|||||||
// the result on the floor, so a rejection here would be unhandled.
|
// the result on the floor, so a rejection here would be unhandled.
|
||||||
async function openApprovalWindow(id) {
|
async function openApprovalWindow(id) {
|
||||||
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
|
||||||
const popupWidth = 360;
|
// As wide as the page: body in src/popup/styles/main.css. The browser
|
||||||
|
// counts the window's frame in this width; the page itself adds back
|
||||||
|
// whatever the frame takes (src/popup/index.js).
|
||||||
|
const popupWidth = 396;
|
||||||
const popupHeight = 600;
|
const popupHeight = 600;
|
||||||
|
|
||||||
let currentWin = null;
|
let currentWin = null;
|
||||||
@@ -473,7 +476,7 @@ async function openApprovalWindow(id) {
|
|||||||
// Centred on a browser window only. The last focused window can be
|
// Centred on a browser window only. The last focused window can be
|
||||||
// another approval window still open, and centring on one can give a
|
// another approval window still open, and centring on one can give a
|
||||||
// position the browser refuses ("Bounds must be at least 50% within
|
// position the browser refuses ("Bounds must be at least 50% within
|
||||||
// visible screen space"): headless Chrome reports this 360x600 popup as
|
// visible screen space"): headless Chrome reports this 396x600 popup as
|
||||||
// 1280x720. The request then failed with no window at all. Over a popup,
|
// 1280x720. The request then failed with no window at all. Over a popup,
|
||||||
// the browser picks the position.
|
// the browser picks the position.
|
||||||
if (currentWin && currentWin.type === "normal") {
|
if (currentWin && currentWin.type === "normal") {
|
||||||
|
|||||||
+42
-2
@@ -21,6 +21,12 @@ const {
|
|||||||
goBack,
|
goBack,
|
||||||
} = require("./views/helpers");
|
} = require("./views/helpers");
|
||||||
const { applyTheme } = require("./theme");
|
const { applyTheme } = require("./theme");
|
||||||
|
const {
|
||||||
|
tabsGetCurrent,
|
||||||
|
tabsGetZoom,
|
||||||
|
windowsGetCurrent,
|
||||||
|
windowsUpdate,
|
||||||
|
} = require("../shared/browserApi");
|
||||||
// Renders a view the popup lands on without having navigated to it forward:
|
// Renders a view the popup lands on without having navigated to it forward:
|
||||||
// on restore here, and on Back. Only the views that can be fully re-rendered
|
// on restore here, and on Back. Only the views that can be fully re-rendered
|
||||||
// from persisted state (RESTORABLE_VIEWS, src/shared/restorableViews.js) go
|
// from persisted state (RESTORABLE_VIEWS, src/shared/restorableViews.js) go
|
||||||
@@ -163,6 +169,40 @@ function fallbackView() {
|
|||||||
showView("main");
|
showView("main");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The background opens the approval window as wide as the page, but the
|
||||||
|
// browser counts the window's frame in that width, and a screen taller than
|
||||||
|
// the window takes a vertical scrollbar out of it too. So once the screen is
|
||||||
|
// up, the window is widened by however far the page would scroll sideways.
|
||||||
|
// That distance is in page pixels and the window's width in screen pixels:
|
||||||
|
// at 150% zoom one page pixel is 1.5 screen pixels. The zoom is read for this
|
||||||
|
// page's own tab, because asked without a tab id Firefox answers for the
|
||||||
|
// window focused last. Firefox draws some zoom steps a little larger than the
|
||||||
|
// zoom it reports, 170% among them, so once the page has its new width (its
|
||||||
|
// resize event) it is measured again, and the window widened again while the
|
||||||
|
// page still falls short, three times at most. A window that cannot grow sends
|
||||||
|
// no resize event, and the page stops there. The connection prompt can also
|
||||||
|
// show in the toolbar popup, whose window is the user's browser window; that
|
||||||
|
// one is left alone.
|
||||||
|
async function widenApprovalWindow() {
|
||||||
|
const page = document.documentElement;
|
||||||
|
if (page.scrollWidth <= page.clientWidth) return;
|
||||||
|
const win = await windowsGetCurrent();
|
||||||
|
if (win.type !== "popup") return;
|
||||||
|
const tab = await tabsGetCurrent();
|
||||||
|
const zoom = await tabsGetZoom(tab.id);
|
||||||
|
let width = win.width;
|
||||||
|
for (let tries = 0; tries < 3; tries++) {
|
||||||
|
const sideways = page.scrollWidth - page.clientWidth;
|
||||||
|
if (sideways <= 0) return;
|
||||||
|
width += Math.ceil(sideways * zoom);
|
||||||
|
const resized = new Promise((resolve) =>
|
||||||
|
window.addEventListener("resize", resolve, { once: true }),
|
||||||
|
);
|
||||||
|
await windowsUpdate(win.id, { width });
|
||||||
|
await resized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
// First, before anything can save: showView() saves on every navigation
|
// First, before anything can save: showView() saves on every navigation
|
||||||
// without awaiting, so a save that fails from here on has somewhere to be
|
// without awaiting, so a save that fails from here on has somewhere to be
|
||||||
@@ -239,8 +279,8 @@ async function init() {
|
|||||||
// the run on both harnesses (Playwright `pageerror`, and the Firefox
|
// the run on both harnesses (Playwright `pageerror`, and the Firefox
|
||||||
// driver's console-service drain), so nothing is lost by leaving it
|
// driver's console-service drain), so nothing is lost by leaving it
|
||||||
// on that path. show() puts the approval's own screen up once the
|
// on that path. show() puts the approval's own screen up once the
|
||||||
// background has described it.
|
// background has described it; the window is widened to fit it then.
|
||||||
approval.show(approvalId);
|
approval.show(approvalId).then(widenApprovalWindow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const {
|
|||||||
const { encryptWithPassword } = require("../../shared/vault");
|
const { encryptWithPassword } = require("../../shared/vault");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { scanForAddresses } = require("../../shared/balances");
|
const { scanForAddresses } = require("../../shared/balances");
|
||||||
|
const { notify } = require("../../shared/browserApi");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an address already exists in ANY wallet (hd, xprv, or key).
|
* Check if an address already exists in ANY wallet (hd, xprv, or key).
|
||||||
@@ -194,6 +195,9 @@ async function importMnemonic(ctx) {
|
|||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = firstAddress;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -215,8 +219,16 @@ async function importMnemonic(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
// The new list leaves out the first address when the scan found it
|
||||||
|
// unused. If it was the active one, the new list's first takes over,
|
||||||
|
// and the background is told as when the user picks one on Home.
|
||||||
|
const activeMoved =
|
||||||
|
state.activeAddress === firstAddress &&
|
||||||
|
wallet.addresses[0].address !== firstAddress;
|
||||||
|
if (activeMoved) state.activeAddress = wallet.addresses[0].address;
|
||||||
await saveState();
|
await saveState();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
|
if (activeMoved) notify({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
showFlash("Found " + scan.addresses.length + " addresses.");
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
||||||
} else {
|
} else {
|
||||||
showFlash("Ready.", 1000);
|
showFlash("Ready.", 1000);
|
||||||
@@ -254,6 +266,9 @@ async function importPrivateKey(ctx) {
|
|||||||
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
||||||
});
|
});
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = addr;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -315,6 +330,9 @@ async function importXprvKey(ctx) {
|
|||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
|
if (state.activeAddress === null) {
|
||||||
|
state.activeAddress = firstAddress;
|
||||||
|
}
|
||||||
await saveState();
|
await saveState();
|
||||||
clearViewStack();
|
clearViewStack();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
@@ -336,8 +354,15 @@ async function importXprvKey(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
// As for a recovery phrase: an active first address the scan left
|
||||||
|
// out gives way to the new list's first.
|
||||||
|
const activeMoved =
|
||||||
|
state.activeAddress === firstAddress &&
|
||||||
|
wallet.addresses[0].address !== firstAddress;
|
||||||
|
if (activeMoved) state.activeAddress = wallet.addresses[0].address;
|
||||||
await saveState();
|
await saveState();
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
|
if (activeMoved) notify({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
showFlash("Found " + scan.addresses.length + " addresses.");
|
showFlash("Found " + scan.addresses.length + " addresses.");
|
||||||
} else {
|
} else {
|
||||||
showFlash("Ready.", 1000);
|
showFlash("Ready.", 1000);
|
||||||
|
|||||||
@@ -243,6 +243,22 @@ function tabsSendMessage(tabId, message) {
|
|||||||
return invoke(tabsApi(), "sendMessage", tabId, message);
|
return invoke(tabsApi(), "sendMessage", tabId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<Object|undefined>} the tab the calling page is in, or
|
||||||
|
* undefined for a page not in a tab, such as the toolbar popup.
|
||||||
|
*/
|
||||||
|
function tabsGetCurrent() {
|
||||||
|
return invoke(tabsApi(), "getCurrent");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} tabId
|
||||||
|
* @returns {Promise<number>} that tab's zoom: 1.5 at 150%.
|
||||||
|
*/
|
||||||
|
function tabsGetZoom(tabId) {
|
||||||
|
return invoke(tabsApi(), "getZoom", tabId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} createData
|
* @param {Object} createData
|
||||||
* @returns {Promise<Object>} the created window.
|
* @returns {Promise<Object>} the created window.
|
||||||
@@ -258,6 +274,22 @@ function windowsGetLastFocused() {
|
|||||||
return invoke(windowsApi(), "getLastFocused");
|
return invoke(windowsApi(), "getLastFocused");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<Object>} the window the calling page is in.
|
||||||
|
*/
|
||||||
|
function windowsGetCurrent() {
|
||||||
|
return invoke(windowsApi(), "getCurrent");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} windowId
|
||||||
|
* @param {Object} updateInfo
|
||||||
|
* @returns {Promise<Object>} the updated window.
|
||||||
|
*/
|
||||||
|
function windowsUpdate(windowId, updateInfo) {
|
||||||
|
return invoke(windowsApi(), "update", windowId, updateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} windowId
|
* @param {number} windowId
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -280,10 +312,14 @@ module.exports = {
|
|||||||
storageRemove,
|
storageRemove,
|
||||||
storageSet,
|
storageSet,
|
||||||
tabsApi,
|
tabsApi,
|
||||||
|
tabsGetCurrent,
|
||||||
|
tabsGetZoom,
|
||||||
tabsQuery,
|
tabsQuery,
|
||||||
tabsSendMessage,
|
tabsSendMessage,
|
||||||
windowsApi,
|
windowsApi,
|
||||||
windowsCreate,
|
windowsCreate,
|
||||||
|
windowsGetCurrent,
|
||||||
windowsGetLastFocused,
|
windowsGetLastFocused,
|
||||||
windowsRemove,
|
windowsRemove,
|
||||||
|
windowsUpdate,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,3 +45,132 @@ test("the approval window shows no screen until the background describes the app
|
|||||||
expect(env.visibleViews()).toEqual(["approve-network"]);
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
||||||
expect(env.pageErrors).toEqual([]);
|
expect(env.pageErrors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The background opens the approval window as wide as the page, but the
|
||||||
|
// browser counts the window's frame in that width, and a screen taller than
|
||||||
|
// the window takes a vertical scrollbar out of it too
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/518). The browser suites run
|
||||||
|
// without frames, and only the Firefox suite's sign prompt at a zoom other
|
||||||
|
// than 100%, so the widening is checked here too.
|
||||||
|
//
|
||||||
|
// A window 396 screen pixels wide whose page is drawn at `drawn` screen pixels
|
||||||
|
// to the page pixel. Widening it gives the page its new width, less the 15
|
||||||
|
// screen pixels of the scrollbar the approval screen has, and then calls the
|
||||||
|
// page's resize listeners, each of which asked to be called once.
|
||||||
|
function windowsStub(type, drawn) {
|
||||||
|
const updates = [];
|
||||||
|
return {
|
||||||
|
updates,
|
||||||
|
getCurrent: (done) => done({ id: 7, type, width: 396 }),
|
||||||
|
update: (id, info, done) => {
|
||||||
|
updates.push({ id, info });
|
||||||
|
done({});
|
||||||
|
globalThis.document.documentElement.clientWidth = Math.floor(
|
||||||
|
(info.width - 15) / drawn,
|
||||||
|
);
|
||||||
|
const resized = globalThis.window.listeners.resize || [];
|
||||||
|
globalThis.window.listeners.resize = [];
|
||||||
|
for (const fn of resized) fn();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The approval page's own tab is at `zoom`. Asked without a tab id, Firefox
|
||||||
|
// answers for the window focused last, here a site's tab at 100%.
|
||||||
|
function tabsStub(zoom) {
|
||||||
|
return {
|
||||||
|
getCurrent: (done) => done({ id: 11 }),
|
||||||
|
getZoom: (...args) => {
|
||||||
|
const done = args.pop();
|
||||||
|
done(args[0] === 11 ? zoom : 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// An approval window at `zoom`, `clientWidth` page pixels wide before its
|
||||||
|
// screen is up, whose background holds its answer until the test gives it.
|
||||||
|
async function bootZoomedApprovalWindow(windows, zoom, clientWidth) {
|
||||||
|
let answer = null;
|
||||||
|
const env = await bootPopup(unversionedValidProfile(), {
|
||||||
|
search: "?approval=approval-1",
|
||||||
|
runtime: {
|
||||||
|
connect: () => ({ postMessage: () => {} }),
|
||||||
|
sendMessage: (msg, reply) => {
|
||||||
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") answer = reply;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
windows,
|
||||||
|
tabs: tabsStub(zoom),
|
||||||
|
documentElement: { scrollWidth: 396, clientWidth },
|
||||||
|
});
|
||||||
|
return { env, answer };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("at 150% zoom the approval window widens by what its screen lacks, scrollbar included", async () => {
|
||||||
|
const windows = windowsStub("popup", 1.5);
|
||||||
|
// At 150% a window 396 screen pixels wide shows 264 page pixels.
|
||||||
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
||||||
|
|
||||||
|
// Not before there is a screen to fit.
|
||||||
|
expect(windows.updates).toEqual([]);
|
||||||
|
|
||||||
|
// The screen is taller than the window, so once it is up a vertical
|
||||||
|
// scrollbar takes 15 screen pixels, 10 page pixels at 150%.
|
||||||
|
env.document.documentElement.clientWidth = 254;
|
||||||
|
answer({
|
||||||
|
type: "network",
|
||||||
|
origin: "https://dapp.example",
|
||||||
|
currentNetworkId: "mainnet",
|
||||||
|
requestedNetworkId: "sepolia",
|
||||||
|
isPhishingDomain: false,
|
||||||
|
});
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
// 396 - 254 = 142 page pixels short, which at 150% is 213 screen pixels.
|
||||||
|
// At the 100% of the site's tab it would be 142, and the screen would
|
||||||
|
// still not fit.
|
||||||
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
||||||
|
expect(windows.updates).toEqual([{ id: 7, info: { width: 609 } }]);
|
||||||
|
expect(env.document.documentElement.clientWidth).toBe(396);
|
||||||
|
expect(env.pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("at 170% the approval window is widened again while its screen still falls short", async () => {
|
||||||
|
// Firefox reports 170% as 1.7 but draws a page pixel about 1.714 screen
|
||||||
|
// pixels wide, so a window 396 wide shows 231 page pixels.
|
||||||
|
const windows = windowsStub("popup", 1.714);
|
||||||
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.7, 231);
|
||||||
|
|
||||||
|
// Less the scrollbar once the screen is up.
|
||||||
|
env.document.documentElement.clientWidth = 222;
|
||||||
|
answer({
|
||||||
|
type: "network",
|
||||||
|
origin: "https://dapp.example",
|
||||||
|
currentNetworkId: "mainnet",
|
||||||
|
requestedNetworkId: "sepolia",
|
||||||
|
isPhishingDomain: false,
|
||||||
|
});
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
// 174 page pixels short, so 296 screen pixels wider at 1.7. That shows 394
|
||||||
|
// page pixels, still 2 short, so 4 screen pixels wider again.
|
||||||
|
expect(windows.updates).toEqual([
|
||||||
|
{ id: 7, info: { width: 692 } },
|
||||||
|
{ id: 7, info: { width: 696 } },
|
||||||
|
]);
|
||||||
|
const page = env.document.documentElement;
|
||||||
|
expect(page.scrollWidth).toBeLessThanOrEqual(page.clientWidth);
|
||||||
|
expect(env.pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a connection prompt in the toolbar popup leaves the browser window alone", async () => {
|
||||||
|
const windows = windowsStub("normal", 1.5);
|
||||||
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
||||||
|
|
||||||
|
answer({ origin: "https://dapp.example", isPhishingDomain: false });
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(env.visibleViews()).toEqual(["approve-site"]);
|
||||||
|
expect(windows.updates).toEqual([]);
|
||||||
|
expect(env.pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2930,7 +2930,7 @@ describe("where an approval window opens", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
expect(bg.created).toHaveLength(1);
|
expect(bg.created).toHaveLength(1);
|
||||||
expect(bg.created[0]).toMatchObject({ left: 460, top: 60 });
|
expect(bg.created[0]).toMatchObject({ left: 442, top: 60 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("not centred on an approval window the user was last in", async () => {
|
test("not centred on an approval window the user was last in", async () => {
|
||||||
@@ -2947,7 +2947,7 @@ describe("where an approval window opens", () => {
|
|||||||
bg.requestSign();
|
bg.requestSign();
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
// Centred, it would be at left 900, the position the browser refused.
|
// Centred, it would be at left 882, the position the browser refused.
|
||||||
expect(bg.created).toHaveLength(1);
|
expect(bg.created).toHaveLength(1);
|
||||||
expect(bg.created[0].left).toBeUndefined();
|
expect(bg.created[0].left).toBeUndefined();
|
||||||
expect(bg.created[0].top).toBeUndefined();
|
expect(bg.created[0].top).toBeUndefined();
|
||||||
@@ -2969,7 +2969,7 @@ describe("where an approval window opens", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
expect(bg.created).toHaveLength(2);
|
expect(bg.created).toHaveLength(2);
|
||||||
expect(bg.created[0]).toMatchObject({ left: 1520, top: 750 });
|
expect(bg.created[0]).toMatchObject({ left: 1502, top: 750 });
|
||||||
expect(bg.created[1].left).toBeUndefined();
|
expect(bg.created[1].left).toBeUndefined();
|
||||||
expect(bg.created[1].top).toBeUndefined();
|
expect(bg.created[1].top).toBeUndefined();
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,26 @@ async function waitForApprovalWindow(d, timeout = 30000) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Firefox keeps one zoom per site, so a zoom set on the popup's tab holds for
|
||||||
|
// every page of the extension, the approval windows included.
|
||||||
|
async function setExtensionZoom(env, zoom) {
|
||||||
|
const d = env.driver;
|
||||||
|
await d.switchToWindow(env.popupWindow);
|
||||||
|
const outcome = await d.executeAsync(
|
||||||
|
`const done = arguments[arguments.length - 1];
|
||||||
|
const zoom = arguments[0];
|
||||||
|
browser.tabs
|
||||||
|
.getCurrent()
|
||||||
|
.then((tab) => browser.tabs.setZoom(tab.id, zoom))
|
||||||
|
.then(() => done({}), (e) => done({ error: String(e) }));`,
|
||||||
|
[zoom],
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
outcome && !outcome.error,
|
||||||
|
"could not set the extension's zoom: " + (outcome && outcome.error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function startRequest(d, key, method, params) {
|
function startRequest(d, key, method, params) {
|
||||||
return d.execute(
|
return d.execute(
|
||||||
"window.__dapp.start(arguments[0], arguments[1], arguments[2]);" +
|
"window.__dapp.start(arguments[0], arguments[1], arguments[2]);" +
|
||||||
@@ -633,6 +653,14 @@ step(
|
|||||||
"personal_sign returns a signature that recovers to the address",
|
"personal_sign returns a signature that recovers to the address",
|
||||||
async (env) => {
|
async (env) => {
|
||||||
const d = env.driver;
|
const d = env.driver;
|
||||||
|
// The window is opened as wide as the page, and widened once the
|
||||||
|
// screen is up if it still scrolls sideways
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/518). This suite is
|
||||||
|
// where that is checked: the Chrome suite's approval windows open at
|
||||||
|
// 1280x720 whatever width the extension asks for. At 170%, one of
|
||||||
|
// Firefox's zoom steps, the page is drawn a little larger than the
|
||||||
|
// 1.7 Firefox reports, so a single widening leaves it a pixel short.
|
||||||
|
await setExtensionZoom(env, 1.7);
|
||||||
await d.switchToWindow(env.dappWindow);
|
await d.switchToWindow(env.dappWindow);
|
||||||
await startRequest(d, "sign", "personal_sign", [SIGN_HEX, env.address]);
|
await startRequest(d, "sign", "personal_sign", [SIGN_HEX, env.address]);
|
||||||
|
|
||||||
@@ -640,6 +668,12 @@ step(
|
|||||||
await d.switchToWindow(popup);
|
await d.switchToWindow(popup);
|
||||||
await d.waitVisible("#view-approve-sign");
|
await d.waitVisible("#view-approve-sign");
|
||||||
|
|
||||||
|
await d.waitFor(
|
||||||
|
"the sign prompt to fit its window without scrolling sideways",
|
||||||
|
"return document.documentElement.scrollWidth <=" +
|
||||||
|
" document.documentElement.clientWidth;",
|
||||||
|
);
|
||||||
|
|
||||||
const screen = await d.execute(
|
const screen = await d.execute(
|
||||||
`return {
|
`return {
|
||||||
origin: document.getElementById("approve-sign-origin").textContent,
|
origin: document.getElementById("approve-sign-origin").textContent,
|
||||||
@@ -674,6 +708,7 @@ step(
|
|||||||
|
|
||||||
await d.switchToWindow(env.dappWindow);
|
await d.switchToWindow(env.dappWindow);
|
||||||
const outcome = await settleRequest(d, "sign");
|
const outcome = await settleRequest(d, "sign");
|
||||||
|
await setExtensionZoom(env, 1);
|
||||||
assert(
|
assert(
|
||||||
outcome.settled === "resolved",
|
outcome.settled === "resolved",
|
||||||
"personal_sign did not resolve: " + JSON.stringify(outcome),
|
"personal_sign did not resolve: " + JSON.stringify(outcome),
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// 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 adds a wallet
|
||||||
|
// through one of the three import tabs and checks which address is active by
|
||||||
|
// opening Receive from Home in the same popup.
|
||||||
|
|
||||||
|
const { HDNodeWallet, Mnemonic, Wallet } = require("ethers");
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
ADDRESS,
|
||||||
|
} = 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;
|
||||||
|
|
||||||
|
// Addresses 1 and 2 of PHRASE used, address 0 not: a scan that answers these
|
||||||
|
// replaces the wallet's list with one that leaves out its first address.
|
||||||
|
const account = HDNodeWallet.fromPhrase(PHRASE, "", "m/44'/60'/0'/0");
|
||||||
|
const used = [1, 2].map((index) => ({
|
||||||
|
address: account.deriveChild(index).address,
|
||||||
|
index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fills in the open AddWallet screen and presses "Import".
|
||||||
|
async function importOn(page, { tab, field, secret }) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the first wallet's first address is active right away", () => {
|
||||||
|
test.each(TABS)("added from $tab, Receive opens on it", async (entry) => {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
expect(page.pageErrors).toEqual([]);
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
await importOn(page, entry);
|
||||||
|
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(
|
||||||
|
entry.address,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a scan that leaves out the first address", () => {
|
||||||
|
// The key tab runs no scan.
|
||||||
|
test.each(TABS.filter((t) => t.tab !== "tab-privkey"))(
|
||||||
|
"added from $tab, the scan's first address is active",
|
||||||
|
async (entry) => {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
scanForAddresses.mockResolvedValueOnce({
|
||||||
|
addresses: used,
|
||||||
|
nextIndex: 3,
|
||||||
|
});
|
||||||
|
await page.click("btn-welcome-add");
|
||||||
|
await importOn(page, entry);
|
||||||
|
|
||||||
|
expect(page.storage.read("autistmask").activeAddress).toBe(
|
||||||
|
used[0].address,
|
||||||
|
);
|
||||||
|
expect(globalThis.chrome.runtime.sendMessage).toHaveBeenCalledWith({
|
||||||
|
type: "AUTISTMASK_ACTIVE_CHANGED",
|
||||||
|
});
|
||||||
|
await page.click("btn-main-receive");
|
||||||
|
expect(page.node("receive-address-block").dataset.full).toBe(
|
||||||
|
used[0].address,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adding a wallet while an address is active", () => {
|
||||||
|
test.each(TABS)(
|
||||||
|
"added from $tab, the active address is unchanged",
|
||||||
|
async (entry) => {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
expect(page.pageErrors).toEqual([]);
|
||||||
|
// The phrase and xprv scans leave out the new wallet's first
|
||||||
|
// address, which was never active here. The key tab runs no scan.
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
scanForAddresses.mockResolvedValueOnce({
|
||||||
|
addresses: used,
|
||||||
|
nextIndex: 3,
|
||||||
|
});
|
||||||
|
await page.click("btn-add-wallet-bottom");
|
||||||
|
await importOn(page, entry);
|
||||||
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
|
||||||
|
expect(page.storage.read("autistmask").activeAddress).toBe(ADDRESS);
|
||||||
|
expect(
|
||||||
|
globalThis.chrome.runtime.sendMessage,
|
||||||
|
).not.toHaveBeenCalledWith({ type: "AUTISTMASK_ACTIVE_CHANGED" });
|
||||||
|
await page.click("btn-main-receive");
|
||||||
|
expect(page.node("receive-address-block").dataset.full).toBe(
|
||||||
|
ADDRESS,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -242,6 +242,13 @@ async function settle() {
|
|||||||
* "?approval=" and an id for the popup opened as an approval window.
|
* "?approval=" and an id for the popup opened as an approval window.
|
||||||
* @param {object} [options.runtime] members of chrome.runtime that replace the
|
* @param {object} [options.runtime] members of chrome.runtime that replace the
|
||||||
* stub's own, for a test that has to answer the background's messages.
|
* stub's own, for a test that has to answer the background's messages.
|
||||||
|
* @param {object} [options.windows] chrome.windows, for a test of the approval
|
||||||
|
* window resizing itself.
|
||||||
|
* @param {object} [options.tabs] chrome.tabs, for the same test: the page's
|
||||||
|
* tab and its zoom.
|
||||||
|
* @param {object} [options.documentElement] members of document.documentElement
|
||||||
|
* that replace the stub's own: scrollWidth and clientWidth, which are equal
|
||||||
|
* unless a test says otherwise, so the page does not scroll sideways.
|
||||||
* @returns {Promise<object>} handles onto the booted page.
|
* @returns {Promise<object>} handles onto the booted page.
|
||||||
*/
|
*/
|
||||||
async function bootPopup(stored, options) {
|
async function bootPopup(stored, options) {
|
||||||
@@ -258,12 +265,17 @@ async function bootPopup(stored, options) {
|
|||||||
formatAddressTotal: () => "",
|
formatAddressTotal: () => "",
|
||||||
getAddressValue: () => ({ usd: null, partial: false }),
|
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", () => ({
|
jest.doMock("../../src/shared/balances", () => ({
|
||||||
fetchTokenBalances: jest.fn(async () => []),
|
fetchTokenBalances: jest.fn(async () => []),
|
||||||
refreshBalances: jest.fn(async () => {}),
|
refreshBalances: jest.fn(async () => {}),
|
||||||
lookupTokenInfo: jest.fn(async () => null),
|
lookupTokenInfo: jest.fn(async () => null),
|
||||||
getProvider: () => ({}),
|
getProvider: () => ({}),
|
||||||
scanForAddresses: jest.fn(async () => []),
|
scanForAddresses: jest.fn(async () => ({
|
||||||
|
addresses: [],
|
||||||
|
nextIndex: 1,
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
// filterTransactions() answers in the real one's shape: Home,
|
// filterTransactions() answers in the real one's shape: Home,
|
||||||
// AddressDetail and AddressToken read both fields, and a bare list makes
|
// AddressDetail and AddressToken read both fields, and a bare list makes
|
||||||
@@ -277,7 +289,13 @@ async function bootPopup(stored, options) {
|
|||||||
(options && options.storage) ||
|
(options && options.storage) ||
|
||||||
makeStorageStub(stored === undefined ? {} : { autistmask: stored });
|
makeStorageStub(stored === undefined ? {} : { autistmask: stored });
|
||||||
const document = makeDocument(POPUP_HTML);
|
const document = makeDocument(POPUP_HTML);
|
||||||
|
Object.assign(
|
||||||
|
document.documentElement,
|
||||||
|
{ scrollWidth: 396, clientWidth: 396 },
|
||||||
|
options && options.documentElement,
|
||||||
|
);
|
||||||
const reloads = [];
|
const reloads = [];
|
||||||
|
const windowListeners = {};
|
||||||
|
|
||||||
globalThis.chrome = {
|
globalThis.chrome = {
|
||||||
storage: { local: storage.local },
|
storage: { local: storage.local },
|
||||||
@@ -287,6 +305,8 @@ async function bootPopup(stored, options) {
|
|||||||
onMessage: { addListener: () => {} },
|
onMessage: { addListener: () => {} },
|
||||||
...(options && options.runtime),
|
...(options && options.runtime),
|
||||||
},
|
},
|
||||||
|
windows: options && options.windows,
|
||||||
|
tabs: options && options.tabs,
|
||||||
};
|
};
|
||||||
globalThis.document = document;
|
globalThis.document = document;
|
||||||
globalThis.window = {
|
globalThis.window = {
|
||||||
@@ -300,7 +320,13 @@ async function bootPopup(stored, options) {
|
|||||||
addEventListener: () => {},
|
addEventListener: () => {},
|
||||||
removeEventListener: () => {},
|
removeEventListener: () => {},
|
||||||
}),
|
}),
|
||||||
addEventListener: () => {},
|
// Recorded, as the document's are, so a test that widens the approval
|
||||||
|
// window can call the page's resize listeners.
|
||||||
|
listeners: windowListeners,
|
||||||
|
addEventListener: (name, fn) => {
|
||||||
|
windowListeners[name] = windowListeners[name] || [];
|
||||||
|
windowListeners[name].push(fn);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// The ten-second refresh init() starts, and a transaction wait's timers,
|
// The ten-second refresh init() starts, and a transaction wait's timers,
|
||||||
// would outlive the test. So every interval is recorded rather than
|
// would outlive the test. So every interval is recorded rather than
|
||||||
|
|||||||
Reference in New Issue
Block a user