From 3398daeb17dc8a6dfcca13d9fde4cb36f8a13ee9 Mon Sep 17 00:00:00 2001 From: Sarah Schwartz <58856580+sarahschwartz@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:18:19 -0300 Subject: [PATCH] docs: test daily spend limit tutorial (#56) adds a test for the daily spend limit tutorial and minor fixes --- .github/workflows/playwright.yaml | 6 + .../daily-spend-limit-account/10.index.md | 166 +++++++++++++++--- tests/configs/config.ts | 4 + tests/configs/daily-spend-limit.ts | 148 ++++++++++++++++ tests/daily-spend-limit.spec.ts | 12 ++ tests/erc20-paymaster.spec.ts | 8 +- tests/how-to-test-contracts.spec.ts | 8 +- tests/utils/files.ts | 8 + tests/utils/runCommand.ts | 54 ++++-- tests/utils/runTest.ts | 22 ++- tests/utils/types.ts | 39 +++- 11 files changed, 424 insertions(+), 51 deletions(-) create mode 100644 tests/configs/daily-spend-limit.ts create mode 100644 tests/daily-spend-limit.spec.ts diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index cda3d60..b6fcc09 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -12,6 +12,7 @@ jobs: tutorial: - "tests/erc20-paymaster.spec.ts" - "tests/how-to-test-contracts.spec.ts" + - "tests/daily-spend-limit.spec.ts" steps: - uses: actions/checkout@v4 @@ -21,6 +22,11 @@ jobs: - uses: actions/setup-node@v4 - name: Install Playwright Browsers run: bun playwright install chromium --with-deps + - name: Install Era Test Node + run: | + curl --proto '=https' -sSf https://raw.githubusercontent.com/matter-labs/era-test-node/main/scripts/install.sh > install.sh + chmod +x install.sh + sudo ./install.sh - name: Run test for ${{ matrix.tutorial }} run: | export TERM=xterm-256color diff --git a/content/tutorials/daily-spend-limit-account/10.index.md b/content/tutorials/daily-spend-limit-account/10.index.md index a6a5b5a..0e456b0 100644 --- a/content/tutorials/daily-spend-limit-account/10.index.md +++ b/content/tutorials/daily-spend-limit-account/10.index.md @@ -42,14 +42,19 @@ with the smart contracts in this project. 1. Initiate a new project by running the command: + :test-action{actionId="initialize-project"} + ```sh npx zksync-cli create custom-spendlimit-tutorial --template hardhat_solidity ``` - This creates a new ZKsync Era project called `custom-spendlimit-tutorial` with a with a few example contracts. + This creates a new ZKsync Era project called `custom-spendlimit-tutorial` with a few example contracts. 1. Navigate into the project directory: + :test-action{actionId="wait-for-init"} + :test-action{actionId="move-into-project"} + ```sh cd custom-spendlimit-tutorial ``` @@ -57,6 +62,8 @@ with the smart contracts in this project. 1. For the purposes of this tutorial, we don't need the example contracts related files. So, proceed by removing all the files inside the `/contracts` and `/deploy` folders manually or by running the following commands: + :test-action{actionId="delete-template-files"} + ```sh rm -rf ./contracts/* rm -rf ./deploy/* @@ -64,10 +71,22 @@ with the smart contracts in this project. 1. Add the ZKsync and OpenZeppelin contract libraries: - ```sh - yarn add -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 + :test-action{actionId="install-deps"} + + ::code-group + + ```bash [npm] + npm install -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 @matterlabs/hardhat-zksync-deploy@1.3.0 + ``` + + ```bash [yarn] + yarn add -D @matterlabs/zksync-contracts @openzeppelin/contracts@4.9.5 @matterlabs/hardhat-zksync-deploy@1.3.0 ``` + :: + + :test-action{actionId="wait-for-install"} + ::callout{icon="i-heroicons-exclamation-triangle"} This project does not use the latest version available of `@openzeppelin/contracts`. Make sure you install the specific version mentioned above. @@ -76,6 +95,8 @@ with the smart contracts in this project. 1. Include the `isSystem: true` setting in the `zksolc` section of the `hardhat.config.ts` configuration file to allow interaction with system contracts: + :test-action{actionId="hardhat-config"} + ```typescript [hardhat.config.ts] import { HardhatUserConfig } from "hardhat/config"; @@ -117,6 +138,9 @@ with the smart contracts in this project. export default config; ``` +:test-action{actionId="start-local-node"} +:test-action{actionId="deploy-to-local-node"} + ## Design Now let’s dive into the design and implementation of the daily spending limit feature. @@ -337,6 +361,12 @@ limit.available -= _amount; 1. In the folder `contracts`, add a file called `SpendLimit.sol` + :test-action{actionId="add-spend-limit-file"} + + ```sh + touch contracts/SpendLimit.sol + ``` + 1. Copy/paste the complete code below. ::callout{icon="i-heroicons-exclamation-triangle"} @@ -345,6 +375,9 @@ limit.available -= _amount; forget to change the value before deploying the contract. :: + :test-action{actionId="open-spend-limit-code-panel"} + :test-action{actionId="spend-limit-contract"} + ::drop-panel ::panel{label="SpendLimit.sol"} @@ -481,6 +514,12 @@ The main difference is that our account has a single signer. 1. Create a file `Account.sol` in the `contracts` folder. + :test-action{actionId="account-contract-file"} + + ```sh + touch contracts/Account.sol + ``` + 2. Copy/paste the code below. The account implements the [IAccount](https://docs.zksync.io/build/developer-reference/account-abstraction/design#iaccount-interface) interface @@ -491,6 +530,9 @@ Since we are building an account with signers, we should also implement The `isValidSignature` method will take care of verifying the signature and making sure the extracted address matches with the owner of the account. +:test-action{actionId="open-account-code-panel"} +:test-action{actionId="add-account-contract"} + ::drop-panel ::panel{label="Account.sol"} @@ -749,7 +791,17 @@ bytes from transaction calldata. The `AAFactory.sol` contract is responsible for deploying instances of the `Account.sol` contract. -1. Create the `AAFactory.sol` file in the `contracts` folder and copy/paste the code below. +1. Create the `AAFactory.sol` file in the `contracts` folder. + + :test-action{actionId="aa-factory-file"} + + ```sh + touch contracts/AAFactory.sol + ``` + +1. Copy/paste the code below. + +:test-action{actionId="aa-factory-contract"} ```solidity [AAFactory.sol] // SPDX-License-Identifier: MIT @@ -795,16 +847,35 @@ contract AAFactory { 1. Compile the contracts from the project root. - ```sh - yarn hardhat compile + :test-action{actionId="compile"} + + ::code-group + + ```bash [npm] + npm run compile + ``` + + ```bash [yarn] + yarn compile ``` -1. Create a file named `deploy/deployFactoryAccount.ts`. Then, copy and paste the following code into it. Remember to + :: + +1. Create a file named `deploy/deploy.ts`. Then, copy and paste the following code into it. Remember to add your `DEPLOYER_PRIVATE_KEY` to the .env file. + :test-action{actionId="create-deploy-factory-script-file"} + + ```sh + touch deploy/deploy.ts + ``` + The script deploys the factory, creates a new smart contract account, and funds it with some ETH. - ```typescript [deploy/deployFactoryAccount.ts] + :test-action{actionId="modify-env-file"} + :test-action{actionId="deploy-factory-script"} + + ```typescript [deploy/deploy.ts] import { utils, Wallet, Provider } from "zksync-ethers"; import * as ethers from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; @@ -818,7 +889,7 @@ contract AAFactory { export default async function (hre: HardhatRuntimeEnvironment) { // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local - const provider = new Provider(hre.config.networks.zkSyncSepoliaTestnet.url); + const provider = new Provider(hre.network.config.url); const wallet = new Wallet(DEPLOYER_PRIVATE_KEY, provider); const deployer = new Deployer(hre, wallet); const factoryArtifact = await deployer.loadArtifact("AAFactory"); @@ -864,10 +935,20 @@ contract AAFactory { 1. Run the script. - ```sh - yarn hardhat deploy-zksync --script deployFactoryAccount.ts + :test-action{actionId="run-deploy-script"} + + ::code-group + + ```bash [npm] + npm run deploy + ``` + + ```bash [yarn] + yarn deploy ``` + :: + You should see the following: ```txt @@ -886,12 +967,24 @@ address in order to track transactions and changes in the balance. 1. Create the file `setLimit.ts` in the `deploy` folder and copy/paste the example code below. + :test-action{actionId="wait-for-script"} + :test-action{actionId="create-set-limit-script"} + + ```sh + touch deploy/setLimit.ts + ``` + 1. Replace `` and `` with the output from the previous section in your `.env` file. + :test-action{actionId="get-deployed-account-address"} + :test-action{actionId="get-private-key"} + To enable the daily spending limit, we execute the `setSpendingLimit` function with two parameters: token address and limit amount. The token address is `ETH_ADDRESS` and the limit parameter is `0.0005` in the example below (and can be any amount). + :test-action{actionId="set-limit-script"} + ```typescript [setLimit.ts] import { utils, Wallet, Provider, Contract, EIP712Signer, types } from "zksync-ethers"; import * as ethers from "ethers"; @@ -908,7 +1001,7 @@ address in order to track transactions and changes in the balance. export default async function (hre: HardhatRuntimeEnvironment) { // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local - const provider = new Provider(hre.config.networks.zkSyncSepoliaTestnet.url); + const provider = new Provider(hre.network.config.url); const owner = new Wallet(DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY, provider); @@ -956,10 +1049,20 @@ address in order to track transactions and changes in the balance. 1. Run the script. - ```sh + :test-action{actionId="run-set-limit-script"} + + ::code-group + + ```bash [npx] + npx hardhat deploy-zksync --script setLimit.ts + ``` + + ```bash [yarn] yarn hardhat deploy-zksync --script setLimit.ts ``` + :: + You should see the following output: ```text @@ -977,10 +1080,19 @@ Let's test the `SpendLimit` contract works to make it refuse ETH transfers that 1. Create `transferETH.ts` and copy/paste the example code below, replacing the placeholder constants as before and adding an account address for `RECEIVER_ACCOUNT`. - ```typescript [transferETH.ts] + :test-action{actionId="add-receiver-account"} + :test-action{actionId="create-transfer-script"} + + ```sh + touch deploy/transferETH.ts + ``` + + :test-action{actionId="add-transfer-script"} + + ```typescript [deploy/transferETH.ts] import { utils, Wallet, Provider, Contract, EIP712Signer, types } from "zksync-ethers"; import * as ethers from "ethers"; - import { HardhatRuntimeEnvironment } from "hardhat/types"; + import type { HardhatRuntimeEnvironment } from "hardhat/types"; // load env file import dotenv from "dotenv"; @@ -994,7 +1106,7 @@ Let's test the `SpendLimit` contract works to make it refuse ETH transfers that export default async function (hre: HardhatRuntimeEnvironment) { // @ts-ignore target zkSyncSepoliaTestnet in config file which can be testnet or local - const provider = new Provider(hre.config.networks.zkSyncSepoliaTestnet.url); + const provider = new Provider(hre.network.config.url); const owner = new Wallet(DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY, provider); @@ -1034,11 +1146,13 @@ Let's test the `SpendLimit` contract works to make it refuse ETH transfers that console.log("Available today: ", limitData.available.toString()); // L1 timestamp tends to be undefined in latest blocks. So it should find the latest L1 Batch first. - let l1BatchRange = await provider.getL1BatchBlockRange(await provider.getL1BatchNumber()); - let l1TimeStamp = (await provider.getBlock(l1BatchRange[1])).l1BatchTimestamp; + if (hre.network.config.ethNetwork !== 'localhost') { + let l1BatchRange = await provider.getL1BatchBlockRange(await provider.getL1BatchNumber()); + let l1TimeStamp = (await provider.getBlock(l1BatchRange[1])).l1BatchTimestamp; - console.log("L1 timestamp: ", l1TimeStamp); - console.log("Limit will reset on timestamp: ", limitData.resetTime.toString()); + console.log('L1 timestamp: ', l1TimeStamp); + console.log('Limit will reset on timestamp: ', limitData.resetTime.toString()); + } // actually do the ETH transfer console.log("Sending ETH transfer from smart contract account"); @@ -1064,10 +1178,20 @@ Let's test the `SpendLimit` contract works to make it refuse ETH transfers that 1. Run the script to attempt to make a transfer. - ```shell + :test-action{actionId="run-transfer-script"} + + ::code-group + + ```bash [npx] + npx hardhat deploy-zksync --script transferETH.ts + ``` + + ```bash [yarn] yarn hardhat deploy-zksync --script transferETH.ts ``` + :: + You should see an error message with the following content so we know it failed because the amount exceeded the limit. ```shell diff --git a/tests/configs/config.ts b/tests/configs/config.ts index 420c697..925f8f0 100644 --- a/tests/configs/config.ts +++ b/tests/configs/config.ts @@ -1,5 +1,6 @@ import { steps as erc20PaymasterSteps } from './erc20-paymaster'; import { steps as howToTestContractsSteps } from './how-to-test-contracts'; +import { steps as dailySpendLimitSteps } from './daily-spend-limit'; export function getConfig(tutorialName: string) { let steps; @@ -10,6 +11,9 @@ export function getConfig(tutorialName: string) { case 'how-to-test-contracts': steps = howToTestContractsSteps; break; + case 'daily-spend-limit': + steps = dailySpendLimitSteps; + break; default: break; } diff --git a/tests/configs/daily-spend-limit.ts b/tests/configs/daily-spend-limit.ts new file mode 100644 index 0000000..8f42cc2 --- /dev/null +++ b/tests/configs/daily-spend-limit.ts @@ -0,0 +1,148 @@ +import type { IStepConfig } from '../utils/types'; + +export const steps: IStepConfig = { + 'initialize-project': { + action: 'runCommand', + prompts: 'Private key of the wallet: |npm: ', + }, + 'wait-for-init': { + action: 'wait', + timeout: 15000, + }, + 'move-into-project': { + action: 'runCommand', + }, + 'delete-template-files': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'install-deps': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'wait-for-install': { + action: 'wait', + timeout: 5000, + }, + 'hardhat-config': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/hardhat.config.ts', + }, + 'start-local-node': { + action: 'runCommand', + useSetCommand: "bun pm2 start 'era_test_node run' --name era-test-node", + }, + 'deploy-to-local-node': { + action: 'modifyFile', + filepath: 'tests-output/custom-spendlimit-tutorial/hardhat.config.ts', + atLine: 9, + removeLines: [9], + useSetData: ' defaultNetwork: "inMemoryNode",', + }, + 'add-spend-limit-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'open-spend-limit-code-panel': { + action: 'clickButtonByText', + buttonText: 'SpendLimit.sol', + }, + 'spend-limit-contract': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/contracts/SpendLimit.sol', + }, + 'account-contract-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'open-account-code-panel': { + action: 'clickButtonByText', + buttonText: 'Account.sol', + }, + 'add-account-contract': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/contracts/Account.sol', + }, + 'aa-factory-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'aa-factory-contract': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/contracts/AAFactory.sol', + }, + compile: { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'create-deploy-factory-script-file': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'modify-env-file': { + action: 'modifyFile', + filepath: 'tests-output/custom-spendlimit-tutorial/.env', + useSetData: 'WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110', + atLine: 1, + removeLines: [1], + }, + 'deploy-factory-script': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/deploy/deploy.ts', + }, + 'run-deploy-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + saveOutput: 'tests-output/custom-spendlimit-tutorial/deploy-outputs.txt', + }, + 'wait-for-script': { + action: 'wait', + timeout: 5000, + }, + 'create-set-limit-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'get-deployed-account-address': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/custom-spendlimit-tutorial/deploy-outputs.txt', + regex: /(?<=SC Account deployed on address\s)0x[a-fA-F0-9]{40}(?![\s\S]*SC Account deployed on address)/, + variableName: 'DEPLOYED_ACCOUNT_ADDRESS', + envFilepath: 'tests-output/custom-spendlimit-tutorial/.env', + }, + 'get-private-key': { + action: 'extractDataToEnv', + dataFilepath: 'tests-output/custom-spendlimit-tutorial/deploy-outputs.txt', + regex: /0x[a-fA-F0-9a-zA-Z]{64,}/g, + variableName: 'DEPLOYED_ACCOUNT_OWNER_PRIVATE_KEY', + envFilepath: 'tests-output/custom-spendlimit-tutorial/.env', + }, + 'set-limit-script': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/deploy/setLimit.ts', + }, + 'run-set-limit-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + checkForOutput: 'Account limit: 500000000000000', + }, + 'create-transfer-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + }, + 'add-transfer-script': { + action: 'writeToFile', + filepath: 'tests-output/custom-spendlimit-tutorial/deploy/transferETH.ts', + }, + 'add-receiver-account': { + action: 'modifyFile', + filepath: 'tests-output/custom-spendlimit-tutorial/.env', + useSetData: 'RECEIVER_ACCOUNT=0x36615Cf349d7F6344891B1e7CA7C72883F5dc049', + atLine: 4, + }, + 'run-transfer-script': { + action: 'runCommand', + commandFolder: 'tests-output/custom-spendlimit-tutorial', + expectError: 'execution reverted', + }, +}; diff --git a/tests/daily-spend-limit.spec.ts b/tests/daily-spend-limit.spec.ts new file mode 100644 index 0000000..13303e0 --- /dev/null +++ b/tests/daily-spend-limit.spec.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { setupAndRunTest } from './utils/runTest'; + +test('Daily Spending Limit Account', async ({ page, context }) => { + await setupAndRunTest( + page, + context, + 'custom-spendlimit-tutorial', + ['/daily-spend-limit-account'], + 'daily-spend-limit' + ); +}); diff --git a/tests/erc20-paymaster.spec.ts b/tests/erc20-paymaster.spec.ts index 358c2d7..787bd03 100644 --- a/tests/erc20-paymaster.spec.ts +++ b/tests/erc20-paymaster.spec.ts @@ -2,11 +2,5 @@ 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' - ); + await setupAndRunTest(page, context, 'custom-paymaster-tutorial', ['/erc20-paymaster'], 'erc20-paymaster'); }); diff --git a/tests/how-to-test-contracts.spec.ts b/tests/how-to-test-contracts.spec.ts index 262b008..9c0ed7f 100644 --- a/tests/how-to-test-contracts.spec.ts +++ b/tests/how-to-test-contracts.spec.ts @@ -2,11 +2,5 @@ import { test } from '@playwright/test'; import { setupAndRunTest } from './utils/runTest'; 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' - ); + await setupAndRunTest(page, context, 'hardhat-project', ['/how-to-test-contracts'], 'how-to-test-contracts'); }); diff --git a/tests/utils/files.ts b/tests/utils/files.ts index c22b96a..10e394c 100644 --- a/tests/utils/files.ts +++ b/tests/utils/files.ts @@ -82,3 +82,11 @@ function getContractId(deploymentFilePath: string) { const json = JSON.parse(deploymentFile); return json.entries[0].address; } + +export function extractDataToEnv(dataFilepath: string, envFilepath: string, regex: RegExp, variableName: string) { + const file = readFileSync(dataFilepath, { encoding: 'utf8' }); + const regexMatches = file.match(regex); + const data = regexMatches?.[0]; + console.log('DATA FROM REGEX:', data); + appendFileSync(envFilepath, `${variableName}=${data}\n`); +} diff --git a/tests/utils/runCommand.ts b/tests/utils/runCommand.ts index 32e342b..46983cd 100644 --- a/tests/utils/runCommand.ts +++ b/tests/utils/runCommand.ts @@ -1,10 +1,11 @@ import type { Page } from '@playwright/test'; -import { execSync } from 'node:child_process'; +import { exec } 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'; +import { expect } from '@playwright/test'; export async function runCommand( page: Page, @@ -13,7 +14,10 @@ export async function runCommand( projectFolder: string = 'hardhat-project', preCommand?: string, useSetCommand?: string, - prompts?: string + prompts?: string, + saveOutput?: string, + checkForOutput?: string, + expectError?: string ) { const copied = await clickCopyButton(page, buttonName); console.log('COPIED', copied); @@ -21,7 +25,7 @@ export async function runCommand( const newHardhatProject = command.includes('npx hardhat init'); if (newHardhatProject) { - createNewHHProject(goToFolder, projectFolder); + await createNewHHProject(goToFolder, projectFolder); } else { if (preCommand) { if (preCommand.includes('')) { @@ -38,25 +42,53 @@ export async function runCommand( if (prompts) { await runWithPrompts(page, command, prompts); } else { - run(command); + await run(command, saveOutput, checkForOutput, expectError); } } + await page.waitForTimeout(1500); } -function run(command: string) { - console.log('COMMAND', command); +async function run(command: string, saveOutput?: string, checkForOutput?: string, expectError?: string): Promise { + console.log('RUNNING COMMAND:', command); + + return new Promise((resolve, reject) => { + exec(command, { encoding: 'utf-8' }, (error, stdout, stderr) => { + console.log('EXPECT ERROR', expectError); + + if (error) { + if (expectError) { + const hasError = [error.message, stdout, stderr].some((message) => message.includes(expectError)); + console.log('HAS ERROR', hasError); + if (hasError) { + resolve(); + } else { + console.log('ERROR:', error); + reject(new Error('Unexpected error: ' + error.message)); + } + } else { + console.log('ERROR:', error); + reject(new Error('Unexpected error: ' + error.message)); + } + } else { + if (checkForOutput) { + expect(stdout).toContain(checkForOutput); + } + + if (saveOutput && stdout) { + fs.writeFileSync(saveOutput, stdout); + } - const commandOutput = execSync(command, { - encoding: 'utf-8', + resolve(); + } + }); }); - console.log('COMMAND OUTPUT', commandOutput); } -function createNewHHProject(goToFolder: string, projectFolder: string) { +async function createNewHHProject(goToFolder: string, projectFolder: string) { const repoDir = 'hardhat'; if (!fs.existsSync(join(goToFolder, repoDir))) { const command = `cd ${goToFolder} && git clone https://github.com/NomicFoundation/hardhat.git`; - run(command); + await run(command); } const folderToCopy = 'packages/hardhat-core/sample-projects/typescript'; diff --git a/tests/utils/runTest.ts b/tests/utils/runTest.ts index ba3c81c..27f9d4a 100644 --- a/tests/utils/runTest.ts +++ b/tests/utils/runTest.ts @@ -3,11 +3,12 @@ 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 { compareToFile, extractDataToEnv, modifyFile, writeToFile } from './files'; import { checkIfBalanceIsZero } from './queries'; import { setupFolders, startLocalServer, stopServers } from './setup'; import { getConfig } from '../configs/config'; import type { IStepConfig } from './types'; +import { clickButtonByText } from './button'; export async function setupAndRunTest( page: Page, @@ -25,7 +26,7 @@ export async function setupAndRunTest( // TEST for (const pageUrl of pageUrls) { - await runTest(page, pageUrl, config!); + await runTest(page, `http://localhost:3000/tutorials${pageUrl}`, config!); } // SHUT DOWN ANY RUNNING PROJECTS @@ -40,10 +41,14 @@ export async function runTest(page: Page, url: string, config: IStepConfig) { console.log('STARTING TEST'); for (const step of steps) { - console.log('STEP:', step); + console.log('STEP:', step.id); await page.waitForTimeout(1000); const stepID = step['id']; const stepData = config[stepID]; + if (!stepData) { + console.log('STEP DATA NOT FOUND:', stepID); + continue; + } switch (stepData.action) { case 'runCommand': await runCommand( @@ -53,7 +58,10 @@ export async function runTest(page: Page, url: string, config: IStepConfig) { stepData.projectFolder, stepData.preCommand, stepData.useSetCommand, - stepData.prompts + stepData.prompts, + stepData.saveOutput, + stepData.checkForOutput, + stepData.expectError ); break; case 'wait': @@ -81,6 +89,12 @@ export async function runTest(page: Page, url: string, config: IStepConfig) { case 'checkIfBalanceIsZero': await checkIfBalanceIsZero(stepData.networkUrl, stepData.address); break; + case 'extractDataToEnv': + extractDataToEnv(stepData.dataFilepath, stepData.envFilepath, stepData.regex, stepData.variableName); + break; + case 'clickButtonByText': + clickButtonByText(page, stepData.buttonText); + break; default: console.log('STEP NOT FOUND:', stepData); } diff --git a/tests/utils/types.ts b/tests/utils/types.ts index 3fa7f34..11986e6 100644 --- a/tests/utils/types.ts +++ b/tests/utils/types.ts @@ -2,15 +2,35 @@ export interface IStepConfig { [key: string]: IStep; } -export type IStep = IRunCommand | IWait | IWriteToFile | IModifyFile | ICompareToFile | ICheckIfBalanceIsZero; +export type IStep = + | IRunCommand + | IWait + | IWriteToFile + | IModifyFile + | ICompareToFile + | ICheckIfBalanceIsZero + | IExtractDataToEnv + | IClickButtonByText; export interface IRunCommand { action: 'runCommand'; + // the directory where it should run the command commandFolder?: string; + // what to name the project folder (only used for npx hardhat init) projectFolder?: string; + // add something before the command preCommand?: string; + // use this command instead of copying from the page useSetCommand?: string; + // if the command has prompts, pass them here + // ex: 'text in prompt 1:answer1|text in prompt 2:answer2' prompts?: string; + // save the output of a command to a given filepath + saveOutput?: string; + // check if the output of a command contains a given string + checkForOutput?: string; + // if the command is expected to fail, pass at least part of the error message here + expectError?: string; } export interface IWait { @@ -44,3 +64,20 @@ export interface ICheckIfBalanceIsZero { networkUrl: string; address: string; } + +export interface IExtractDataToEnv { + action: 'extractDataToEnv'; + // the file where the data is stored + dataFilepath: string; + // the filepath for the .env file to modify + envFilepath: string; + // the regex to match the data + regex: RegExp; + // the name of the env variable to store the data + variableName: string; +} + +export interface IClickButtonByText { + action: 'clickButtonByText'; + buttonText: string; +}