Skip to content

Commit

Permalink
chore: introduce device action hooks + tests basic cases
Browse files Browse the repository at this point in the history
  • Loading branch information
gre committed Aug 8, 2023
1 parent 0c53a73 commit 7817773
Show file tree
Hide file tree
Showing 61 changed files with 912 additions and 120 deletions.
73 changes: 69 additions & 4 deletions apps/ledger-live-mobile/e2e/bridge/client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,76 @@
import { Platform } from "react-native";
import { DescriptorEventType } from "@ledgerhq/hw-transport";
import invariant from "invariant";
import { Subject } from "rxjs";
import { Subject, Observable } from "rxjs";
import { AccountRaw } from "@ledgerhq/types-live";
import { ConnectAppEvent } from "@ledgerhq/live-common/hw/connectApp";
import { Event as AppEvent } from "@ledgerhq/live-common/hw/actions/app";
import { ConnectManagerEvent } from "@ledgerhq/live-common/hw/connectManager";
import { store } from "../../src/context/LedgerStore";
import { importSettings } from "../../src/actions/settings";
import { setAccounts } from "../../src/actions/accounts";
import { acceptGeneralTermsLastVersion } from "../../src/logic/terms";
import accountModel from "../../src/logic/accountModel";
import { navigate } from "../../src/rootnavigation";
import { SettingsState } from "../../src/reducers/types";
import { AccountRaw } from "@ledgerhq/types-live";
import { BleState, SettingsState } from "../../src/reducers/types";
import { importBle } from "../../src/actions/ble";
import { InstallLanguageEvent } from "@ledgerhq/live-common/hw/installLanguage";
import { LoadImageEvent } from "@ledgerhq/live-common/hw/staxLoadImage";
import { SwapRequestEvent } from "@ledgerhq/live-common/exchange/swap/types";
import { FetchImageEvent } from "@ledgerhq/live-common/hw/staxFetchImage";
import { ExchangeRequestEvent } from "@ledgerhq/live-common/hw/actions/startExchange";
import { CompleteExchangeRequestEvent } from "@ledgerhq/live-common/exchange/platform/types";
import { RemoveImageEvent } from "@ledgerhq/live-common/hw/staxRemoveImage";
import { RenameDeviceEvent } from "@ledgerhq/live-common/hw/renameDevice";

export type MockDeviceEvent =
| ConnectAppEvent
| AppEvent
| ConnectManagerEvent
| InstallLanguageEvent
| LoadImageEvent
| FetchImageEvent
| ExchangeRequestEvent
| SwapRequestEvent
| RemoveImageEvent
| RenameDeviceEvent
| CompleteExchangeRequestEvent
| { type: "complete" };

const mockDeviceEventSubject = new Subject<MockDeviceEvent>();

// these adaptor will filter the event type to satisfy typescript (workaround), it works because underlying exec usage will ignore unknown event type
export const connectAppExecMock = (): Observable<ConnectAppEvent> =>
mockDeviceEventSubject as Observable<ConnectAppEvent>;
export const initSwapExecMock = (): Observable<SwapRequestEvent> =>
mockDeviceEventSubject as Observable<SwapRequestEvent>;
export const startExchangeExecMock = (): Observable<ExchangeRequestEvent> =>
mockDeviceEventSubject as Observable<ExchangeRequestEvent>;
export const connectManagerExecMock = (): Observable<ConnectManagerEvent> =>
mockDeviceEventSubject as Observable<ConnectManagerEvent>;
export const staxFetchImageExecMock = (): Observable<FetchImageEvent> =>
mockDeviceEventSubject as Observable<FetchImageEvent>;
export const staxLoadImageExecMock = (): Observable<LoadImageEvent> =>
mockDeviceEventSubject as Observable<LoadImageEvent>;
export const staxRemoveImageExecMock = (): Observable<RemoveImageEvent> =>
mockDeviceEventSubject as Observable<RemoveImageEvent>;
export const installLanguageExecMock = (): Observable<InstallLanguageEvent> =>
mockDeviceEventSubject as Observable<InstallLanguageEvent>;
export const completeExchangeExecMock = (): Observable<CompleteExchangeRequestEvent> =>
mockDeviceEventSubject as Observable<CompleteExchangeRequestEvent>;
export const renameDeviceExecMock = (): Observable<RenameDeviceEvent> =>
mockDeviceEventSubject as Observable<RenameDeviceEvent>;

