From 7453c9a59b890202b7d5b10f801c75927d3870c4 Mon Sep 17 00:00:00 2001 From: Sarah Schwartz <58856580+sarahschwartz@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:01:11 -0600 Subject: [PATCH] test: add erc20 paymaster test and refactor testing --- .github/workflows/playwright.yaml | 1 + bun.lockb | Bin 834505 -> 835176 bytes content/tutorials/erc20-paymaster/10.index.md | 110 ++++++++++++++--- .../how-to-test-contracts/10.index.md | 33 +++--- package.json | 1 + tests/configs/config.ts | 18 +++ tests/configs/erc20-paymaster.ts | 112 ++++++++++++++++++ tests/configs/how-to-test-contracts.ts | 68 +++++++++++ tests/erc20-paymaster.spec.ts | 12 ++ tests/how-to-test-contracts.spec.ts | 22 ++-- tests/utils/files.ts | 26 +++- tests/utils/getTestActions.ts | 2 +- tests/utils/runCommand.ts | 46 ++++++- tests/utils/runTest.ts | 69 ++++++++--- tests/utils/types.ts | 46 +++++++ 15 files changed, 492 insertions(+), 74 deletions(-) create mode 100644 tests/configs/config.ts create mode 100644 tests/configs/erc20-paymaster.ts create mode 100644 tests/configs/how-to-test-contracts.ts create mode 100644 tests/erc20-paymaster.spec.ts create mode 100644 tests/utils/types.ts diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index b0f110a..7eb5eaa 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -10,6 +10,7 @@ jobs: strategy: matrix: tutorial: + - "tests/erc20-paymaster.spec.ts" - "tests/how-to-test-contracts.spec.ts" steps: diff --git a/bun.lockb b/bun.lockb index 192d16273d7ec125268c94a2306b06b06c47dbd1..c16d1f9b1fa50072799d6be9cacc7ea9496fcd78 100755 GIT binary patch delta 65767 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 - This creates a new ZKsync Era project called `custom-paymaster-tutorial` with a basic `Greeter` contract. +```sh +npx zksync-cli create custom-paymaster-tutorial --template hardhat_solidity +``` -1. Navigate into the project directory: +This creates a new ZKsync Era project called `custom-paymaster-tutorial` with a basic `Greeter` contract. - ```sh - cd custom-paymaster-tutorial - ``` +Next, navigate into the project directory and install the dependencies: + + + +```shb +cd custom-paymaster-tutorial && npm install +``` + + ::callout{icon="i-heroicons-exclamation-circle"} The template project includes multiple example contracts. Feel free to delete them. @@ -213,7 +219,17 @@ _first_ check that the user provided enough allowance before calling `transferFr ## Paymaster Contract Full Code -Create the `contracts/MyPaymaster.sol` file and copy/paste the following: +Create the `contracts/MyPaymaster.sol` file with + + + +```sh +touch contracts/MyPaymaster.sol +``` + +and copy/paste the following: + + ```solidity // SPDX-License-Identifier: MIT @@ -338,10 +354,18 @@ contract MyPaymaster is IPaymaster { ## Create ERC20 Contract -For the sake of simplicity we will use a modified OpenZeppelin ERC20 implementation: +For the sake of simplicity we will use a modified OpenZeppelin ERC20 implementation. + + + +```sh +touch contracts/MyERC20.sol +``` Create the `contracts/MyERC20.sol` file and copy/paste the following: + + ```solidity [contracts/MyERC20.sol] // SPDX-License-Identifier: UNLICENSED @@ -378,7 +402,17 @@ It also mints some `MyERC20` tokens into the account we use to deploy the contra In addition, the script sends `0.06ETH` to the paymaster contract so it can pay the transaction fees we send later on. 1. In the `deploy` folder, create the file `deploy-paymaster.ts` and copy/paste the following. - Make sure the private key of the account used to deploy the contracts is configured in the `.env` file of the project.: + + + + ```sh + touch deploy/deploy-paymaster.ts + ``` + + Make sure the private key of the account used to deploy the contracts is configured in the `.env` file of the project. + + + ```ts [deploy-paymaster.ts] import { deployContract, getWallet, getProvider } from "./utils"; @@ -416,16 +450,41 @@ In addition, the script sends `0.06ETH` to the paymaster contract so it can pay 1. Compile and the contracts from the project root: - ```sh + + + + ::code-group + + ```bash [npx] + npx hardhat compile + ``` + + ```bash [yarn] yarn hardhat compile ``` + :: + 1. Execute the deployment script: - ```sh + + + + + + + ::code-group + + ```bash [npx] + npx hardhat deploy-zksync --script deploy-paymaster.ts + ``` + + ```bash [yarn] yarn hardhat deploy-zksync --script deploy-paymaster.ts ``` + :: + The output should be roughly the following: ```sh @@ -461,10 +520,18 @@ Make sure you delete the `artifacts-zk` and `cache-zk` folders before recompilin 1. Create the `use-paymaster.ts` script in the `deploy` folder, replacing the parameter placeholders with the details from the previous deploy step. + + + ```sh + touch deploy/use-paymaster.ts + ``` + ::callout{icon="i-heroicons-exclamation-triangle"} Make sure you use the private key of the wallet created by the previous script as that wallet contains the ERC20 tokens. :: + + ```ts [deploy/use-paymaster.ts] import { utils, Wallet } from "zksync-ethers"; import { getWallet, getProvider } from "./utils"; @@ -534,12 +601,25 @@ Make sure you delete the `artifacts-zk` and `cache-zk` folders before recompilin } ``` + + + 1. Run the script: - ```sh + + + ::code-group + + ```bash [npx] + npx hardhat deploy-zksync --script use-paymaster.ts + ``` + + ```bash [yarn] yarn hardhat deploy-zksync --script use-paymaster.ts ``` + :: + The output should look something like this: ```txt diff --git a/content/tutorials/how-to-test-contracts/10.index.md b/content/tutorials/how-to-test-contracts/10.index.md index 85f8c8a..8da9c07 100644 --- a/content/tutorials/how-to-test-contracts/10.index.md +++ b/content/tutorials/how-to-test-contracts/10.index.md @@ -26,7 +26,7 @@ During the alpha phase, ZKsync Era Test Nodes are currently undergoing developme First, initialize a new Hardhat TypeScript project: - + ```bash npx hardhat init @@ -36,7 +36,7 @@ Select the `Create a TypeScript project` option and install the sample project's To install the `hardhat-zksync` plugin, execute the following command: - + ::code-group @@ -52,8 +52,7 @@ yarn add -D @matterlabs/hardhat-zksync Once installed, add the plugin at the top of your `hardhat.config.ts` file. - + ```ts [hardhat.config.ts] import "@matterlabs/hardhat-zksync"; @@ -63,7 +62,7 @@ import "@matterlabs/hardhat-zksync"; You can now safely run the **ZKsync Era Test Node** with the following command: - + ::code-group @@ -77,9 +76,9 @@ yarn hardhat node-zksync :: - + - + ::callout{icon="i-heroicons-exclamation-circle"} We'll want to verify the correctness of our installations and test if we can run a **ZKsync Era Test Node**, @@ -107,8 +106,7 @@ To enable the usage of ZKsync Era Test Node in Hardhat, add the `zksync:true` option to the hardhat network in the `hardhat.config.ts` file and the `latest` version of `zksolc`: - + ```ts zksolc: { @@ -128,7 +126,7 @@ it's necessary to use the `hardhat-chai-matchers` plugin. In the root directory of your project, execute this command: - + ::code-group @@ -144,8 +142,7 @@ yarn add -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6 After installing it, add the plugin at the top of your `hardhat.config.ts` file: - + ```ts [hardhat.config.ts] import "@nomicfoundation/hardhat-chai-matchers"; @@ -153,7 +150,7 @@ import "@nomicfoundation/hardhat-chai-matchers"; With the previous steps completed, your `hardhat.config.ts` file should now be properly configured to include settings for local testing. - + ```ts [hardhat.config.ts] import { HardhatUserConfig } from "hardhat/config"; @@ -182,7 +179,7 @@ To set up the environment for using chai matchers and writing tests, you'll need Inside the **contracts** folder, rename the example contract file to **Greeter.sol**. - + ```bash mv contracts/Lock.sol contracts/Greeter.sol @@ -190,7 +187,7 @@ mv contracts/Lock.sol contracts/Greeter.sol Now replace the example contract in **Greeter.sol** with the new `Greeter` contract below: - + ```solidity [Greeter.sol] // SPDX-License-Identifier: MIT @@ -223,7 +220,7 @@ Now you can create a test with the `hardhat-chai-matchers` plugin: Inside the `/test` folder, rename the example test file to `test.ts`. - + ```bash mv test/Lock.ts test/test.ts @@ -231,7 +228,7 @@ mv test/Lock.ts test/test.ts Replace the old test with this example showcasing the functionalities of the contract: - + ```typescript import * as hre from "hardhat"; @@ -284,7 +281,7 @@ describe("Greeter", function () { Execute the following command in your terminal to run the tests: - + ::code-group diff --git a/package.json b/package.json index fe0d0fa..0d1a40d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "lint-staged": "^15.2.4", "markdownlint": "^0.34.0", "markdownlint-cli2": "^0.13.0", + "node-pty": "^1.0.0", "pm2": "^5.4.2", "prettier": "^3.2.5", "prettier-eslint": "^16.3.0", diff --git a/tests/configs/config.ts b/tests/configs/config.ts new file mode 100644 index 0000000..420c697 --- /dev/null +++ b/tests/configs/config.ts @@ -0,0 +1,18 @@ +import { steps as erc20PaymasterSteps } from './erc20-paymaster'; +import { steps as howToTestContractsSteps } from './how-to-test-contracts'; + +export function getConfig(tutorialName: string) { + let steps; + switch (tutorialName) { + case 'erc20-paymaster': + steps = erc20PaymasterSteps; + break; + case 'how-to-test-contracts': + steps = howToTestContractsSteps; + break; + default: + break; + } + + return steps; +} diff --git a/tests/configs/erc20-paymaster.ts b/tests/configs/erc20-paymaster.ts new file mode 100644 index 0000000..0a0104c --- /dev/null +++ b/tests/configs/erc20-paymaster.ts @@ -0,0 +1,112 @@ +import type { IStepConfig } from '../utils/types'; + +export const steps: IStepConfig = { + 'initialize-hardhat-project': { + action: 'runCommand', + prompts: 'Private key of the wallet:0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110|npm: ', + }, + 'npm-install': { + action: 'runCommand', + }, + 'wait-for-install': { + action: 'wait', + timeout: 5000, + }, + 'create-contract-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'add-paymaster-contract': { + action: 'writeToFile', + filepath: 'tests-output/custom-paymaster-tutorial/contracts/MyPaymaster.sol', + }, + 'create-erc20-contract-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'add-erc20-contract': { + action: 'writeToFile', + filepath: 'tests-output/custom-paymaster-tutorial/contracts/MyERC20.sol', + }, + 'create-deploy-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'add-testing-private-key': { + action: 'modifyFile', + filepath: 'tests-output/custom-paymaster-tutorial/.env', + atLine: 1, + removeLines: [1], + useSetData: 'WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110', + }, + 'add-deploy-script': { + action: 'writeToFile', + filepath: 'tests-output/custom-paymaster-tutorial/deploy/deploy-paymaster.ts', + }, + 'create-ts-config': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + useSetCommand: 'touch tsconfig.json', + }, + 'compile-contracts': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'use-local-network': { + action: 'modifyFile', + filepath: 'tests-output/custom-paymaster-tutorial/hardhat.config.ts', + atLine: 6, + removeLines: [6], + useSetData: ' defaultNetwork: "inMemoryNode",', + }, + 'start-local-network': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + useSetCommand: "bun pm2 start 'npx hardhat node-zksync' --name hh-zknode", + }, + 'wait-for-hh-node': { + action: 'wait', + timeout: 7000, + }, + 'temp-fix-import': { + action: 'modifyFile', + filepath: 'tests-output/custom-paymaster-tutorial/deploy/utils.ts', + atLine: 4, + removeLines: [4], + useSetData: "import * as dotenv from 'dotenv';", + }, + 'deploy-contracts': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'create-deploy-paymaster-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, + 'add-use-paymaster': { + action: 'writeToFile', + filepath: 'tests-output/custom-paymaster-tutorial/deploy/use-paymaster.ts', + }, + 'paymaster-address': { + action: 'modifyFile', + filepath: 'tests-output/custom-paymaster-tutorial/deploy/use-paymaster.ts', + atLine: 7, + removeLines: [7], + getContractId: + 'tests-output/custom-paymaster-tutorial/deployments-zk/inMemoryNode/contracts/MyPaymaster.sol/MyPaymaster.json', + useSetData: "const PAYMASTER_ADDRESS = '<*GET_CONTRACT_ID*>';", + }, + 'token-address': { + action: 'modifyFile', + filepath: 'tests-output/custom-paymaster-tutorial/deploy/use-paymaster.ts', + atLine: 10, + removeLines: [10], + getContractId: + 'tests-output/custom-paymaster-tutorial/deployments-zk/inMemoryNode/contracts/MyERC20.sol/MyERC20.json', + useSetData: "const TOKEN_ADDRESS = '<*GET_CONTRACT_ID*>';", + }, + 'run-use-paymaster': { + action: 'runCommand', + commandFolder: 'tests-output/custom-paymaster-tutorial', + }, +}; diff --git a/tests/configs/how-to-test-contracts.ts b/tests/configs/how-to-test-contracts.ts new file mode 100644 index 0000000..d36c1c2 --- /dev/null +++ b/tests/configs/how-to-test-contracts.ts @@ -0,0 +1,68 @@ +import type { IStepConfig } from '../utils/types'; + +export const steps: IStepConfig = { + 'initialize-hardhat-project': { + action: 'runCommand', + }, + 'install-hh-zksync': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + }, + 'import-zksync-config': { + action: 'modifyFile', + filepath: 'tests-output/hardhat-project/hardhat.config.ts', + atLine: 3, + }, + 'run-hh-node': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + preCommand: "bun pm2 start '' --name hh-zknode", + }, + 'wait-for-hh-node': { + action: 'wait', + timeout: 7000, + }, + 'test-hh-node': { + action: 'checkIfBalanceIsZero', + networkUrl: 'http://127.0.0.1:8011', + address: '0xe2b8Cb53a43a56d4d2AB6131C81Bd76B86D3AFe5', + }, + 'zksync-hh-network': { + action: 'modifyFile', + filepath: 'tests-output/hardhat-project/hardhat.config.ts', + atLine: 7, + }, + 'install-chai-ethers': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + }, + 'import-chai-matchers': { + action: 'modifyFile', + filepath: 'tests-output/hardhat-project/hardhat.config.ts', + atLine: 4, + }, + 'compare-config': { + action: 'compareToFile', + filepath: 'tests-output/hardhat-project/hardhat.config.ts', + }, + 'rename-greeter-file': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + }, + 'create-greeter-contract': { + action: 'writeToFile', + filepath: 'tests-output/hardhat-project/contracts/Greeter.sol', + }, + 'rename-test-file': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + }, + 'create-test': { + action: 'writeToFile', + filepath: 'tests-output/hardhat-project/test/test.ts', + }, + 'run-test': { + action: 'runCommand', + commandFolder: 'tests-output/hardhat-project', + }, +}; diff --git a/tests/erc20-paymaster.spec.ts b/tests/erc20-paymaster.spec.ts new file mode 100644 index 0000000..358c2d7 --- /dev/null +++ b/tests/erc20-paymaster.spec.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { setupAndRunTest } from './utils/runTest'; + +test('Build an ERC20 custom paymaster', async ({ page, context }) => { + await setupAndRunTest( + page, + context, + 'custom-paymaster-tutorial', + ['http://localhost:3000/tutorials/erc20-paymaster'], + 'erc20-paymaster' + ); +}); diff --git a/tests/how-to-test-contracts.spec.ts b/tests/how-to-test-contracts.spec.ts index 94e2944..262b008 100644 --- a/tests/how-to-test-contracts.spec.ts +++ b/tests/how-to-test-contracts.spec.ts @@ -1,16 +1,12 @@ import { test } from '@playwright/test'; -import { setupFolders, stopServers, startLocalServer } from './utils/setup'; -import { runTest } from './utils/runTest'; +import { setupAndRunTest } from './utils/runTest'; -test('how-to-test-contracts-with-hardhat', async ({ page, context }) => { - // SETUP - await startLocalServer(page); - await context.grantPermissions(['clipboard-read', 'clipboard-write']); - await setupFolders('hardhat-test-example'); - - // TEST - await runTest(page, 'http://localhost:3000/tutorials/how-to-test-contracts'); - - // SHUT DOWN ANY RUNNING PROJECTS - stopServers(); +test('How to test smart contracts with Hardhat', async ({ page, context }) => { + await setupAndRunTest( + page, + context, + 'hardhat-project', + ['http://localhost:3000/tutorials/how-to-test-contracts'], + 'how-to-test-contracts' + ); }); diff --git a/tests/utils/files.ts b/tests/utils/files.ts index c4be919..c22b96a 100644 --- a/tests/utils/files.ts +++ b/tests/utils/files.ts @@ -15,10 +15,21 @@ export async function modifyFile( addSpacesBefore?: number, addSpacesAfter?: number, atLine?: number, - removeLines?: string, - useSetData?: string + removeLines?: number[], + useSetData?: string, + deploymentFilePath?: string ) { let contentText = useSetData; + + if (deploymentFilePath) { + const contractId = getContractId(deploymentFilePath); + if (contentText?.includes('<*GET_CONTRACT_ID*>')) { + contentText = contentText.replace('<*GET_CONTRACT_ID*>', contractId); + } else { + contentText = contractId; + } + } + if (!contentText) { contentText = await clickCopyButton(page, buttonName); } @@ -31,9 +42,8 @@ export async function modifyFile( } else { const lines = readFileSync(filePath, 'utf8').split('\n'); if (removeLines) { - const removeLinesArray = JSON.parse(removeLines); - removeLinesArray.forEach((lineNumber: string) => { - lines[Number.parseInt(lineNumber) - 1] = '~~~REMOVE~~~'; + removeLines.forEach((lineNumber: number) => { + lines[lineNumber - 1] = '~~~REMOVE~~~'; }); } if (atLine) { @@ -66,3 +76,9 @@ export function compareOutputs(expected: string, actual: string) { expect(trimmedLineA).toEqual(trimmedLineB); }); } + +function getContractId(deploymentFilePath: string) { + const deploymentFile = readFileSync(deploymentFilePath, { encoding: 'utf8' }); + const json = JSON.parse(deploymentFile); + return json.entries[0].address; +} diff --git a/tests/utils/getTestActions.ts b/tests/utils/getTestActions.ts index aba344b..1d303e4 100644 --- a/tests/utils/getTestActions.ts +++ b/tests/utils/getTestActions.ts @@ -1,7 +1,7 @@ import { type Page, expect } from '@playwright/test'; export async function getTestActions(page: Page) { - const testActions = await page.$$eval('span[data-name]', (elements: Element[]) => { + const testActions = await page.$$eval('span[data-test-action]', (elements: Element[]) => { return elements.map((el) => { const dataAttributes: { [key: string]: string; diff --git a/tests/utils/runCommand.ts b/tests/utils/runCommand.ts index 0f70ea5..1dae351 100644 --- a/tests/utils/runCommand.ts +++ b/tests/utils/runCommand.ts @@ -3,17 +3,21 @@ import { execSync } from 'node:child_process'; import { clickCopyButton } from './button'; import fs from 'fs'; import { join } from 'path'; +import os from 'os'; +import pty from 'node-pty'; export async function runCommand( page: Page, buttonName: string, goToFolder: string = 'tests-output', projectFolder: string = 'hardhat-project', - preCommand?: string + preCommand?: string, + useSetCommand?: string, + prompts?: string ) { const copied = await clickCopyButton(page, buttonName); console.log('COPIED', copied); - let command = copied; + let command = useSetCommand ?? copied; const newHardhatProject = command.includes('npx hardhat init'); if (newHardhatProject) { @@ -31,7 +35,11 @@ export async function runCommand( command = `cd ${goToFolder} && ${command}`; } - run(command); + if (prompts) { + await runWithPrompts(page, command, prompts); + } else { + run(command); + } } } @@ -77,3 +85,35 @@ function copyFolder(source: string, destination: string) { copyRecursive(source, destination); } + +export async function runWithPrompts(page: Page, command: string, prompts: string) { + const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash'; + + const ptyProcess = pty.spawn(shell, [], { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.cwd(), + env: process.env, + }); + + const promptsArray = prompts.split('|').map((pair) => { + const [prompt, answer] = pair.split(':'); + return { prompt, answer }; + }); + + ptyProcess.onData((data) => { + process.stdout.write(data); + + for (let index = 0; index < promptsArray.length; index++) { + const promptObject = promptsArray[index]; + if (data.includes(promptObject.prompt)) { + ptyProcess.write(promptObject.answer + '\r'); + } + } + }); + + ptyProcess.write(command + '\r'); + + await page.waitForTimeout(5000); +} diff --git a/tests/utils/runTest.ts b/tests/utils/runTest.ts index 90ef725..ba3c81c 100644 --- a/tests/utils/runTest.ts +++ b/tests/utils/runTest.ts @@ -1,12 +1,38 @@ -import type { Page } from '@playwright/test'; +import type { BrowserContext, Page } from '@playwright/test'; import { runCommand } from './runCommand'; import { getTestActions } from './getTestActions'; import { visit } from './visit'; import { compareToFile, modifyFile, writeToFile } from './files'; import { checkIfBalanceIsZero } from './queries'; +import { setupFolders, startLocalServer, stopServers } from './setup'; +import { getConfig } from '../configs/config'; +import type { IStepConfig } from './types'; -export async function runTest(page: Page, url: string) { +export async function setupAndRunTest( + page: Page, + context: BrowserContext, + folderName: string, + pageUrls: string[], + tutorialName: string +) { + // SETUP + await startLocalServer(page); + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await setupFolders(folderName); + + const config = getConfig(tutorialName); + + // TEST + for (const pageUrl of pageUrls) { + await runTest(page, pageUrl, config!); + } + + // SHUT DOWN ANY RUNNING PROJECTS + stopServers(); +} + +export async function runTest(page: Page, url: string, config: IStepConfig) { await visit(page, url); console.log('GETTING TEST ACTIONS'); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -16,42 +42,47 @@ export async function runTest(page: Page, url: string) { for (const step of steps) { console.log('STEP:', step); await page.waitForTimeout(1000); - switch (step['data-name']) { + const stepID = step['id']; + const stepData = config[stepID]; + switch (stepData.action) { case 'runCommand': await runCommand( page, - step.id, - step['data-command-folder'], - step['data-project-folder'], - step['data-pre-command'] + stepID, + stepData.commandFolder, + stepData.projectFolder, + stepData.preCommand, + stepData.useSetCommand, + stepData.prompts ); break; case 'wait': - await page.waitForTimeout(Number.parseInt(step['data-timeout'])); + await page.waitForTimeout(stepData.timeout); break; case 'writeToFile': - await writeToFile(page, step.id, step['data-filepath']); + await writeToFile(page, stepID, stepData.filepath); break; case 'modifyFile': await modifyFile( page, - step.id, - step['data-filepath'], - Number.parseInt(step['data-add-spaces-before']), - step['data-add-spaces-after'], - Number.parseInt(step['data-at-line']), - step['data-remove-lines'], - step['data-use-set-data'] + stepID, + stepData.filepath, + stepData.addSpacesBefore, + stepData.addSpacesAfter, + stepData.atLine, + stepData.removeLines, + stepData.useSetData, + stepData.getContractId ); break; case 'compareToFile': - await compareToFile(page, step.id, step['data-filepath']); + await compareToFile(page, stepID, stepData.filepath); break; case 'checkIfBalanceIsZero': - await checkIfBalanceIsZero(step['data-network-url'], step['data-address']); + await checkIfBalanceIsZero(stepData.networkUrl, stepData.address); break; default: - console.log('STEP NOT FOUND:', step); + console.log('STEP NOT FOUND:', stepData); } } } diff --git a/tests/utils/types.ts b/tests/utils/types.ts new file mode 100644 index 0000000..3fa7f34 --- /dev/null +++ b/tests/utils/types.ts @@ -0,0 +1,46 @@ +export interface IStepConfig { + [key: string]: IStep; +} + +export type IStep = IRunCommand | IWait | IWriteToFile | IModifyFile | ICompareToFile | ICheckIfBalanceIsZero; + +export interface IRunCommand { + action: 'runCommand'; + commandFolder?: string; + projectFolder?: string; + preCommand?: string; + useSetCommand?: string; + prompts?: string; +} + +export interface IWait { + action: 'wait'; + timeout: number; +} + +export interface IWriteToFile { + action: 'writeToFile'; + filepath: string; +} + +export interface IModifyFile { + action: 'modifyFile'; + filepath: string; + addSpacesBefore?: number; + addSpacesAfter?: number; + atLine?: number; + removeLines?: number[]; + useSetData?: string; + getContractId?: string; +} + +export interface ICompareToFile { + action: 'compareToFile'; + filepath: string; +} + +export interface ICheckIfBalanceIsZero { + action: 'checkIfBalanceIsZero'; + networkUrl: string; + address: string; +}