diff --git a/.changeset/serious-dogs-wash.md b/.changeset/serious-dogs-wash.md deleted file mode 100644 index 98709abc3cf..00000000000 --- a/.changeset/serious-dogs-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@fuel-ts/account": patch ---- - -chore: add browser testing infrastructure diff --git a/packages/account/src/test-utils/launchNode.ts b/packages/account/src/test-utils/launchNode.ts index 4f89ab09041..a2bf58f1dcb 100644 --- a/packages/account/src/test-utils/launchNode.ts +++ b/packages/account/src/test-utils/launchNode.ts @@ -3,6 +3,7 @@ import { randomBytes } from '@fuel-ts/crypto'; import type { SnapshotConfigs } from '@fuel-ts/utils'; import { defaultConsensusKey, hexlify, defaultSnapshotConfigs } from '@fuel-ts/utils'; import type { ChildProcessWithoutNullStreams } from 'child_process'; +import { spawn } from 'child_process'; import { randomUUID } from 'crypto'; import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; import os from 'os'; @@ -216,8 +217,6 @@ export const launchNode = async ({ snapshotDirToUse = tempDir; } - const { spawn } = await import('child_process'); - const child = spawn( command, [ diff --git a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts index e96963923ab..2e03d5fa8d0 100644 --- a/packages/account/src/test-utils/setup-test-provider-and-wallets.ts +++ b/packages/account/src/test-utils/setup-test-provider-and-wallets.ts @@ -23,7 +23,6 @@ export interface LaunchCustomProviderAndGetWalletsOptions { snapshotConfig: PartialDeep; } >; - launchNodeServerPort?: string; } const defaultWalletConfigOptions: WalletsConfigOptions = { @@ -53,7 +52,6 @@ export async function setupTestProviderAndWallets({ walletsConfig: walletsConfigOptions = {}, providerOptions, nodeOptions = {}, - launchNodeServerPort = process.env.LAUNCH_NODE_SERVER_PORT || undefined, }: Partial = {}): Promise { // @ts-expect-error this is a polyfill (see https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management) Symbol.dispose ??= Symbol('Symbol.dispose'); @@ -66,7 +64,7 @@ export async function setupTestProviderAndWallets({ } ); - const launchNodeOptions: LaunchNodeOptions = { + const { cleanup, url } = await launchNode({ loggingEnabled: false, ...nodeOptions, snapshotConfig: mergeDeepRight( @@ -74,25 +72,7 @@ export async function setupTestProviderAndWallets({ walletsConfig.apply(nodeOptions?.snapshotConfig) ), port: '0', - }; - - let cleanup: () => void; - let url: string; - if (launchNodeServerPort) { - const serverUrl = `http://localhost:${launchNodeServerPort}`; - url = await ( - await fetch(serverUrl, { method: 'POST', body: JSON.stringify(launchNodeOptions) }) - ).text(); - - cleanup = () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - fetch(`${serverUrl}/cleanup/${url}`); - }; - } else { - const settings = await launchNode(launchNodeOptions); - url = settings.url; - cleanup = settings.cleanup; - } + }); let provider: Provider; diff --git a/packages/fuel-gauge/src/call-test-contract.test.ts b/packages/fuel-gauge/src/call-test-contract.test.ts index e47cdfe0540..53eb58ab080 100644 --- a/packages/fuel-gauge/src/call-test-contract.test.ts +++ b/packages/fuel-gauge/src/call-test-contract.test.ts @@ -1,30 +1,27 @@ import { ASSET_A } from '@fuel-ts/utils/test-utils'; import type { Contract } from 'fuels'; import { BN, bn, toHex } from 'fuels'; -import { launchTestNode } from 'fuels/test-utils'; +import type { CallTestContractAbi } from '../test/typegen/contracts'; import { CallTestContractAbi__factory } from '../test/typegen/contracts'; -import bytecode from '../test/typegen/contracts/CallTestContractAbi.hex'; - -const setupContract = async () => { - const { - contracts: [contract], - cleanup, - } = await launchTestNode({ - contractsConfigs: [{ deployer: CallTestContractAbi__factory, bytecode }], - }); - return Object.assign(contract, { [Symbol.dispose]: cleanup }); -}; +import binHexlified from '../test/typegen/contracts/CallTestContractAbi.hex'; + +import { createSetupConfig } from './utils'; + +const setupContract = createSetupConfig({ + contractBytecode: binHexlified, + abi: CallTestContractAbi__factory.abi, + cache: true, +}); const U64_MAX = bn(2).pow(64).sub(1); /** * @group node - * @group browser */ describe('CallTestContract', () => { it.each([0, 1337, U64_MAX.sub(1)])('can call a contract with u64 (%p)', async (num) => { - using contract = await setupContract(); + const contract = await setupContract(); const { value } = await contract.functions.foo(num).call(); expect(value.toHex()).toEqual(bn(num).add(1).toHex()); }); @@ -37,14 +34,14 @@ describe('CallTestContract', () => { [{ a: false, b: U64_MAX.sub(1) }], [{ a: true, b: U64_MAX.sub(1) }], ])('can call a contract with structs (%p)', async (struct) => { - using contract = await setupContract(); + const contract = await setupContract(); const { value } = await contract.functions.boo(struct).call(); expect(value.a).toEqual(!struct.a); expect(value.b.toHex()).toEqual(bn(struct.b).add(1).toHex()); }); it('can call a function with empty arguments', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const { value: empty } = await contract.functions.empty().call(); expect(empty.toHex()).toEqual(toHex(63)); @@ -62,7 +59,7 @@ describe('CallTestContract', () => { }); it('function with empty return should resolve undefined', async () => { - using contract = await setupContract(); + const contract = await setupContract(); // Call method with no params but with no result and no value on config const { value } = await contract.functions.return_void().call(); @@ -139,9 +136,9 @@ describe('CallTestContract', () => { async (method, { values, expected }) => { // Type cast to Contract because of the dynamic nature of the test // But the function names are type-constrained to correct Contract's type - using contract = await setupContract(); + const contract = (await setupContract()) as Contract; - const { value } = await (contract as Contract).functions[method](...values).call(); + const { value } = await contract.functions[method](...values).call(); if (BN.isBN(value)) { expect(toHex(value)).toBe(toHex(expected)); @@ -152,7 +149,7 @@ describe('CallTestContract', () => { ); it('Forward amount value on contract call', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const baseAssetId = contract.provider.getBaseAssetId(); const { value } = await contract.functions .return_context_amount() @@ -164,7 +161,7 @@ describe('CallTestContract', () => { }); it('Forward asset_id on contract call', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const assetId = ASSET_A; const { value } = await contract.functions @@ -177,7 +174,7 @@ describe('CallTestContract', () => { }); it('Forward asset_id on contract simulate call', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const assetId = ASSET_A; const { value } = await contract.functions @@ -190,7 +187,7 @@ describe('CallTestContract', () => { }); it('can make multiple calls', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const num = 1337; const numC = 10; @@ -225,14 +222,14 @@ describe('CallTestContract', () => { }); it('Calling a simple contract function does only one dry run', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const dryRunSpy = vi.spyOn(contract.provider.operations, 'dryRun'); await contract.functions.no_params().call(); expect(dryRunSpy).toHaveBeenCalledOnce(); }); it('Simulating a simple contract function does two dry runs', async () => { - using contract = await setupContract(); + const contract = await setupContract(); const dryRunSpy = vi.spyOn(contract.provider.operations, 'dryRun'); await contract.functions.no_params().simulate(); diff --git a/packages/fuels/src/setup-launch-node-server.test.ts b/packages/fuels/src/setup-launch-node-server.test.ts deleted file mode 100644 index 92479b00e54..00000000000 --- a/packages/fuels/src/setup-launch-node-server.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Provider } from '@fuel-ts/account'; -import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; -import { spawn } from 'node:child_process'; - -import { launchTestNode } from './test-utils'; - -interface ServerInfo extends Disposable { - serverUrl: string; - closeServer: () => Promise; -} - -function startServer(port: number = 0): Promise { - return new Promise((resolve, reject) => { - const cp = spawn(`pnpm tsx packages/fuels/src/setup-launch-node-server.ts ${port}`, { - detached: true, - shell: 'sh', - }); - - const server = { - killed: false, - url: undefined as string | undefined, - }; - - const closeServer = async () => { - if (server.killed) { - return; - } - server.killed = true; - await fetch(`${server.url}/close-server`); - }; - - cp.stderr?.on('data', (chunk) => { - // eslint-disable-next-line no-console - console.log(chunk.toString()); - }); - - cp.stdout?.on('data', (chunk) => { - // first message is server url and we resolve immediately because that's what we care about - const message: string[] = chunk.toString().split('\n'); - const serverUrl = message[0]; - server.url ??= serverUrl; - resolve({ - serverUrl, - closeServer, - [Symbol.dispose]: closeServer, - }); - }); - - cp.on('error', async (err) => { - await closeServer(); - reject(err); - }); - - cp.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Server process exited with code ${code}`)); - } - }); - - process.on('SIGINT', closeServer); - process.on('SIGUSR1', closeServer); - process.on('SIGUSR2', closeServer); - process.on('uncaughtException', closeServer); - process.on('unhandledRejection', closeServer); - process.on('beforeExit', closeServer); - }); -} - -/** - * @group node - */ -describe( - 'setup-launch-node-server', - () => { - test('can start server on specific port', async () => { - using launched = await startServer(9876); - expect(launched.serverUrl).toEqual('http://localhost:9876'); - }); - - test('the /close-server endpoint closes the server', async () => { - const { serverUrl } = await startServer(); - await fetch(`${serverUrl}/close-server`); - - await waitUntilUnreachable(serverUrl); - }); - - test('returns a valid fuel-core node url on request', async () => { - using launched = await startServer(); - - const url = await (await fetch(launched.serverUrl)).text(); - // fetches node-related data - // would fail if fuel-core node is not running on url - await Provider.create(url); - }); - - test('the /cleanup endpoint kills the node', async () => { - using launched = await startServer(); - const url = await (await fetch(launched.serverUrl)).text(); - - await fetch(`${launched.serverUrl}/cleanup/${url}`); - - // if the node remained live then the test would time out - await waitUntilUnreachable(url); - }); - - test('kills all nodes when the server is shut down', async () => { - const { serverUrl, closeServer: killServer } = await startServer(); - const url1 = await (await fetch(serverUrl)).text(); - const url2 = await (await fetch(serverUrl)).text(); - - await killServer(); - - // if the nodes remained live then the test would time out - await waitUntilUnreachable(url1); - await waitUntilUnreachable(url2); - }); - - test('launchTestNode launches and kills node ', async () => { - using launchedServer = await startServer(); - const port = launchedServer.serverUrl.split(':')[2]; - const { cleanup, provider } = await launchTestNode({ - launchNodeServerPort: port, - }); - - cleanup(); - - await waitUntilUnreachable(provider.url); - }); - }, - { timeout: 25000 } -); diff --git a/packages/fuels/src/setup-launch-node-server.ts b/packages/fuels/src/setup-launch-node-server.ts deleted file mode 100644 index f8d09deac96..00000000000 --- a/packages/fuels/src/setup-launch-node-server.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable no-console */ -import type { LaunchNodeOptions } from '@fuel-ts/account/test-utils'; -import { launchNode } from '@fuel-ts/account/test-utils'; -import { waitUntilUnreachable } from '@fuel-ts/utils/test-utils'; -import http from 'http'; -import type { AddressInfo } from 'net'; - -process.setMaxListeners(Infinity); - -async function parseBody(req: http.IncomingMessage): Promise { - return new Promise((resolve, reject) => { - const body: Buffer[] = []; - req.on('data', (chunk) => { - body.push(chunk); - }); - req.on('end', () => { - try { - resolve(JSON.parse(body.length === 0 ? '{}' : Buffer.concat(body).toString())); - } catch (err) { - reject(err); - } - }); - req.on('error', reject); - }); -} - -const cleanupFns: Map Promise> = new Map(); - -const server = http.createServer(async (req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - - if (req.url === '/') { - try { - const body = await parseBody(req); - - const node = await launchNode({ - port: '0', - loggingEnabled: false, - debugEnabled: false, - ...body, - fuelCorePath: 'fuels-core', - }); - - cleanupFns.set(node.url, async () => { - node.cleanup(); - await waitUntilUnreachable(node.url); - cleanupFns.delete(node.url); - }); - - res.end(node.url); - } catch (err) { - console.error(err); - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(JSON.stringify(err)); - } - return; - } - - if (req.url?.startsWith('/cleanup')) { - const nodeUrl = req.url?.match(/\/cleanup\/(.+)/)?.[1]; - if (nodeUrl) { - const cleanupFn = cleanupFns.get(nodeUrl); - await cleanupFn?.(); - res.end(); - } - } -}); - -const closeServer = (event?: string) => (reason?: unknown) => { - console.log(event); - if (reason) { - console.log(reason); - } - return new Promise((resolve) => { - if (!server.listening) { - resolve(); - return; - } - - server.close(async () => { - const cleanupCalls: Promise[] = []; - cleanupFns.forEach((fn) => cleanupCalls.push(fn())); - await Promise.all(cleanupCalls); - process.exit(); - }); - - resolve(); - }); -}; - -server.on('request', async (req, res) => { - if (req.url === '/close-server') { - await closeServer('request to /close-server')(); - res.end(); - } -}); - -const port = process.argv[2] ? parseInt(process.argv[2], 10) : 49342; - -server.listen(port); - -server.on('listening', () => { - const usedPort = (server.address() as AddressInfo).port; - const serverUrl = `http://localhost:${usedPort}`; - console.log(serverUrl); - console.log(`Server is listening on: ${serverUrl}`); - console.log("To launch a new fuel-core node and get its url, make a POST request to '/'."); - console.log( - "To kill the node, make a POST request to '/cleanup/' where is the url of the node you want to kill." - ); - console.log('All nodes will be killed when the server is closed.'); - console.log('You can close the server by sending a request to /close-server.'); -}); - -process.on('exit', closeServer('exit')); -process.on('SIGINT', closeServer('SIGINT')); -process.on('SIGUSR1', closeServer('SIGUSR1')); -process.on('SIGUSR2', closeServer('SIGUSR2')); -process.on('uncaughtException', closeServer('uncaughtException')); -process.on('unhandledRejection', closeServer('unhandledRejection')); -process.on('beforeExit', closeServer('beforeExit')); diff --git a/scripts/tests-ci.sh b/scripts/tests-ci.sh index 930fa37cf58..1ecfac1a748 100755 --- a/scripts/tests-ci.sh +++ b/scripts/tests-ci.sh @@ -4,12 +4,11 @@ pkill fuel-core pnpm node:clean -pnpm node:run >/dev/null 2>&1 & +pnpm node:run > /dev/null 2>&1 & echo "Started Fuel-Core node in background." if [[ "$*" == *"--browser"* ]]; then - pnpm pretest pnpm test:browser TEST_RESULT=$? elif [[ "$*" == *"--node"* ]]; then diff --git a/vitest.browser.config.mts b/vitest.browser.config.mts index 48852149d53..8c7b3b0132f 100644 --- a/vitest.browser.config.mts +++ b/vitest.browser.config.mts @@ -20,9 +20,6 @@ const config: UserConfig = { "timers/promises", "util", "stream", - "path", - "fs", - "os", ], overrides: { fs: "memfs", @@ -34,10 +31,6 @@ const config: UserConfig = { include: ["events", "timers/promises"], }, test: { - env: { - LAUNCH_NODE_SERVER_PORT: "49342", - }, - globalSetup: ["./vitest.global-browser-setup.ts"], coverage: { reportsDirectory: "coverage/environments/browser", }, diff --git a/vitest.global-browser-setup.ts b/vitest.global-browser-setup.ts deleted file mode 100644 index d227e092f36..00000000000 --- a/vitest.global-browser-setup.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-floating-promises */ -import { spawn } from 'node:child_process'; - -export default function setup() { - return new Promise((resolve, reject) => { - const server = { - closed: false, - }; - const teardown = async () => { - if (server.closed) { - return; - } - server.closed = true; - const serverUrl = `http://localhost:49342`; - try { - await fetch(`${serverUrl}/close-server`); - } catch (e) { - console.log('closing of server failed', e); - } - process.exit(); - }; - - const cp = spawn('pnpm tsx packages/fuels/src/setup-launch-node-server.ts', { - detached: true, - shell: 'sh', - }); - - cp.stderr?.on('data', (chunk) => { - console.log(chunk.toString()); - }); - - cp.stdout?.on('data', (data) => { - console.log(data.toString()); - // Return teardown function to be called when tests finish - // It will kill the server - resolve(teardown); - }); - - cp.on('error', (err) => { - console.log(err); - // Ensure server is killed if there's an error - teardown(); - reject(err); - }); - - cp.on('exit', (code, signal) => { - console.log('error code', code, signal); - if (code !== 0) { - reject(new Error(`Server process exited with code ${code}`)); - } - }); - - process.on('SIGINT', teardown); - process.on('SIGUSR1', teardown); - process.on('SIGUSR2', teardown); - process.on('uncaughtException', teardown); - process.on('unhandledRejection', teardown); - process.on('beforeExit', teardown); - }); -}