Back to Napcraft
SOURCE / PINNED RELEASE

Made of little things.

Napcraft

Release
ee47ac6a25df…
Author-recorded commit
410dea87e109…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-52f9e22f5ce

Archive hash verified: 9e6e0cabb18d0d46…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/multiplayer.mjs
import {
  connectTestIdentity,
  approveLocalPrompts,
} from './browser-support.mjs';
export default async ({ players, check, measure, diagnostics, connectIdentity, approveBackendAccount }) => {
  const [a, b] = players;
  for (const p of players)
    await p.frame.evaluate(() => {
      window.testRtc = [];
      window.napplet.webrtc.onEvent((e) => window.testRtc.push(e));
    });
  const stopA = connectIdentity ? () => {} : approveLocalPrompts(a.page),
    stopB = connectIdentity ? () => {} : approveLocalPrompts(b.page);
  try {
    if (connectIdentity) await connectIdentity(a);
    else await connectTestIdentity(a.page);
    await a.frame.locator('#create').click();
    await a.frame.locator('#name').fill('Two friends test');
    await a.frame.locator('#confirmCreate').click();
    if (approveBackendAccount) await approveBackendAccount(a,'worlds');
    await a.frame
      .locator('#share')
      .waitFor({ state: 'visible', timeout: 20000 });
    await a.frame.locator('#share').click();
    const code = await a.frame.locator('#shareCode').inputValue();
    check('persistent world code produced', code.startsWith('NC1-'));
    await a.frame.locator('#close').click();
    await b.frame.locator('#join').click();
    await b.frame.locator('#code').fill(code);
    await b.frame.locator('#confirmJoin').click();
    await b.frame.locator('#share').waitFor({ state: 'visible' });
    try {
      await a.frame.waitForFunction(
        () => document.querySelector('canvas')?.dataset.peerCount === '1',
        {},
        { timeout: 30000 },
      );
    } catch (e) {
      for (const p of players)
        console.log(
          'DEBUG',
          await p.frame.evaluate(() => ({
            events: window.testRtc,
            body: document.body.innerText,
          })),
        );
      console.log('DIAG', JSON.stringify(await diagnostics()));
      throw e;
    }
    await b.frame.waitForFunction(
      () => document.querySelector('canvas')?.dataset.peerCount === '1',
    );
    check(
      'two independent players see each other',
      (await diagnostics()).every((rows) =>
        rows.some((p) => p.state === 'connected'),
      ),
    );
    const latency = await b.frame.evaluate(async () => {
      const canvas = document.querySelector('canvas'),
        before = canvas.dataset.localX,
        start = performance.now();
      canvas.dispatchEvent(
        new KeyboardEvent('keydown', { code: 'KeyD', bubbles: true }),
      );
      await new Promise((resolve, reject) => {
        const poll = () => {
          if (canvas.dataset.localX !== before) resolve();
          else if (performance.now() - start > 2000)
            reject(Error('Player did not move'));
          else requestAnimationFrame(poll);
        };
        requestAnimationFrame(poll);
      });
      canvas.dispatchEvent(
        new KeyboardEvent('keyup', { code: 'KeyD', bubbles: true }),
      );
      return performance.now() - start;
    });
    measure('guest input to rendered movement', latency, 70);
    const guestBefore = Number(await b.frame.locator('canvas').getAttribute('data-local-x'));
    await b.frame.locator('canvas').focus();
    await b.page.keyboard.down('ArrowRight');
    await b.page.waitForTimeout(500);
    await b.page.keyboard.up('ArrowRight');
    await b.page.waitForTimeout(600);
    check(
      'host sees moved guest',
      JSON.parse(
        await a.frame.locator('canvas').getAttribute('data-peer-positions'),
      )[0][1] > guestBefore + 0.5,
    );
    await b.frame.locator('#place').click();
    await b.frame.waitForFunction(
      () => document.querySelector('canvas').dataset.edits === '1',
    );
    await a.frame.waitForFunction(
      () => document.querySelector('canvas').dataset.edits === '1',
    );
    check('guest block reaches creator', true);
    await b.page.waitForTimeout(850);
    await b.frame.locator('#remove').click();
    await a.frame.waitForFunction(
      () => document.querySelector('canvas').dataset.edits === '2',
    );
    check('guest removal reaches creator', true);
    await a.frame.locator('#share').click();
    await a.frame.locator('#lock').click();
    await b.page.waitForTimeout(3000);
    check(
      'owner can pause building',
      (await b.frame.locator('#saveStatus').innerText()).includes('paused'),
    );
    await a.frame.locator('#share').click();
    await a.frame.locator('#lock').click();
    await b.page.waitForTimeout(3000);
    check(
      'owner can resume building',
      !(await b.frame.locator('#saveStatus').innerText()).includes('paused'),
    );
    // Leave both sessions, then rejoin with the code. Persistence is independent of peers.
    await Promise.all([a.page.reload(), b.page.reload()]);
    for (const p of [a, b]) {
      await p.page.locator('iframe').waitFor();
      p.frame = await (
        await p.page.locator('iframe').elementHandle()
      ).contentFrame();
      await p.frame.locator('#app[data-ready=true]').waitFor();
    }
    await b.frame.locator('#join').click();
    await b.frame.locator('#code').fill(code);
    await b.frame.locator('#confirmJoin').click();
    await b.frame.locator('#share').waitFor({ state: 'visible' });
    check(
      'world survives everyone leaving',
      (await b.frame.locator('canvas').getAttribute('data-edits')) === '2',
    );
  } finally {
    stopA();
    stopB();
  }
};