Compare commits
3
Commits
main
...
18188c2f62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18188c2f62 | ||
|
|
99292b9188 | ||
|
|
1197d2171b |
@@ -800,7 +800,12 @@ discoverable.
|
||||
addresses visually, as a security feature.
|
||||
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
|
||||
styling. Tailwind is configured with a minimal monochrome palette. This keeps
|
||||
the styling co-located with the markup and eliminates CSS file management.
|
||||
the styling co-located with the markup and eliminates CSS file management. The
|
||||
handful of classes in `styles/main.css` are not styling: `.copy-flash-*`
|
||||
carries the copy feedback animation, and `.am-address` carries the rule that
|
||||
an address never wraps. Both are invariants that hold in every place they
|
||||
appear, and spelling either out as repeated utilities is how one of those
|
||||
places drifts away from the rest.
|
||||
- **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small
|
||||
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
|
||||
@@ -849,6 +854,12 @@ that the portions still displayed will be more than adequate for the user to
|
||||
verify addresses even in the case of address spoofing attacks. Clicking an
|
||||
address will always copy the full, untruncated value.
|
||||
|
||||
As of the address-row layout change, no view invokes that exception: every
|
||||
address in the popup is rendered on a row of its own, wide enough for all 42
|
||||
characters, and no screen truncates one to fit. The cap is still enforced in
|
||||
`truncateMiddle()` and the 32-character floor in `renderAddressHtml()`, so the
|
||||
guarantee holds for any future caller; there simply are none today.
|
||||
|
||||
**Specific Exception — Transaction Detail view:** The transaction detail screen
|
||||
is the authoritative record of a specific transaction and shows the exact,
|
||||
untruncated amount with all meaningful decimal places (e.g. "0.00498824598498216
|
||||
@@ -1182,13 +1193,17 @@ view would leave a wallet one click from deletion.
|
||||
- Send / Receive quick-action buttons, both acting on the active address
|
||||
- ETH/USD price display
|
||||
- Wallet list: each wallet shows its name (tap to rename inline) and a "+"
|
||||
button for HD and xprv wallets, then one block per address with "Address
|
||||
N" (bold when active), the ENS name if resolved, the full address, an
|
||||
`[info]` button, an `[x]` button (only on HD and xprv wallets holding more
|
||||
than one address), the address USD total, and a balance line for ETH and
|
||||
for each token shown for that address
|
||||
button for HD and xprv wallets, then one block per address. The block
|
||||
opens with a row carrying the colour dot, "Address N" (bold when active),
|
||||
an `[info]` button and an `[x]` button (only on HD and xprv wallets
|
||||
holding more than one address); the ENS name, if resolved, is below it;
|
||||
then the full address on a row of its own, followed by the address USD
|
||||
total and a balance line for ETH and for each token shown for that address
|
||||
- "Recent Transactions": up to 25 transactions merged across every address
|
||||
of every wallet, deduplicated by hash and filtered
|
||||
of every wallet, deduplicated by hash and filtered. Each row is three
|
||||
lines: age and direction, then the counterparty's colour dot (with our own
|
||||
name for it, where it is one of our addresses) and the amount, then the
|
||||
counterparty's full address on a row of its own
|
||||
- "Add additional wallet..." link at bottom
|
||||
- **Transitions**:
|
||||
- Tap address row → sets the active address and broadcasts
|
||||
|
||||
@@ -45,6 +45,48 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-09-21: A transaction response is honoured only for a transaction
|
||||
approval, and the three remaining approval-settlement paths are pinned
|
||||
([#262](https://git.eeqj.de/sneak/AutistMask/issues/262)). The liveness fix
|
||||
the issue asks for — settle `4001` on release when the window it would be
|
||||
retried in is gone — already landed with
|
||||
[#271](https://git.eeqj.de/sneak/AutistMask/issues/271); this closes the rest.
|
||||
`AUTISTMASK_TX_RESPONSE` now refuses any approval that is not a transaction
|
||||
approval, so a reject no longer retires a sign or connection approval and a
|
||||
signed artifact never runs the broadcast path against one, which before only
|
||||
failed closed by throwing deeper in. Tests pin the site-connection port's
|
||||
approve, reject and disconnect paths against a transaction approval
|
||||
broadcasting behind them: each is declined and the dApp still receives its
|
||||
broadcast result.
|
||||
|
||||
- 2026-09-21: Adding a second wallet no longer accepts a different password with
|
||||
nothing saying it is a separate one
|
||||
([#374](https://git.eeqj.de/sneak/AutistMask/issues/374)). Each wallet has its
|
||||
own encrypted secret, so per-wallet passwords are by design; the add-wallet
|
||||
screen said only "Choose a password". A note now appears on that screen when
|
||||
the profile already holds a wallet, stating that each wallet has its own
|
||||
password and this one need not match any already in use. It is shown only
|
||||
then, since the first wallet has no other password to differ from, and it
|
||||
stays consistent with the no-reset reality of
|
||||
[#312](https://git.eeqj.de/sneak/AutistMask/issues/312) by promising no
|
||||
recovery or reset.
|
||||
|
||||
- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the
|
||||
common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The
|
||||
wallet list was the reported case: the address shared one row with the
|
||||
`[info]` and `[x]` controls and folded onto a second line, which turns one
|
||||
42-character string the user is meant to compare into two shorter ones — the
|
||||
shape an address-poisoning attack wants. The fix is layout, not CSS: every
|
||||
address in the popup now sits alone on a full-width row, with the colour dot,
|
||||
the wallet title, the ENS name and the explorer link moved onto a strip above
|
||||
it, and the transaction rows carry the counterparty's whole address instead of
|
||||
a `truncateMiddle()`d one squeezed in beside the amount. `truncateMiddle()`
|
||||
keeps its 10-character cap and its 32-character floor moved into
|
||||
`renderAddressHtml()`, so the guarantee outlives having no callers. The e2e
|
||||
suite measures every rendered address in a real Chromium — whole, one line
|
||||
box, inside its row and inside the popup — across Home, the address, token,
|
||||
receive, send and transaction detail screens, the confirmation screen and the
|
||||
dApp transaction prompt.
|
||||
- 2026-08-23: Both manifests declare toolbar icons, and real PNGs at
|
||||
16/32/48/128 ship inside both archives
|
||||
([#371](https://git.eeqj.de/sneak/AutistMask/issues/371)). Neither manifest
|
||||
|
||||
@@ -1344,6 +1344,15 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
const approval = pendingApprovals[msg.id];
|
||||
if (!approval) return false;
|
||||
|
||||
// This message signs and broadcasts a transaction, so it is honoured
|
||||
// only for a transaction approval. A sign or connection approval
|
||||
// carries no approvedTx, and reaching the broadcast path with one used
|
||||
// to fail closed by throwing deeper in; refusing here keeps a future
|
||||
// refactor from turning that incidental throw into a live path, and
|
||||
// keeps a reject on this message from retiring an approval of another
|
||||
// kind.
|
||||
if (approval.type !== "tx") return false;
|
||||
|
||||
// A reject arriving while an attempt holds the approval is refused,
|
||||
// not honoured: the attempt is on its way to broadcasting the
|
||||
// transaction, and resolving 4001 here would tell the page the request
|
||||
|
||||
+35
-29
@@ -152,6 +152,21 @@
|
||||
|
||||
<!-- Shared password fields -->
|
||||
<div class="mb-2" id="add-wallet-password-section">
|
||||
<!-- Shown only when the profile already holds a wallet:
|
||||
each wallet has its own password (its own
|
||||
encryptedSecret), so a second wallet does not reuse
|
||||
the first one's. addWallet.js toggles this on screen
|
||||
entry from state.wallets.length, so it is constant
|
||||
while the screen is up and moves nothing. -->
|
||||
<p
|
||||
class="text-xs mb-2 border border-border border-dashed p-2 hidden"
|
||||
id="add-wallet-separate-password-note"
|
||||
>
|
||||
You already have a wallet. Each wallet has its own
|
||||
password: the one you choose here is only for this new
|
||||
wallet, and it need not match any password you already
|
||||
use.
|
||||
</p>
|
||||
<label class="block mb-1">Choose a password</label>
|
||||
<!-- The hint is swapped in place when the import tab
|
||||
changes, and it sits directly above the password
|
||||
@@ -213,10 +228,7 @@
|
||||
</div>
|
||||
|
||||
<!-- active address display -->
|
||||
<div
|
||||
id="active-address-display"
|
||||
class="text-xs break-all mb-3"
|
||||
></div>
|
||||
<div id="active-address-display" class="text-xs mb-3"></div>
|
||||
|
||||
<!-- quick actions for active address -->
|
||||
<div class="flex gap-2 mb-2">
|
||||
@@ -292,7 +304,7 @@
|
||||
class="font-bold mb-1 hidden flex items-center"
|
||||
></div>
|
||||
<div
|
||||
class="text-xs mb-1 cursor-pointer break-all"
|
||||
class="text-xs mb-1 cursor-pointer"
|
||||
title="Click to copy"
|
||||
id="address-line"
|
||||
>
|
||||
@@ -380,14 +392,14 @@
|
||||
></div>
|
||||
<h2 class="font-bold mb-1">Export Private Key</h2>
|
||||
<p class="text-xs mb-1" id="export-privkey-title"></p>
|
||||
<p class="text-xs mb-3">
|
||||
<div class="text-xs mb-3">
|
||||
<span id="export-privkey-dot"></span>
|
||||
<span
|
||||
id="export-privkey-address"
|
||||
class="cursor-pointer"
|
||||
title="Click to copy"
|
||||
></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs mb-3 text-muted">
|
||||
Warning: anyone with this private key can access and
|
||||
transfer all funds from this address. Never share it.
|
||||
@@ -440,7 +452,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-xs mb-1 cursor-pointer break-all"
|
||||
class="text-xs mb-1 cursor-pointer"
|
||||
title="Click to copy"
|
||||
id="address-token-line"
|
||||
>
|
||||
@@ -573,19 +585,16 @@
|
||||
<!-- ERC-20 token contract (hidden for ETH) -->
|
||||
<div id="confirm-token-section" class="mb-3 hidden">
|
||||
<div class="text-xs text-muted mb-1">Token contract</div>
|
||||
<div
|
||||
id="confirm-token-contract"
|
||||
class="text-xs break-all"
|
||||
></div>
|
||||
<div id="confirm-token-contract" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="confirm-from" class="text-xs break-all"></div>
|
||||
<div id="confirm-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="confirm-to" class="text-xs break-all"></div>
|
||||
<div id="confirm-to" class="text-xs"></div>
|
||||
<div
|
||||
id="confirm-to-ens"
|
||||
class="text-xs text-muted hidden"
|
||||
@@ -728,7 +737,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="wait-tx-to" class="text-xs break-all"></div>
|
||||
<div id="wait-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
@@ -747,7 +756,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="success-tx-to" class="text-xs break-all"></div>
|
||||
<div id="success-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Block</div>
|
||||
@@ -774,7 +783,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="error-tx-to" class="text-xs break-all"></div>
|
||||
<div id="error-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div
|
||||
@@ -811,9 +820,9 @@
|
||||
<canvas id="receive-qr"></canvas>
|
||||
</div>
|
||||
<div
|
||||
class="border border-border p-2 break-all mb-3 text-xs cursor-pointer"
|
||||
class="border border-border p-2 mb-3 text-xs cursor-pointer"
|
||||
>
|
||||
<span id="receive-address-block" class="select-all"></span>
|
||||
<div id="receive-address-block" class="select-all"></div>
|
||||
<span id="receive-etherscan-link"></span>
|
||||
</div>
|
||||
<button
|
||||
@@ -1239,7 +1248,7 @@
|
||||
</p>
|
||||
<div
|
||||
id="delete-address-value"
|
||||
class="text-xs mb-2 break-all min-h-[1rem]"
|
||||
class="text-xs mb-2 min-h-[1rem]"
|
||||
></div>
|
||||
<div
|
||||
class="text-xs mb-2 border border-border border-dashed p-2"
|
||||
@@ -1429,14 +1438,11 @@
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div
|
||||
id="tx-detail-from"
|
||||
class="text-xs break-all"
|
||||
></div>
|
||||
<div id="tx-detail-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="tx-detail-to" class="text-xs break-all"></div>
|
||||
<div id="tx-detail-to" class="text-xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1473,7 +1479,7 @@
|
||||
</div>
|
||||
<div
|
||||
id="tx-detail-token-contract"
|
||||
class="text-xs break-all"
|
||||
class="text-xs"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1567,11 +1573,11 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="approve-tx-from" class="text-xs break-all"></div>
|
||||
<div id="approve-tx-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Contract</div>
|
||||
<div id="approve-tx-to" class="text-xs break-all"></div>
|
||||
<div id="approve-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Value</div>
|
||||
@@ -1673,7 +1679,7 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="approve-sign-from" class="text-xs break-all"></div>
|
||||
<div id="approve-sign-from" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -44,3 +44,23 @@ body {
|
||||
background-color 225ms ease-out,
|
||||
color 225ms ease-out;
|
||||
}
|
||||
|
||||
/* An address is one atomic string, so it gets a row of its own and never
|
||||
* breaks across lines. A wrapped address reads as two shorter strings, and
|
||||
* two shorter strings are exactly what an address-poisoning attack needs
|
||||
* the user to compare instead of the whole thing. Every view that shows an
|
||||
* address puts it in one of these, alone: the colour dot, the wallet title,
|
||||
* the ENS name and the explorer link all live on their own line above, so
|
||||
* nothing competes with the 42 characters for width.
|
||||
*
|
||||
* overflow-x is the escape hatch, not the mechanism. The row is wide enough
|
||||
* for a full address at every nesting depth the popup uses; if that ever
|
||||
* stops being true — a font with wider glyphs, a browser zoom — the row
|
||||
* scrolls and the user can still reach the last character, rather than the
|
||||
* tail being clipped away by #app's overflow-x-hidden with nothing to say
|
||||
* it happened. tests/e2e asserts the scroll is never actually needed. */
|
||||
.am-address {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -100,9 +100,24 @@ function clear() {
|
||||
$("add-wallet-phrase-warning").style.visibility = "hidden";
|
||||
}
|
||||
|
||||
// Each wallet has its own password (its own encryptedSecret), so adding a
|
||||
// second wallet does not reuse the first one's. The note that says so is
|
||||
// only meaningful once a wallet exists — on the first wallet there is no
|
||||
// other password to be separate from — so it is shown only then. This is
|
||||
// decided on entry and stays put while the screen is up, so it does not
|
||||
// move the password fields the way a per-tab hint would.
|
||||
function updateSeparatePasswordNote() {
|
||||
const hasExistingWallet = state.wallets.length > 0;
|
||||
$("add-wallet-separate-password-note").classList.toggle(
|
||||
"hidden",
|
||||
!hasExistingWallet,
|
||||
);
|
||||
}
|
||||
|
||||
function show() {
|
||||
clear();
|
||||
switchMode("mnemonic");
|
||||
updateSeparatePasswordNote();
|
||||
showView("add-wallet");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
@@ -229,10 +228,12 @@ function renderTransactions(txs) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr =
|
||||
title || ensName || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title or ENS name, where there is one, names
|
||||
// it on the line above rather than replacing it.
|
||||
const nameStr = escapeHtml(title || ensName || "");
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -240,7 +241,8 @@ function renderTransactions(txs) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
balanceLine,
|
||||
unknownableAmount,
|
||||
renderAddressHtml,
|
||||
@@ -305,10 +304,12 @@ function renderTransactions(txs) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr =
|
||||
title || ensName || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title or ENS name, where there is one, names
|
||||
// it on the line above rather than replacing it.
|
||||
const nameStr = escapeHtml(title || ensName || "");
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -316,7 +317,8 @@ function renderTransactions(txs) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
|
||||
+33
-15
@@ -331,6 +331,12 @@ function addressHoldsFunds(addr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The fewest characters of an address any caller may ask to display. The
|
||||
// 10-character cap inside truncateMiddle() is the other half of the same
|
||||
// guarantee; this is the half that used to be spelled out at each call
|
||||
// site, and is now enforced once in renderAddressHtml().
|
||||
const ADDRESS_MIN_DISPLAY_LEN = 32;
|
||||
|
||||
// Truncate the middle of a string, replacing removed characters with "…".
|
||||
// Safety: refuses to truncate more than 10 characters, which is the maximum
|
||||
// that still prevents address spoofing attacks (see Display Consistency in
|
||||
@@ -518,17 +524,29 @@ function attachCopyHandlers(container) {
|
||||
|
||||
// Unified address rendering.
|
||||
//
|
||||
// Produces consistent HTML for any Ethereum address:
|
||||
// • Color dot
|
||||
// • Optional title (e.g. "Wallet 1 — Address 2") shown bold above address
|
||||
// • Optional ENS name shown bold above address
|
||||
// • Full address (or truncated via maxLen) with dashed-underline click-to-copy
|
||||
// • Etherscan external link icon
|
||||
// Two stacked rows, in this order:
|
||||
// 1. Identity strip — colour dot, optional title (e.g. "Wallet 1 —
|
||||
// Address 2") and the explorer link icon. Optional ENS name below it.
|
||||
// 2. The address itself, alone on a full-width row that never wraps
|
||||
// (see .am-address in styles/main.css).
|
||||
//
|
||||
// The split is the point. Everything used to sit on one line: dot, address
|
||||
// and link together, with `break-all` to let the address fold when the line
|
||||
// ran out. In the wallet list, where the row also carried [info] and [x],
|
||||
// it ran out every time — the bug in #380 — and a folded address is a
|
||||
// spoofing hazard, not a cosmetic one. Nothing shares the address's row
|
||||
// now, so all 42 characters fit at every nesting depth the popup uses and
|
||||
// nothing has to be dropped or folded to make room.
|
||||
//
|
||||
// Options object:
|
||||
// title — wallet title string (from addressTitle)
|
||||
// ensName — ENS name string
|
||||
// maxLen — if set, truncate address display (min 32 chars enforced)
|
||||
// maxLen — if set, truncate address display. Floored at 32 characters
|
||||
// here rather than by the caller: no view passes it any more
|
||||
// (every address row is wide enough for all 42 characters),
|
||||
// so a floor that lived in the callers would have gone away
|
||||
// with them, and the "at least 32 characters" guarantee has
|
||||
// to survive having no current callers to be a guarantee.
|
||||
// noLink — if true, omit etherscan link
|
||||
//
|
||||
// After inserting the returned HTML into the DOM, call
|
||||
@@ -536,22 +554,22 @@ function attachCopyHandlers(container) {
|
||||
function renderAddressHtml(address, opts) {
|
||||
const { title, ensName, maxLen, noLink } = opts || {};
|
||||
const dot = addressDotHtml(address);
|
||||
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
|
||||
const displayAddr = maxLen
|
||||
? truncateMiddle(address, Math.max(ADDRESS_MIN_DISPLAY_LEN, maxLen))
|
||||
: address;
|
||||
const link = etherscanAddressUrl(address);
|
||||
const extLink = noLink ? "" : etherscanLinkHtml(link);
|
||||
|
||||
let html = "";
|
||||
html += `<div class="flex items-center">${dot}`;
|
||||
if (title) {
|
||||
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
|
||||
html += `<span class="font-bold">${escapeHtml(title)}</span>`;
|
||||
}
|
||||
html += `${extLink}</div>`;
|
||||
if (ensName) {
|
||||
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
|
||||
}
|
||||
if (title || ensName) {
|
||||
html += `<div class="flex items-center">${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
|
||||
} else {
|
||||
html += `<div class="flex items-center">${dot}${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
|
||||
html += `<div class="font-bold">${escapeHtml(ensName)}</div>`;
|
||||
}
|
||||
html += `<div class="am-address">${copyableHtml(displayAddr)}</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
+19
-11
@@ -9,7 +9,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
pushCurrentView,
|
||||
@@ -117,10 +116,13 @@ function renderHomeTxList(ctx) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title, when it is one of our own addresses,
|
||||
// names it on the line above rather than replacing it.
|
||||
const title = addressTitle(counterparty, state.wallets);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr = title || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
const titleStr = title ? escapeHtml(title) : "";
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -128,7 +130,8 @@ function renderHomeTxList(ctx) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${titleStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
@@ -252,17 +255,22 @@ function walletListHtml() {
|
||||
: "";
|
||||
const dot = addressDotHtml(addr.address);
|
||||
const titleBold = isActive ? "font-bold" : "";
|
||||
html += `<div class="text-xs ${titleBold}">Address ${ai + 1}</div>`;
|
||||
// [info] and [x] ride on the "Address N" line, which was empty
|
||||
// to its right, so the address below gets the row to itself.
|
||||
// They used to sit beside the address and take about a third of
|
||||
// the width off it, which is what made a 42-character address
|
||||
// fold onto a second line here and nowhere else (#380).
|
||||
html += `<div class="flex text-xs items-center justify-between">`;
|
||||
html += `<span class="flex items-center ${titleBold}">${dot}Address ${ai + 1}</span>`;
|
||||
html += `<span class="flex-shrink-0 ml-1">${infoBtn}${removeBtn}</span>`;
|
||||
html += `</div>`;
|
||||
if (addr.ensName) {
|
||||
// An ENS reverse record is whatever the name owner set it
|
||||
// to; renderAddressHtml() escapes its own copy of this and
|
||||
// this list was the one that did not.
|
||||
html += `<div class="text-xs font-bold flex items-center">${dot}${escapeHtml(addr.ensName)}</div>`;
|
||||
html += `<div class="text-xs font-bold">${escapeHtml(addr.ensName)}</div>`;
|
||||
}
|
||||
html += `<div class="flex text-xs items-center justify-between">`;
|
||||
html += `<span class="flex items-center break-all">${addr.ensName ? "" : dot}${escapeHtml(addr.address)}</span>`;
|
||||
html += `<span class="flex-shrink-0 ml-1">${infoBtn}${removeBtn}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="am-address text-xs">${escapeHtml(addr.address)}</div>`;
|
||||
const addrTotal = formatAddressTotal(getAddressValue(addr));
|
||||
html += `<div class="text-xs text-muted text-right min-h-[1rem]">${addrTotal || " "}</div>`;
|
||||
html += balanceLinesForAddress(
|
||||
|
||||
@@ -137,10 +137,16 @@ function render() {
|
||||
if (tx.contractAddress) {
|
||||
const dot = addressDotHtml(tx.contractAddress);
|
||||
const link = explorerUrl("token", tx.contractAddress);
|
||||
// Hand-rolled rather than renderAddressHtml() because the
|
||||
// link goes to the explorer's /token/ page, not /address/.
|
||||
// Same two-row shape though: dot and link on the strip, the
|
||||
// contract address alone on the row below it.
|
||||
tokenContractEl.innerHTML =
|
||||
`<div class="flex items-center">${dot}` +
|
||||
copyableHtml(tx.contractAddress, "break-all") +
|
||||
etherscanLinkHtml(link) +
|
||||
`</div>` +
|
||||
`<div class="am-address">` +
|
||||
copyableHtml(tx.contractAddress) +
|
||||
`</div>`;
|
||||
tokenContractSection.classList.remove("hidden");
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Adding a second wallet accepts a password different from the first one's
|
||||
// with nothing on screen saying the two are separate — each wallet has its
|
||||
// own encryptedSecret, so per-wallet passwords are by design, but the add
|
||||
// screen said only "Choose a password"
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/374).
|
||||
//
|
||||
// The fix is copy: a note on the password screen that says each wallet has
|
||||
// its own password and this one need not match. It is only meaningful once
|
||||
// a wallet exists — on the very first wallet there is no other password to
|
||||
// be separate from — so it is shown then and hidden otherwise. These boot
|
||||
// the real popup and reach the add-wallet screen through the same button a
|
||||
// user presses, so the note's visibility is decided by the real show().
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
POPUP_HTML,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
const NOTE = "add-wallet-separate-password-note";
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
});
|
||||
|
||||
describe("second-wallet password note", () => {
|
||||
test("hidden while onboarding the first wallet", async () => {
|
||||
const page = await bootPopup(undefined);
|
||||
expect(page.pageErrors).toEqual([]);
|
||||
await page.click("btn-welcome-add");
|
||||
expect(page.visibleViews()).toContain("add-wallet");
|
||||
expect(page.hidden(NOTE)).toBe(true);
|
||||
});
|
||||
|
||||
test("shown when a wallet already exists", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.pageErrors).toEqual([]);
|
||||
await page.click("btn-main-add-wallet");
|
||||
expect(page.visibleViews()).toContain("add-wallet");
|
||||
expect(page.hidden(NOTE)).toBe(false);
|
||||
});
|
||||
|
||||
// The copy states the two facts the definition of done asks for — each
|
||||
// wallet has its own password, and this one need not match — and stays
|
||||
// consistent with the no-password-reset reality of
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/312 by not promising any
|
||||
// recovery or reset here.
|
||||
test("the note says the password is per-wallet and need not match", () => {
|
||||
const note = /id="add-wallet-separate-password-note"[^>]*>([^]*?)<\/p>/
|
||||
.exec(POPUP_HTML)[1]
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
expect(note).toContain("its own");
|
||||
expect(note).toContain("need not match");
|
||||
expect(note).not.toMatch(/recover|reset/i);
|
||||
});
|
||||
});
|
||||
@@ -1541,6 +1541,149 @@ describe("a claimed approval outlives every other retirement path", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The approval popup connects a port named for its approval whatever the
|
||||
// approval's kind, so a decision or a disconnect on that port can reach a
|
||||
// transaction approval. Both must be declined: the port decides only
|
||||
// site-connection approvals, and settling a transaction approval it does not
|
||||
// own — while an attempt is broadcasting behind it — is the round-3 fund-loss
|
||||
// bug, where the page is told the request was rejected as the transaction goes
|
||||
// out. These three paths route through settleApproval() and, before this
|
||||
// suite, were exercised only against site approvals.
|
||||
describe("the site-connection port never retires a transaction approval", () => {
|
||||
async function txMidBroadcast() {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestTx();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
const inFlight = deferred();
|
||||
bg.broadcastTransaction.mockReturnValue(inFlight.promise);
|
||||
const first = bg.send(
|
||||
{
|
||||
type: "AUTISTMASK_TX_RESPONSE",
|
||||
id,
|
||||
approved: true,
|
||||
rawSignedTx: await signedAtNonce(7),
|
||||
},
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(bg.broadcastTransaction).toHaveBeenCalledTimes(1);
|
||||
return { bg, pending, id, inFlight, first };
|
||||
}
|
||||
|
||||
test("an approve on the port does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).decide(true, false);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
|
||||
test("a reject on the port does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).decide(false, false);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
|
||||
test("a port disconnect does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
});
|
||||
|
||||
// AUTISTMASK_TX_RESPONSE signs and broadcasts a transaction, so it is honoured
|
||||
// only for a transaction approval. A reject shaped as this message used to
|
||||
// retire a sign or connection approval outright, and an approve carrying a
|
||||
// signed artifact used to run the broadcast path against an approval that names
|
||||
// no transaction, failing closed only by throwing deeper in.
|
||||
describe("a transaction response is honoured only for a transaction approval", () => {
|
||||
test("a reject does not retire a sign approval", async () => {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestSign();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_TX_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
// Still live: its own reject settles it.
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_SIGN_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({
|
||||
error: { code: 4001, message: "User rejected the request." },
|
||||
});
|
||||
});
|
||||
|
||||
test("a reject does not retire a connection approval", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
const pending = bg.requestSite();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_TX_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
// Still live: the port that owns it connects the site.
|
||||
const port = bg.connectApproval(id);
|
||||
port.decide(true, false);
|
||||
port.disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
||||
});
|
||||
|
||||
test("an approve carrying a signed transaction never broadcasts against a sign approval", async () => {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestSign();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{
|
||||
type: "AUTISTMASK_TX_RESPONSE",
|
||||
id,
|
||||
approved: true,
|
||||
rawSignedTx: await signedAtNonce(7),
|
||||
},
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(bg.broadcastTransaction).not.toHaveBeenCalled();
|
||||
expect(pending.result()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// A handler that throws must still answer. `sendResponse` is the only thing
|
||||
// that settles the page's window.ethereum.request() promise, so a throw that
|
||||
// escapes a handler leaves that promise pending forever — no error, no
|
||||
|
||||
@@ -1525,6 +1525,7 @@ async function goToConfirm(page, { token, balance, amount }) {
|
||||
await page.fill("#send-amount", amount);
|
||||
await page.click("#btn-send-review");
|
||||
await visible(page, "#view-confirm-tx");
|
||||
await assertAddressesFit(page, "the confirmation screen");
|
||||
}
|
||||
|
||||
// A balance as the main view renders it: balanceLinesForAddress() writes
|
||||
@@ -3262,6 +3263,8 @@ test("eth_sendTransaction signs the approved transaction and broadcasts it (#183
|
||||
JSON.stringify(screen.data),
|
||||
);
|
||||
|
||||
await assertAddressesFit(popup, "the dApp transaction prompt");
|
||||
|
||||
const broadcastBefore = env.routeOpts.broadcastTransactions.length;
|
||||
await popup.fill("#approve-tx-password", PASSWORD);
|
||||
await popup.click("#btn-approve-tx");
|
||||
@@ -3425,6 +3428,206 @@ test("the password never crossed either boundary in this section (#183)", async
|
||||
await env.dapp.close();
|
||||
});
|
||||
|
||||
// ------------------------------------------- address layout (#380)
|
||||
//
|
||||
// "addresses should never wrap in the common views. this doesn't mean to
|
||||
// just change the css, but update the layout itself so the untruncated
|
||||
// addresses are shown in full and don't mess up the layout."
|
||||
//
|
||||
// Every one of these questions is about glyph advances and the width of
|
||||
// the box an address landed in, and nothing in the markup answers any of
|
||||
// them: a row can hold `white-space: nowrap` and still be too narrow, and
|
||||
// the popup's own `overflow-x-hidden` would then hide the evidence by
|
||||
// clipping the tail. So they are measured in a real Chromium, on the real
|
||||
// rendered views, one assertion per property #380 names:
|
||||
//
|
||||
// - the whole address is there (42 characters, no ellipsis)
|
||||
// - it occupies exactly one line box
|
||||
// - it fits its row, so the overflow-x escape hatch never engages
|
||||
// - its row ends inside the popup's content box
|
||||
// - and the document itself does not scroll sideways
|
||||
//
|
||||
// The narrowest containers the popup has are covered here — the
|
||||
// transaction detail wells (`bg-well p-3 mx-1`) and the token contract
|
||||
// well — so the wider ones cannot fail while these pass.
|
||||
|
||||
// Everything on screen that carries an address, measured in one pass.
|
||||
// Views other than the current one are display:none and measure zero, so
|
||||
// filtering on width leaves exactly what a user can see right now.
|
||||
function addressRowReport(page) {
|
||||
return page.evaluate(() => {
|
||||
const app = document.getElementById("app");
|
||||
const appRight = app.getBoundingClientRect().right;
|
||||
const rows = [];
|
||||
for (const el of document.querySelectorAll(".am-address")) {
|
||||
const box = el.getBoundingClientRect();
|
||||
if (box.width === 0) continue;
|
||||
// Line boxes are counted off the inline content, because the
|
||||
// element's own rect is one box whether the text inside it
|
||||
// wrapped or not. A Range yields a rect per contained node as
|
||||
// well as per line, so it is the distinct tops that count:
|
||||
// a copyable span and the text inside it share one.
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const tops = new Set(
|
||||
Array.from(range.getClientRects()).map((r) =>
|
||||
Math.round(r.top),
|
||||
),
|
||||
);
|
||||
rows.push({
|
||||
text: el.innerText.trim(),
|
||||
lineBoxes: tops.size,
|
||||
overflow: el.scrollWidth - el.clientWidth,
|
||||
overhang: Math.round(box.right - appRight),
|
||||
});
|
||||
}
|
||||
return {
|
||||
rows,
|
||||
pageOverflow:
|
||||
document.documentElement.scrollWidth -
|
||||
document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function assertAddressesFit(page, where) {
|
||||
const report = await addressRowReport(page);
|
||||
assert(
|
||||
report.rows.length > 0,
|
||||
where + ": no address rows were rendered, so nothing was measured",
|
||||
);
|
||||
for (const row of report.rows) {
|
||||
assert(
|
||||
/^0x[0-9a-fA-F]{40}$/.test(row.text),
|
||||
where +
|
||||
": the address is not shown whole: " +
|
||||
JSON.stringify(row.text),
|
||||
);
|
||||
assert(
|
||||
row.lineBoxes === 1,
|
||||
where +
|
||||
": " +
|
||||
row.text +
|
||||
" wrapped onto " +
|
||||
row.lineBoxes +
|
||||
" lines",
|
||||
);
|
||||
assert(
|
||||
row.overflow <= 1,
|
||||
where +
|
||||
": " +
|
||||
row.text +
|
||||
" is " +
|
||||
row.overflow +
|
||||
"px wider than the row holding it",
|
||||
);
|
||||
assert(
|
||||
row.overhang <= 1,
|
||||
where +
|
||||
": " +
|
||||
row.text +
|
||||
" reaches " +
|
||||
row.overhang +
|
||||
"px past the popup's content box",
|
||||
);
|
||||
}
|
||||
assert(
|
||||
report.pageOverflow <= 0,
|
||||
where + ": the popup scrolls sideways by " + report.pageOverflow + "px",
|
||||
);
|
||||
return report.rows.length;
|
||||
}
|
||||
|
||||
// Back to Home from wherever the suite above finished, without assuming
|
||||
// which screen that was. Every screen the popup can rest on has a Back
|
||||
// button, and Home has none, so unwinding until Home shows is the one
|
||||
// route that does not depend on the order of the tests before this point.
|
||||
async function unwindToHome(page) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (await page.isVisible("#view-main")) return;
|
||||
const back = page
|
||||
.locator(".view:not(.hidden) button", { hasText: "Back" })
|
||||
.first();
|
||||
if ((await back.count()) === 0) break;
|
||||
await back.click();
|
||||
await page.waitForTimeout(150);
|
||||
}
|
||||
await visible(page, "#view-main");
|
||||
}
|
||||
|
||||
// The reproduction from the issue: a wallet holding more than one address.
|
||||
// Every address in the list is a full 42 characters competing with the
|
||||
// [info] and [x] controls for one row's width, which is the state the
|
||||
// wallet view was reported wrapping in.
|
||||
test("a wallet with two addresses lists both in full, unwrapped (#380)", async (env) => {
|
||||
await unwindToHome(env.page);
|
||||
|
||||
const before = await env.page
|
||||
.locator("#wallet-list .btn-addr-info")
|
||||
.count();
|
||||
await env.page.locator("#wallet-list .btn-add-address").first().click();
|
||||
await env.page.waitForFunction(
|
||||
(n) =>
|
||||
document.querySelectorAll("#wallet-list .btn-addr-info").length > n,
|
||||
before,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
|
||||
const shown = await assertAddressesFit(env.page, "the wallet list");
|
||||
assert(
|
||||
shown >= before + 1,
|
||||
"the wallet list measured " +
|
||||
shown +
|
||||
" addresses, fewer than the " +
|
||||
(before + 1) +
|
||||
" it now holds",
|
||||
);
|
||||
|
||||
// The [x] control only exists on a wallet holding more than one
|
||||
// address, so its presence is also the proof the second one landed.
|
||||
const removable = await env.page
|
||||
.locator("#wallet-list .btn-remove-address")
|
||||
.count();
|
||||
assert(removable > 0, "the second address did not reach the wallet list");
|
||||
});
|
||||
|
||||
test("every common view shows its addresses in full on one line (#380)", async (env) => {
|
||||
await unwindToHome(env.page);
|
||||
await assertAddressesFit(env.page, "Home");
|
||||
|
||||
await env.page.locator("#wallet-list .btn-addr-info").first().click();
|
||||
await visible(env.page, "#view-address");
|
||||
await visible(env.page, "#tx-list .tx-row");
|
||||
await assertAddressesFit(env.page, "the address screen");
|
||||
|
||||
await env.page.click("#btn-receive");
|
||||
await visible(env.page, "#view-receive");
|
||||
await assertAddressesFit(env.page, "the receive screen");
|
||||
await env.page.click("#btn-receive-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await env.page.click("#btn-send");
|
||||
await visible(env.page, "#view-send");
|
||||
await assertAddressesFit(env.page, "the send screen");
|
||||
await env.page.click("#btn-send-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
// The transaction detail screen carries the narrowest address rows in
|
||||
// the popup: its fields sit inside a well that takes another 24px of
|
||||
// padding and 8px of margin off the content width, and the token
|
||||
// contract row there is narrower still.
|
||||
await env.page.locator("#address-balances .balance-row").first().click();
|
||||
await visible(env.page, "#view-address-token");
|
||||
await assertAddressesFit(env.page, "the token screen");
|
||||
await env.page.click("#btn-address-token-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await env.page.locator("#tx-list .tx-row").first().click();
|
||||
await visible(env.page, "#view-transaction");
|
||||
await visible(env.page, "#tx-detail-token-contract-section");
|
||||
await assertAddressesFit(env.page, "the transaction detail screen");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- runner
|
||||
|
||||
async function main() {
|
||||
|
||||
Reference in New Issue
Block a user