diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml
new file mode 100644
index 00000000..b0f110a6
--- /dev/null
+++ b/.github/workflows/playwright.yaml
@@ -0,0 +1,24 @@
+name: Playwright Tests
+
+on:
+ pull_request:
+
+jobs:
+ test-tutorials:
+ timeout-minutes: 20
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ tutorial:
+ - "tests/how-to-test-contracts.spec.ts"
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+ - name: Install Dependencies
+ run: bun install --frozen-lockfile
+ - uses: actions/setup-node@v4
+ - name: Install Playwright Browsers
+ run: bun playwright install chromium --with-deps
+ - name: Run test for ${{ matrix.tutorial }}
+ run: bun test:github ${{ matrix.tutorial }}
diff --git a/.gitignore b/.gitignore
index ec3ec23e..0d687db8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,9 @@ logs
.DS_Store
.fleet
.idea
+tests-output
+test-results
+playwright-report
# Local env files
.env
diff --git a/bun.lockb b/bun.lockb
index 37e6a8cd..11c0277e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/content/tutorials/how-to-test-contracts/10.index.md b/content/tutorials/how-to-test-contracts/10.index.md
index 11ba47a2..50578da8 100644
--- a/content/tutorials/how-to-test-contracts/10.index.md
+++ b/content/tutorials/how-to-test-contracts/10.index.md
@@ -12,7 +12,6 @@ To facilitate this process of running tests on the **ZKsync Era Test Node**, you
- Node.js installed (version 14.x or later)
- Either yarn or npm installed
-- Initialized Hardhat TypeScript project
## Era-test-node plugin
@@ -25,8 +24,20 @@ During the alpha phase, ZKsync Era Test Nodes are currently undergoing developme
### Installation
+First, initialize a new Hardhat TypeScript project:
+
+
+
+```bash
+npx hardhat init
+```
+
+Select the `Create a TypeScript project` option and install the sample project's dependencies `hardhat` and `@nomicfoundation/hardhat-toolbox`.
+
To install the `hardhat-zksync-node` plugin and additional necessary packages, execute the following command:
+
+
::code-group
```bash [npm]
@@ -41,6 +52,9 @@ yarn add -D @matterlabs/hardhat-zksync-node
Once installed, add the plugin at the top of your `hardhat.config.ts` file.
+
+
```ts [hardhat.config.ts]
import "@matterlabs/hardhat-zksync-node";
```
@@ -49,10 +63,12 @@ import "@matterlabs/hardhat-zksync-node";
You can now safely run the **ZKsync Era Test Node** with the following command:
+
+
::code-group
-```bash [npm]
-npm hardhat node-zksync
+```bash [npx]
+npx hardhat node-zksync
```
```bash [yarn]
@@ -61,6 +77,10 @@ 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**,
without further use of this command in the tutorial.
@@ -83,9 +103,17 @@ we can shut it down and continue with the tutorial.
### Integration with Hardhat
-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:
+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`:
+
+
-```json
+```ts
+zksolc: {
+ version: "latest",
+ },
networks: {
hardhat: {
zksync: true,
@@ -100,34 +128,45 @@ it's necessary to use the `hardhat-chai-matchers` plugin.
In the root directory of your project, execute this command:
+
+
::code-group
```bash [npm]
-npm i -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6 @nomiclabs/hardhat-ethers
+npm i -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6 @matterlabs/hardhat-zksync
```
```bash [yarn]
-yarn add -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6 @nomiclabs/hardhat-ethers
+yarn add -D @nomicfoundation/hardhat-chai-matchers chai@4.3.6 @matterlabs/hardhat-zksync
```
::
-After installing it, add the plugin at the top of your `hardhat.config.ts` file:
+After installing it, add the plugins at the top of your `hardhat.config.ts` file:
+
+
```ts [hardhat.config.ts]
-import "@nomicfoundation/hardhat-chai-matchers"
+import "@matterlabs/hardhat-zksync";
+import "@nomicfoundation/hardhat-chai-matchers";
```
## Smart contract example
To set up the environment for using chai matchers and writing tests, you'll need to create some contracts.
-Follow these steps:
-1. Navigate to the root of your project.
-1. Create a folder named **contracts**.
-1. Inside the **contracts** folder, create a file named **Greeter.sol**.
+Inside the **contracts** folder, rename the example contract file to **Greeter.sol**.
-Now we should add some code to the new **Greeter.sol** file:
+
+
+```bash
+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
@@ -158,42 +197,50 @@ contract Greeter {
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";
-
-import "@matterlabs/hardhat-zksync"
+import "@nomicfoundation/hardhat-toolbox";
+import "@matterlabs/hardhat-zksync-node";
+import "@matterlabs/hardhat-zksync";
import "@nomicfoundation/hardhat-chai-matchers";
const config: HardhatUserConfig = {
+ solidity: "0.8.24",
zksolc: {
version: "latest",
},
- solidity: "0.8.19",
networks: {
hardhat: {
zksync: true,
},
},
};
+
export default config;
```
-Here are the steps to create test cases with the `hardhat-chai-matchers` plugin:
+Now you can create a test with the `hardhat-chai-matchers` plugin:
+
+Inside the `/test` folder, renamefile named `test.ts`.
-1. Navigate to your project's root directory.
-1. Create a new folder named `/test`.
-1. Inside the `/test` folder, create a file named `test.ts`.
+
-Once you've completed these steps, you'll be ready to write your tests using the `hardhat-chai-matchers` plugin.
+```bash
+mv test/Lock.ts test/test.ts
+```
+
+Replace the old test with this example showcasing the functionalities of the contract:
-Here's a brief example showcasing the functionalities of the contract:
+
```typescript
import * as hre from "hardhat";
import { expect } from "chai";
import { Wallet, Provider, Contract } from "zksync-ethers";
import { Deployer } from "@matterlabs/hardhat-zksync";
-import { ZkSyncArtifact } from "@matterlabs/hardhat-zksync/src/types";
+import { ZkSyncArtifact } from "@matterlabs/hardhat-zksync-deploy/src/types";
const RICH_PRIVATE_KEY = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";
@@ -239,14 +286,16 @@ describe("Greeter", function () {
Execute the following command in your terminal to run the tests:
+
+
::code-group
-```bash [yarn]
-yarn hardhat test
+```bash [npx]
+npx hardhat test
```
-```bash [npm]
-npm hardhat test
+```bash [yarn]
+yarn hardhat test
```
::
diff --git a/cspell-config/cspell-zksync.txt b/cspell-config/cspell-zksync.txt
index 61c5b716..d13800fa 100644
--- a/cspell-config/cspell-zksync.txt
+++ b/cspell-config/cspell-zksync.txt
@@ -8,6 +8,7 @@ zkcast
ZKEVM
zkevm
zkforge
+zknode
zkout
zksolc
zkstack
diff --git a/package.json b/package.json
index f8c4e11a..85afd076 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
"lint:eslint": "eslint .",
"lint:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
+ "test:github": "playwright test",
+ "test:local": "playwright test --headed",
"prepare": "node .husky/install.mjs",
"postinstall": "nuxt prepare",
"ci:check": "bun run lint:eslint && bun run lint:prettier && bun run lint:spelling && bun run lint:markdown",
@@ -27,24 +29,29 @@
"@nuxt/ui-pro": "^1.1.0",
"@nuxtjs/seo": "^2.0.0-rc.10",
"dayjs": "^1.11.10",
+ "ethers": "^6.0.0",
"nuxt": "^3.11.2",
"nuxt-gtag": "^2.0.6",
"nuxt-headlessui": "^1.2.0",
"nuxt-og-image": "^3.0.0-rc.53",
"rehype-katex": "^7.0.0",
"remark-math": "^6.0.0",
- "vue-tsc": "^2.0.16"
+ "vue-tsc": "^2.0.16",
+ "zksync-ethers": "^6.10.0"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
+ "@playwright/test": "^1.45.2",
"@vue/test-utils": "^2.4.5",
"cspell": "^8.7.0",
"eslint": "^8.57.0",
+ "hardhat": "^2.22.6",
"husky": "^9.0.11",
"lint-staged": "^15.2.4",
"markdownlint": "^0.34.0",
"markdownlint-cli2": "^0.13.0",
+ "pm2": "^5.4.2",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"prettier-plugin-tailwindcss": "^0.5.14"
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..ca3ff957
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,80 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 1 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+ timeout: 10 * 60 * 1000,
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ // {
+ // name: 'firefox',
+ // use: { ...devices['Desktop Firefox'] },
+ // },
+
+ // {
+ // name: 'webkit',
+ // use: { ...devices['Desktop Safari'] },
+ // },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'bun run dev',
+ url: 'http://localhost:3000',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120 * 1000,
+ },
+});
diff --git a/tests/how-to-test-contracts.spec.ts b/tests/how-to-test-contracts.spec.ts
new file mode 100644
index 00000000..94e29447
--- /dev/null
+++ b/tests/how-to-test-contracts.spec.ts
@@ -0,0 +1,16 @@
+import { test } from '@playwright/test';
+import { setupFolders, stopServers, startLocalServer } from './utils/setup';
+import { runTest } 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();
+});
diff --git a/tests/utils/button.ts b/tests/utils/button.ts
new file mode 100644
index 00000000..5aeafae4
--- /dev/null
+++ b/tests/utils/button.ts
@@ -0,0 +1,14 @@
+import { type Page } from '@playwright/test';
+
+export async function clickButtonByText(page: Page, selector: string | RegExp) {
+ await page.locator('button').getByText(selector).click();
+}
+
+export async function clickCopyButton(page: Page, id: string) {
+ const buttonAriaLabel = 'Copy code to clipboard';
+ const selector = `//*[@id='${id}']//following::button[@aria-label='${buttonAriaLabel}'][1]`;
+ const button = page.locator(selector);
+ await button.click();
+ const rawText: string = await page.evaluate('navigator.clipboard.readText()');
+ return rawText;
+}
diff --git a/tests/utils/files.ts b/tests/utils/files.ts
new file mode 100644
index 00000000..c4be9197
--- /dev/null
+++ b/tests/utils/files.ts
@@ -0,0 +1,68 @@
+import { writeFileSync, appendFileSync, readFileSync } from 'node:fs';
+import { clickCopyButton } from './button';
+import { expect, type Page } from '@playwright/test';
+import { EOL } from 'os';
+
+export async function writeToFile(page: Page, buttonName: string, filePath: string) {
+ const content = await clickCopyButton(page, buttonName);
+ writeFileSync(filePath, `${content}\n\n`);
+}
+
+export async function modifyFile(
+ page: Page,
+ buttonName: string,
+ filePath: string,
+ addSpacesBefore?: number,
+ addSpacesAfter?: number,
+ atLine?: number,
+ removeLines?: string,
+ useSetData?: string
+) {
+ let contentText = useSetData;
+ if (!contentText) {
+ contentText = await clickCopyButton(page, buttonName);
+ }
+ contentText = contentText.trim().replace(/\u00A0/g, ' ');
+ const spacesBefore = addSpacesBefore ? '\n'.repeat(addSpacesBefore) : '';
+ const spacesAfter = addSpacesAfter ? '\n'.repeat(addSpacesAfter) : '';
+ if (!atLine && !removeLines) {
+ const finalContent = spacesBefore + contentText + spacesAfter;
+ appendFileSync(filePath, `${finalContent}\n\n`);
+ } 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~~~';
+ });
+ }
+ if (atLine) {
+ lines.splice(atLine - 1, 0, contentText);
+ }
+ let finalContent = lines.filter((line: string) => line !== '~~~REMOVE~~~').join('\n');
+ finalContent = spacesBefore + finalContent + spacesAfter;
+ writeFileSync(filePath, finalContent, 'utf8');
+ }
+}
+
+export async function compareToFile(page: Page, buttonName: string, pathName: string) {
+ const expected = await clickCopyButton(page, buttonName);
+ const actual = readFileSync(pathName, { encoding: 'utf8' });
+ compareOutputs(expected, actual);
+}
+
+export function compareOutputs(expected: string, actual: string) {
+ const split1 = expected.trim().split(EOL);
+ const split2 = actual.trim().split(EOL);
+ expect(split1.length === split2.length).toBeTruthy();
+ split1.forEach((line, i) => {
+ const trimmedLineA = line.trim().replace(/\u00A0/g, ' ');
+ const trimmedLineB = split2[i].trim().replace(/\u00A0/g, ' ');
+ if (trimmedLineA !== trimmedLineB) {
+ console.log('DIFFERENT LINES');
+ console.log('LINE A:', trimmedLineA);
+ console.log('LINE B:', trimmedLineB);
+ }
+ expect(trimmedLineA).toEqual(trimmedLineB);
+ });
+}
diff --git a/tests/utils/getTestActions.ts b/tests/utils/getTestActions.ts
new file mode 100644
index 00000000..aba344ba
--- /dev/null
+++ b/tests/utils/getTestActions.ts
@@ -0,0 +1,20 @@
+import { type Page, expect } from '@playwright/test';
+
+export async function getTestActions(page: Page) {
+ const testActions = await page.$$eval('span[data-name]', (elements: Element[]) => {
+ return elements.map((el) => {
+ const dataAttributes: {
+ [key: string]: string;
+ } = {};
+ const attributesArray = Array.from(el.attributes);
+ for (const attr of attributesArray) {
+ dataAttributes[attr.name] = attr.value;
+ }
+ dataAttributes['id'] = el.id;
+ return dataAttributes;
+ });
+ });
+ console.log('GOT TEST ACTIONS:', testActions);
+ expect(testActions.length).toBeGreaterThan(0);
+ return testActions;
+}
diff --git a/tests/utils/queries.ts b/tests/utils/queries.ts
new file mode 100644
index 00000000..0b309b3f
--- /dev/null
+++ b/tests/utils/queries.ts
@@ -0,0 +1,8 @@
+import { expect } from '@playwright/test';
+import { Provider } from 'zksync-ethers';
+
+export async function checkIfBalanceIsZero(networkUrl: string, address: string) {
+ const provider = new Provider(networkUrl);
+ const balance = await provider.getBalance(address);
+ expect(balance).toBeGreaterThan(0);
+}
diff --git a/tests/utils/runCommand.ts b/tests/utils/runCommand.ts
new file mode 100644
index 00000000..0f70ea57
--- /dev/null
+++ b/tests/utils/runCommand.ts
@@ -0,0 +1,79 @@
+import type { Page } from '@playwright/test';
+import { execSync } from 'node:child_process';
+import { clickCopyButton } from './button';
+import fs from 'fs';
+import { join } from 'path';
+
+export async function runCommand(
+ page: Page,
+ buttonName: string,
+ goToFolder: string = 'tests-output',
+ projectFolder: string = 'hardhat-project',
+ preCommand?: string
+) {
+ const copied = await clickCopyButton(page, buttonName);
+ console.log('COPIED', copied);
+ let command = copied;
+ const newHardhatProject = command.includes('npx hardhat init');
+
+ if (newHardhatProject) {
+ createNewHHProject(goToFolder, projectFolder);
+ } else {
+ if (preCommand) {
+ if (preCommand.includes('')) {
+ command = preCommand.replace('', copied);
+ } else {
+ command = preCommand + copied;
+ }
+ }
+
+ if (goToFolder) {
+ command = `cd ${goToFolder} && ${command}`;
+ }
+
+ run(command);
+ }
+}
+
+function run(command: string) {
+ console.log('COMMAND', command);
+
+ const commandOutput = execSync(command, {
+ encoding: 'utf-8',
+ });
+ console.log('COMMAND OUTPUT', commandOutput);
+}
+
+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);
+ }
+ const folderToCopy = 'packages/hardhat-core/sample-projects/typescript';
+
+ const sourceFolder = join(goToFolder, repoDir, folderToCopy);
+ const destinationFolder = join(goToFolder, projectFolder);
+ copyFolder(sourceFolder, destinationFolder);
+ const installCommand = `cd ${destinationFolder} && npm init -y && npm install --save-dev "hardhat@^2.22.6" "@nomicfoundation/hardhat-toolbox@^5.0.0" `;
+ run(installCommand);
+}
+
+function copyFolder(source: string, destination: string) {
+ fs.mkdirSync(destination, { recursive: true });
+
+ const copyRecursive = (src: string, dest: string) => {
+ if (fs.statSync(src).isDirectory()) {
+ if (!fs.existsSync(dest)) {
+ fs.mkdirSync(dest);
+ }
+ fs.readdirSync(src).forEach((item) => {
+ copyRecursive(join(src, item), join(dest, item));
+ });
+ } else {
+ fs.copyFileSync(src, dest);
+ }
+ };
+
+ copyRecursive(source, destination);
+}
diff --git a/tests/utils/runTest.ts b/tests/utils/runTest.ts
new file mode 100644
index 00000000..90ef725e
--- /dev/null
+++ b/tests/utils/runTest.ts
@@ -0,0 +1,57 @@
+import type { 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';
+
+export async function runTest(page: Page, url: string) {
+ await visit(page, url);
+ console.log('GETTING TEST ACTIONS');
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const steps: any[] = await getTestActions(page);
+
+ console.log('STARTING TEST');
+ for (const step of steps) {
+ console.log('STEP:', step);
+ await page.waitForTimeout(1000);
+ switch (step['data-name']) {
+ case 'runCommand':
+ await runCommand(
+ page,
+ step.id,
+ step['data-command-folder'],
+ step['data-project-folder'],
+ step['data-pre-command']
+ );
+ break;
+ case 'wait':
+ await page.waitForTimeout(Number.parseInt(step['data-timeout']));
+ break;
+ case 'writeToFile':
+ await writeToFile(page, step.id, step['data-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']
+ );
+ break;
+ case 'compareToFile':
+ await compareToFile(page, step.id, step['data-filepath']);
+ break;
+ case 'checkIfBalanceIsZero':
+ await checkIfBalanceIsZero(step['data-network-url'], step['data-address']);
+ break;
+ default:
+ console.log('STEP NOT FOUND:', step);
+ }
+ }
+}
diff --git a/tests/utils/setup.ts b/tests/utils/setup.ts
new file mode 100644
index 00000000..3fc5d3ae
--- /dev/null
+++ b/tests/utils/setup.ts
@@ -0,0 +1,44 @@
+import { execSync } from 'child_process';
+import fs from 'fs';
+import type { Page } from '@playwright/test';
+
+export async function startLocalServer(page: Page) {
+ console.log('STARTING...');
+ await page.waitForTimeout(15000);
+ console.log('WAITED 15 SECONDS FOR LOCAL SERVER TO START');
+}
+
+export function stopServers() {
+ const isRunning = checkIfServersRunning();
+ if (isRunning) {
+ console.log('STOPPING SERVERS');
+ // stop & delete pm2 servers
+ const STOP_SERVERS = 'bun pm2 delete all';
+ execSync(STOP_SERVERS, {
+ encoding: 'utf-8',
+ });
+ console.log('DONE STOPPING SERVERS');
+ }
+}
+
+export function checkIfServersRunning() {
+ try {
+ const output = execSync('bun pm2 list --no-color').toString();
+ return output.includes('online');
+ } catch (error) {
+ console.error('Error checking PM2 servers:', error);
+ return false;
+ }
+}
+
+export async function setupFolders(projectFolder: string) {
+ console.log('SETTING UP FOLDERS');
+ fs.mkdirSync('tests-output', { recursive: true });
+ const projectPath = `tests-output/${projectFolder}`;
+ if (fs.existsSync(projectPath)) {
+ await fs.promises.rm(projectPath, {
+ recursive: true,
+ force: true,
+ });
+ }
+}
diff --git a/tests/utils/visit.ts b/tests/utils/visit.ts
new file mode 100644
index 00000000..0363193a
--- /dev/null
+++ b/tests/utils/visit.ts
@@ -0,0 +1,9 @@
+import type { Page } from '@playwright/test';
+
+export async function visit(page: Page, pathname: string) {
+ console.log('GOING TO URL:', pathname);
+ await page.waitForTimeout(2000);
+ const pageFinal = await page.goto(`${pathname}`);
+ await page.waitForTimeout(2000);
+ return pageFinal;
+}