From 461b1e04a2a6289ed3ced8d1715f0f09e1cdcec5 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 24 May 2023 12:10:06 +0600 Subject: [PATCH] test: record Ledger api to replay it in e2e tests [wip] --- src/main.js | 3 + src/test.js | 118 +++++++++++++++++++++++++++++++++++ tests/e2e/specs/ledger.cy.js | 92 +++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 src/test.js create mode 100644 tests/e2e/specs/ledger.cy.js diff --git a/src/main.js b/src/main.js index 57ffe03ea..ad9ad4f5a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import 'normalize.css'; +import { t } from './test'; import Vue from 'vue'; import './lib/setGlobalPolyfills'; import store from './store'; @@ -14,3 +15,5 @@ if (process.env.NODE_ENV === 'development') { if (RUNNING_IN_POPUP) import(/* webpackChunkName: "popup" */ './popup'); else if (!RUNNING_IN_FRAME) import(/* webpackChunkName: "ui" */ './ui'); /* eslint-enable no-unused-expressions */ + +console.log(t); diff --git a/src/test.js b/src/test.js new file mode 100644 index 000000000..ac4980649 --- /dev/null +++ b/src/test.js @@ -0,0 +1,118 @@ +import { isEqual } from 'lodash-es'; + +const NativePromise = window.Promise; + +const genId = () => Math.random().toString().slice(2); + +function logging(contentWindow, value, recordedActions) { + const res = wrapWithRecordingProxy(contentWindow, value, recordedActions); + console.log(value, 'wrapped to', res); + return res; +} + +function wrapWithRecordingProxy(contentWindow, value, recordedActions) { + switch (typeof value) { + case 'undefined': + case 'boolean': + case 'number': + case 'string': + case 'bigint': + return [value, { value }]; + case 'function': + case 'object': + if (value == null) return [value, { value }]; + + if (contentWindow.Buffer && value instanceof contentWindow.Buffer) { + return [value, { type: 'buffer', value: value.toString('base64') }]; + } + + if (value instanceof contentWindow.Uint8Array) { + return [value, { type: 'Uint8Array', value: Array.from(value) }]; + } + + if (value instanceof contentWindow.ArrayBuffer) { + return [value, { type: 'ArrayBuffer', value: Array.from(Uint8Array.from(value)) }]; + } + + if (value instanceof contentWindow.Error) { + return [value, { type: 'error', value: value.message }]; + } + + if (value instanceof contentWindow.Array) { + const items = value.map((i) => logging(contentWindow, i, recordedActions)); + return [items.map(([v]) => v), { type: 'array', value: items.map(([v, s]) => s) }]; + } + + if (value instanceof NativePromise || value instanceof Promise) { + const complexValue = { type: 'promise' }; + const val = value.then( + (value) => { + const [v, s] = logging(contentWindow, value, recordedActions); + complexValue.resolve = s; + return v; + }, + (error) => { + const [v, s] = logging(contentWindow, error, recordedActions); + complexValue.reject = s; + return v; + }, + ); + return [val, complexValue]; + } + + try { + const s = JSON.parse(JSON.stringify(value)); + if (isEqual(value, s)) return [value, { value: JSON.parse(JSON.stringify(value)) }]; + } catch {} + + const proxyId = genId(); + const proxy = new contentWindow.Proxy(value, { + ...Object.fromEntries([ + 'get', 'apply', 'has', 'construct', 'defineProperty', 'deleteProperty', 'getOwnPropertyDescriptor', + 'isExtensible', 'ownKeys', 'preventExtensions', 'set', 'setPrototypeOf', + ].map((name) => [name, () => { throw new Error(`${name} proxy request`); }])), + // getPrototypeOf + get(target, property, receiver) { + let value; + try { + value = contentWindow.Reflect.get(target, property, receiver); + } catch { + value = contentWindow.Reflect.get(target, property); + } + value = [ + 'getDevices', 'requestDevice', 'addEventListener', + 'transferOut', 'transferIn', 'open', 'close', 'claimInterface', 'reset', + ].includes(property) ? value.bind(target) : value; + const [wrapped, ser] = logging(contentWindow, value, recordedActions); + recordedActions.push({ proxyId, action: 'get', property, value: ser }); + console.log('get', ...arguments, 'result', wrapped); + return wrapped; + }, + apply(target, thisArg, argArray) { + const value = contentWindow.Reflect.apply(target, thisArg, argArray); + const [wrapped, ser] = logging(contentWindow, value, recordedActions); + recordedActions.push({ + proxyId, + action: 'apply', + arguments: argArray.map((arg) => logging(contentWindow, arg, recordedActions)[1]), + value: ser, + }); + console.log('apply', ...arguments, 'result', wrapped); + return wrapped; + } + }); + return [proxy, { type: 'proxy', id: proxyId }]; + default: + throw new Error(`Unsupported value type: ${typeof value}`); + } +} + +const contentWindow = window; +const recordedApi = []; + +const wrappedUsb = logging(contentWindow, contentWindow.navigator.usb, recordedApi); +contentWindow.Object.defineProperty(contentWindow.navigator, 'usb', { value: wrappedUsb[0] }); +console.log('contentWindow.navigator.usb', contentWindow.navigator.usb); +contentWindow.recordedApi = recordedApi; + +export const t = 10; diff --git a/tests/e2e/specs/ledger.cy.js b/tests/e2e/specs/ledger.cy.js new file mode 100644 index 000000000..302f8d187 --- /dev/null +++ b/tests/e2e/specs/ledger.cy.js @@ -0,0 +1,92 @@ +import { isEqual } from 'lodash-es'; + +const genId = () => Math.random().toString().slice(2); + +function logging(contentWindow, value, recordedActions) { + console.log('trying to wrap', value); + const res = wrapWithRecordingProxy(contentWindow, value, recordedActions); + console.log('wrapped to', res[1]); + return res; +} + +function wrapWithRecordingProxy(contentWindow, value, recordedActions) { + switch (typeof value) { + case 'undefined': + case 'boolean': + case 'number': + case 'string': + case 'bigint': + return [value, { value }]; + case 'function': + case 'object': + if (value == null) return [value, { value }]; + + if (value instanceof contentWindow.Buffer) { + return [value, { type: 'buffer', value: value.toString('base64') }]; + } + + if (value instanceof contentWindow.Error) { + return [value, { type: 'error', value: value.message }]; + } + + if (value instanceof contentWindow.Promise) { + const complexValue = { type: 'promise' }; + const val = value.then( + (value) => { + const [v, s] = logging(contentWindow, value, recordedActions); + complexValue.resolve = s; + return v; + }, + (error) => { + const [v, s] = logging(contentWindow, error, recordedActions); + complexValue.resolve = s; + return v; + }, + ); + return [val, complexValue]; + } + + try { + const s = JSON.parse(JSON.stringify(value)); + if (isEqual(value, s)) return [value, { value: JSON.parse(JSON.stringify(value)) }]; + } catch {} + + const proxyId = genId(); + const proxy = new contentWindow.Proxy(value, { + get(target, prop, receiver) { + const value = contentWindow.Reflect.get(target, prop, receiver); + recordedActions.push({ proxyId, action: 'get', value: logging(contentWindow, value, recordedActions) }); + return result; + }, + apply(target, thisArg, argArray) { + const value = contentWindow.Reflect.apply(target, thisArg, argArray); + recordedActions.push({ + proxyId, + action: 'apply', + arguments: argArray.map((arg) => logging(contentWindow, arg, recordedActions)[1]), + value: logging(contentWindow, value, recordedActions), + }); + return value; + } + }); + return [proxy, { type: 'proxy', id: proxyId }]; + default: + throw new Error(`Unsupported value type: ${typeof value}`); + } +} + +describe('Ledger HW', () => { + it('test', () => { + cy.visit('/settings', { isDesktop: true }); + cy.window().then((contentWindow) => { + const recordedApi = []; + const wrappedUsb = logging(contentWindow, contentWindow.navigator.usb, recordedApi); + // contentWindow.Object.defineProperty(contentWindow.navigator, 'usb', { value: wrappedUsb }); + contentWindow.Object.defineProperty(contentWindow.navigator, 'usb', { value: { test: 'qwerty' } }); + console.log('contentWindow.navigator.usb', contentWindow.navigator.usb); + contentWindow.recordedApi = recordedApi; + window.contentWindow = contentWindow; + }) + cy.pause(); + }); +});