šŸ§Ŗ Announcing PicJS: TypeScript/JavaScript support for PocketIC!

Iā€™m happy to announce PicJS, a canister testing library with TypeScript/JavaScript support for PocketIC!

PicJS allows you to write integration tests for your canisters using your favorite JavaScript test runners and runtimes. If youā€™d like to try it out, you can:

PocketIC is a canister testing platform that supports deterministic, programmatic canister testing. If you want to learn more about it, you can check out the previous posts from the Testing & Verification team that have put together this wonderful tool:

Writing tests with PicJS is as easy as this:

import { resolve } from 'node:path';
import { Principal } from '@dfinity/principal';
import { PocketIc } from '@hadronous/pic';
import { Actor, idlFactory, _SERVICE } from '../declarations/counter';

const wasmPath = resolve('..', '..', 'counter.wasm');

describe('Counter', () => {
  let pic: PocketIc;
  let actor: Actor<_SERVICE>;

  beforeEach(async () => {
    pic = await PocketIc.create();
    const fixture = await pic.setupCanister<_SERVICE>(
      idlFactory,
      WASM_PATH,
    );
    actor = fixture.actor;
  });

  afterEach(async () => {
    await pic.tearDown();
  });

  it('should increment the counter', async () => {
    const initialCount = await actor.get();

    await actor.inc();
    const countAfterFirstInc = await actor.get();

    await actor.inc();
    const finalCount = await actor.get();

    expect(initialCount).toEqual(0n);
    expect(countAfterFirstInc).toEqual(1n);
    expect(finalCount).toEqual(2n);
  });
});

Iā€™m happy to hear any feedback or answer any questions that you might have.

Disclaimer: I work for DFINITY, but this is not a DFINITY project. Iā€™m posting in my capacity as a part-time community member.

23 Likes

great work! i think this is worth adding to awesome icp (maybe together with pocket ic)

4 Likes

Thank you!

I agree, expect a PR tomorrow :wink:

3 Likes

Version 0.3.0 of PicJS has been released.

The main changes include:

  • Support for PocketIC server version 3.0.0
    • Support for multiple subnets
    • Support for cross-subnet calls
  • Added updateCanisterSettings to allow updating canister settings after creation (controllers etcā€¦)
  • tick now accepts an optional number to tick multiple times in a single call
  • many public methods accept a single object instead of a list of parameters (ex: installCode)
  • Added a multicanister Motoko example with composite query calls and cross subnet update calls

I look forward to seeing how developers continue to improve the quality of their testing infrastructure using PocketIC :rocket:

8 Likes

I seem to have some trouble getting and keeping pocket ic running. I start it from the command line:

/Users/afat/.cache/dfinity/pocket-ic/pocket-ic

everything looks good:

2024-04-02T00:31:24.975295Z INFO pocket_ic_server: The PocketIC server is listening on port 60626

but after a bit I get:

2024-04-02T00:32:25.087578Z INFO pocket_ic_server: The PocketIC server will terminate

How do I keep it running? Do I need to specify a port to PicJS anywhere?

Iā€™m getting an error on create canister that it canā€™t decode some text value:

 Canister ryjl3-tyaaa-aaaaa-aaaba-cai trapped explicitly: failed to decode call arguments: Custom(Fail to decode argument 0

    Caused by:
        Subtyping error: text)

      64 |       
      65 |
    > 66 |         nnsledger = await pic.setupCanister<_NNSLedgerService>({
         |                     ^
      67 |           wasm : "packages/pic/wasms/ic-icrc1-ledger.wasm",
      68 |           idlFactory : nnsIdlFactory,
      69 |           targetCanisterId: Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai"),

I may be doing something horribly wrong because Iā€™m using the nns-ledger wasm I found in my /Users/afat/.cache/dfinity/versions/0.18.0/wasms folder and a did file I pulled from someone elseā€™s project. Iā€™ve found it all super hard to debug so far.

1 Like

When you start PocketIC manually, pass a time-to-live (the default is 60s):

./pocket-ic --ttl 600

Whenever PocketIC receives an HTTP request, it bumps its TTL.

wasm : ā€œpackages/pic/wasms/ic-icrc1-ledger.wasmā€,

I donā€™t know the JS API, but is it possible that you should pass wasm bytes as the first argument, rather than the path of the wasm file?

PicJS includes and runs the PocketIC server for you, you donā€™t need to run it manually. Thats a distinction from the Rust and Python versions.

So the server youā€™re running manually isnā€™t being used and thatā€™s why it just terminates.

The issue with the argument is harder to tell. Does this canister expect any init args? The path to your WASM looks relative, maybe try an absolute path.

That is super convenient! Nice!

The wasm is being pulled in correctly.

The failure is happening on the server ise after the post. It doesnā€™t like the args even though they were successfully parsed with the IDL. Iā€™m guessing there some discrepancy between the IDL that I have and the wasm delivered by dfx extension install nns.

I guess generally it would be awesome to have some way to have PocketIC install all the NNS canisters similar to dfx nns install.

I guess generally it would be awesome to have some way to have PocketIC install all the NNS canisters similar to dfx nns install.

This will be possible in the next release of PocketIC, because tools like ic-admin will work out of the box. @mraszyk there is no workaround in the current version, right?

Itā€™s possible, but a slight pain to setup. I wrote a guide on how to do that here: Working with the NNS | PicJS

Oh wowā€¦ok. So I need to pull the subnet ID because it will be different every time. Do we know who it will give the ICP tokens to? I guess Iā€™ll need that identity in PicJs. What would be the best way to load it? So far Iā€™ve only used the createIdentity function but Iā€™m guessing Iā€™ll need to load the test_nns pem file to access those ICP?

I also got these errors as I was running dfx nns install:

2024-04-02 14:07:37.033346 UTC: [Canister rkp4c-7iaaa-aaaaa-aaaca-cai] Panicked at 'Deserialization Failed: "Cannot parse header 286f7074207265636f7264207b206379636c65735f6c65646765725f63616e69737465725f6964203d206f7074207072696e636970616c2022756d3569772d72716161612d61616161712d71616162612d63616922207d29"', rs/rust_canisters/dfn_core/src/endpoint.rs:49:41
2024-04-02 14:07:37.097886 UTC: [Canister r7inp-6aaaa-aaaaa-aaabq-cai] [Root Canister] start_canister call successful. Ok(())
2024-04-02 14:07:37.097886 UTC: [Canister r7inp-6aaaa-aaaaa-aaabq-cai] Panicked at 'called `Result::unwrap()` on an `Err` value: (CanisterError, "Canister rkp4c-7iaaa-aaaaa-aaaca-cai trapped explicitly: Panicked at 'Deserialization Failed: \"Cannot parse header 286f7074207265636f7264207b206379636c65735f6c65646765725f63616e69737465725f6964203d206f7074207072696e636970616c2022756d3569772d72716161612d61616161712d71616162612d63616922207d29\"', rs/rust_canisters/dfn_core/src/endpoint.rs:49:41")', rs/nervous_system/root/src/change_canister.rs:246:9

The donā€™t look innocent.

The subnet Id assignment is deterministic in PocketIc, Iā€™m not sure about DFX. The main point is that you need to know what subnet Id was assigned by DFX so you can use the same one in PocketIc.

There are some pre-assigned accounts that have some ICP that you can use, thatā€™s documented in the DFX docs: sdk/docs/cli-reference/dfx-nns.mdx at master Ā· dfinity/sdk Ā· GitHub. And the example project referenced by the guide I posted above has some example usage of transferring ICP, setting up a neuron, staking and then creating proposals.

This will be possible in the next release of PocketIC, because tools like ic-admin will work out of the box. @mraszyk there is no workaround in the current version, right?

Thereā€™s no easy workaround in the current version.

The most relevant code for getting control of an Identity that has ICP already loaded is here: pic-js/examples/nns_proxy/tests/src/support/identity.ts at main Ā· hadronous/pic-js Ā· GitHub

1 Like

Awesomeā€¦I found all the minter identity stuff and am up and running now. Sorry for not RTFM. :(. Thank you for writing it up!

1 Like

What is the likely hood of future dfx versions braking this state?

Iā€™ve no idea what changes may occur in the state folder, so Iā€™d definitely encourage making a dedicated copy for your tests and keeping it separate from the .dfx directory so that thereā€™s no unexpected mutations by DFX.

1 Like

Three years of debugging motoko with debug statments have left me relying on the cruch of using them when things go sideways and I canā€™t track down what is going wrong.

Can pic give me the equivalent of the dfx output that has these logs in it?

Edit:

I was able to get these in my console by editing pocket-ic-server.js:

serverProcess.on('error', error => {
            if ((0, util_1.isArm)() && (0, util_1.isDarwin)()) {
                throw new error_1.BinStartMacOSArmError(error);
            }
            throw new error_1.BinStartError(error);
        });
        serverProcess.stdout.on('data', data => {
          //console.log(`stdout server: ${data}`);
        });
        serverProcess.stderr.on('data', data => {
          stderrBuffer += data.toString();
    
          // Split based on newline characters
          let lines = stderrBuffer.split('\n');
          
          // Keep the last incomplete line in the buffer
          stderrBuffer = lines.pop();
          
          // Process complete lines
          lines.forEach((line) => {
              console.error(`debug: ${line}`);
          });
        });

        serverProcess.stderr.on('close', () => {
          if (stderrBuffer.length > 0) {
              console.error(`debug final: ${stderrBuffer}`);
              stderrBuffer = ''; // Clear the buffer
          }
        });
        return await (0, util_1.poll)(async () => {
            const isPocketIcReady = await (0, util_1.exists)(readyFilePath);
            if (isPocketIcReady) {
                const portString = await (0, util_1.readFileAsString)(portFilePath);
                const port = parseInt(portString);
                return new PocketIcServer(serverProcess, port);
            }
            throw new error_1.BinTimeoutError();
        });
    }

My process isnā€™t stoping properly so Iā€™ve likely messed something up, but I can see my debug statements now!