| name | e2e-test-creation |
| description | Create Cucumber BDD end-to-end tests for new features, enhancements, or updates. Use this when asked to write e2e tests, acceptance tests, feature files, step definitions, or Cucumber scenarios for Daedalus. |
E2E Test Creation Skill
Create Cucumber BDD end-to-end tests for the Daedalus wallet application. This skill covers writing .feature files, step definitions, and helpers following existing project conventions.
Test Framework
| Component | Technology |
|---|
| BDD Framework | Cucumber.js with Gherkin syntax |
| App Driver | Spectron (Electron testing) + WebDriver.io |
| Assertions | Chai (expect) |
| Mocking | Sinon (unit tests only) |
| TypeScript | Compiled via @swc-node/register |
| Test Runner | yarn test:e2e / yarn test:unit |
Directory Structure Convention
Tests are organized by domain under tests/. Each domain may have e2e/ and/or unit/ subtrees:
tests/{domain}/
├── e2e/
│ ├── features/
│ │ └── {feature-name}.feature # Gherkin scenarios
│ ├── steps/
│ │ ├── {feature-name}.ts # Step definitions
│ │ └── helpers.ts # Domain-specific helpers
│ └── documents/ # Test fixtures (optional)
└── unit/
├── features/
│ └── {feature-name}.feature
└── steps/
└── {feature-name}.ts
Existing Domains
| Domain | Path | Description |
|---|
wallets | tests/wallets/ | Wallet CRUD, restore, ordering |
transactions | tests/transactions/ | Send, receive, UTxO display |
delegation | tests/delegation/ | Staking, stake pools, rewards |
navigation | tests/navigation/ | Sidebar, tabs, routing |
settings | tests/settings/ | Language, terms of use, preferences |
news | tests/news/ | Newsfeed, alerts, incidents |
paper-wallets | tests/paper-wallets/ | Paper wallet certificates |
addresses | tests/addresses/ | Address management |
app | tests/app/ | App lifecycle, updates, dialogs |
common | tests/common/ | Shared utilities, mnemonics |
assets | tests/assets/ | Native tokens, NFTs |
When adding tests for a new domain, create the full subtree. When adding tests for an existing domain, add files to the existing structure.
Step 1: Write a Feature File
File Location
tests/{domain}/e2e/features/{feature-name}.feature
Required Tags
Every e2e feature file MUST start with a tag line. Use the appropriate tags:
| Tag | Meaning |
|---|
@e2e | Required — marks as an end-to-end test |
@unit | Marks as a unit test (use for unit/ features) |
@wip | Work in progress — skipped in CI |
@skip | Permanently skipped |
@watch | Run in watch mode during development |
@noReload | Skip app reload between scenarios |
@restartApp | Restart Electron app after this scenario |
@reconnectApp | Reconnect app backend after this scenario |
@rewardsCsv | Clean up exported rewards CSV after scenario |
Feature File Template
@e2e
Feature: {Human-readable Feature Name}
Background:
Given I have completed the basic setup
Scenario: {Descriptive Scenario Name}
Given {precondition}
When {user action}
Then {expected outcome}
Gherkin Conventions
-
Background section: Use for shared preconditions across scenarios. Almost all e2e tests include Given I have completed the basic setup which handles initial app setup (language, terms of use acceptance).
-
Scenario Outline with Examples: Use for testing multiple data variations:
Scenario Outline: Switching Between Wallet Tabs
Given I am on the "Test wallet" wallet "<FROM>" screen
When I click the wallet <TO> button
Then I should be on the "Test wallet" wallet "<TO>" screen
Examples:
| FROM | TO |
| summary | send |
| summary | receive |
-
Data Tables: Use for structured test data:
Given I have the following wallets:
| name |
| Test wallet |
-
Quoted strings: Use double quotes for dynamic values referenced in step definitions:
Given I am on the "Test wallet" wallet "send" screen
Common Reusable Steps
These steps are already defined and available for use in feature files:
# Setup
Given I have completed the basic setup
Given I dont have a language set
Given I didn't accept "Terms of use"
# Wallet management
Given I have the following wallets:
| name |
| My Wallet |
Given I have a "{wallet}" wallet with funds
Given I am on the "{wallet}" wallet "{screen}" screen
# Navigation
When I click the wallet {button} button
When I click on the "{category}" category in the sidebar
# Sidebar
Given the sidebar submenu is hidden|visible
When I click on the sidebar toggle button
Then the sidebar submenu should be hidden|visible
# General assertions
Then I should be on the "{wallet}" wallet "{screen}" screen
Step 2: Write Step Definitions
File Location
tests/{domain}/e2e/steps/{feature-name}.ts
Step Definition Template
import { Given, When, Then } from 'cucumber';
import { expect } from 'chai';
const SELECTORS = {
COMPONENT: '.MyComponent_component',
BUTTON: '.MyComponent_button',
DIALOG: '.MyDialog_overlay',
};
Given(/^I am on the my feature screen$/, async function () {
await this.client.waitForVisible(SELECTORS.COMPONENT);
});
When(/^I click the action button$/, async function () {
await this.waitAndClick(SELECTORS.BUTTON);
});
Then(/^I should see the dialog$/, async function () {
await this.client.waitForVisible(SELECTORS.DIALOG);
});
Then(, () {
..(., , );
});
Step Definition Conventions
-
Import pattern: Always import Given, When, Then from 'cucumber' and expect from 'chai'.
-
Regex matching: Use regex patterns for step matching. Capture dynamic values with groups:
Given(/^I have a "([^"]*)" wallet$/, async function (walletName) {
});
-
Async functions: All step definitions MUST use async function (not arrow functions — Cucumber needs this context).
-
this context: The test context provides these properties:
this.client
this.app
this.browserWindow
this.context
this.intl
this.waitAndClick(selector)
this.waitAndGetText(selector)
this.waitAndSetValue(selector, value)
Step 3: Write Domain-Specific Helpers
File Location
tests/{domain}/e2e/steps/helpers.ts
Helper Function Patterns
import { DEFAULT_TIMEOUT } from '../../../common/e2e/steps/config';
export const getElementData = async (client, selector: string) => {
await client.waitForVisible(selector);
return client.getText(selector);
};
export const waitUntilCondition = async function (conditionFn: () => boolean) {
await this.client.waitUntil(
async () => {
const result = await this.client.execute(conditionFn);
return result.value === true;
},
DEFAULT_TIMEOUT
);
};
export const navigateTo = async function (route: string) {
await this.client.( {
daedalus....({ : r });
}, route);
};
waitUntilUrlEquals = () {
maxWait = ;
freq = ;
attempts = maxWait / freq;
= () => {
url = ..();
(url..(expectedUrl)) ;
(--attempts <= ) ();
( (r, freq));
();
};
();
};
Helper Conventions
- Export all helpers as named exports — never use default exports.
- Accept
client as first parameter for functions that don't need this context.
- Use
function (not arrow) for helpers that need this binding — they'll be called with .call(this, ...).
- Import
DEFAULT_TIMEOUT from tests/common/e2e/steps/config.ts for consistent timeouts.
- Keep helpers focused — one responsibility per function.
Step 4: Common Available Helpers
From tests/common/e2e/steps/helpers.ts
| Helper | Purpose |
|---|
waitAndClick(selector) | Wait visible + enabled, then click |
waitAndGetText(selector) | Wait for text content, then retrieve it |
waitAndSetValue(sel, val) | Wait for element, then set its value |
scrollIntoView(client, sel) | Scroll element into viewport |
clickInputByLabel(label) | Click input field by its label text |
clickOptionByValue(value) | Click select option by value |
clickOptionByIndex(index) | Click select option by index |
saveScreenshot(app, file) | Capture screenshot (used on failure) |
skippablePromise(name, fn) | Wrap promise with skip support |
From tests/common/e2e/steps/config.ts
export const DEFAULT_TIMEOUT = 20000;
From tests/wallets/e2e/steps/helpers.ts
| Helper | Purpose |
|---|
createWallets(wallets, options) | Create wallets via API |
waitUntilWalletIsLoaded(name) | Poll until wallet appears in sidebar |
restoreWalletWithFunds(client, opts) | Restore Shelley wallet with funds |
navigateTo(route) | Navigate to a specific app route |
waitUntilUrlEquals(url) | Wait until URL matches expected |
getWalletType(type) | Determine Shelley vs Byron wallet type |
Step 5: Run & Verify Tests
Development Workflow
-
Tag your new test with @watch for iterative development:
@e2e @watch
Feature: My New Feature
-
Run in watch mode:
KEEP_APP_AFTER_TESTS=true yarn test:e2e --tags '@e2e and @watch'
-
Run a single feature file:
yarn cucumber:run tests/{domain}/e2e/features/{feature-name}.feature
-
Run all e2e tests:
yarn test:e2e
-
Run with fail-fast (stop on first failure):
yarn test:e2e:fail-fast
-
Re-run only failed tests:
yarn test:e2e:rerun
Before Committing
- Remove
@watch and @wip tags from finished tests
- Ensure the test is tagged
@e2e (or @unit for unit tests)
- Verify tests pass:
yarn test:e2e --tags '@e2e and @{your-feature-tag}'
Complete Example: New Feature Test
Scenario: Testing a "Bookmark Wallet" feature
1. Feature file — tests/wallets/e2e/features/bookmark-wallet.feature:
@e2e
Feature: Bookmark Wallet
Background:
Given I have completed the basic setup
And I have the following wallets:
| name |
| Test wallet |
Scenario: User bookmarks a wallet
Given I am on the "Test wallet" wallet "summary" screen
When I click the bookmark button
Then the wallet should be marked as bookmarked
And I should see the bookmark indicator
Scenario: User removes a bookmark
Given I am on the "Test wallet" wallet "summary" screen
And the wallet is bookmarked
When I click the bookmark button
Then the wallet should not be marked as bookmarked
2. Step definitions — tests/wallets/e2e/steps/bookmark-wallet.ts:
import { Given, When, Then } from 'cucumber';
import { expect } from 'chai';
const SELECTORS = {
BOOKMARK_BUTTON: '.WalletSummary_bookmarkButton',
BOOKMARK_ACTIVE: '.WalletSummary_bookmarkActive',
BOOKMARK_INDICATOR: '.WalletSummary_bookmarkIndicator',
};
Given(/^the wallet is bookmarked$/, async function () {
const isBookmarked = await this.client.isVisible(SELECTORS.BOOKMARK_ACTIVE);
if (!isBookmarked) {
await this.waitAndClick(SELECTORS.BOOKMARK_BUTTON);
await this.client.waitForVisible(SELECTORS.BOOKMARK_ACTIVE);
}
});
When(/^I click the bookmark button$/, async function () {
await this.waitAndClick(SELECTORS.);
});
(, () {
..(.);
});
(, () {
..(., , );
});
(, () {
..(.);
});
Creating Unit Tests (Non-E2E)
For unit tests that don't require the full Electron app:
Feature File — tests/{domain}/unit/features/{name}.feature
@unit
Feature: {Feature Name}
Scenario: {Scenario Name}
Given {setup}
When {action}
Then {assertion}
Step Definitions — tests/{domain}/unit/steps/{name}.ts
import { Given, When, Then } from 'cucumber';
import { expect } from 'chai';
import sinon from 'sinon';
Given('I have input data {string}', function (data) {
this.context.input = data;
});
When('I process the data', function () {
this.context.result = processData(this.context.input);
});
Then('the output should be {string}', function (expected) {
expect(this.context.result).to.equal(expected);
});
Unit test conventions:
- Use
@unit tag (not @e2e)
- Use
this.context to share data between steps (set up in setup-common.ts)
- Import source code directly — no Spectron/WebDriver needed
- Use Sinon for mocks/stubs (auto-restored in
After hook)
- Use plain string matchers instead of regex when parameters are simple
Checklist
When creating e2e tests for a new feature, verify: