Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Integration (Playwright + Soroban)

on:
workflow_dispatch:
pull_request:
branches: [main]

jobs:
integration:
runs-on: ubuntu-latest
services:
stellar:
image: stellar/quickstart:testing
ports:
- 8000:8000
options: >-
--health-cmd "curl -fsSL -H 'Content-Type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}' http://localhost:8000/rpc | grep -q healthy"
--health-interval 10s
--health-timeout 5s
--health-retries 10
command: ["--local", "--enable-stellar-rpc"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:integration
env:
CI: "true"
NEXT_PUBLIC_SOROBAN_RPC_URL: http://127.0.0.1:8000/rpc
PLAYWRIGHT_BASE_URL: http://127.0.0.1:3000

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

# testing
/coverage
/test-results
/playwright-report

# next.js
/.next/
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ Open [http://localhost:3000](http://localhost:3000) to view the dashboard.
- Gas buffer status tracking
- Provider and consumer dashboards

## Integration Testing (Playwright + Soroban)

This repository includes an end-to-end integration test harness built on Playwright, designed to run against a local Soroban-capable Stellar node (Quickstart).

### 1) Start local Soroban RPC (Quickstart)

```bash
docker compose -f docker-compose.test.yml up -d
```

The Soroban/Stellar RPC endpoint will be available at `http://localhost:8000/rpc`.

### 2) Run integration tests

```bash
npm run test:integration
```

PowerShell:

```powershell
$env:NEXT_PUBLIC_SOROBAN_RPC_URL="http://localhost:8000/rpc"
npm run test:integration
```

## Learn More

- [Equipchain Contracts](https://github.com/EquipChain/EquipChain-contracts)
Expand Down
16 changes: 16 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
stellar:
image: stellar/quickstart:testing
command: ["--local", "--enable-stellar-rpc"]
ports:
- "8000:8000"
healthcheck:
test:
[
"CMD-SHELL",
"wget -qO- http://localhost:8000/rpc -O- --header='Content-Type: application/json' --post-data='{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}' | grep -q healthy",
]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
63 changes: 63 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "next start",
"lint": "eslint",
"test": "echo \"No tests configured yet. Add tests and update this script.\" && exit 0",
"test:integration": "playwright test -c playwright.integration.config.ts",
"format": "prettier --write \"**/*.{ts,tsx,css,mjs,mts}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,css,mjs,mts}\"",
"typecheck": "tsc --noEmit",
Expand All @@ -33,6 +34,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@playwright/test": "^1.52.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
31 changes: 31 additions & 0 deletions playwright.integration.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineConfig, devices } from "@playwright/test";

const port = Number(process.env.PORT ?? "3000");
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`;

export default defineConfig({
testDir: "./src/test/integration/specs",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
timeout: 90_000,
expect: { timeout: 15_000 },
reporter: [["list"]],
outputDir: "test-results/playwright-integration",
use: {
baseURL,
trace: "retain-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: `npm run dev -- -p ${port}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});

90 changes: 90 additions & 0 deletions src/test/factories/meterFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* meterFactory – generates realistic meter test-data objects.
*
* Generates deterministic or random meter records that match the shape
* expected by the EquipChain backend and UI.
*/

export type MeterStatus = "Active" | "Inactive" | "Maintenance";
export type MeterType = "Electric" | "Water" | "Gas";

export type Meter = {
id: string;
name: string;
type: MeterType;
status: MeterStatus;
lastReading: string;
totalConsumption: string;
rate: string;
lastUpdated: string;
ownerAddress: string;
};

export type MeterFactoryInput = {
id?: string;
name?: string;
type?: MeterType;
status?: MeterStatus;
lastReading?: string;
totalConsumption?: string;
rate?: string;
lastUpdated?: string;
ownerAddress?: string;
};

const METER_TYPES: MeterType[] = ["Electric", "Water", "Gas"];
const METER_STATUSES: MeterStatus[] = ["Active", "Inactive", "Maintenance"];

const UNIT_BY_TYPE: Record<MeterType, string> = {
Electric: "kWh",
Water: "gal",
Gas: "m³",
};

const RATE_BY_TYPE: Record<MeterType, string> = {
Electric: "$0.12/kWh",
Water: "$0.05/gal",
Gas: "$0.08/m³",
};

function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

/** Produce a single meter record with sensible defaults. */
export function meterFactory(input: MeterFactoryInput = {}): Meter {
const type = input.type ?? METER_TYPES[randomInt(0, 2)]!;
const unit = UNIT_BY_TYPE[type];
const lastReadingValue = randomInt(1_000, 20_000).toLocaleString();
const totalConsumptionValue = randomInt(50_000, 200_000).toLocaleString();

const id = input.id ?? `meter-${String(randomInt(1, 999)).padStart(3, "0")}`;

return {
id,
name: input.name ?? `Test Meter ${id}`,
type,
status: input.status ?? METER_STATUSES[randomInt(0, 1)]!, // weighted toward Active/Inactive
lastReading: input.lastReading ?? `${lastReadingValue} ${unit}`,
totalConsumption:
input.totalConsumption ?? `${totalConsumptionValue} ${unit}`,
rate: input.rate ?? RATE_BY_TYPE[type],
lastUpdated: input.lastUpdated ?? new Date().toISOString().slice(0, 10),
ownerAddress:
input.ownerAddress ??
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
};
}

/**
* Produce an array of `count` meters. Optionally override shared fields for
* all records via `sharedInput`.
*/
export function meterBatch(
count: number,
sharedInput: MeterFactoryInput = {}
): Meter[] {
return Array.from({ length: count }, (_, i) =>
meterFactory({ id: `meter-${String(i + 1).padStart(3, "0")}`, ...sharedInput })
);
}
68 changes: 68 additions & 0 deletions src/test/factories/readingFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* readingFactory – generates realistic meter-reading test-data objects.
*/

export type Reading = {
meterId: string;
value: number;
unit: string;
timestamp: string;
/** Simulated on-chain transaction ID (hex string) */
txHash?: string;
};

export type ReadingFactoryInput = {
meterId?: string;
value?: number;
unit?: string;
timestamp?: string;
txHash?: string;
};

function randomFloat(min: number, max: number, decimals = 2): number {
const factor = Math.pow(10, decimals);
return Math.round((Math.random() * (max - min) + min) * factor) / factor;
}

function randomHex(bytes = 32): string {
return Array.from({ length: bytes }, () =>
Math.floor(Math.random() * 256)
.toString(16)
.padStart(2, "0")
).join("");
}

/** Produce a single reading record. */
export function readingFactory(input: ReadingFactoryInput = {}): Reading {
return {
meterId: input.meterId ?? "meter-001",
value: input.value ?? randomFloat(10, 500),
unit: input.unit ?? "kWh",
timestamp: input.timestamp ?? new Date().toISOString(),
txHash: input.txHash ?? randomHex(),
};
}

/**
* Generate a time-series of readings for a single meter.
*
* @param count - Number of readings to generate
* @param meterId - Meter to associate readings with
* @param intervalHours - Hours between consecutive readings (default 1)
*/
export function readingTimeSeries(
count: number,
meterId = "meter-001",
intervalHours = 1
): Reading[] {
const now = new Date();
return Array.from({ length: count }, (_, i) => {
const ts = new Date(
now.getTime() - (count - 1 - i) * intervalHours * 60 * 60 * 1000
);
return readingFactory({
meterId,
timestamp: ts.toISOString(),
});
});
}
Loading
Loading