This folder contains automated test tooling for Hedera Transaction Tool:
- Playwright functional end-to-end tests (
tests/**/*.test.ts) - Playwright UI performance tests (
tests/ui-performance) - k6 API/load test scripts (
k6/)
- Node.js
>= 24.20.0 <25 pnpm- One of:
- a built Hedera Transaction Tool executable (launch mode), or
- a running front-end Electron app with remote debugging enabled (attach mode)
-
Clone the repository.
-
Install dependencies from the repository root — the repo is a single pnpm workspace, so all modules (
back-end,front-end,automation) install together:cd hedera-transaction-tool pnpm install pnpm approve-builds # only if pnpm requests approval
-
Create your env file:
cd automation cp example.env .env
Automation-specific scripts can be run via the workspace filter from any directory:
pnpm -F hedera-transaction-tool-tests-v2 <script>Or by cd automation and running the script as before — both work.
Launch mode example:
ELECTRON_APP_MODE='launch'
EXECUTABLE_PATH='/Applications/Hedera Transaction Tool.app/Contents/MacOS/Hedera Transaction Tool'
DATABASE_DEBUG='false'
PLAYWRIGHT_TEST=true
PLAYWRIGHT_WORKERS=2
PLAYWRIGHT_SHARED_ENV=true
PRIVATE_KEY= # hex encoded
OPERATOR_KEY= # DER encoded
ENVIRONMENT='LOCALNET'
ORGANIZATION_URL='https://localhost:3001'
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgresAttach mode example:
ELECTRON_APP_MODE='attach'
ELECTRON_ATTACH_URL='http://127.0.0.1:9222'
ELECTRON_REMOTE_DEBUGGING_PORT='9222'
ELECTRON_ATTACH_TIMEOUT_MS='30000'DATABASE_DEBUG defaults to off and should stay false for normal runs. CI ignores this flag and keeps DB debug logging disabled. Set DATABASE_DEBUG=true only for local automation debugging when you need SQL connection/query logging. Query parameters are redacted in that debug output.
If you use attach mode, start the front-end first from front-end/:
PLAYWRIGHT_TEST=true ELECTRON_REMOTE_DEBUGGING_PORT=9222 pnpm devENVIRONMENT can be LOCALNET, TESTNET, or PREVIEWNET.
TESTNET/PREVIEWNET: use ECDSA keysLOCALNET: use ED25519 keys
Run all Playwright tests:
pnpm testList all discovered tests:
pnpm test:listRun all tests without the TypeScript pre-step:
pnpm exec playwright testRun the shared E2E buckets by tag:
pnpm exec playwright test --grep '@local-transactions|@organization-basic|@organization-advanced'Run a single suite:
pnpm exec playwright test tests/local-basic/registrationTests.test.tsRun all UI performance tests:
pnpm exec playwright test tests/ui-performanceOpen the Playwright HTML report:
pnpm report:playwrightEach functional test file carries exactly one suite tag in its test.describe(...) title. The tag determines what backend infrastructure the test expects and which CI job picks it up. Pick the tag that matches the minimum infrastructure your test actually needs — the lighter the suite, the faster the CI job.
| Tag | Solo (Hedera localnet) | Org back-end + Postgres | Typical use |
|---|---|---|---|
@local-basic |
No | No | Pure UI flows: login, registration, settings panels, form validation that doesn't need ledger lookups |
@local-transactions |
Yes | No | Personal-mode transactions executed against the local Hedera node, mirror-node lookups, account/file CRUD |
@organization-basic |
Yes | Yes | Single-user org flows: contact list, settings inside an org, notifications panel |
@organization-advanced |
Yes | Yes | Multi-user / multi-signer org flows: group transactions, signing lifecycles, council-scale regression |
- Need a logged-in user only? Use
@local-basic. Tests there cannot rely on mirror-node lookups (no live network) but can poke the renderer's Pinia stores via test hooks (seetests/helpers/support/localOrganizationConnectionSupport.tsfor examples). - Need to actually execute a transaction or read mirror data? Use
@local-transactions. - Need a connected organization (e.g., the Notifications settings tab, contact list, signing flows)? Use
@organization-basicfor single-user scenarios,@organization-advancedwhen the test creates additional org users / multi-signer accounts.
The faster the suite, the cheaper your test is to run on every PR. A test that only needs to assert a button is disabled doesn't belong in @organization-advanced.
@local-basic
accountResetTests.test.tsloginTests.test.tsregistrationAccountSetupTests.test.tsregistrationPersistenceTests.test.tsregistrationTests.test.tssettingsGeneralTests.test.tssettingsKeysImportTests.test.tssettingsKeysManagementTests.test.tssettingsKeysTests.test.tssettingsOrganizationsTabTests.test.tssettingsProfileTests.test.tstransactionAllowanceValidationTests.test.tstransactionComplexKeyValidationTests.test.tstransactionFileIdValidationTests.test.tstransactionHeaderValidationTests.test.tstransactionTransferValidationTests.test.ts
@local-transactions
groupTransactionExecutionTests.test.tsgroupTransactionItemTests.test.tsgroupTransactionTests.test.tstransactionAccountCreateExecutionTests.test.tstransactionAccountCreateValidationTests.test.tstransactionAccountDatabaseTests.test.tstransactionAccountDeleteTests.test.tstransactionAccountUpdateTests.test.tstransactionDraftAccountPersistenceTests.test.tstransactionDraftFileTests.test.tstransactionDraftKeySafetyTests.test.tstransactionDraftTests.test.tstransactionFileTests.test.tstransactionTransferAllowanceTests.test.tsworkflowFileNavigationTests.test.tsworkflowHistoryDetailsTests.test.tsworkflowHistoryFileBreadcrumbDetailsTests.test.tsworkflowHistoryTransferAllowanceDetailsTests.test.tsworkflowTests.test.ts
@organization-basic
organizationContactListAdminTests.test.tsorganizationContactListBulkTests.test.tsorganizationContactListTests.test.tsorganizationLoginTests.test.tsorganizationNotificationTests.test.tsorganizationSettingsConnectionTests.test.tsorganizationSettingsGeneralTests.test.tsorganizationSettingsNotificationsTests.test.tsorganizationSettingsRecoveryTests.test.tsorganizationSettingsTransactionAccessTests.test.ts
@organization-advanced
organizationGroupCsvLoadTests.test.tsorganizationGroupTests.test.tsorganizationRegressionTests.test.tsorganizationTransactionCompatibilityTests.test.tsorganizationTransactionExecutionTests.test.tsorganizationTransactionLifecycleTests.test.tsorganizationTransactionObserverTests.test.tsorganizationTransactionTests.test.ts
Some organization suites are currently marked skip in the source, but they remain listed here because they still belong to the suite structure and CI tag layout.
.github/workflows/test-frontend.yaml runs two Playwright matrix entries:
| CI job | Tag(s) | PLAYWRIGHT_WORKERS |
Infrastructure |
|---|---|---|---|
| Local Basic | @local-basic |
2 | Headless Electron only |
| Shared E2E | @local-transactions, @organization-basic, @organization-advanced |
8 | Solo + back-end + Postgres |
PLAYWRIGHT_SHARED_ENV: true is set on the Shared E2E job so per-worker isolation (TEST_WORKER_INDEX, partition, user-data dir, remote debugging port) is wired correctly.
PLAYWRIGHT_WORKERS controls how many Playwright worker processes can run at once.
fullyParallel: falseinplaywright.config.ts— tests inside a single file run sequentially within one worker.- Workers are scheduled at the file level: different files can run on different workers, but every test in the same file shares one worker process (one Electron app, one suite
beforeAll). .envis loaded at config time (top ofplaywright.config.ts), soPLAYWRIGHT_WORKERS=2in.envis honored locally without inline shell exports.- Splitting a large file into smaller files is the simplest way to improve worker utilization — each new file becomes its own scheduling unit.
For long files where the per-test runtime dominates over the per-worker bootstrap (Electron launch + suite setup), you can opt a single describe into test-level parallelism:
test.describe('Organization Transaction status/signing lifecycle tests @organization-advanced', () => {
test.describe.configure({ mode: 'parallel' });
// ...
});What this does:
- Tests inside the describe become independent scheduling units; multiple workers can pull from the same file simultaneously.
- Each worker still runs its own
beforeAll— bootstrap is per-worker, not per-test. If two workers run two tests from the same file, you pay bootstrap twice. - Module-level
letstate in the test file is per-worker (each worker is a separate Node process), so it does not bleed between parallel tests.
When it pays off:
- A single test takes minutes (signing + ledger waits) → overlapping tests on different workers shrinks the file's wall-clock to roughly
max(single test) + bootstrap. - Tests inside the describe are genuinely independent — no shared state mutated by one test that another reads.
When it breaks:
- Tests share fixtures across the describe (e.g., a complex-key account created in
beforeAll, or a cached file ID built by the first test and reused by later tests). Concurrent execution will see partial state and fail intermittently. - Backend rate-limiters or single-instance constraints serialize the workers anyway, so the gain is eaten by waiting.
Roll out one file at a time, watch the CI report, and if a flake appears revert the configure line.
In shared environment mode (PLAYWRIGHT_SHARED_ENV=true):
- Workers share the same Solo cluster and back-end deployment.
- Electron user-data dirs, session partitions, and remote debugging ports are isolated per
TEST_WORKER_INDEX(seeutils/setup/playwrightIsolation.ts). - Org nicknames are uniquified per test via
resolveOrganizationNickname(testInfo.title)in the advanced suite hooks, so org-advanced tests can run concurrently against the same backend without colliding.
Playwright writes three reporters in parallel:
- HTML report →
reports/playwright/(open withpnpm report:playwright). - JSON report →
reports/playwright-json/results.json(sibling folder so the HTML reporter's cleanup step doesn't wipe it). list(console) andgithubannotations on CI.
Both report folders are uploaded as the playwright-report-<suite-name> artifact in CI.
To see per-worker utilization (find the long-pole worker, spot idle workers):
jq -r '
[.. | objects | select(.results?[0].workerIndex != null)
| { worker: .results[0].workerIndex, dur: .results[0].duration, title: .title }]
| group_by(.worker)
| map({ worker: .[0].worker, total_min: ((map(.dur) | add) / 60000 | floor), tests: length })
| sort_by(.worker)
' reports/playwright-json/results.jsonFor the slowest 15 tests with which worker ran each:
jq -r '
[.. | objects | select(.results?[0].workerIndex != null)
| { worker: .results[0].workerIndex, dur_min: (.results[0].duration / 60000), title: .title }]
| sort_by(-.dur_min)
| .[0:15]
' reports/playwright-json/results.jsonpages/— page-object classes (SettingsPage,TransactionPage,OrganizationPage, etc.). Tests should not contain raw selectors or DOM CSS strings; add a selector + helper here instead.tests/helpers/bootstrap/— per-suite app launch/teardown (setupLocalSuiteApp,setupOrganizationSuiteApp,registerOrganizationAdvancedSuiteHooks).tests/helpers/fixtures/— reusable test fixtures (org settings suite, group transaction suite, etc.).tests/helpers/support/— small renderer-state helpers (e.g., poking Pinia stores orwindow.__testHooks__for state that isn't exposed via the UI).
A test-only escape hatch lives at window.__testHooks__ (see front-end/src/renderer/main.ts); it exposes module-scoped renderer refs (currently version-state setters) so Playwright can simulate states the UI doesn't otherwise let you reach. Use it sparingly — prefer driving the UI when possible.
- Error toasts never auto-dismiss.
ToastManager.error(in the renderer) is configured withduration: 0, so an error toast persists in the DOM until the user clicks it. If your test asserts an error toast and a later test readsgetToastMessageByVariant('error'), the stale toast can be returned. Either (a) usewaitForToastMessageByVariant(variant, message)which filters by exact text, or (b) click the toast at the end of your test to dismiss it andwaitFor({ state: 'detached' })before exiting. - Mirror-node-dependent validation in
@local-basic. Several form validations (Sign disabled when payer key not resolved,Invalid checksum on Link Existing,Insufficient balance) only surface after a successful mirror-node lookup. They aren't reachable in@local-basic(no live mirror) — if you need the toast/inline error, place the test in@local-transactions. - Drafts modal can intercept clicks. Most
beforeEachhooks calltransactionPage.closeDraftModal()after navigating to Transactions. Skip it and your first click may go to the modal instead of the form. - Don't ship
test.only(...)ortest.describe.only(...). They skip every other test in the file, which silently turns a CI run green while running almost nothing.
- UI performance tests are documented in
tests/ui-performance/README.md. - k6 scripts are under
k6/and can be run withpnpm k6:*scripts frompackage.json(for examplepnpm k6:smoke).