diff --git a/.eslintrc.js b/.eslintrc.js index 13a1b4ee..47766486 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,11 +1,12 @@ module.exports = { env: { browser: true, - es6: true + es6: true, + jest: true }, extends: ['airbnb/base', 'plugin:prettier/recommended'], parserOptions: { - ecmaVersion: 2017, + ecmaVersion: 2018, sourceType: 'module' }, plugins: ['prettier'], @@ -24,5 +25,12 @@ module.exports = { 'no-nested-ternary': 'off', 'import/no-cycle': 'off', 'no-lonely-if': 'off' + }, + settings: { + 'import/resolver': { + 'babel-plugin-root-import': { + rootPathSuffix: 'src' + } + } } } diff --git a/README.md b/README.md index 38abf3f9..e3ea9784 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,16 @@ yarn add bnc-assist #### Script Tag The library uses [semantic versioning](https://semver.org/spec/v2.0.0.html). -The current version is 0.6.0. +The current version is 0.6.1. There are minified and non-minified versions. Put this script at the top of your `
` ```html - + - + ``` ### Initialize the Library diff --git a/babel.config.js b/babel.config.js index 253b2de3..e6e87389 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,12 +4,19 @@ module.exports = { '@babel/preset-env', { useBuiltIns: 'entry', + corejs: '2.0.0', modules: false, targets: '> 2%' } ] ], plugins: [ + [ + 'babel-plugin-root-import', + { + rootPathSuffix: 'src' + } + ], [ '@babel/plugin-transform-runtime', { @@ -18,7 +25,14 @@ module.exports = { regenerator: true, useESModules: false } - ] + ], + ['@babel/plugin-proposal-object-rest-spread'], + ['inline-import-data-uri', { + 'extensions': [ + '.png', + '.jpg' + ] + }] ], env: { test: { diff --git a/build.js b/build.js index 7d1c73c7..7dc89757 100644 --- a/build.js +++ b/build.js @@ -7,7 +7,6 @@ const resolve = require('rollup-plugin-node-resolve') const commonjs = require('rollup-plugin-commonjs') const { uglify } = require('rollup-plugin-uglify') const string = require('rollup-plugin-string') -const image = require('rollup-plugin-img') const json = require('rollup-plugin-json') const defaultPlugins = [ @@ -17,10 +16,6 @@ const defaultPlugins = [ string({ include: '**/*.css' }), - image({ - exclude: ['node_modules/**'], - limit: 51200 - }), resolve({ jsnext: true, main: true, diff --git a/internals/jestSetup.js b/internals/jestSetup.js new file mode 100644 index 00000000..c5d79c0f --- /dev/null +++ b/internals/jestSetup.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import MockDate from 'mockdate' + +// Mock Date.now() +MockDate.set('1/1/2010') diff --git a/jest.config.js b/jest.config.js index dc1a2764..57c173fc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -60,9 +60,7 @@ module.exports = { }, // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + moduleDirectories: ['node_modules', 'src'], // An array of file extensions your modules use // moduleFileExtensions: [ @@ -121,7 +119,7 @@ module.exports = { // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], + setupFiles: ['./internals/jestSetup.js'], // The path to a module that runs some code to configure or set up the testing framework before each test // setupTestFrameworkScriptFile: null, diff --git a/package.json b/package.json index 45138ce4..ee7cb6c9 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "bnc-assist", - "version": "0.6.0", + "version": "0.6.1", "description": "Blocknative Assist js library for Dapp developers", "main": "lib/assist.min.js", "scripts": { "build": "yarn lint && yarn test && node build.js", + "build:dev": "node build.js", "lint": "eslint src/ --fix", - "test": "jest" + "test": "TZ=Europe/Paris jest" }, "husky": { "hooks": { @@ -29,24 +30,29 @@ "homepage": "https://github.com/blocknative/assist#readme", "dependencies": { "@babel/polyfill": "^7.0.0", - "babel-eslint": "^10.0.1", "bluebird": "^3.5.3", "bowser": "^2.0.0-beta.3", + "core-js": "2", "uuid": "^3.3.2" }, "devDependencies": { "@babel/core": "^7.2.2", + "@babel/plugin-proposal-object-rest-spread": "^7.4.3", "@babel/plugin-transform-runtime": "^7.1.0", "@babel/preset-env": "^7.1.6", "@babel/runtime": "^7.1.5", "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^10.0.1", "babel-jest": "^23.6.0", "babel-plugin-external-helpers": "^6.22.0", + "babel-plugin-inline-import-data-uri": "^1.0.1", + "babel-plugin-root-import": "^6.1.0", "dom-testing-library": "^3.16.2", "eslint": "^5.9.0", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^3.3.0", - "eslint-plugin-import": "^2.14.0", + "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", + "eslint-plugin-import": "^2.17.2", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-prettier": "^3.0.0", "husky": "^1.3.1", @@ -54,13 +60,14 @@ "jest-cli": "^23.6.0", "jest-css-modules": "^1.1.0", "jest-dom": "^3.0.0", + "mock-socket": "^8.0.5", + "mockdate": "^2.0.2", "prettier": "^1.15.2", "regenerator-runtime": "^0.13.1", "rollup": "^0.67.3", "rollup-plugin-babel": "^4.0.3", "rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-eslint": "^5.0.0", - "rollup-plugin-img": "^1.1.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^3.4.0", "rollup-plugin-string": "^2.0.2", @@ -70,4 +77,4 @@ "eslintIgnore": [ "package.json" ] -} \ No newline at end of file +} diff --git a/src/__integration-tests__/initialization/initialization.test.js b/src/__integration-tests__/initialization/initialization.test.js new file mode 100644 index 00000000..c84702bc --- /dev/null +++ b/src/__integration-tests__/initialization/initialization.test.js @@ -0,0 +1,237 @@ +/* + * Runs through variations of how the library can be initialized. + * For each variation assert that expected side effects occur. + */ + +import bowser from 'bowser' +import da from '~/js' +import * as events from '~/js/helpers/events' +import { state, initialState, updateState } from '~/js/helpers/state' +import { storeTransactionQueue } from '~/js/helpers/storage' +import { version as packageVersion } from '../../../package.json' + +describe('init is called', () => { + describe('with a basic valid config', () => { + const config = { dappId: '123', networkId: '1' } + test('state.version should be set correctly', () => { + da.init(config) + expect(state.version).toEqual(packageVersion) + }) + test('state.userAgent should be set', () => { + da.init(config) + expect(state.userAgent).toHaveProperty('browser') + }) + test('an empty iframe should be created and accessible via state', async () => { + da.init(config) + // iframe exists + expect(document.body.innerHTML.includes('iframe')).toEqual(true) + expect(state).toHaveProperty('iframe') + expect(state).toHaveProperty('iframeDocument') + expect(state).toHaveProperty('iframeWindow') + + // after all promises are resolved the iframe doesn't contain any elements + await new Promise(res => setImmediate(() => res())) + expect(state.iframeDocument.body.innerHTML.includes('<')).toBeFalsy() + }) + // skip due to having issues mocking window.addEventListener + xtest(`eventListener is added for 'unload'`, () => { + da.init(config) + const spy = jest.spyOn(window, 'addEventListener') + expect(spy).toHaveBeenCalledWith('unload', storeTransactionQueue) + spy.mockRestore() + }) + test('event initState should be emitted with expected payload', async () => { + const handleEventSpy = jest.spyOn(events, 'handleEvent') + const assistInstance = da.init(config) + const initState = await assistInstance.getState() + expect(events.handleEvent).toHaveBeenCalledWith({ + eventCode: 'initState', + categoryCode: 'initialize', + state: { + accessToAccounts: initState.accessToAccounts, + correctNetwork: initState.correctNetwork, + legacyWallet: initState.legacyWallet, + legacyWeb3: initState.legacyWeb3, + minimumBalance: initState.minimumBalance, + mobileDevice: initState.mobileDevice, + modernWallet: initState.modernWallet, + modernWeb3: initState.modernWeb3, + walletEnabled: initState.walletEnabled, + walletLoggedIn: initState.walletLoggedIn, + web3Wallet: initState.web3Wallet, + validBrowser: initState.validBrowser + } + }) + handleEventSpy.mockRestore() + }) + test('should return object with expected properties', () => { + const initalizedAssist = da.init(config) + expect(initalizedAssist).toHaveProperty('onboard') + expect(initalizedAssist).toHaveProperty('Contract') + expect(initalizedAssist).toHaveProperty('Transaction') + expect(initalizedAssist).toHaveProperty('getState') + }) + + describe('when onboarding is in progress from a previous session', () => { + beforeEach(() => { + window.localStorage.setItem('onboarding', 'true') + }) + test('onboard modal should appear', async () => { + da.init(config) + // wait for promises to resolve + await new Promise(res => setImmediate(() => res())) + + // iframe should exist in DOM + expect(document.body.innerHTML.includes('iframe')).toBeTruthy() + // iframe should display a modal + expect( + state.iframeDocument.body.innerHTML.includes('bn-onboard-modal') + ).toBeTruthy() + }) + }) + + describe('when transactionQueue is set in localStorage', () => { + const tx = { transaction: { startTime: Date.now() } } + beforeEach(() => { + window.localStorage.setItem('transactionQueue', JSON.stringify([tx])) + }) + test('state.transactionQueue should be initialized', () => { + da.init(config) + expect(state.transactionQueue).toEqual([tx]) + }) + test('transactions older than 150000ms should be ignored', () => { + const oldTx = { transaction: { startTime: Date.now() - 150001 } } + window.localStorage.setItem( + 'transactionQueue', + JSON.stringify([tx, oldTx]) + ) + da.init(config) + expect(state.transactionQueue).toEqual([tx]) + }) + }) + + describe('from a mobile device', () => { + let getParserSpy + beforeAll(() => { + // Set platform to mobile + const userAgent = { platform: { type: 'mobile' } } + getParserSpy = jest + .spyOn(bowser, 'getParser') + .mockImplementation(() => ({ + parse: () => ({ parsedResult: userAgent }), + satisfies: () => false + })) + }) + afterAll(() => { + getParserSpy.mockRestore() + }) + test('state.mobileDevice should be set to true', () => { + da.init(config) + expect(state.mobileDevice).toEqual(true) + }) + + describe('mobileBlocked is true in config', () => { + const configMobBlocked = { ...config, mobileBlocked: true } + test('state.validBrowser should be false', () => { + da.init(configMobBlocked) + expect(state.validBrowser).toEqual(false) + }) + test('event mobileBlocked should be emitted', () => { + const handleEventSpy = jest.spyOn(events, 'handleEvent') + da.init(configMobBlocked) + expect(handleEventSpy).toHaveBeenCalledWith({ + eventCode: 'mobileBlocked', + categoryCode: 'initialize' + }) + handleEventSpy.mockRestore() + }) + }) + }) + }) + + describe('without a config argument', () => { + it('should throw', () => { + expect(() => { + da.init() + }).toThrow() + }) + it('event initFail should be emitted correctly', () => { + const handleEventSpy = jest.spyOn(events, 'handleEvent') + expect(() => { + da.init() + }).toThrow() + expect(handleEventSpy).toHaveBeenCalledWith({ + eventCode: 'initFail', + categoryCode: 'initialize', + reason: 'A config object is needed to initialize assist' + }) + handleEventSpy.mockRestore() + }) + }) + + describe(`with a config missing 'dappId'`, () => { + const config = { networkId: 1 } + test(`should throw`, () => { + expect(() => { + da.init(config) + }).toThrow() + }) + test(`event initFail should be emitted correctly`, () => { + const handleEventSpy = jest.spyOn(events, 'handleEvent') + expect(() => { + da.init(config) + }).toThrow() + expect(events.handleEvent).toHaveBeenCalledWith({ + eventCode: 'initFail', + categoryCode: 'initialize', + reason: 'No API key provided to init function' + }) + handleEventSpy.mockRestore() + }) + test(`state.validApiKey should be set to false`, () => { + expect(() => { + da.init(config) + }).toThrow() + expect(state.validApiKey).toEqual(false) + }) + }) + + describe('with headlessMode: true', () => { + const config = { dappId: '123', networkId: '1', headlessMode: true } + test('no iframe should be created', () => { + da.init(config) + expect(document.body.innerHTML.includes('iframe')).toEqual(false) + }) + }) + + describe('with a legacy web3 object', () => { + const mockLegacyWeb3 = { version: { api: '0.20' } } + const config = { dappId: '123', networkId: '1', web3: mockLegacyWeb3 } + test('web3 state should be set correctly', () => { + da.init(config) + expect(state.legacyWeb3).toEqual(true) + expect(state.modernWeb3).toEqual(false) + expect(state.web3Version).toEqual('0.20') + expect(state.web3Instance).toEqual(mockLegacyWeb3) + }) + }) + + describe('with a modern web3 object', () => { + const mockModernWeb3 = { version: '1.20' } + const config = { dappId: '123', networkId: '1', web3: mockModernWeb3 } + test('web3 state should be set correctly', () => { + da.init(config) + expect(state.legacyWeb3).toEqual(false) + expect(state.modernWeb3).toEqual(true) + expect(state.web3Version).toEqual('1.20') + expect(state.web3Instance).toEqual(mockModernWeb3) + }) + }) +}) + +afterEach(() => { + document.body.innerHTML = '' + updateState(initialState) + window.localStorage.clear() + jest.clearAllMocks() +}) diff --git a/src/__integration-tests__/ui-rendering/__snapshots__/index.test.js.snap b/src/__integration-tests__/ui-rendering/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..1e88c8e3 --- /dev/null +++ b/src/__integration-tests__/ui-rendering/__snapshots__/index.test.js.snap @@ -0,0 +1,2183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dom-rendering event activeContract-txAwaitingApproval should trigger correct DOM render [Custom state 0] 1`] = ` +" + + " +`; + +exports[`dom-rendering event activeContract-txAwaitingApproval should trigger correct DOM render [Custom state 1] 1`] = ` +" + + " +`; + +exports[`dom-rendering event activeContract-txConfirmReminder should trigger correct DOM render [Custom state 0] 1`] = ` +" + +Please confirm your transaction to continue (hint: the transaction window may be behind your browser)
+ +You have successfully completed all the steps necessary to use this application. Welcome to the world of blockchain.
+You have successfully completed all the steps necessary to use this application. Welcome to the world of blockchain.
+We use a product called MetaMask to manage everything you need to interact with a blockchain application like this one. MetaMask is free, installs right into your browser, hyper secure, and can be used for any other blockchain application you may want to use. Get MetaMask now
+To use this feature you’ll need to be set up and ready to use the blockchain. This onboarding guide will walk you through each step of the process. It won’t take long and at any time you can come back and pick up where you left off.
+To use this feature you’ll need to be set up and ready to use the blockchain. This onboarding guide will walk you through each step of the process. It won’t take long and at any time you can come back and pick up where you left off.
+Please confirm your transaction to continue (hint: the transaction window may be behind your browser)
+ +This Dapp is not supported in Chrome. Please visit us in one of the following browsers. Thank You!
+
+
+
+
+ Chrome
+
+
+
+
+ Firefox
+
+
Powered by + + + +
+You have successfully completed all the steps necessary to use this application. Welcome to the world of blockchain.
+You have successfully completed all the steps necessary to use this application. Welcome to the world of blockchain.
+We use a product called MetaMask to manage everything you need to interact with a blockchain application like this one. MetaMask is free, installs right into your browser, hyper secure, and can be used for any other blockchain application you may want to use. Get MetaMask now
+To use this feature you’ll need to be set up and ready to use the blockchain. This onboarding guide will walk you through each step of the process. It won’t take long and at any time you can come back and pick up where you left off.
+To use this feature you’ll need to be set up and ready to use the blockchain. This onboarding guide will walk you through each step of the process. It won’t take long and at any time you can come back and pick up where you left off.
+