Files

317 lines
22 KiB
JavaScript

import { test, expect } from '@playwright/test';
test('create → mine → mempool → inscription → block/transaction → reload/load', async ({page}) => {
const errors=[], remote=[];
page.on('pageerror',e=>errors.push(e.message));
page.on('request',r=>{ if(!r.url().startsWith('http://127.0.0.1:4178')) remote.push(r.url()); });
await page.goto('/');
await expect(page.getByRole('button',{name:'Mine now'})).toBeDisabled();
await expect(page.locator('#mempool-list')).toContainText('Mempool empty');
await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#balance')).toHaveText('50.00000000');
await expect(page.locator('#blocks .block-card')).toHaveCount(1);
await expect(page.locator('#blocks .block-card').first()).toContainText('HEIGHT 1');
const payload='<script>window.unwanted=true</script> Hello 🌱';
await page.getByLabel('What will you put on your chain?').fill(payload);
await page.getByRole('button',{name:'Review inscription'}).click();
await expect(page.locator('#review-text')).toHaveText(payload);
await expect(page.locator('#pending')).toHaveText('0');
await page.getByRole('button',{name:'Sign & inscribe'}).click();
await expect(page.locator('#pending')).toHaveText('2');
await expect(page.locator('#mempool-list .mempool-card')).toHaveCount(2);
await expect(page.locator('#mempool-list')).toContainText('INSCRIPTION COMMIT');
await expect(page.locator('#mempool-list')).toContainText('INSCRIPTION REVEAL');
await expect(page.locator('.collectible pre')).toHaveText(payload);
await expect(page.locator('.collectible .tag')).toContainText('PENDING');
await expect(page.locator('#balance')).toHaveText('49.99999000');
await page.locator('#mempool-list .mempool-card').last().click();
await expect(page.locator('#inspector')).toContainText('Pending in mempool');
await page.reload();
await expect(page.locator('.collectible pre')).toHaveText(payload);
await expect(page.locator('#pending')).toHaveText('2');
await expect(page.locator('#mempool-list .mempool-card')).toHaveCount(2);
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#mempool-list')).toContainText('Mempool empty');
await expect(page.locator('#blocks .block-card')).toHaveCount(2);
await expect(page.locator('#blocks .block-card').first()).toContainText('HEIGHT 2');
await expect(page.locator('.collectible .tag')).toHaveText('CONFIRMED · BLOCK 2');
await page.getByRole('link',{name:'View reveal transaction'}).click();
await expect(page.locator('#inspector')).toContainText('1 confirmation(s)');
await expect(page.locator('#inspector')).toContainText('Raw Bitcoin transaction');
await expect(page.locator('#inspector')).toContainText('INSCRIPTION REVEAL');
await page.getByRole('link',{name:'Inspect commit transaction'}).click();
await expect(page.locator('#inspector')).toContainText('4,999,998,000 sats');
await expect(page.locator('#inspector')).toContainText('INSCRIPTION COMMIT');
await page.getByRole('link',{name:'View block 2'}).click();
await expect(page.locator('#inspector h2')).toHaveText('Block 2');
await expect(page.locator('#inspector')).toContainText('Transactions');
await expect(page.locator('#inspector a')).toHaveCount(4);
await page.getByRole('link',{name:/^Inscription /}).click();
await expect(page.locator('#inspector')).toContainText('Inscription detail');
const [download] = await Promise.all([page.waitForEvent('download'),page.getByRole('button',{name:'Save complete lab'}).click()]);
const backupPath=await download.path();
page.on('dialog',dialog=>dialog.accept());
await page.getByRole('button',{name:'Reset local lab'}).click();
await expect(page.getByRole('button',{name:'Create test wallet'})).toBeVisible();
await page.locator('#load-wallet').setInputFiles(backupPath);
await expect(page.locator('.collectible pre')).toHaveText(payload);
await expect(page.locator('#height')).toHaveText('2');
await page.reload();
await expect(page.locator('#balance')).toHaveText('99.99999000');
expect(await page.evaluate(()=>window.unwanted)).toBeUndefined();
expect(errors).toEqual([]); expect(remote).toEqual([]);
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBeTruthy();
await page.screenshot({path:`test-results/${test.info().project.name}-complete.png`,fullPage:true});
});
for (const colorScheme of ['light', 'dark']) {
test(`dark default and explicit theme persistence with ${colorScheme} system preference`, async ({page}) => {
await page.emulateMedia({colorScheme});
await page.goto('/');
await expect(page.locator('html')).toHaveAttribute('data-theme','dark');
await expect(page.getByRole('button',{name:'Switch to light mode'})).toBeVisible();
await page.getByRole('button',{name:'Switch to light mode'}).click();
await expect(page.locator('html')).toHaveAttribute('data-theme','light');
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme','light');
await expect(page.getByRole('button',{name:'Switch to dark mode'})).toBeVisible();
expect(await page.evaluate(() => localStorage.getItem('super-testnet-theme'))).toBe('light');
await page.getByRole('button',{name:'Switch to dark mode'}).click();
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme','dark');
expect(await page.evaluate(() => localStorage.getItem('super-testnet-theme'))).toBe('dark');
});
}
test('wallet backup reload and rejected oversized or invalid input',async({page})=>{
await page.goto('/'); await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
const address=await page.locator('#address').inputValue();
const [download]=await Promise.all([page.waitForEvent('download'),page.getByRole('button',{name:'Save test wallet'}).click()]);
await page.getByRole('button',{name:'Mine now'}).click();
await page.locator('#payload').fill('🌱'.repeat(4097));
await page.getByRole('button',{name:'Review inscription'}).click();
await expect(page.getByRole('status')).toContainText('at most 16384');
await expect(page.locator('#pending')).toHaveText('0');
await page.locator('#load-wallet').setInputFiles({name:'bad.json',mimeType:'application/json',buffer:Buffer.from('{"network":"mainnet"}')});
await expect(page.getByRole('status')).toContainText('Only Super Testnet');
await expect(page.locator('#address')).toHaveValue(address);
page.on('dialog',d=>d.accept());
await page.locator('#load-wallet').setInputFiles(await download.path());
await expect(page.getByRole('status')).toContainText('Test wallet loaded.');
await expect(page.locator('#address')).toHaveValue(address);
await expect(page.locator('#balance')).toHaveText('50.00000000');
});
test('two tabs serialize mining and refresh shared storage',async({page,context})=>{
await page.goto('/'); await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
const other=await context.newPage(); await other.goto('/');
await Promise.all([page.getByRole('button',{name:'Mine now'}).click(),other.getByRole('button',{name:'Mine now'}).click()]);
await expect(page.locator('#height')).toHaveText('2');
await expect(other.locator('#height')).toHaveText('2');
await expect(page.locator('#balance')).toHaveText('100.00000000');
});
test('storage failure leaves the displayed and persisted chain unchanged', async ({page}) => {
await page.goto('/');
await page.getByRole('button',{name:'Create test wallet'}).click();
await expect(page.locator('#address')).toHaveValue(/^bcrt1p/);
await page.evaluate(()=>{ Storage.prototype.setItem = () => { throw new DOMException('Quota exceeded','QuotaExceededError'); }; });
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.getByRole('status')).toContainText('No changes were saved');
await expect(page.locator('#height')).toHaveText('0');
await expect(page.locator('#balance')).toHaveText('0.00000000');
await page.reload();
await expect(page.locator('#height')).toHaveText('0');
await page.getByRole('button',{name:'Mine now'}).click();
await expect(page.locator('#balance')).toHaveText('50.00000000');
});
test('MIME upload, reset only after success, and parent/child navigation', async ({ page }) => {
await page.goto('/');
await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#payload').fill('parent'); await page.locator('#review').click(); await page.locator('#sign').click();
await expect(page.locator('#payload')).toHaveValue('');
await expect(page.locator('#byte-count')).toHaveText('0 / 16384 bytes');
const parent = await page.locator('#parent option').nth(1).getAttribute('value');
await page.locator('#parent').selectOption(parent);
await page.locator('#body-file').setInputFiles({ name: 'art.svg', mimeType: 'image/svg+xml', buffer: Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="orange"/></svg>') });
await page.locator('#review').click();
await expect(page.locator('#review-preview iframe')).toHaveAttribute('sandbox', '');
await page.evaluate(() => { window.originalSetItem = Storage.prototype.setItem; Storage.prototype.setItem = () => { throw new Error('quota'); }; });
await page.locator('#sign').click();
await expect(page.getByRole('status')).toContainText('No changes were saved');
await expect(page.locator('#review-panel')).toBeVisible();
await expect(page.locator('#parent')).toHaveValue(parent);
expect(await page.locator('#body-file').evaluate(e => e.files.length)).toBe(1);
await page.evaluate(() => { Storage.prototype.setItem = window.originalSetItem; });
await page.locator('#sign').click();
await expect(page.locator('.collectible')).toHaveCount(2);
await expect(page.locator('#review-panel')).toBeHidden();
await expect(page.locator('#parent')).toHaveValue('');
await expect(page.locator('#byte-count')).toHaveText('0 / 16384 bytes');
expect(await page.locator('#body-file').evaluate(e => e.files.length)).toBe(0);
await page.locator('.collectible').getByRole('link', { name: /^Parent / }).click();
await expect(page.locator('#inspector pre').first()).toHaveText('parent');
await page.locator('#inspector').getByRole('link', { name: /^Child / }).click();
await expect(page.locator('#inspector')).toContainText('image/svg+xml');
await page.reload();
await expect(page.locator('#inspector iframe')).toBeVisible();
});
test('HTML preview cannot execute, navigate or fetch remote resources', async ({ page }) => {
const remote = [];
page.on('request', r => { if (/^https?:/.test(r.url()) && !r.url().startsWith('http://127.0.0.1:4178')) remote.push(r); });
await page.goto('/'); await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#mime-type').selectOption('text/html');
await page.locator('#payload').fill('<meta http-equiv="refresh" content="0;url=https://example.com"><script>parent.unwanted=true</script><img src="https://example.com/a"><iframe src="https://example.com"></iframe><p style="background:url(https://example.com/b)" onclick="parent.unwanted=true">safe words</p><a href="https://example.com">link</a>');
await page.locator('#review').click();
await expect(page.frameLocator('#review-preview iframe').locator('p')).toHaveText('safe words');
await page.locator('#sign').click();
await expect(page.frameLocator('.collectible iframe').locator('p')).toHaveText('safe words');
expect(await page.evaluate(() => window.unwanted)).toBeUndefined();
for (const request of remote) await expect.poll(() => request.failure()?.errorText).toMatch(/csp|ERR_BLOCKED_BY_CSP/i);
await expect(page).toHaveURL('http://127.0.0.1:4178/');
});
test('metadata review validation, witness persistence, indexes/reducer and local API explorer', async ({page,context}) => {
await page.goto('/'); await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#payload').fill('independent body');
await page.locator('#metadata').fill('{bad'); await page.locator('#review').click();
await expect(page.getByRole('status')).toContainText('Invalid metadata JSON');
await expect(page.locator('#review-panel')).toBeHidden();
await page.locator('#metadata').fill(JSON.stringify({large:'x'.repeat(2048)})); await page.locator('#review').click();
await expect(page.getByRole('status')).toContainText('2048 CBOR');
await expect(page.locator('#pending')).toHaveText('0');
const metadata={protocol:'example',note:'chunked '+ 'x'.repeat(600),nested:{data:true},code:'window.unwanted=true'};
await page.locator('#metadata').fill(JSON.stringify(metadata));
await page.locator('#review').click();
await expect(page.locator('#review-metadata')).toContainText('CBOR, tag 5');
await page.locator('#sign').click();
await expect(page.locator('#metadata')).toHaveValue('');
await expect(page.locator('#metadata-count')).toContainText('0 / 2048');
await expect(page.locator('.metadata-document')).toContainText('example');
await page.reload();
await expect(page.locator('.metadata-document')).toContainText('chunked');
await page.getByRole('link',{name:/^Inscription /}).click();
await expect(page.locator('#inspector .metadata-document')).toContainText('example');
await expect(page.locator('#index-stats')).toContainText('1 events · 1 with metadata · 0 confirmed · 1 pending · 1 protocols');
await page.locator('#protocol').selectOption('"example"');
await expect(page.locator('#protocol-events')).not.toContainText('independent'); // Body is not part of metadata events.
await page.locator('#reducer').selectOption('event-counter');
await expect(page.locator('#derived-state')).toContainText('"count": 1');
await expect(page.locator('#protocol-events')).toContainText('"status": "pending"');
await page.locator('#mine').click();
await expect(page.locator('#protocol-events')).toContainText('"status": "confirmed"');
await expect(page.locator('#derived-state')).toContainText('"confirmed": 1');
await expect(page.locator('#api-explorer')).toContainText('LOCAL / IN-BROWSER');
await page.locator('#api-preset').selectOption('/r/inscription/:id'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('"protocol": "example"');
await page.locator('#api-preset').selectOption('/content/:id'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('independent body');
await page.locator('#api-path').fill('/r/blockheight'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('"body": 2');
await context.grantPermissions(['clipboard-read','clipboard-write']);
await page.locator('#api-copy').click();
expect(await page.evaluate(()=>navigator.clipboard.readText())).toContain('"body": 2');
await page.locator('#api-path').fill('/missing'); await page.locator('#api-try').click();
await expect(page.locator('#api-response')).toContainText('404');
expect(await page.evaluate(()=>window.unwanted)).toBeUndefined();
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBeTruthy();
});
test('auto miner default, preference reload, countdown, pending confirmation, manual reset and OFF', async ({page}) => {
await page.clock.install();
await page.goto('/'); await page.locator('#create-wallet').click();
await expect(page.locator('#miner-interval')).toHaveValue('0');
await expect(page.locator('#miner-status')).toHaveText('Auto mining OFF');
await page.locator('#miner-interval').selectOption('10');
await expect(page.locator('#miner-status')).toContainText('10s interval');
await page.clock.runFor(10000);
await expect(page.locator('#height')).toHaveText('1');
await page.locator('#payload').fill('auto confirmed'); await page.locator('#review').click(); await page.locator('#sign').click();
await expect(page.locator('#pending')).toHaveText('2');
await page.clock.runFor(10000);
await expect(page.locator('#height')).toHaveText('2'); await expect(page.locator('#pending')).toHaveText('0');
await page.locator('#miner-interval').selectOption('30');
await page.reload(); await expect(page.locator('#miner-interval')).toHaveValue('30');
await page.locator('#miner-interval').selectOption('60');
await page.clock.runFor(5000); await expect(page.locator('#miner-status')).toContainText('55s');
await page.locator('#mine').click(); await expect(page.locator('#height')).toHaveText('3');
await expect(page.locator('#miner-status')).toContainText('60s');
await page.locator('#miner-interval').selectOption('0');
await page.clock.runFor(65000); await expect(page.locator('#height')).toHaveText('3');
});
test('two tabs race one auto deadline under Web Locks', async ({page,context}) => {
const now = new Date('2026-10-05T12:00:00Z');
await page.clock.install({time:now}); await page.goto('/'); await page.locator('#create-wallet').click();
const other=await context.newPage(); await other.clock.install({time:now}); await other.goto('/');
await page.locator('#miner-interval').selectOption('10');
await expect(other.locator('#miner-interval')).toHaveValue('10');
await Promise.all([page.clock.runFor(10000),other.clock.runFor(10000)]);
await expect(page.locator('#height')).toHaveText('1'); await expect(other.locator('#height')).toHaveText('1');
await page.locator('#miner-interval').selectOption('0');
await expect(other.locator('#miner-interval')).toHaveValue('0');
});
test('recursive local fetch, delegate identity, external blocking and worker DOM isolation', async ({page}) => {
const remote=[];
page.on('request',r=>{if(/^https?:/.test(r.url()) && !r.url().startsWith('http://127.0.0.1:4178')) remote.push(r);});
await page.goto('/'); await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#payload').fill('local recursive body'); await page.locator('#review').click(); await page.locator('#sign').click();
const content=await page.locator('#parent option').nth(1).getAttribute('value');
await page.locator('#mime-type').selectOption('text/html');
await page.locator('#metadata').fill('{"protocol":"recursive","name":"source"}');
await page.locator('#payload').fill(`<p>Isolated recursion</p><script>
const result={id:ord.id,content:await (await fetch('/content/${content}')).text(),height:await (await fetch('/r/blockheight')).json(),metadata:await (await fetch('/r/metadata/'+ord.id)).json()};
try{await fetch('https://example.com/leak');result.external='escaped'}catch{result.external='blocked'}
try{parent.document.body.innerHTML='escaped';result.dom='escaped'}catch{result.dom='blocked'}
try{await new Promise((ok,bad)=>{const x=new XMLHttpRequest();x.open('GET','https://example.com/xhr');x.onload=ok;x.onerror=bad;x.send()});result.xhr='escaped'}catch{result.xhr='blocked'}
try{importScripts('https://example.com/script.js');result.import='escaped'}catch{result.import='blocked'}
ord.render(JSON.stringify(result));
</script>`);
await page.locator('#review').click(); await page.locator('#sign').click();
const source=await page.locator('#parent option').nth(2).getAttribute('value');
const output=page.frameLocator('.collectible iframe').locator('#recursive-output');
await expect(output).toContainText('local recursive body');
await expect(output).toContainText('"height":1');
await expect(output).toContainText('"external":"blocked"');
await expect(output).toContainText('"dom":"blocked"');
await expect(output).toContainText('"xhr":"blocked"');
await expect(output).toContainText('"import":"blocked"');
await expect(output).toContainText(source);
await page.locator('#mime-type').selectOption('text/plain'); await page.locator('#payload').fill('delegating');
await page.locator('#delegate').fill(source); await page.locator('#metadata').fill('{"name":"requested"}');
await page.locator('#review').click(); await page.locator('#sign').click();
const delegated=await page.locator('#parent option').nth(3).getAttribute('value');
const delegatedOutput=page.frameLocator('.collectible:first-child iframe').locator('#recursive-output');
await expect(delegatedOutput).toContainText(delegated);
await expect(delegatedOutput).toContainText('a1646e616d6569726571756573746564');
await expect(page.locator('.collectible:first-child')).toContainText('Delegate');
await expect(page.locator('.collectible').nth(1)).toContainText('Delegated by');
// Chromium emits request events for CSP-denied XHR attempts; none reached the network.
for (const request of remote) expect(request.failure()?.errorText).toMatch(/csp|ERR_BLOCKED_BY_CSP/i);
await expect(page.locator('h1')).toContainText('Bitcoin universe');
});
test('recursive template and reinscription stack persist across mining and reload', async ({page}) => {
await page.goto('/'); await page.locator('#create-wallet').click(); await page.locator('#mine').click();
await page.locator('#recursive-demo').click(); await page.locator('#review').click(); await page.locator('#sign').click();
const id=await page.locator('#parent option').nth(1).getAttribute('value');
await expect(page.frameLocator('.collectible iframe').locator('#recursive-output')).toContainText(id);
await page.locator('#reinscribe').selectOption(id); await page.locator('#mime-type').selectOption('text/plain');
await page.locator('#payload').fill('second on same sat'); await page.locator('#review').click();
await expect(page.locator('#review-parent')).toContainText(`Reinscribe: ${id}`);
await page.locator('#sign').click();
await expect(page.locator('.collectible')).toHaveCount(2);
await expect(page.locator('.collectible').first()).toContainText('stack position 1');
await expect(page.locator('.collectible').last()).toContainText('stack position 0');
await expect(page.locator('.collectible').first().getByRole('link',{name:/Sat history/})).toHaveCount(2);
await page.locator('#mine').click(); await page.reload();
await expect(page.locator('.collectible')).toHaveCount(2);
await expect(page.frameLocator('.collectible iframe').locator('#recursive-output')).toContainText('"height": 2');
});