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=' 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('') }); 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.url()); }); 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('

safe words

link'); 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(); expect(remote).toEqual([]); 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'); });