2026-10-14 17:28:33 -07:00
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
|
|
async function open(page) {
|
|
|
|
|
|
const problems = [];
|
|
|
|
|
|
page.on('console', m => { if (m.type() === 'error') problems.push(m.text()); });
|
|
|
|
|
|
page.on('pageerror', e => problems.push(String(e)));
|
|
|
|
|
|
await page.goto('./');
|
|
|
|
|
|
await expect(page.getByRole('heading', { name: 'Orgchain' })).toBeVisible();
|
|
|
|
|
|
return problems;
|
|
|
|
|
|
}
|
|
|
|
|
|
const say = page => page.locator('#say');
|
|
|
|
|
|
const row = (page, name) => page.locator('#tree-rows .sel').filter({ has: page.locator('.l', { hasText: new RegExp(`^${name}$`) }) });
|
|
|
|
|
|
const verb = (page, name) => page.locator(`[data-verb="${name}"]`).click();
|
|
|
|
|
|
const submit = (page, form) => page.locator(`#f-${form} [type="submit"]`).click();
|
|
|
|
|
|
async function found(page, name) { await page.locator('#found-name').fill(name); await submit(page, 'found'); }
|
|
|
|
|
|
async function birth(page, name, kind = 'org') {
|
|
|
|
|
|
await verb(page, 'birth');
|
|
|
|
|
|
await page.locator('#birth-name').fill(name);
|
|
|
|
|
|
await page.locator(`#f-birth input[value="${kind}"]`).check();
|
|
|
|
|
|
await submit(page, 'birth');
|
|
|
|
|
|
}
|
|
|
|
|
|
const slide = (page, beat) => page.locator('#beat').evaluate((el, v) => { el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); }, String(beat));
|
|
|
|
|
|
|
|
|
|
|
|
test('found a holding, then birth, award, decide, graduate and close at the default tempo', async ({ page }) => {
|
|
|
|
|
|
const problems = await open(page);
|
|
|
|
|
|
await found(page, 'Acme Holdings');
|
|
|
|
|
|
await expect(say(page)).toHaveText('acme-holdings is founded, at beat 2.');
|
|
|
|
|
|
await birth(page, 'studio');
|
|
|
|
|
|
await expect(say(page)).toHaveText('studio is born to acme-holdings, at beat 3.');
|
|
|
|
|
|
await row(page, 'studio').click();
|
|
|
|
|
|
await birth(page, 'bot', 'agent');
|
|
|
|
|
|
await expect(say(page)).toHaveText('bot is born to studio, at beat 4.');
|
|
|
|
|
|
await verb(page, 'award');
|
|
|
|
|
|
await page.locator('#award-to').selectOption({ label: 'acme-holdings / studio / bot (agent)' });
|
|
|
|
|
|
await page.locator('#award-skill').fill('launch');
|
|
|
|
|
|
await page.locator('#award-belt').selectOption('green');
|
|
|
|
|
|
await submit(page, 'award');
|
|
|
|
|
|
await expect(say(page)).toHaveText('bot holds a green belt in launch, at beat 5.');
|
|
|
|
|
|
await verb(page, 'decide');
|
|
|
|
|
|
await page.locator('#decide-title').fill('Ship weekly');
|
|
|
|
|
|
await submit(page, 'decide');
|
|
|
|
|
|
await expect(say(page)).toHaveText('studio decided “Ship weekly”, at beat 6.');
|
|
|
|
|
|
await verb(page, 'graduate');
|
|
|
|
|
|
await page.locator('#graduate-go').click();
|
|
|
|
|
|
await expect(say(page)).toHaveText('studio graduates from acme-holdings, at beat 7. Its edge is adopted now.');
|
|
|
|
|
|
await expect(row(page, 'studio')).toContainText('graduated');
|
|
|
|
|
|
await row(page, 'acme-holdings').click();
|
|
|
|
|
|
await verb(page, 'close');
|
|
|
|
|
|
await page.locator('#close-go').click();
|
|
|
|
|
|
await expect(page.locator('#close-go')).toHaveText('Yes, close acme-holdings');
|
|
|
|
|
|
await page.locator('#close-go').click();
|
|
|
|
|
|
await expect(say(page)).toHaveText('acme-holdings is closed, at beat 8.');
|
|
|
|
|
|
await expect(row(page, 'acme-holdings')).toContainText('closed');
|
|
|
|
|
|
await expect(row(page, 'studio')).not.toContainText('closed'); // graduated: it holds its own root
|
|
|
|
|
|
await row(page, 'bot').click();
|
|
|
|
|
|
await verb(page, 'birth');
|
|
|
|
|
|
await expect(page.locator('#act-why')).toHaveText('Agents do not give birth.');
|
|
|
|
|
|
await page.locator('#timeline-list .ev', { hasText: 'Ship weekly' }).click();
|
|
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText('"op": "decide"');
|
|
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText('applied');
|
|
|
|
|
|
expect(problems).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('a timed tempo cues acts; Beat now and the clock land them', async ({ page }) => {
|
|
|
|
|
|
const problems = await open(page);
|
|
|
|
|
|
await found(page, 'acme');
|
|
|
|
|
|
await page.locator('[data-tempo="10"]').click();
|
|
|
|
|
|
await birth(page, 'a');
|
|
|
|
|
|
await expect(say(page)).toHaveText('a is born to acme, cued for the next beat.');
|
|
|
|
|
|
await birth(page, 'b');
|
|
|
|
|
|
await expect(page.locator('#clock-cued')).toContainText('2 cued · next beat in');
|
|
|
|
|
|
await expect(row(page, 'b')).toContainText('cued');
|
|
|
|
|
|
await page.locator('#beat-now').click();
|
2026-10-14 22:25:38 -07:00
|
|
|
|
await expect(say(page)).toHaveText('Beat 3: 2 cued acts landed, and acme earned 50 tBTC.');
|
2026-10-14 17:28:33 -07:00
|
|
|
|
await birth(page, 'c');
|
2026-10-14 22:25:38 -07:00
|
|
|
|
await expect(say(page)).toHaveText('Beat 4: 1 cued act landed, and acme earned 50 tBTC.', { timeout: 15_000 });
|
2026-10-14 17:28:33 -07:00
|
|
|
|
await page.locator('[data-tempo="manual"]').click();
|
|
|
|
|
|
await birth(page, 'd');
|
|
|
|
|
|
await expect(page.locator('#clock-cued')).toHaveText('1 cued · waiting for Beat now');
|
|
|
|
|
|
await page.locator('[data-tempo="each"]').click();
|
|
|
|
|
|
await expect(page.locator('#clock-cued')).toHaveText('nothing cued');
|
|
|
|
|
|
await expect(page.locator('#clock-beat')).toHaveText('5');
|
|
|
|
|
|
expect(problems).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-14 22:25:38 -07:00
|
|
|
|
test('beats pay the holding, and test BTC moves down its tree', async ({ page }) => {
|
|
|
|
|
|
const problems = await open(page), inspect = page.locator('#inspect-body');
|
|
|
|
|
|
await found(page, 'acme');
|
|
|
|
|
|
await expect(row(page, 'acme')).toContainText('50 tBTC');
|
|
|
|
|
|
await expect(page.locator('#clock-pays')).toHaveText('beats pay acme');
|
|
|
|
|
|
await birth(page, 'ops');
|
|
|
|
|
|
await row(page, 'ops').click();
|
|
|
|
|
|
await birth(page, 'bot', 'agent');
|
|
|
|
|
|
await verb(page, 'send');
|
|
|
|
|
|
await expect(page.locator('#act-why')).toHaveText('ops has no test BTC to send yet. Each beat pays acme, which can send some down.');
|
|
|
|
|
|
await row(page, 'acme').click();
|
|
|
|
|
|
await verb(page, 'send');
|
|
|
|
|
|
await expect(page.locator('#send-has')).toHaveText('150 tBTC');
|
|
|
|
|
|
await expect(page.locator('#act-key')).toHaveText("Signs as acme: acme root · m/84'/1'/0'/0/0");
|
|
|
|
|
|
await page.locator('#send-to').selectOption({ label: 'acme / ops' });
|
|
|
|
|
|
await page.locator('#send-amount').fill('12.5');
|
|
|
|
|
|
await submit(page, 'send');
|
|
|
|
|
|
await expect(say(page)).toHaveText('acme sent 12.5 tBTC to ops, at beat 5.');
|
|
|
|
|
|
await expect(row(page, 'ops')).toContainText('12.5 tBTC');
|
|
|
|
|
|
await expect(row(page, 'acme')).toContainText('≈187.5 tBTC');
|
|
|
|
|
|
await row(page, 'ops').click();
|
|
|
|
|
|
await verb(page, 'send');
|
|
|
|
|
|
await page.locator('#send-to').selectOption({ label: 'acme / ops / bot (agent)' });
|
|
|
|
|
|
await page.locator('#send-max').click();
|
|
|
|
|
|
await expect(page.locator('#send-amount')).toHaveValue('12.499995');
|
|
|
|
|
|
await page.locator('#send-amount').fill('1');
|
|
|
|
|
|
await submit(page, 'send');
|
|
|
|
|
|
await expect(say(page)).toHaveText('ops sent 1 tBTC to bot, at beat 6.');
|
|
|
|
|
|
await expect(row(page, 'bot')).toContainText('1 tBTC');
|
|
|
|
|
|
await page.locator('#timeline-list .ev', { hasText: 'ops → bot' }).click();
|
|
|
|
|
|
for (const text of ['500 sats, retired', '11.499995 tBTC back to ops', "ops's SegWit key (BIP-143, SIGHASH_ALL)"]) await expect(inspect).toContainText(text);
|
|
|
|
|
|
await page.locator('#beat-now').click();
|
|
|
|
|
|
await expect(say(page)).toHaveText('Beat 7: acme earned 50 tBTC.');
|
|
|
|
|
|
await page.locator('[data-tempo="manual"]').click();
|
|
|
|
|
|
await row(page, 'acme').click();
|
|
|
|
|
|
await verb(page, 'send');
|
|
|
|
|
|
await page.locator('#send-to').selectOption({ label: 'acme / ops / bot (agent)' });
|
|
|
|
|
|
await page.locator('#send-amount').fill('0.25');
|
|
|
|
|
|
await submit(page, 'send');
|
|
|
|
|
|
await expect(say(page)).toHaveText('acme sent 0.25 tBTC to bot, cued for the next beat.');
|
|
|
|
|
|
await expect(page.locator('#clock-cued')).toHaveText('1 cued · waiting for Beat now');
|
|
|
|
|
|
await page.locator('#beat-now').click();
|
|
|
|
|
|
await expect(say(page)).toHaveText('Beat 8: 1 cued send landed, and acme earned 50 tBTC.');
|
|
|
|
|
|
await expect(row(page, 'bot')).toContainText('1.25 tBTC');
|
|
|
|
|
|
await slide(page, 4);
|
|
|
|
|
|
await expect(row(page, 'ops')).not.toContainText('tBTC');
|
|
|
|
|
|
await expect(row(page, 'acme')).toContainText('150 tBTC');
|
|
|
|
|
|
await row(page, 'acme').click();
|
|
|
|
|
|
await expect(inspect).toContainText('as of beat 4');
|
|
|
|
|
|
await page.locator('#to-now').click();
|
|
|
|
|
|
await expect(page.locator('#holdings-list')).toContainText('tBTC');
|
|
|
|
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
|
|
|
|
expect(problems).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-14 17:28:33 -07:00
|
|
|
|
test('the sample replays, any beat can be inspected, and the lab survives a reload', async ({ page }) => {
|
|
|
|
|
|
const problems = await open(page);
|
|
|
|
|
|
await page.locator('#sample').click();
|
|
|
|
|
|
await expect(say(page)).toContainText('Loaded the yolopreneur sample', { timeout: 20_000 });
|
|
|
|
|
|
await expect(page.locator('#tree-rows .sel')).toHaveCount(12);
|
|
|
|
|
|
await expect(page.locator('#beat-label')).toHaveText('Now · beat 7');
|
|
|
|
|
|
await expect(row(page, 'lineage')).toContainText('graduated');
|
2026-10-14 22:25:38 -07:00
|
|
|
|
await expect(row(page, 'time')).toContainText('≈38.5 tBTC');
|
|
|
|
|
|
await expect(page.locator('#timeline-list')).toContainText('40 tBTC yolopreneur → time');
|
2026-10-14 17:28:33 -07:00
|
|
|
|
await expect(page.locator('#sample')).toBeDisabled();
|
|
|
|
|
|
await slide(page, 3);
|
|
|
|
|
|
await expect(page.locator('#beat-label')).toHaveText('Beat 3 of 7');
|
|
|
|
|
|
await expect(page.locator('#tree-rows .sel')).toHaveCount(3);
|
|
|
|
|
|
await expect(page.locator('#act-past')).toBeVisible();
|
|
|
|
|
|
await page.locator('#replay').click();
|
|
|
|
|
|
await expect(page.locator('#beat-label')).toHaveText('Now · beat 7', { timeout: 15_000 });
|
|
|
|
|
|
await page.locator('#timeline-list .ev', { hasText: 'graduate lineage from well-known' }).click();
|
|
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText('"op": "graduate"');
|
2026-10-14 18:22:22 -07:00
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText("well-known's key");
|
|
|
|
|
|
await row(page, 'lineage').click();
|
2026-10-14 19:49:05 -07:00
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText('lineage root · account 0');
|
|
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText("BIP-86 at m/86'/1'/0'/0/0");
|
2026-10-14 18:22:22 -07:00
|
|
|
|
await expect(page.locator('#inspect-body')).toContainText('its own key');
|
|
|
|
|
|
await row(page, 'time').click();
|
2026-10-14 19:49:05 -07:00
|
|
|
|
await expect(page.locator('#act-key')).toHaveText("Signs as time: yolopreneur root › 1' · m/86'/1'/0'/0/0");
|
2026-10-14 19:09:50 -07:00
|
|
|
|
const inspect = page.locator('#inspect-body');
|
|
|
|
|
|
for (const text of ['ssh-ca', 'lineage-ca@time.yolopreneur', 'mkey:']) await expect(inspect).toContainText(text);
|
|
|
|
|
|
await row(page, 'sol').click();
|
|
|
|
|
|
const fact = name => inspect.locator('.f').filter({ has: page.locator('dt', { hasText: new RegExp(`^${name}$`) }) });
|
2026-10-14 19:49:05 -07:00
|
|
|
|
for (const [name, text] of [['Taproot', 'bcrt1p'], ['Taproot', "BIP-86 at m/86'/1'/2'/0/0"], ['SegWit', 'bcrt1q'], ['SegWit', "BIP-84 at m/84'/1'/2'/0/0"],
|
|
|
|
|
|
['npub', 'npub1'], ['npub', "NIP-06 at m/44'/1237'/2'/0/0"], ['From', "yolopreneur root › 1' · account 2"]]) await expect(fact(name)).toContainText(text);
|
2026-10-14 19:09:50 -07:00
|
|
|
|
for (const text of ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5', 'sol@time.yolopreneur namespaces="git"', 'sol._domainkey.time.yolopreneur. IN TXT "v=DKIM1; k=ed25519; p=', 'PublicKey = ']) {
|
|
|
|
|
|
await expect(inspect.locator('pre')).toContainText(text);
|
|
|
|
|
|
}
|
2026-10-14 20:14:11 -07:00
|
|
|
|
await expect(fact('Tapkey')).toContainText('bcrt1p');
|
|
|
|
|
|
await expect(page.locator('#reveal')).toHaveAttribute('aria-expanded', 'false');
|
|
|
|
|
|
await page.locator('#reveal').click();
|
|
|
|
|
|
const secrets = inspect.locator('.sb');
|
|
|
|
|
|
for (const text of ['nsec1', 'BEGIN OPENSSH PRIVATE KEY', 'PrivateKey = ', 'account 2 of time']) await expect(secrets).toContainText(text);
|
|
|
|
|
|
await expect(secrets).toContainText(/tr\(c[1-9A-HJ-NP-Za-km-z]{51}\)#[a-z0-9]{8}/);
|
|
|
|
|
|
await secrets.locator('.cp').first().click();
|
|
|
|
|
|
await expect(secrets.locator('.cp').first()).toHaveText(/Copied|Selected/);
|
|
|
|
|
|
await row(page, 'time').click();
|
|
|
|
|
|
await expect(page.locator('#reveal')).toHaveAttribute('aria-expanded', 'false');
|
|
|
|
|
|
await page.locator('#reveal').click();
|
|
|
|
|
|
await expect(secrets).toContainText("Seed · yolopreneur root › 1'");
|
|
|
|
|
|
await expect(secrets).toContainText('BEGIN PRIVATE KEY');
|
|
|
|
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
|
|
|
|
await page.locator('#reveal').click();
|
|
|
|
|
|
await expect(secrets).toHaveCount(0);
|
2026-10-14 17:28:33 -07:00
|
|
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
await expect(page.locator('#tree-rows .sel')).toHaveCount(12);
|
|
|
|
|
|
await page.locator('#theme').click();
|
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
|
|
|
|
|
await page.locator('#theme').click();
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
|
|
|
|
|
await expect(page.locator('#theme')).toHaveText('Theme: dark');
|
|
|
|
|
|
await page.locator('#reset').click();
|
|
|
|
|
|
await page.locator('#confirm button', { hasText: 'Erase' }).click();
|
|
|
|
|
|
await expect(say(page)).toHaveText('Erased. This is a fresh lab.');
|
|
|
|
|
|
await expect(page.locator('#holdings-list li')).toHaveCount(0);
|
|
|
|
|
|
expect(problems).toEqual([]);
|
|
|
|
|
|
});
|