195 lines
9.2 KiB
TypeScript
195 lines
9.2 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* explorer-account-card smoke — cp321.
|
|
*
|
|
* The block-explorer account page (/explorer/account/[name]) got a batch of
|
|
* fixes this checkpoint. This smoke locks them in so they can't silently
|
|
* regress. Two of them were REAL bugs (not cosmetics):
|
|
*
|
|
* • tx / block links 404'd. `morphitExplorerTxUrl` / `morphitExplorerBlockUrl`
|
|
* return LOCALE-LESS paths (`/explorer/tx/<hash>`), but every route lives
|
|
* under `[lang]`, so an un-prefixed path 404s. The fix wraps them in
|
|
* `lp(...)` at the call site. (The no-bare-root-href smoke can't catch this
|
|
* class — the href is a function-returned expression, not a literal `/`.)
|
|
*
|
|
* • "Load older operations" silently failed near the start of an account's
|
|
* history. Blurt's get_account_history rejects `from < limit - 1`, so once
|
|
* `oldestSeqLoaded` drops below a full page the call errored and nothing
|
|
* loaded. The fix clamps the page to `Math.min(PAGE_SIZE, oldestSeqLoaded)`.
|
|
*
|
|
* The rest are UX correctness guards: the refresh button must not flip to the
|
|
* red `cursor-not-allowed` while spinning; op timestamps must run through the
|
|
* canonical `formatDayMonthTime` formatter; the third balance stat is the
|
|
* correctly-labelled "Voting" power (Blurt has a single voting_manabar — there
|
|
* is no separate RC mana, so the old "MANA" label was voting power mislabelled);
|
|
* and the keys card shows all four public keys (owner/active/posting/memo)
|
|
* fetched via the indexer's /keys proxy.
|
|
*
|
|
* Tamper tests (each must turn this smoke red):
|
|
* - Revert the tx link to the bare `morphitExplorerTxUrl(op.trxId) ?? '#'`.
|
|
* - Drop the Math.min clamp from loadMore.
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = join(__dirname, '..', '..', '..');
|
|
|
|
const P = {
|
|
accountPage: join(REPO, 'apps/web/src/routes/[lang]/explorer/account/[name=account]/+page.svelte'),
|
|
enLocale: join(REPO, 'apps/web/src/lib/i18n/locales/en.json')
|
|
} as const;
|
|
|
|
const read = (p: string): string => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
|
|
let pass = 0;
|
|
let fail = 0;
|
|
const ok = (m: string): void => {
|
|
console.log(` \u2713 ${m}`);
|
|
pass++;
|
|
};
|
|
const bad = (m: string): void => {
|
|
console.error(` \u2717 ${m}`);
|
|
fail++;
|
|
};
|
|
|
|
const page = read(P.accountPage);
|
|
|
|
// 1. tx link is locale-prefixed (the 404 fix), NOT the old bare form.
|
|
{
|
|
const fixed = /href=\{txUrl \? lp\(txUrl\) : '#'\}/.test(page);
|
|
const oldBuggy = /href=\{morphitExplorerTxUrl\(op\.trxId\)\s*\?\?\s*'#'\}/.test(page);
|
|
if (fixed && !oldBuggy) ok('tx link is locale-prefixed via lp(txUrl)');
|
|
else if (oldBuggy) bad('tx link still uses the locale-less morphitExplorerTxUrl(...) ?? "#" form → 404s under [lang]');
|
|
else bad('tx link not wired through lp(txUrl)');
|
|
}
|
|
|
|
// 2. block link is locale-prefixed.
|
|
{
|
|
const fixed = /href=\{blockUrl \? lp\(blockUrl\) : '#'\}/.test(page);
|
|
const oldBuggy = /href=\{morphitExplorerBlockUrl\(op\.block\)\s*\?\?\s*'#'\}/.test(page);
|
|
if (fixed && !oldBuggy) ok('block link is locale-prefixed via lp(blockUrl)');
|
|
else if (oldBuggy) bad('block link still uses the locale-less morphitExplorerBlockUrl(...) ?? "#" form → 404s under [lang]');
|
|
else bad('block link not wired through lp(blockUrl)');
|
|
}
|
|
|
|
// 3. loadMore clamps the page so Blurt's `from >= limit-1` rule holds near the start.
|
|
{
|
|
if (/Math\.min\(PAGE_SIZE, oldestSeqLoaded\)/.test(page)) ok('loadMore clamps the page size to Math.min(PAGE_SIZE, oldestSeqLoaded)');
|
|
else bad('loadMore missing the Math.min(PAGE_SIZE, oldestSeqLoaded) clamp → load-older fails near history start');
|
|
}
|
|
|
|
// 4. fetchHistory accepts an optional limit (so the clamp can take effect).
|
|
{
|
|
if (/async function fetchHistory\(from: number, noCache = false, limit = PAGE_SIZE\)/.test(page))
|
|
ok('fetchHistory takes an optional limit param');
|
|
else bad('fetchHistory does not accept a limit param (clamp would have no effect)');
|
|
}
|
|
|
|
// 5. op timestamps run through the canonical formatter (not the raw ISO string).
|
|
{
|
|
const importsIt = /import \{ formatDayMonthTime \} from '\$i18n\/formatters'/.test(page);
|
|
const usesIt = /formatDayMonthTime\(iso\)/.test(page);
|
|
if (importsIt && usesIt) ok('op timestamps formatted via formatDayMonthTime(iso)');
|
|
else bad('op timestamps not run through formatDayMonthTime');
|
|
}
|
|
|
|
// 6. refresh button: clear hover + cursor-pointer, and the busy state is a
|
|
// wait cursor — NEVER the red cursor-not-allowed (Ken's "red warning
|
|
// cursor" report).
|
|
{
|
|
const hasPointer = /cursor-pointer/.test(page);
|
|
const hasWait = /disabled:cursor-wait/.test(page);
|
|
const hasRed = /disabled:cursor-not-allowed/.test(page);
|
|
if (hasPointer && hasWait && !hasRed) ok('refresh button uses cursor-pointer + disabled:cursor-wait (no red cursor-not-allowed)');
|
|
else if (hasRed) bad('refresh button still uses disabled:cursor-not-allowed (the red "warning" cursor)');
|
|
else bad('refresh button missing cursor-pointer / disabled:cursor-wait');
|
|
}
|
|
|
|
// 7. third balance stat is "Voting" power (not the mislabelled "MANA").
|
|
{
|
|
const votingLabel = /\$_\('explorer\.account\.voting_label'\)/.test(page);
|
|
const votingValue = /formatPercentage\(voting\)/.test(page);
|
|
const oldMana = /profile\.my_balance\.mana_label/.test(page);
|
|
if (votingLabel && votingValue && !oldMana) ok('third stat is Voting power (explorer.account.voting_label + formatPercentage(voting))');
|
|
else if (oldMana) bad('account page still shows the mislabelled profile.my_balance.mana_label stat');
|
|
else bad('Voting power stat not wired (voting_label / formatPercentage(voting))');
|
|
}
|
|
|
|
// 7b. BP stat reads "BP (staked BLURT)" and shows the live APR underneath
|
|
// (consistency with the private balance card; encourages staking).
|
|
{
|
|
const stakedLabel = /\$_\('profile\.my_balance\.bp_staked_label'\)/.test(page);
|
|
const oldBp = /\$_\('profile\.my_balance\.bp_label'\)/.test(page);
|
|
const aprComputed = /vestingApr\s*=\s*computeBlurtVestingApr\(/.test(page);
|
|
const aprShown =
|
|
/Number\.isFinite\(vestingApr\)/.test(page) &&
|
|
/\$_\('profile\.my_balance\.apr_label'/.test(page) &&
|
|
/formatApr\(vestingApr\)/.test(page);
|
|
if (stakedLabel && !oldBp) ok('BP stat uses bp_staked_label ("BP (staked BLURT)"), not the old bp_label');
|
|
else if (oldBp) bad('BP stat still uses the old bp_label — should be bp_staked_label');
|
|
else bad('BP stat label not wired to bp_staked_label');
|
|
if (aprComputed && aprShown) ok('BP APR computed from DGP (computeBlurtVestingApr) and shown via apr_label/formatApr');
|
|
else bad('BP APR not wired (computeBlurtVestingApr + apr_label + formatApr under the BP figure)');
|
|
}
|
|
|
|
// 8. Public Keys card shows all four keys, fetched via the /keys proxy.
|
|
{
|
|
const fetches = /fetchAccountKeys\(/.test(page) && /import \{ fetchAccountKeys \} from '\$blurt\/accountKeys'/.test(page);
|
|
const heading = /\$_\('explorer\.account\.public_keys_heading'\)/.test(page);
|
|
const allFour =
|
|
/keys\.owner/.test(page) &&
|
|
/keys\.active/.test(page) &&
|
|
/keys\.posting/.test(page) &&
|
|
/keys\.memo/.test(page);
|
|
const labels =
|
|
/explorer\.account\.key_owner/.test(page) &&
|
|
/explorer\.account\.key_active/.test(page) &&
|
|
/explorer\.account\.key_posting/.test(page) &&
|
|
/explorer\.account\.key_memo/.test(page);
|
|
if (fetches && heading && allFour && labels) ok('Public Keys card renders owner/active/posting/memo from fetchAccountKeys');
|
|
else bad('Public Keys card not fully wired (fetchAccountKeys / heading / four keys / four labels)');
|
|
}
|
|
|
|
// 9. en locale carries the new keys.
|
|
{
|
|
let en: Record<string, unknown> = {};
|
|
try {
|
|
en = JSON.parse(read(P.enLocale));
|
|
} catch {
|
|
/* handled below */
|
|
}
|
|
const acc = ((en.explorer as Record<string, unknown>)?.account ?? {}) as Record<string, unknown>;
|
|
const present =
|
|
typeof acc.voting_label === 'string' &&
|
|
typeof acc.public_keys_heading === 'string' &&
|
|
typeof acc.key_owner === 'string' &&
|
|
typeof acc.key_active === 'string' &&
|
|
typeof acc.key_posting === 'string' &&
|
|
typeof acc.key_memo === 'string';
|
|
if (present) ok('en locale has voting_label + public_keys_heading + key_{owner,active,posting,memo}');
|
|
else bad('en locale missing one of the new explorer.account keys');
|
|
}
|
|
|
|
// ── Tamper tests ──
|
|
{
|
|
const mutated = page.replace("href={txUrl ? lp(txUrl) : '#'}", "href={morphitExplorerTxUrl(op.trxId) ?? '#'}");
|
|
const fixedStill = /href=\{txUrl \? lp\(txUrl\) : '#'\}/.test(mutated);
|
|
const oldBuggy = /href=\{morphitExplorerTxUrl\(op\.trxId\)\s*\?\?\s*'#'\}/.test(mutated);
|
|
if (mutated === page) bad('tamper wiring error: could not revert the tx link');
|
|
else if (fixedStill || !oldBuggy) bad('tamper NOT caught: reverting the tx link to the bare form still passes (toothless)');
|
|
else ok('tamper caught: reverting the tx link to the locale-less form turns the link check red');
|
|
}
|
|
{
|
|
const mutated = page.replace('Math.min(PAGE_SIZE, oldestSeqLoaded)', 'PAGE_SIZE');
|
|
const stillClamped = /Math\.min\(PAGE_SIZE, oldestSeqLoaded\)/.test(mutated);
|
|
if (mutated === page) bad('tamper wiring error: could not drop the loadMore clamp');
|
|
else if (stillClamped) bad('tamper NOT caught: dropping the clamp still passes (toothless)');
|
|
else ok('tamper caught: dropping the loadMore clamp turns its check red');
|
|
}
|
|
|
|
console.log(`\n${pass} ok, ${fail} failing`);
|
|
if (fail > 0) process.exit(1);
|
|
console.log(`\u2713 all ${pass} scenarios passed`);
|