export type MessageData =
| {
type: DescriptorEventType;
payload: { id: string; name: string; serviceUUID: string };
}
| { type: "open" }
| {
type: "mockDeviceEvent";
payload: MockDeviceEvent[];
}
| { type: "acceptTerms" }
| { type: "navigate"; payload: string }
| { type: "importSettings"; payload: Partial<SettingsState> }
Expand All @@ -27,6 +81,10 @@ export type MessageData =
version: number;
}[];
}
| {
type: "importBle";
payload: BleState;
}
| {
type: "setGlobals";
payload: { [key: string]: unknown };
Expand Down Expand Up @@ -57,7 +115,6 @@ function onMessage(event: WebSocketMessageEvent) {
invariant(msg.type, "[E2E Bridge Client]: type is missing");

log(`Message\n${JSON.stringify(msg, null, 2)}`);
log(`Message type: ${msg.type}`);

e2eBridgeClient.next(msg);

Expand All @@ -75,10 +132,18 @@ function onMessage(event: WebSocketMessageEvent) {
store.dispatch(setAccounts(msg.payload.map(accountModel.decode)));
break;
}
case "mockDeviceEvent": {
msg.payload.forEach(e => mockDeviceEventSubject.next(e));
break;
}
case "importSettings": {
store.dispatch(importSettings(msg.payload));
break;
}
case "importBle": {
store.dispatch(importBle(msg.payload));
break;
}
case "navigate":
navigate(msg.payload, {});
break;
Expand Down
26 changes: 25 additions & 1 deletion apps/ledger-live-mobile/e2e/bridge/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Server } from "ws";
import path from "path";
import fs from "fs";
import { toAccountRaw } from "@ledgerhq/live-common/account/index";
import { NavigatorName } from "../../src/const";
import { Subject } from "rxjs";
import { MessageData } from "./client";
import { MessageData, MockDeviceEvent } from "./client";
import { BleState } from "../../src/reducers/types";
import { Account } from "@ledgerhq/types-live";

type ServerData = {
type: "walletAPIResponse";
Expand Down Expand Up @@ -48,13 +51,34 @@ export function loadConfig(fileName: string, agreed: true = true): void {
}
}

export function loadBleState(bleState: BleState) {
postMessage({ type: "importBle", payload: bleState });
}

export function loadAccounts(accounts: Account[]) {
postMessage({
type: "importAccounts",
payload: accounts.map(account => ({
version: 1,
data: toAccountRaw(account),
})),
});
}

function navigate(name: string) {
postMessage({
type: "navigate",
payload: name,
});
}

export function mockDeviceEvent(...args: MockDeviceEvent[]) {
postMessage({
type: "mockDeviceEvent",
payload: args,
});
}

export function addDevices(
deviceNames: string[] = ["Nano X de David", "Nano X de Arnaud", "Nano X de Didier Duchmol"],
): string[] {
Expand Down
2 changes: 1 addition & 1 deletion apps/ledger-live-mobile/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { by, device, element, waitFor } from "detox";
import { by, element, waitFor, device } from "detox";
import { Direction } from "react-native-modal";

const DEFAULT_TIMEOUT = 60000;
Expand Down
216 changes: 216 additions & 0 deletions apps/ledger-live-mobile/e2e/models/DeviceAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import BigNumber from "bignumber.js";
import { DeviceModelId } from "@ledgerhq/types-devices";
import {
deviceInfo155 as deviceInfo,
deviceInfo210lo5,
mockListAppsResult as innerMockListAppResult,
} from "@ledgerhq/live-common/apps/mock";
import { AppOp } from "@ledgerhq/live-common/apps/types";
import { AppType, DeviceInfo } from "@ledgerhq/types-live/lib/manager";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import { Transaction } from "@ledgerhq/live-common/generated/types";
import { waitForElementById, tapById, delay } from "../helpers";
import { mockDeviceEvent } from "../bridge/server";
import { DeviceLike } from "../../src/reducers/types";

const mockListAppsResult = (
appDesc: string,
installedDesc: string,
deviceInfo: DeviceInfo,
deviceModelId?: DeviceModelId,
) => {
const result = innerMockListAppResult(appDesc, installedDesc, deviceInfo, deviceModelId);
Object.keys(result?.appByName).forEach(key => {
result.appByName[key] = { ...result.appByName[key], type: AppType.currency };
});
return result;
};

// this implement mock for the "legacy" device action (the one working with live-common/lib/hw/actions/*)
export default class DeviceAction {
deviceLike: DeviceLike;
device: Device;

constructor(deviceLike: DeviceLike) {
this.deviceLike = deviceLike;
this.device = this.deviceLikeToDevice(deviceLike);
}

deviceLikeToDevice(d: DeviceLike): Device {
return {
deviceId: d.id,
deviceName: d.name,
modelId: d.modelId,
wired: false,
};
}

async selectMockDevice() {
const elId = "device-item-" + this.deviceLike.id;
await waitForElementById(elId);
await tapById(elId);
}

async waitForSpinner() {
await waitForElementById("device-action-loading");
}

async openApp() {
await this.waitForSpinner();
mockDeviceEvent({ type: "opened" });
}

async genuineCheck(appDesc = "Bitcoin", installedDesc = "Bitcoin") {
await this.waitForSpinner();

const { device } = this;
const result = mockListAppsResult(appDesc, installedDesc, deviceInfo);
mockDeviceEvent(
{
type: "deviceChange",
device,
},
{
type: "listingApps",
deviceInfo,
},
{
type: "result",
result,
},
{ type: "complete" },
);
}

async accessManager(
appDesc = "Bitcoin,Tron,Litecoin,Ethereum,Ripple,Stellar",
installedDesc = "Bitcoin,Litecoin,Ethereum (outdated)",
) {
await this.waitForSpinner();

const { device } = this;

const result = mockListAppsResult(appDesc, installedDesc, deviceInfo, device.modelId);
mockDeviceEvent(
{
type: "deviceChange",
device,
},
{
type: "listingApps",
deviceInfo,
},
{
type: "result",
result,
},
{ type: "complete" },
);
}

async accessManagerWithL10n(
appDesc = "Bitcoin,Tron,Litecoin,Ethereum,Ripple,Stellar",
installedDesc = "Bitcoin,Litecoin,Ethereum (outdated)",
) {
await this.waitForSpinner();

const { device } = this;

const result = mockListAppsResult(appDesc, installedDesc, deviceInfo210lo5);
mockDeviceEvent(
{
type: "deviceChange",
device,
},
{
type: "listingApps",
deviceInfo: deviceInfo210lo5,
},
{
type: "result",
result,
},
{ type: "complete" },
);
}

async complete() {
mockDeviceEvent({ type: "complete" });
}

async initiateLanguageInstallation() {
mockDeviceEvent({ type: "devicePermissionRequested" });
}

async add50ProgressToLanguageInstallation() {
mockDeviceEvent({ type: "progress", progress: 0.5 });
}

async installSetOfAppsMocked(
progress: number,
itemProgress: number,
currentAppOp: AppOp,
installQueue: string[],
) {
mockDeviceEvent({
type: "inline-install",
progress: progress,
itemProgress: itemProgress,
currentAppOp: currentAppOp,
installQueue: installQueue,
});
}

async resolveDependenciesMocked(installQueue: string[]) {
mockDeviceEvent({
type: "listed-apps",
installQueue: installQueue,
});
}

async completeLanguageInstallation() {
mockDeviceEvent({ type: "languageInstalled" });
}

async requestImageLoad() {
mockDeviceEvent({ type: "loadImagePermissionRequested" });
}

async loadImageWithProgress(progress: number) {
mockDeviceEvent({ type: "progress", progress });
}

async requestImageCommit() {
mockDeviceEvent({ type: "commitImagePermissionRequested" });
}

async confirmImageLoaded(imageSize: number, imageHash: string) {
mockDeviceEvent({ type: "imageLoaded", imageSize, imageHash });
}

async initiateSwap(estimatedFees: BigNumber) {
mockDeviceEvent({ type: "opened" });
await delay(2000); // enough time to allow the UI to switch from one action to another
mockDeviceEvent({ type: "init-swap-requested", estimatedFees });
}

async confirmSwap(transaction: Transaction) {
mockDeviceEvent(
{
type: "init-swap-result",
initSwapResult: {
transaction,
swapId: "12345",
},
},
{
type: "complete",
},
);
}

async silentSign() {
await this.waitForSpinner();
mockDeviceEvent({ type: "opened" }, { type: "complete" });
}
}
3 changes: 3 additions & 0 deletions apps/ledger-live-mobile/e2e/models/accounts/accountsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ export default class accountsPage {
async waitForAccountsPageToLoad() {
await waitForElementById("accounts-list-title");
}
async waitForAccountsCoinPageToLoad(coin: string) {
await waitForElementById(`accounts-title-${coin}`);
}
}
Loading

0 comments on commit 7817773

Please sign in to comment.