Add unit & integration testing

* Add Cypress component testing
* Add SeedAddress component testing
* Add Error component test
* Add Search component test
* Add code coverage instrumentation
* Add BranchSelector component testing
* Add coverage check to CI
* Require node version 14

Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
Sebastian Martinez
2022-01-31 16:05:23 +01:00
committed by Alexis Sellier
parent 7ba5ce0f60
commit 0a2481792c
21 changed files with 6225 additions and 5457 deletions
+14
View File
@@ -0,0 +1,14 @@
name: check-components
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- run: npm ci
- run: ./scripts/test
shell: bash
+4
View File
@@ -1,3 +1,7 @@
build/ build/
node_modules/ node_modules/
NOTES NOTES
# Coverage
.nyc_output
coverage
+1
View File
@@ -0,0 +1 @@
14
+10
View File
@@ -0,0 +1,10 @@
{
"all": true,
"extension": [
".svelte",
".ts"
],
"include": [
"src/**/*"
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"video": false,
"componentFolder": "src",
"testFiles": "**/*spec.{js,ts}",
"pluginsFile": "cypress/plugins/index.cjs",
"baseUrl": "http://localhost:3000"
}
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { startDevServer } = require('@cypress/vite-dev-server');
const path = require('path');
module.exports = (on, config) => {
on('dev-server:start', (options) => {
return startDevServer({
options,
viteConfig: {
configFile: path.resolve(__dirname, '..', '..', 'vite.config.ts'),
},
});
});
require('@cypress/code-coverage/task')(on, config);
return config;
};
+19
View File
@@ -0,0 +1,19 @@
import '@cypress/code-coverage/support';
import '@testing-library/cypress/add-commands';
export const styles = {
cssFile: "public/index.css",
style: `
.wrapper {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
`,
html: `
<div class="wrapper">
<div id="here" />
</div>
`
};
+5817 -5349
View File
File diff suppressed because it is too large Load Diff
+14 -6
View File
@@ -5,20 +5,28 @@
"serve": "vite preview", "serve": "vite preview",
"build": "scripts/build", "build": "scripts/build",
"check": "scripts/check", "check": "scripts/check",
"test": "echo \"This template does not include a test runner by default.\" && exit 1" "test": "cypress run-ct"
}, },
"type": "module", "type": "module",
"engines": {
"node": ">=14"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "1.0.0-next.10", "@cypress/code-coverage": "^3.9.12",
"@tsconfig/svelte": "^1.0.10", "@cypress/vite-dev-server": "^2.2.2",
"@types/node": "^15.12.2", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.36",
"@testing-library/cypress": "^8.0.2",
"@tsconfig/svelte": "^1.0.13",
"@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1", "@typescript-eslint/parser": "^4.26.1",
"cypress": "^9.3.1",
"cypress-svelte-unit-test": "git+https://github.com/flakolefluk/cypress-svelte-unit-test.git",
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-plugin-svelte3": "^3.2.0", "eslint-plugin-svelte3": "^3.2.0",
"svelte-check": "^1.5.2", "svelte-check": "^1.5.2",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"vite": "^2.3.7", "vite": "^2.7.13",
"vite-plugin-istanbul": "^2.4.0",
"vite-plugin-rewrite-all": "^0.1.2" "vite-plugin-rewrite-all": "^0.1.2"
}, },
"dependencies": { "dependencies": {
@@ -44,7 +52,7 @@
"multibase": "^4.0.4", "multibase": "^4.0.4",
"multihashes": "^4.0.2", "multihashes": "^4.0.2",
"pure-svg-code": "^1.0.6", "pure-svg-code": "^1.0.6",
"svelte": "^3.32.3", "svelte": "^3.44.3",
"svelte-preprocess": "^4.7.2", "svelte-preprocess": "^4.7.2",
"svelte-routing": "^1.6.0" "svelte-routing": "^1.6.0"
} }
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# For now we only check the Svelte files.
npm test
npx nyc report
+42
View File
@@ -0,0 +1,42 @@
import Error from "./Error.svelte";
import { mount } from "cypress-svelte-unit-test";
import { Failure } from '@app/error';
import { styles } from "@test/support/index";
describe('Error', function () {
it("Open Error modal with default props", () => {
mount(Error, { props: {
subtitle: "Subtitle of Modal",
error: {
type: Failure.InsufficientBalance,
txHash: "0x8b678e51f970c5307bf45a8bcea373b597f9acbcea5c5ba784a1d383361a89d1",
message: "Not enough RAD"
}
}
}, styles);
cy.get("[slot=title]").should("have.text", " Error");
cy.get("[slot=subtitle]").should("have.text", "Subtitle of Modal");
cy.get("[slot=body]").should("have.text", "Error: Not enough RAD");
cy.get("button").should("have.text", "Back");
});
it("Open Error modal with custom message", () => {
mount(Error, { props: {
subtitle: "Subtitle of Modal",
message: "Error message to check for",
} }, styles);
cy.get("[slot=subtitle]").should("have.text", "Subtitle of Modal");
cy.get("[slot=body]").should("have.text", "Error: Error message to check for");
cy.get("button").should("have.text", "Back");
});
it("Check floating modal changes button label to Close", () => {
mount(Error, { props: {
title: "Title of Modal",
subtitle: "Subtitle of Modal",
message: "Error message to check for",
floating: true
} }, styles);
cy.get("button").should("have.text", "Close");
});
});
+18
View File
@@ -0,0 +1,18 @@
import Search from "./Search.svelte";
import { mount } from "cypress-svelte-unit-test";
import { styles } from "@test/support/index";
describe('Search', function () {
it("Renders correctly", () => {
mount(Search, {}, styles);
cy.get("input").should("exist");
cy.get("input").should("have.attr", "placeholder", "Search a name or address...");
});
it("Search input displays and navigates correctly", () => {
mount(Search, {}, styles);
cy.get("input").type("cloudhead{enter}");
cy.get("input").should("have.value", "cloudhead");
cy.url().should("contain", "/resolver/query?q=cloudhead");
});
});
+27
View File
@@ -0,0 +1,27 @@
import SeedAddress from "./SeedAddress.svelte";
import { mount } from "cypress-svelte-unit-test";
import { styles } from "@test/support/index";
describe('SeedAddress', function () {
it("Renders correctly", () => {
mount(SeedAddress, {
props: {
id: "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1",
host: "seed.cloudhead.io",
port: 8776
}
}, styles);
cy.findByText("hydkkk…coygh1@seed.cloudhead.io").should("exist").should("have.attr", "href", "/seeds/seed.cloudhead.io");
cy.findByText(":8776").should("exist");
cy.findByText("Copy").should("exist");
});
it("Copies seed address correctly", () => {
cy.findByText("Copy").click().should("exist");
cy.findByText("Copy ✓").should("have.attr", "disabled");
// We invoke the cy.window here since doing the clipboard action in the runner window, would throw an error
cy.window().its("navigator.clipboard").invoke("readText").should("equal", "hydkkkf5ksbe5fuszdhpqhytu3q36gwagj874wxwpo5a8ti8coygh1@seed.cloudhead.io:8776");
cy.wait(4000);
cy.findByText("Copy").click().should("exist").should("not.have.attr", "disabled");
});
});
+1 -3
View File
@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Config } from "./config";
import { formatSeedAddress, formatSeedId, toClipboard } from "./utils"; import { formatSeedAddress, formatSeedId, toClipboard } from "./utils";
export let config: Config;
export let id: string; export let id: string;
export let host: string; export let host: string;
export let port: number; export let port: number;
@@ -10,7 +8,7 @@
let seedCopied = false; let seedCopied = false;
const copySeed = (seedId: string, seedHost: string) => { const copySeed = (seedId: string, seedHost: string) => {
return () => toClipboard(formatSeedAddress(seedId, seedHost, config)).then(() => { return () => toClipboard(formatSeedAddress(seedId, seedHost, port)).then(() => {
seedCopied = true; seedCopied = true;
setTimeout(() => { setTimeout(() => {
seedCopied = false; seedCopied = false;
+1 -1
View File
@@ -251,7 +251,7 @@
<!-- Seed Address --> <!-- Seed Address -->
{#if profile.seedId && profile.seedHost} {#if profile.seedId && profile.seedHost}
<div class="label">Seed</div> <div class="label">Seed</div>
<SeedAddress {config} id={profile.seedId} host={profile.seedHost} port={config.seed.link.port} /> <SeedAddress id={profile.seedId} host={profile.seedHost} port={config.seed.link.port} />
{/if} {/if}
<!-- Name/Profile --> <!-- Name/Profile -->
<div class="label">Profile</div> <div class="label">Profile</div>
+111
View File
@@ -0,0 +1,111 @@
import BranchSelector from "./BranchSelector.svelte";
import { mount } from "cypress-svelte-unit-test";
import { styles } from "@test/support/index";
const defaultProps = {
project: {
head: "e678629cd37c770c640a2cd997fc76303c815772",
meta: {
name: "nakamoto",
description: "Privacy-preserving Bitcoin light-client implementation in Rust",
defaultBranch: "master",
maintainers: [
"rad:git:hnrkqdpm9ub19oc8dccx44echy76hzfsezyio"
],
delegates: [
"hyn9diwfnytahjq8u3iw63h9jte1ydcatxax3saymwdxqu1zo645pe"
]
}
},
branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"]
],
revision: "e678629cd37c770c640a2cd997fc76303c815772",
};
describe('BranchSelector', function () {
it("Render with commit = head and branch listing", () => {
mount(BranchSelector, {
props: defaultProps
}, styles
);
cy.findByText("master").should("exist");
cy.get("div.commit > div.hash.desktop").should("have.text", "e678629");
});
it("Test Branch selection", () => {
// The viewport here is to simulate desktop behaviour
cy.viewport(800, 300);
mount(BranchSelector, {
props: {
...defaultProps, branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"],
["feature-branch", "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5"],
["xyz", "debf82ef3623ec11751a993bda85bac2ff1c6f00"],
],
branchesDropdown: true,
}, callbacks: {
revisionChanged: cy.stub().as("revisionChanged")
}
}, styles
);
cy.get("div.dropdown > div").first().click();
cy.get("@revisionChanged")
.should("be.calledOnce")
.its("firstCall.args.0.detail")
.should("equal", "feature-branch");
});
it("Render with commit != head, passing a branch as rev and branch listing", () => {
mount(BranchSelector, {
props: {
...defaultProps, branches: [
["master", "e678629cd37c770c640a2cd997fc76303c815772"],
["feature-branch", "29e8b7b0f3019b8e8a6d9bfb0964ee78f4ff12f5"],
["xyz", "debf82ef3623ec11751a993bda85bac2ff1c6f00"],
["xyz", "8b18e8006849e36872cb928d87179e4e222fb689"]
],
revision: "feature-branch"
}
}, styles
);
cy.findByText("feature-branch").should("exist");
cy.get("div.commit > div.hash.desktop").should("have.text", "29e8b7b");
});
it("Render with commit != head passing a commit as rev and branch listing", () => {
mount(BranchSelector, {
props: {
...defaultProps,
revision: "debf82ef3623ec11751a993bda85bac2ff1c6f00"
}
}, styles
);
cy.get("div.commit > div.hash.desktop").should("have.text", "debf82ef3623ec11751a993bda85bac2ff1c6f00");
});
it("Render with commit = head, without branch listing", () => {
mount(BranchSelector, {
props: {
...defaultProps,
revision: "e678629cd37c770c640a2cd997fc76303c815772",
branches: [],
}
}, styles
);
cy.findByText("master").should("exist");
cy.get("div.commit > div.hash").should("have.text", "e678629");
});
it("Render without branch listing, commit != head", () => {
mount(BranchSelector, {
props: {
...defaultProps,
revision: "6b84e519d3c535879eb2b9ee8457bb70ca275a75",
branches: [],
}
}, styles
);
cy.get("div.commit > div.hash.desktop").should("have.text", "6b84e519d3c535879eb2b9ee8457bb70ca275a75");
});
});
+1 -1
View File
@@ -96,7 +96,7 @@
<!-- Seed Address --> <!-- Seed Address -->
<div class="label">Address</div> <div class="label">Address</div>
{#if info.version === "0.2.0" && info.host} {#if info.version === "0.2.0" && info.host}
<SeedAddress {config} id={info.id} host={info.host} port={config.seed.link.port} /> <SeedAddress id={info.id} host={info.host} port={config.seed.link.port} />
{:else} {:else}
<div class="seed-address subtle">N/A</div> <div class="seed-address subtle">N/A</div>
<div class="desktop" /> <div class="desktop" />
+85 -85
View File
@@ -161,96 +161,96 @@
</div> </div>
</div> </div>
</header> </header>
<div class="fields"> <div class="fields">
<!-- Address --> <!-- Address -->
<div class="label">Address</div> <div class="label">Address</div>
<div class="desktop"><Address {config} address={profile.address} /></div> <div class="desktop"><Address {config} address={profile.address} /></div>
<div class="mobile"><Address compact {config} address={profile.address} /></div> <div class="mobile"><Address compact {config} address={profile.address} /></div>
<div class="desktop" />
<!-- Project anchors -->
{#if profile.anchorsAccount}
<div class="label">Anchors</div>
<div class="desktop"><Address {config} address={profile.anchorsAccount} /></div>
<div class="mobile"><Address compact {config} address={profile.anchorsAccount} /></div>
<div class="desktop" /> <div class="desktop" />
<!-- Project anchors --> {/if}
{#if profile.anchorsAccount} <!-- Seed Address -->
<div class="label">Anchors</div> {#if profile.seedId && profile.seedHost}
<div class="desktop"><Address {config} address={profile.anchorsAccount} /></div> <div class="label">Seed</div>
<div class="mobile"><Address compact {config} address={profile.anchorsAccount} /></div> <SeedAddress id={profile.seedId} host={profile.seedHost} port={config.seed.link.port} />
<div class="desktop" /> {/if}
<!-- Profile -->
<div class="label">Profile</div>
<div>
{#if profile.name}
<a href={profile.registry(config)} class="link">{profile.name}</a>
{:else}
<span class="subtle">Not set</span>
{/if} {/if}
<!-- Seed Address -->
{#if profile.seedId && profile.seedHost}
<div class="label">Seed</div>
<SeedAddress {config} id={profile.seedId} host={profile.seedHost} port={config.seed.link.port} />
{/if}
<!-- Profile -->
<div class="label">Profile</div>
<div>
{#if profile.name}
<a href={profile.registry(config)} class="link">{profile.name}</a>
{:else}
<span class="subtle">Not set</span>
{/if}
</div>
<div class="desktop">
{#if isAuthorized(profile.address)}
<button class="tiny secondary" on:click={setName}>
Set
</button>
{/if}
</div>
</div> </div>
{#await Org.getOrgsByMember(profile.address, config)} <div class="desktop">
<Loading center /> {#if isAuthorized(profile.address)}
{:then orgs} <button class="tiny secondary" on:click={setName}>
{#if orgs.length > 0} Set
<div class="members"> </button>
{#each orgs as org}
<div class="member">
{#await Profile.get(org.address, ProfileType.Minimal, config)}
<Loading small margins />
{:then profile}
<div class="member-icon">
<Link to="/orgs/{profile.address}">
<Avatar source={profile.avatar ?? profile.address} address={profile.address} />
</Link>
</div>
<div class="desktop">
<Address address={profile.address} compact
resolve noBadge noAvatar {profile} {config} />
</div>
{/await}
</div>
{/each}
</div>
{/if} {/if}
{:catch err} </div>
<Message error> </div>
<strong>Error: </strong> failed to load orgs: {err.message}. {#await Org.getOrgsByMember(profile.address, config)}
</Message> <Loading center />
{/await} {:then orgs}
<div class="projects"> {#if orgs.length > 0}
{#if profile.anchorsAccount} <div class="members">
{#await Org.get(profile.anchorsAccount, config)} {#each orgs as org}
<Loading center fadeIn /> <div class="member">
{:then org} {#await Profile.get(org.address, ProfileType.Minimal, config)}
{#if org} <Loading small margins />
{#await org.getProjects(config) then projects} {:then profile}
{#each projects as project} <div class="member-icon">
<div class="project"> <Link to="/orgs/{profile.address}">
<Project {project} user={addressOrName} config={profile.config(config)}> <Avatar source={profile.avatar ?? profile.address} address={profile.address} />
<span slot="stateHash"> </Link>
<span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span> </div>
<span class="desktop">commit {project.anchor.stateHash}</span> <div class="desktop">
</span> <Address address={profile.address} compact
</Project> resolve noBadge noAvatar {profile} {config} />
</div> </div>
{/each}
{:catch err}
<Message error>
<strong>Error: </strong> failed to load projects: {err.message}.
</Message>
{/await} {/await}
{/if} </div>
{/await} {/each}
{/if} </div>
</div> {/if}
{:catch err}
<Message error>
<strong>Error: </strong> failed to load orgs: {err.message}.
</Message>
{/await}
<div class="projects">
{#if profile.anchorsAccount}
{#await Org.get(profile.anchorsAccount, config)}
<Loading center fadeIn />
{:then org}
{#if org}
{#await org.getProjects(config) then projects}
{#each projects as project}
<div class="project">
<Project {project} user={addressOrName} config={profile.config(config)}>
<span slot="stateHash">
<span class="mobile">commit {formatCommit(project.anchor.stateHash)}</span>
<span class="desktop">commit {project.anchor.stateHash}</span>
</span>
</Project>
</div>
{/each}
{:catch err}
<Message error>
<strong>Error: </strong> failed to load projects: {err.message}.
</Message>
{/await}
{/if}
{/await}
{/if}
</div>
</main> </main>
<svelte:component this={setNameForm} entity={new User(profile.address)} {config} on:close={() => setNameForm = null} /> <svelte:component this={setNameForm} entity={new User(profile.address)} {config} on:close={() => setNameForm = null} />
{:catch err} {:catch err}
+8 -7
View File
@@ -72,8 +72,8 @@ export function isAddressEqual(left: string, right: string): boolean {
return left.toLowerCase() === right.toLowerCase(); return left.toLowerCase() === right.toLowerCase();
} }
export function formatSeedAddress(id: string, host: string, config: Config): string { export function formatSeedAddress(id: string, host: string, port: number): string {
return `${id}@${host}:${config.seed.link.port}`; return `${id}@${host}:${port}`;
} }
export function formatSeedId(id: string): string { export function formatSeedId(id: string): string {
@@ -111,13 +111,14 @@ export function formatIpfsFile(ipfs: string | undefined): string | undefined {
} }
export function formatHash(hash: string): string { export function formatHash(hash: string): string {
if (hash.length < 10) return hash;
return hash.substring(0, 6) return hash.substring(0, 6)
+ '...' + '...'
+ hash.substring(hash.length - 4, hash.length); + hash.substring(hash.length - 4, hash.length);
} }
export function formatCommit(oid: string): string { export function formatCommit(oid: string): string {
return oid.substring(0,7); return oid.substring(0, 7);
} }
export function formatOrg(input: string, config: Config): string { export function formatOrg(input: string, config: Config): string {
@@ -130,7 +131,7 @@ export function formatOrg(input: string, config: Config): string {
export function capitalize(s: string): string { export function capitalize(s: string): string {
if (s === "") return s; if (s === "") return s;
return s[0].toUpperCase() + s.substr(1); return s[0].toUpperCase() + s.substring(1);
} }
// Takes a domain name, eg. 'cloudhead.radicle.eth' and // Takes a domain name, eg. 'cloudhead.radicle.eth' and
@@ -196,7 +197,7 @@ export function isENSName(input: string, config: Config): boolean {
return regEx.test(input); return regEx.test(input);
} }
// Check whether the input is an Ethereum address. // Check whether the input is an checksummed or all lowercase Ethereum address.
export function isAddress(input: string): boolean { export function isAddress(input: string): boolean {
return ethers.utils.isAddress(input); return ethers.utils.isAddress(input);
} }
@@ -424,7 +425,7 @@ export function isMarkdownPath(path: string): boolean {
// Also accepts in dev env 0.0.0.0 as domain // Also accepts in dev env 0.0.0.0 as domain
export function isDomain(input: string): boolean { export function isDomain(input: string): boolean {
return (/^[a-z][a-z0-9.-]+$/.test(input) && /\.[a-z]+$/.test(input)) return (/^[a-z][a-z0-9.-]+$/.test(input) && /\.[a-z]+$/.test(input))
|| (!import.meta.env.PROD && /^0.0.0.0$/.test(input)) ; || (! import.meta.env.PROD && /^0.0.0.0$/.test(input));
} }
// Propose a Gnosis Safe multi-sig transaction. // Propose a Gnosis Safe multi-sig transaction.
@@ -504,7 +505,7 @@ export async function executeSignedSafeTransaction(
...signedTx, ...signedTx,
gasPrice: Number(signedTx.gasPrice), gasPrice: Number(signedTx.gasPrice),
data: signedTx.data data: signedTx.data
} ); });
signedTx.confirmations.forEach(confirmation => { signedTx.confirmations.forEach(confirmation => {
const signature = new EthSignSignature(confirmation.owner, confirmation.signature); const signature = new EthSignSignature(confirmation.owner, confirmation.signature);
+7 -3
View File
@@ -1,7 +1,8 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "@tsconfig/svelte/tsconfig.json",
"include": [ "include": [
"src" "src",
"cypress"
], ],
"exclude": [ "exclude": [
"node_modules/*" "node_modules/*"
@@ -11,7 +12,9 @@
"module": "es2020", "module": "es2020",
"types": [ "types": [
"svelte", "svelte",
"vite/client" "vite/client",
"cypress",
"@testing-library/cypress"
], ],
"sourceMap": true, "sourceMap": true,
"moduleResolution": "node", "moduleResolution": "node",
@@ -22,7 +25,8 @@
"importsNotUsedAsValues": "error", "importsNotUsedAsValues": "error",
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"@app/*": ["./src/*"] "@app/*": ["./src/*"],
"@test/*": ["./cypress/*"]
} }
}, },
"noEmit": true "noEmit": true
+15 -2
View File
@@ -1,18 +1,31 @@
/// <reference types="vitest" />
import path from 'path'; import path from 'path';
import { UserConfig } from 'vite'; import { UserConfig } from 'vite';
import svelte from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
import rewriteAll from 'vite-plugin-rewrite-all'; import rewriteAll from 'vite-plugin-rewrite-all';
import IstanbulPlugin from 'vite-plugin-istanbul';
const config: UserConfig = { const config: UserConfig = {
optimizeDeps: { optimizeDeps: {
exclude: ['svelte-routing', '@pedrouid/environment', '@pedrouid/iso-crypto'] exclude: ['svelte-routing', '@pedrouid/environment', '@pedrouid/iso-crypto']
}, },
plugins: [svelte(), rewriteAll()], plugins: [
svelte({ hot: !process.env.VITEST }),
rewriteAll(),
IstanbulPlugin({
include: "src/**/*",
exclude: ["node_modules"],
extension: [".ts", ".svelte"],
cypress: true
})
],
resolve: { resolve: {
alias: { alias: {
// This is needed for vite not to choke. // This is needed for vite not to choke.
"caip": path.resolve("./node_modules/caip/dist/umd/index.min.js"), "caip": path.resolve("./node_modules/caip/dist/umd/index.min.js"),
'@app': path.resolve('./src'), '@app': path.resolve('./src'),
'@test': path.resolve('./cypress'),
// Polyfill for Node.js 'stream' library. // Polyfill for Node.js 'stream' library.
'stream': path.resolve('./src/polyfills/stream.ts'), 'stream': path.resolve('./src/polyfills/stream.ts'),
'typedarray-to-buffer': path.resolve('./src/polyfills/typedarray-to-buffer.js'), 'typedarray-to-buffer': path.resolve('./src/polyfills/typedarray-to-buffer.js'),