diff --git a/cypress/integration/transaction_view.spec.ts b/cypress/integration/transaction_view.spec.ts index 6a7c9ea83c..ce94d22f6e 100644 --- a/cypress/integration/transaction_view.spec.ts +++ b/cypress/integration/transaction_view.spec.ts @@ -1,14 +1,60 @@ /// +import ITransaction from '../../src/interfaces/ITransaction'; +import ISmartContract from '../../src/interfaces/ISmartContract'; chai.should(); describe('Cypress', () => { + const transactionOne: ITransaction = { + name: 'transactionOne', + parameters: [{ + description: '', + name: 'name', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const transactionTwo: ITransaction = { + name: 'transactionTwo', + parameters: [{ + description: '', + name: 'size', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const greenContract: ISmartContract = { + name: 'greenContract', + version: '0.0.1', + channel: 'mychannel', + label: 'greenContract@0.0.1', + transactions: [transactionOne, transactionTwo], + namespace: 'GreenContract' + }; + + const blueContract: ISmartContract = { + name: 'blueContract', + version: '0.0.1', + channel: 'mychannel', + label: 'blueContract@0.0.1', + transactions: [transactionOne, transactionTwo], + namespace: 'BlueContract' + }; + describe('Transaction home screen', () => { - const mockMessage: {path: string, state: {smartContracts: Array, activeSmartContract: string}} = { + const mockMessage: {path: string, state: {smartContracts: Array, activeSmartContract: ISmartContract}} = { path: 'transaction', state: { - smartContracts: ['greenContract@0.0.1', 'blueContract@0.0.1'], - activeSmartContract: 'greenContract@0.0.1' + smartContracts: [greenContract, blueContract], + activeSmartContract: greenContract } }; @@ -58,21 +104,52 @@ describe('Cypress', () => { beforeEach(() => { - const mockMessage: {path: string, state: {smartContracts: Array, activeSmartContract: string}} = { + const mockMessage: {path: string, state: {smartContracts: Array, activeSmartContract: ISmartContract}} = { path: 'transaction/create', state: { - smartContracts: ['greenContract@0.0.1', 'blueContract@0.0.1'], - activeSmartContract: 'greenContract@0.0.1' + smartContracts: [greenContract, blueContract], + activeSmartContract: greenContract } }; - cy.visit('build/index.html').then((window: Window) => { window.postMessage(mockMessage, '*'); }); }); - it('is a transaction create screen', () => { - expect(true).to.equal(true); + it('generates appropriate arguments when a transaction is selected', () => { + cy.get('#transaction-name-select').select('transactionOne'); + cy.get('#arguments-text-area') + .invoke('val') + .then((text: JQuery): void => { + it('is a transaction create screen', () => { + expect(text).to.equal('name: '); + }); + }); + }); + + it('replaces generated arguments when a new transaction is selected', () => { + cy.get('#transaction-name-select').select('transactionOne'); + cy.get('#arguments-text-area') + .invoke('val') + .then((text: JQuery): void => { + it('is a transaction create screen', () => { + expect(text).to.equal('name: '); + }); + }); + + cy.get('#transaction-name-select').select('transactionTwo'); + cy.get('#arguments-text-area') + .invoke('val') + .then((text: JQuery): void => { + it('is a transaction create screen', () => { + expect(text).to.equal('size: '); + }); + }); + }); + + it('can navigate back to the home screen', () => { + cy.get('.titles-container > span').click(); + cy.url().should('include', '/transaction'); }); }); }); diff --git a/enzyme/tests/App.test.tsx b/enzyme/tests/App.test.tsx new file mode 100644 index 0000000000..59efdc1f6b --- /dev/null +++ b/enzyme/tests/App.test.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import App from '../../src/App'; +import chai from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import ISmartContract from '../../src/interfaces/ISmartContract'; +import ITransaction from '../../src/interfaces/ITransaction'; +import Utils from '../../src/Utils'; +chai.should(); +chai.use(sinonChai); + +describe('App', () => { + + let mySandBox: sinon.SinonSandbox; + + const mockTxn: ITransaction = { + name: 'mockTxn', + parameters: [{ + description: '', + name: 'name', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const greenContract: ISmartContract = { + name: 'greenContract', + version: '0.0.1', + channel: 'mychannel', + label: 'greenContract@0.0.1', + transactions: [mockTxn], + namespace: 'GreenContract' + }; + + const blueContract: ISmartContract = { + name: 'blueContract', + version: '0.0.1', + channel: 'mychannel', + label: 'blueContract@0.0.1', + transactions: [mockTxn], + namespace: 'BlueContract' + }; + + const mockState: { smartContracts: Array, activeSmartContract: ISmartContract } = { + smartContracts: [greenContract, blueContract], + activeSmartContract: greenContract + }; + + beforeEach(async () => { + mySandBox = sinon.createSandbox(); + }); + + afterEach(async () => { + mySandBox.restore(); + }); + + it('should redirect to the transaction home view', async () => { + const component: any = mount(); + + const msg: MessageEvent = new MessageEvent('message', { + data: { + path: '/transaction', + state: mockState + } + }); + dispatchEvent(msg); + expect(component.state().redirectPath).toBe('/transaction'); + }); + + it('should redirect to the transaction create view', async () => { + const component: any = mount(); + + const msg: MessageEvent = new MessageEvent('message', { + data: { + path: '/transaction/create', + state: { + smartContracts: [greenContract, blueContract], + activeSmartContract: greenContract + } + } + }); + dispatchEvent(msg); + expect(component.state().redirectPath).toBe('/transaction/create'); + }); + + it('does not overwrite state when redirecting to a different page', async () => { + const component: any = mount(); + + const msg: MessageEvent = new MessageEvent('message', { + data: { + path: '/transaction', + state: mockState + } + }); + dispatchEvent(msg); + expect(component.state().childState).toBe(mockState); + + Utils.changeRoute('/transaction/create'); + expect(component.state().childState).toBe(mockState); + }); + + it('updates the state correctly when switching smart contracts', async () => { + const component: any = mount(); + + const msg: MessageEvent = new MessageEvent('message', { + data: { + path: '/transaction', + state: mockState + } + }); + dispatchEvent(msg); + expect(component.state().childState.activeSmartContract).toBe(greenContract); + + component.instance().switchSmartContract('blueContract@0.0.1'); + expect(component.state().childState.activeSmartContract).toBe(blueContract); + }); + +}); diff --git a/enzyme/tests/TransactionCreate.test.tsx b/enzyme/tests/TransactionCreate.test.tsx index 396d76cb2e..43c7fb1a37 100644 --- a/enzyme/tests/TransactionCreate.test.tsx +++ b/enzyme/tests/TransactionCreate.test.tsx @@ -1,18 +1,61 @@ - +// tslint:disable no-unused-expression import React from 'react'; import renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; import TransactionCreate from '../../src/components/TransactionCreate/TransactionCreate'; import chai from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; +import ITransaction from '../../src/interfaces/ITransaction'; +import ISmartContract from '../../src/interfaces/ISmartContract'; +import Utils from '../../src/Utils'; chai.should(); chai.use(sinonChai); describe('TransactionCreate component', () => { let mySandbox: sinon.SinonSandbox; + let changeRouteStub: sinon.SinonStub; + let getTransactionArgumentsSpy: sinon.SinonSpy; + + const transactionOne: ITransaction = { + name: 'transactionOne', + parameters: [{ + description: '', + name: 'name', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const transactionTwo: ITransaction = { + name: 'transactionTwo', + parameters: [{ + description: '', + name: 'size', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const greenContract: ISmartContract = { + name: 'greenContract', + version: '0.0.1', + channel: 'mychannel', + label: 'greenContract@0.0.1', + transactions: [transactionOne, transactionTwo], + namespace: 'GreenContract' + }; beforeEach(async () => { mySandbox = sinon.createSandbox(); + changeRouteStub = mySandbox.stub(Utils, 'changeRoute').resolves(); + getTransactionArgumentsSpy = mySandbox.spy(TransactionCreate.prototype, 'getTransactionArguments'); }); afterEach(async () => { @@ -21,8 +64,38 @@ describe('TransactionCreate component', () => { it('should render the expected snapshot', async () => { const component: any = renderer - .create() + .create() .toJSON(); expect(component).toMatchSnapshot(); }); + + it('redirects back to the home page when the appropriate link is clicked on', async () => { + const component: any = mount(); + component.find('.titles-container > span').simulate('click'); + changeRouteStub.should.have.been.called; + }); + + it('generates transaction arguments when an option from the transaction select is chosen', async () => { + const component: any = mount(); + expect(component.state().transactionArguments).toBe(''); + component.find('select').at(0).prop('onChange')( { currentTarget: { value: 'transactionOne' } }); + getTransactionArgumentsSpy.should.have.been.called; + expect(component.state().transactionArguments).toBe('name: \n'); + }); + + it('does not generate arguments in the event that the chosen transaction doesn\'t exist', async () => { + const component: any = mount(); + expect(component.state().transactionArguments).toBe(''); + component.find('select').at(0).prop('onChange')( { currentTarget: { value: 'anotherTransaction' } }); + getTransactionArgumentsSpy.should.have.been.called; + expect(component.state().transactionArguments).toBe(''); + }); + + it('updates when the user types in the textarea', async () => { + const component: any = mount(); + expect(component.state().transactionArguments).toBe(''); + component.find('textarea').prop('onChange')( { currentTarget: { value: 'hello' } } ); + expect(component.state().transactionArguments).toBe('hello'); + }); + }); diff --git a/enzyme/tests/TransactionHome.test.tsx b/enzyme/tests/TransactionHome.test.tsx index 0af5d9e52c..7d6fdb67b9 100644 --- a/enzyme/tests/TransactionHome.test.tsx +++ b/enzyme/tests/TransactionHome.test.tsx @@ -3,6 +3,8 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import TransactionHome from '../../src/components/TransactionHome/TransactionHome'; +import ITransaction from '../../src/interfaces/ITransaction'; +import ISmartContract from '../../src/interfaces/ISmartContract'; import chai from 'chai'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; @@ -12,16 +14,47 @@ chai.use(sinonChai); describe('TransactionHome component', () => { let mySandBox: sinon.SinonSandbox; - let switchSmartContractSpy: sinon.SinonSpy; + let switchSmartContractStub: sinon.SinonStub; - const mockState: {smartContracts: Array, activeSmartContract: string} = { - smartContracts: ['greenContract@0.0.1', 'blueContract@0.0.1'], - activeSmartContract: 'greenContract@0.0.1' + const mockTxn: ITransaction = { + name: 'mockTxn', + parameters: [{ + description: '', + name: 'name', + schema: {} + }], + returns: { + type: '' + }, + tag: ['submit'] + }; + + const greenContract: ISmartContract = { + name: 'greenContract', + version: '0.0.1', + channel: 'mychannel', + label: 'greenContract@0.0.1', + transactions: [mockTxn], + namespace: 'GreenContract' + }; + + const blueContract: ISmartContract = { + name: 'blueContract', + version: '0.0.1', + channel: 'mychannel', + label: 'blueContract@0.0.1', + transactions: [mockTxn], + namespace: 'BlueContract' + }; + + const mockState: {smartContracts: Array, activeSmartContract: ISmartContract} = { + smartContracts: [greenContract, blueContract], + activeSmartContract: greenContract }; beforeEach(async () => { mySandBox = sinon.createSandbox(); - switchSmartContractSpy = mySandBox.spy(TransactionHome.prototype, 'switchSmartContract'); + switchSmartContractStub = mySandBox.stub().resolves(); }); afterEach(async () => { @@ -30,23 +63,21 @@ describe('TransactionHome component', () => { it('should render the expected snapshot', async () => { const component: any = renderer - .create() + .create() .toJSON(); expect(component).toMatchSnapshot(); }); it('should change the active smart contract if another contract is selected', async () => { - const component: any = mount(); + const component: any = mount(); component.find('li').at(1).simulate('click'); - switchSmartContractSpy.should.have.been.calledOnce; - expect(component.state().activeSmartContract).toBe('blueContract@0.0.1'); + switchSmartContractStub.should.have.been.calledOnce; }); it('should do nothing if the current smart contract is selected', async () => { - const component: any = mount(); + const component: any = mount(); component.find('li').at(0).simulate('click'); - switchSmartContractSpy.should.not.have.been.called; - expect(component.state().activeSmartContract).toBe('greenContract@0.0.1'); + switchSmartContractStub.should.not.have.been.called; }); }); diff --git a/enzyme/tests/__snapshots__/TransactionCreate.test.tsx.snap b/enzyme/tests/__snapshots__/TransactionCreate.test.tsx.snap index ffe2f59c58..807b3bd214 100644 --- a/enzyme/tests/__snapshots__/TransactionCreate.test.tsx.snap +++ b/enzyme/tests/__snapshots__/TransactionCreate.test.tsx.snap @@ -57,8 +57,12 @@ exports[`TransactionCreate component should render the expected snapshot 1`] = `
- - smartContract@0.0.1 home + + greenContract@0.0.1 + home
- - @@ -326,10 +316,12 @@ exports[`TransactionCreate component should render the expected snapshot 1`] = ` className="bx--text-area" cols={50} disabled={false} + id="arguments-text-area" onChange={[Function]} onClick={[Function]} placeholder="" rows={4} + value="" />
diff --git a/extension/commands/openTransactionViewCommand.ts b/extension/commands/openTransactionViewCommand.ts index bc8876d444..ef51859a14 100644 --- a/extension/commands/openTransactionViewCommand.ts +++ b/extension/commands/openTransactionViewCommand.ts @@ -27,7 +27,9 @@ import { GlobalState } from '../util/GlobalState'; export async function openTransactionView(treeItem?: InstantiatedTreeItem): Promise { const outputAdapter: VSCodeBlockchainOutputAdapter = VSCodeBlockchainOutputAdapter.instance(); outputAdapter.log(LogType.INFO, undefined, `Open Transaction View`); - let smartContract: string; + let smartContractLabel: string; + let contract: { name: string, contractInstance: {}, transactions: Array<{}>, info: {} }; + let data: { name: string, version: string, channel: string, label: string, transactions: Array<{}>, namespace: string }; let connection: IFabricClientConnection = FabricConnectionManager.instance().getConnection(); @@ -41,30 +43,48 @@ export async function openTransactionView(treeItem?: InstantiatedTreeItem): Prom } if (treeItem) { - smartContract = treeItem.name + '@' + treeItem.version; + smartContractLabel = treeItem.name + '@' + treeItem.version; } else { const chosenSmartContract: IBlockchainQuickPickItem<{ name: string, channel: string, version: string }> = await UserInputUtil.showClientInstantiatedSmartContractsQuickPick(`Choose a smart contract`, null); if (!chosenSmartContract) { return; } - smartContract = chosenSmartContract.data.name + '@' + chosenSmartContract.data.version; + smartContractLabel = chosenSmartContract.data.name + '@' + chosenSmartContract.data.version; } const channelMap: Map> = await connection.createChannelMap(); - const instantiatedChaincodes: Array = []; + const instantiatedChaincodes: Array<{ label: string, channel: string }> = []; + + let metadataObj: any = { + contracts: { + '' : { + name: '', + transactions: [], + } + } + }; for (const [thisChannelName] of channelMap) { const chaincodes: Array = await connection.getInstantiatedChaincode(thisChannelName); // returns array of objects for (const chaincode of chaincodes) { - const data: string = chaincode.name + '@' + chaincode.version; + metadataObj = await connection.getMetadata(chaincode.name, thisChannelName); + contract = metadataObj.contracts[Object.keys(metadataObj.contracts)[0]]; + data = { + name: chaincode.name, + version: chaincode.version, + channel: thisChannelName, + label: chaincode.name + '@' + chaincode.version, + transactions: contract.transactions, + namespace: contract.name + }; instantiatedChaincodes.push(data); } } - const appState: {smartContracts: Array, activeSmartContract: string} = { + const appState: {} = { smartContracts: instantiatedChaincodes, - activeSmartContract: smartContract + activeSmartContract: instantiatedChaincodes.filter((obj: any) => obj.label === smartContractLabel)[0] }; const context: vscode.ExtensionContext = GlobalState.getExtensionContext(); diff --git a/src/App.tsx b/src/App.tsx index da1825617b..a0513d8c8f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,10 +3,14 @@ import './App.scss'; import { HashRouter as Router, Route, Redirect } from 'react-router-dom'; import TransactionHome from './components/TransactionHome/TransactionHome'; import TransactionCreate from './components/TransactionCreate/TransactionCreate'; +import ISmartContract from './interfaces/ISmartContract'; interface AppState { redirectPath: string; - childState: any; + childState: { + activeSmartContract: ISmartContract, + smartContracts: Array + }; } class App extends React.Component<{}, AppState> { @@ -14,30 +18,50 @@ class App extends React.Component<{}, AppState> { super(props); this.state = { redirectPath: '', - childState: {} + childState: { + activeSmartContract: { + name: '', + version: '', + channel: '', + label: '', + transactions: [], + namespace: '' + }, + smartContracts : [] + } }; + + this.switchSmartContract = this.switchSmartContract.bind(this); } componentDidMount(): void { window.addEventListener('message', (event: MessageEvent) => { this.setState({ redirectPath: event.data.path, - childState: event.data.state + childState: event.data.state ? event.data.state : this.state.childState }); }); } + public switchSmartContract(newActiveContractLabel: string): void { + this.setState({ + childState: { + activeSmartContract: this.state.childState.smartContracts.filter((obj: ISmartContract) => obj.label === newActiveContractLabel)[0], + smartContracts: this.state.childState.smartContracts + } + }); + } + public render(): any { if (this.state.redirectPath === '') { - // Maybe we should display a loading spinner instead? return
; } else { return (
- }> - }> - }> + }> + }> + }>
); diff --git a/src/Utils.ts b/src/Utils.ts new file mode 100644 index 0000000000..75fce2443f --- /dev/null +++ b/src/Utils.ts @@ -0,0 +1,13 @@ +const Utils: any = { + + changeRoute(newPath: string): void { + dispatchEvent(new MessageEvent('message', { + data: { + path: newPath + } + })); + } + +}; + +export default Utils; diff --git a/src/components/TransactionCreate/TransactionCreate.scss b/src/components/TransactionCreate/TransactionCreate.scss index d26cbeda99..6de6082929 100644 --- a/src/components/TransactionCreate/TransactionCreate.scss +++ b/src/components/TransactionCreate/TransactionCreate.scss @@ -15,6 +15,14 @@ fieldset > legend { } } +.home-link { + color: $carbon--blue-50; + + &:hover { + cursor: pointer; + } +} + .select-width { width: 100%; @@ -28,6 +36,7 @@ fieldset > legend { width: 100%; textarea { + font-size: 100%; width: 100%; resize: vertical; } diff --git a/src/components/TransactionCreate/TransactionCreate.tsx b/src/components/TransactionCreate/TransactionCreate.tsx index c53ee31ec0..45f9dffac4 100644 --- a/src/components/TransactionCreate/TransactionCreate.tsx +++ b/src/components/TransactionCreate/TransactionCreate.tsx @@ -1,64 +1,113 @@ import React, { Component } from 'react'; import './TransactionCreate.scss'; import Sidebar from '../TransactionSidebar/TransactionSidebar'; +import Utils from '../../Utils'; import { Button, Form, FormGroup, TextInput, Select, SelectItem, Checkbox, TextArea } from 'carbon-components-react'; +import ITransaction from '../../interfaces/ITransaction'; +import ISmartContract from '../../interfaces/ISmartContract'; -class TransactionCreate extends Component { +interface CreateProps { + activeSmartContract: ISmartContract; +} + +interface CreateState { + activeSmartContract: ISmartContract; + transactionArguments: string; +} + +class TransactionCreate extends Component { + constructor(props: Readonly) { + super(props); + this.state = { + activeSmartContract: this.props.activeSmartContract, + transactionArguments: '' + }; + this.getTransactionArguments = this.getTransactionArguments.bind(this); + this.updateTextArea = this.updateTextArea.bind(this); + } + + public getTransactionArguments(event: React.FormEvent): void { + const transaction: ITransaction | undefined = this.state.activeSmartContract.transactions.find((txn: ITransaction) => txn.name === event.currentTarget.value); + + let templateText: string = ''; + + if (transaction !== undefined) { + for (let i: number = 0; i < transaction.parameters.length; i++) { + templateText += (transaction.parameters[i].name + ': \n'); + } + this.setState({ + transactionArguments: templateText + }); + } + } public render(): any { return (
-
-
- smartContract@0.0.1 home -
-
-
-
- -
- - +
+
+ Utils.changeRoute('/transaction')}>{this.state.activeSmartContract.label} home +
+
+
+ + +
+ + +
+
+ + + + + + + + +