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 setupwhich 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';
// CSS selectors — group all selectors at the top
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(/^I should not see the dialog$/, async function () {
await this.client.waitForVisible(SELECTORS.DIALOG, null, true);
});
Step Definition Conventions
Import pattern: Always import
Given,When,Thenfrom'cucumber'andexpectfrom'chai'.Regex matching: Use regex patterns for step matching. Capture dynamic values with groups:
Given(/^I have a "([^"]*)" wallet$/, async function (walletName) { // walletName captured from quotes in feature file });Async functions: All step definitions MUST use
async function(not arrow functions — Cucumber needsthiscontext).thiscontext: The test context provides these properties:this.client // WebDriver.io client — DOM interaction this.app // Spectron Application instance this.browserWindow // Electron BrowserWindow this.context // Shared data object between steps this.intl // i18n translation helper this.waitAndClick(selector) // Wait for visible + enabled, then click this.waitAndGetText(selector) // Wait for text, then get it this.waitAndSetValue(selector, value) // Wait for element, then set valueCSS Selectors: Use CSS class selectors matching React component SCSS module classes. Daedalus uses SCSS modules, so class names follow
{ComponentName}_{className}pattern:'.WalletSendForm_component' '.SidebarCategory_active' '.TopBar_leftIcon' '.NavButton_component.summary'Selectors object: Group all selectors in a
SELECTORSconst at the top of the file for maintainability.Executing code in the app context: Use
this.client.execute()orthis.client.executeAsync()to run code inside the Electron renderer process:// Synchronous execution const result = await this.client.execute(() => { return daedalus.stores.wallets.active?.name; }); // result.value contains the return value // Async execution (with callback) await this.client.executeAsync((done) => { daedalus.stores.wallets.refreshWalletsData() .then(done) .catch((error) => done(error)); });Accessing Daedalus internals: Inside
execute/executeAsync, the globaldaedalusobject exposes:daedalus.stores // MobX stores (wallets, networkStatus, sidebar, profile, etc.) daedalus.actions // MobX actions daedalus.api // API client (daedalus.api.ada) daedalus.translations // i18n translation objectsAssertions: Use Chai's
expectfor assertions:const text = await this.waitAndGetText(SELECTORS.TITLE); expect(text).to.equal('Expected Title'); expect(text).to.include('partial'); expect(result.value).to.be.true;
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';
/**
* Wait for an element and extract data from it.
*/
export const getElementData = async (client, selector: string) => {
await client.waitForVisible(selector);
return client.getText(selector);
};
/**
* Wait until a condition is true in the app state.
*/
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
);
};
/**
* Navigate to a specific route in the app.
*/
export const navigateTo = async function (route: string) {
await this.client.execute((r) => {
daedalus.actions.router.goToRoute.trigger({ route: r });
}, route);
};
/**
* Wait until a specific URL is reached.
*/
export const waitUntilUrlEquals = async function (expectedUrl: string) {
const maxWait = DEFAULT_TIMEOUT;
const freq = 500;
let attempts = maxWait / freq;
const check = async () => {
const url = await this.client.url();
if (url.value.includes(expectedUrl)) return true;
if (--attempts <= 0) throw new Error(`URL never reached: ${expectedUrl}`);
await new Promise((r) => setTimeout(r, freq));
return check();
};
await check();
};
Helper Conventions
- Export all helpers as named exports — never use default exports.
- Accept
clientas first parameter for functions that don't needthiscontext. - Use
function(not arrow) for helpers that needthisbinding — they'll be called with.call(this, ...). - Import
DEFAULT_TIMEOUTfromtests/common/e2e/steps/config.tsfor 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
@watchfor iterative development:@e2e @watch Feature: My New FeatureRun 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}.featureRun all e2e tests:
yarn test:e2eRun with fail-fast (stop on first failure):
yarn test:e2e:fail-fastRe-run only failed tests:
yarn test:e2e:rerun
Before Committing
- Remove
@watchand@wiptags from finished tests - Ensure the test is tagged
@e2e(or@unitfor 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.BOOKMARK_BUTTON);
});
Then(/^the wallet should be marked as bookmarked$/, async function () {
await this.client.waitForVisible(SELECTORS.BOOKMARK_ACTIVE);
});
Then(/^the wallet should not be marked as bookmarked$/, async function () {
await this.client.waitForVisible(SELECTORS.BOOKMARK_ACTIVE, null, true);
});
Then(/^I should see the bookmark indicator$/, async function () {
await this.client.waitForVisible(SELECTORS.BOOKMARK_INDICATOR);
});
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
@unittag (not@e2e) - Use
this.contextto share data between steps (set up insetup-common.ts) - Import source code directly — no Spectron/WebDriver needed
- Use Sinon for mocks/stubs (auto-restored in
Afterhook) - Use plain string matchers instead of regex when parameters are simple
Checklist
When creating e2e tests for a new feature, verify:
- Feature file is in
tests/{domain}/e2e/features/with.featureextension - Feature file starts with
@e2etag - Background includes
Given I have completed the basic setup - Step definitions are in
tests/{domain}/e2e/steps/with.tsextension - Step definitions import
{ Given, When, Then }from'cucumber' - Step definitions use
async function(not arrow functions) - CSS selectors follow
{ComponentName}_{className}SCSS module pattern - Selectors are grouped in a
SELECTORSconstant - Domain-specific helpers are in a separate
helpers.tsfile - Reusable steps from other domains are referenced, not duplicated
-
@watch/@wiptags are added during development and removed before merge - Tests pass with
yarn test:e2e --tags '@e2e and @watch'