diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eba59e2..71678d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,38 +1,32 @@ -name: Tests +name: tests on: - # push: - # branches: [ "main" ] workflow_dispatch: pull_request: - -concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true + types: [synchronize, opened, reopened, ready_for_review, unlabeled] jobs: - build: + test: runs-on: ubuntu-22.04 - strategy: - matrix: - node-version: [18] - steps: - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + + - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} - - name: Clear npm cache - run: npm cache clean --force + node-version: 18 + - name: Install dfx uses: dfinity/setup-dfx@main + - name: Install mops uses: ZenVoich/setup-mops@v1 - - name: Start dfx - run: | - dfx cache install - dfx start --background - - run: npm run setup - - run: npm test + + - name: generate declarations + run: dfx generate backend + + - name: install dependencies + run: npm i + + - name: run tests + run: npm run test diff --git a/README.md b/README.md index 98b9273..5a3233d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ > Due to current limitations, this template does not work in a Browser Editor when using gitpod or codespaces. Please use VS Code for desktop instead. > Screenshot 2024-01-29 at 12 44 57 - [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/letmejustputthishere/vite-sveltekit-motoko) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/letmejustputthishere/vite-sveltekit-motoko?quickstart=1) @@ -18,7 +17,7 @@ For an example of a real-world dapp built using this starter project, check out ## ๐Ÿ“ฆ Create a New Project > [!IMPORTANT] -> Make sure that [Node.js](https://nodejs.org/en/) `>= 18`, [mops](https://docs.mops.one/quick-start) `>=0.39.2` and [`dfx`](https://internetcomputer.org/docs/current/developer-docs/build/install-upgrade-remove) `>= 0.16` are installed on your system. +> Make sure that [Node.js](https://nodejs.org/en/) `>= 18`, [mops](https://docs.mops.one/quick-start) `>=0.39.2` and [`dfx`](https://internetcomputer.org/docs/current/developer-docs/build/install-upgrade-remove) `>= 0.16.1` are installed on your system. Run the following commands in a new, empty project directory: @@ -45,6 +44,11 @@ When ready, run `dfx deploy --network ic` to deploy your application to the Inte - [mo-dev](https://github.com/dfinity/motoko-dev-server#readme): a live reload development server for Motoko - [eslint](https://eslint.org/): a static code analysis tool used in software development for identifying problematic patterns or code that doesn't adhere to certain style guidelines in JavaScript and TypeScript - [Internet Identity](https://github.com/dfinity/internet-identity/tree/main): a decentralized identity provider for the Internet Computer +- [pic.js](https://github.com/hadronous/pic-js): an Internet Computer Protocol canister testing library for TypeScript and JavaScript + +## ๐Ÿงช Testing + +You can run `npm run test` to run unit tests using [`mops test`](https://docs.mops.one/cli/mops-test) and end-to-end tests using [`pic.js`](https://hadronous.github.io/pic-js/). ## ๐Ÿ“š Documentation @@ -55,10 +59,11 @@ When ready, run `dfx deploy --network ic` to deploy your application to the Inte - [Motoko developer docs](https://internetcomputer.org/docs/current/developer-docs/build/cdks/motoko-dfinity/motoko/) - [Mops usage instructions](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install) - [Internet Identity docs](https://internetcomputer.org/docs/current/developer-docs/integrations/internet-identity/overview) +- [pic-js](https://hadronous.github.io/pic-js/) ## ๐Ÿ’ก Tips and Tricks - Customize your project's code style by editing the `.prettierrc` file and then running `npm run format`. - Reduce the latency of update calls by passing the `--emulator` flag to `dfx start`. -- Install a Motoko package by running `npx ic-mops add `. Here is a [list of available packages](https://mops.one/). +- Install a Motoko package by running `mops add `. Here is a [list of available packages](https://mops.one/). - Split your frontend and backend console output by running `npm run frontend` and `npm run backend` in separate terminals. diff --git a/backend/main.mo b/backend/main.mo index 74cc1a5..26e62d4 100644 --- a/backend/main.mo +++ b/backend/main.mo @@ -1,6 +1,6 @@ -actor class Main() { +actor class Main(initArgs : { phrase : Text }) { public query func greet(name : Text) : async Text { - return "Hello, " # name # "!"; + return initArgs.phrase # ", " # name # "!"; }; public query ({ caller }) func whoAmI() : async Principal { diff --git a/backend/tests/main.greet.test.mo b/backend/tests/main.greet.test.mo index 42371c6..0300c4d 100644 --- a/backend/tests/main.greet.test.mo +++ b/backend/tests/main.greet.test.mo @@ -1,5 +1,5 @@ import { Main } "../main"; -let main = await Main(); +let main = await Main({ phrase = "Hello" }); assert (await main.greet("Moritz")) == "Hello, Moritz!"; diff --git a/package-lock.json b/package-lock.json index 79ba82e..a298ab7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@dfinity/auth-client": "^0.21.4", "@dfinity/candid": "^0.21.4", "@dfinity/principal": "^0.21.4", + "@hadronous/pic": "^0.3.0-b0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", @@ -595,6 +596,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hadronous/pic": { + "version": "0.3.0-b0", + "resolved": "https://registry.npmjs.org/@hadronous/pic/-/pic-0.3.0-b0.tgz", + "integrity": "sha512-KPiivseCIpIHtLAQ1sZr0p8R85pS5QNsCvovPzZDgdiBuJddz24Lt2K2YpEOOmSpikqNdYRelTl/HkCtFSb0kQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bip39": "^3.1.0" + }, + "peerDependencies": { + "@dfinity/agent": "~0.21.4", + "@dfinity/candid": "~0.21.4", + "@dfinity/identity": "~0.21.4", + "@dfinity/principal": "~0.21.4" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1732,6 +1749,15 @@ "node": ">=8" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dev": true, + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", diff --git a/package.json b/package.json index b8dfcb1..ff7b4c2 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,16 @@ "version": "0.0.1", "private": true, "scripts": { - "setup": "npm i && npm run init-ii && dfx generate backend && dfx generate internet_identity && dfx deploy backend && dfx deps deploy", + "setup": "npm i && npm run init-ii && dfx generate backend && dfx generate internet_identity && dfx deploy backend --argument '(record {phrase = \"Hello\"})' && dfx deps deploy", "init-ii": "dfx deps pull && dfx deps init internet_identity --argument '(null)'", "start": "run-p frontend backend", "frontend": "vite --host", "backend": "mo-dev --generate --deploy -y", "build": "vite build", - "test": "run-s test:backend test:frontend", - "test:frontend": "vitest run", - "test:backend": "mops test", + "pretest": "dfx generate backend", + "test": "run-s test:e2e test:unit", + "test:e2e": "vitest run", + "test:unit": "mops test", "format": "prettier --write .", "sources": "mops sources", "postinstall": "mops install", @@ -26,6 +27,7 @@ "@dfinity/auth-client": "^0.21.4", "@dfinity/candid": "^0.21.4", "@dfinity/principal": "^0.21.4", + "@hadronous/pic": "^0.3.0-b0", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", diff --git a/tests/canister.test.ts b/tests/canister.test.ts new file mode 100644 index 0000000..8fd55ae --- /dev/null +++ b/tests/canister.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, afterEach, beforeEach } from 'vitest'; + +import { AnonymousIdentity } from '@dfinity/agent'; +import { PocketIc, createIdentity, type Actor } from '@hadronous/pic'; +import type { _SERVICE } from '../src/declarations/backend/backend.did'; +import { deployCanister } from './setup'; + +describe('canister tests', () => { + let pic: PocketIc; + let actor: Actor<_SERVICE>; + + const alice = createIdentity('superSecretAlicePassword'); + const bob = createIdentity('superSecretBobPassword'); + + afterEach(async () => { + await pic.tearDown(); + }); + + describe('when calling greet on the canister deployed with the default init args', () => { + beforeEach(async () => { + ({ pic, actor } = await deployCanister({ + deployer: alice.getPrincipal() + })); + }); + + it('the argument should be prefixed with `Hello, `', async () => { + await expect(actor.greet('Moritz')).resolves.toEqual('Hello, Moritz!'); + }); + + it('the argument should be prefixed with `Hello, `, even for very long names', async () => { + const veryLongName = 'a'.repeat(1000); + await expect(actor.greet(veryLongName)).resolves.toEqual('Hello, ' + veryLongName + '!'); + }); + + it('a call to whoami with the anonymous principal should return the anonymous principal', async () => { + actor.setIdentity(new AnonymousIdentity()); + await expect(actor.whoAmI()).resolves.toEqual(new AnonymousIdentity().getPrincipal()); + }); + + it('a call to whoami with the alice principal should return the alice principal', async () => { + actor.setIdentity(alice); + await expect(actor.whoAmI()).resolves.toEqual(alice.getPrincipal()); + }); + + it('a call to whoami with the bob principal should return the bob principal', async () => { + actor.setIdentity(bob); + await expect(actor.whoAmI()).resolves.toEqual(bob.getPrincipal()); + }); + }); + + describe('when calling greet on the canister deployed with `bonjour` as an init arg', () => { + beforeEach(async () => { + ({ pic, actor } = await deployCanister({ + initArgs: { phrase: 'bonjour' }, + deployer: alice.getPrincipal() + })); + }); + + it('the argument should be prefixed with `bonjour, `', async () => { + await expect(actor.greet('Moritz')).resolves.toEqual('bonjour, Moritz!'); + }); + + it('the argument should be prefixed with `Hello, `, even for very long names', async () => { + const veryLongName = 'a'.repeat(1000); + await expect(actor.greet(veryLongName)).resolves.toEqual('bonjour, ' + veryLongName + '!'); + }); + + it('a call to whoami with the anonymous principal should return the anonymous principal', async () => { + actor.setIdentity(new AnonymousIdentity()); + await expect(actor.whoAmI()).resolves.toEqual(new AnonymousIdentity().getPrincipal()); + }); + + it('a call to whoami with the alice principal should return the alice principal', async () => { + actor.setIdentity(alice); + await expect(actor.whoAmI()).resolves.toEqual(alice.getPrincipal()); + }); + + it('a call to whoami with the bob principal should return the bob principal', async () => { + actor.setIdentity(bob); + await expect(actor.whoAmI()).resolves.toEqual(bob.getPrincipal()); + }); + }); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts deleted file mode 100644 index e07cbbd..0000000 --- a/tests/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..ab12c03 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,37 @@ +import { IDL } from '@dfinity/candid'; +import { idlFactory, init } from '../src/declarations/backend/backend.did.js'; +import type { _SERVICE } from '../src/declarations/backend/backend.did'; +import { resolve } from 'node:path'; +import { PocketIc } from '@hadronous/pic'; +import { Principal } from '@dfinity/principal'; + +type InitArgs = { + phrase: string; +}; +const defaultInitArgs: InitArgs = { + phrase: 'Hello' +}; +const WASM_PATH = resolve(__dirname, '..', '.dfx', 'local', 'canisters', 'backend', 'backend.wasm'); + +interface DeployOptions { + initArgs?: InitArgs; + deployer?: Principal; +} + +export async function deployCanister({ + initArgs = defaultInitArgs, + deployer = Principal.anonymous() +}: DeployOptions) { + const encodedInitArgs = IDL.encode(init({ IDL }), [initArgs]); + const pic = await PocketIc.create(); + const fixture = await pic.setupCanister<_SERVICE>( + idlFactory, + WASM_PATH, + undefined, + encodedInitArgs, + deployer + ); + const actor = fixture.actor; + const canisterId = fixture.canisterId; + return { pic, actor, canisterId }; +}