Skip to content

Commit

Permalink
feat: move extrinsic submission to context.
Browse files Browse the repository at this point in the history
  • Loading branch information
gdethier committed Nov 13, 2023
1 parent 21d129a commit f341e22
Show file tree
Hide file tree
Showing 19 changed files with 598 additions and 237 deletions.
86 changes: 19 additions & 67 deletions src/ClientExtrinsicSubmitter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ISubmittableResult } from '@logion/client';
import { useEffect, useState } from 'react';

import ExtrinsicSubmissionResult, { isSuccessful } from './ExtrinsicSubmissionResult';
import ExtrinsicSubmissionResult from './ExtrinsicSubmissionResult';
import { useLogionChain } from './logion-chain';
import { Call, CallCallback } from './logion-chain/LogionChainContext';

export type CallCallback = (result: ISubmittableResult) => void;

export type Call = (callback: CallCallback) => Promise<void>;
export type { Call, CallCallback };

export interface Props {
successMessage?: string | JSX.Element,
Expand All @@ -15,87 +14,40 @@ export interface Props {
slim?: boolean,
}

interface State {
result: ISubmittableResult | null;
error: any;
submitted: boolean;
notified: boolean;
callEnded: boolean;
call?: Call;
setState: (newState: State) => void;
}

const INITIAL_STATE: State = {
result: null,
error: null,
submitted: false,
notified: false,
callEnded: false,
setState: () => {},
};

let persistentState: State = INITIAL_STATE;

function setPersistentState(stateUpdate: Partial<State>) {
persistentState = {
...persistentState,
...stateUpdate
};
persistentState.setState(persistentState);
}

export function resetPersistenState() {
persistentState = INITIAL_STATE;
}

export default function ClientExtrinsicSubmitter(props: Props) {
const [ state, setState ] = useState<State>(persistentState);
if(setState !== persistentState.setState) {
persistentState.setState = setState;
}
const { extrinsicSubmissionState, submitCall, resetSubmissionState } = useLogionChain();
const [ notified, setNotified ] = useState(false);

useEffect(() => {
if(props.call !== undefined && (!state.submitted || (state.notified && props.call !== state.call))) {
setPersistentState({
...INITIAL_STATE,
call: props.call,
submitted: true,
setState,
});
(async function() {
try {
await props.call!((callbackResult: ISubmittableResult) => setPersistentState({ result: callbackResult }));
setPersistentState({ callEnded: true });
} catch(e) {
console.log(e);
setPersistentState({ callEnded: true, error: e});
}
})();
if(props.call !== undefined && extrinsicSubmissionState.canSubmit()) {
submitCall(props.call);
}
}, [ state, props ]);
}, [ extrinsicSubmissionState, submitCall, props ]);

useEffect(() => {
if (state.result !== null && isSuccessful(state.result) && !state.notified && props.onSuccess && state.callEnded) {
setPersistentState({ notified: true });
if (extrinsicSubmissionState.isSuccessful() && !notified && props.onSuccess) {
setNotified(true);
resetSubmissionState();
props.onSuccess();
}
}, [ state, props ]);
}, [ extrinsicSubmissionState, props, notified, resetSubmissionState ]);

useEffect(() => {
if (state.error !== null && !state.notified && props.onError) {
setPersistentState({ notified: true });
if (extrinsicSubmissionState.isError() && !notified && props.onError) {
setNotified(true);
resetSubmissionState();
props.onError();
}
}, [ state, props ]);
}, [ extrinsicSubmissionState, props, notified, resetSubmissionState ]);

if(props.call === undefined) {
return null;
}

return (
<ExtrinsicSubmissionResult
result={state.result}
error={state.error}
result={extrinsicSubmissionState.result}
error={extrinsicSubmissionState.error}
successMessage={ props.successMessage }
slim={ props.slim }
/>
Expand Down
7 changes: 1 addition & 6 deletions src/ExtrinsicSubmissionResult.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { ISubmittableResult } from '@logion/client';
import Spinner from 'react-bootstrap/Spinner';

import { SignedTransaction } from './logion-chain/Signature';
import { SIGN_AND_SEND_STRATEGY } from './logion-chain/LogionChainContext';
import { isSuccessful } from './logion-chain/LogionChainContext';
import Alert from './common/Alert';

import './ExtrinsicSubmissionResult.css';

export function isSuccessful(result: ISubmittableResult): boolean {
return !result.dispatchError && SIGN_AND_SEND_STRATEGY.canUnsub(result);
}

export interface Props {
result?: SignedTransaction | null,
error: any,
Expand Down
195 changes: 98 additions & 97 deletions src/ExtrinsicSubmitter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,107 @@
import { screen, render, waitFor, act } from '@testing-library/react';
import { SignedTransaction } from './logion-chain/Signature';
import { ISubmittableResult } from "@logion/client";
import { screen, render, waitFor } from '@testing-library/react';

import { mockSubmittableResult } from './logion-chain/__mocks__/SignatureMock';
import ExtrinsicSubmitter, { SignAndSubmit } from './ExtrinsicSubmitter';

test("Submitter empty with null signAndSubmit", () => {
const onSuccess = jest.fn();
const onError = jest.fn();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ null }
onSuccess={ onSuccess }
onError={ onError }
/>);

expect(screen.getByRole('generic')).toBeEmptyDOMElement();
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
});

test("Submitter initially showing submitting", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(screen.getByText("Submitting...")).toBeInTheDocument());
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
import { expectSubmitting } from "./test/Util";
import { FAILED_SUBMISSION, NO_SUBMISSION, PENDING_SUBMISSION, SUCCESSFUL_SUBMISSION, setExtrinsicSubmissionState } from "./logion-chain/__mocks__/LogionChainMock";

jest.mock("./logion-chain");

describe("ExtrinsicSubmitter", () => {

it("is empty with null signAndSubmit", () => {
const onSuccess = jest.fn();
const onError = jest.fn();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ null }
onSuccess={ onSuccess }
onError={ onError }
/>);

expect(screen.getByRole('generic')).toBeEmptyDOMElement();
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
});

it("is initially showing submitting", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

setExtrinsicSubmissionState(NO_SUBMISSION);
render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expectSubmitting());
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
});

it("shows error and calls onError", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

setExtrinsicSubmissionState(FAILED_SUBMISSION);
render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(screen.getByText("Submission failed: error")).toBeInTheDocument());
expect(onSuccess).not.toBeCalled();
expect(onError).toBeCalled();
});

it("shows progress", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

setExtrinsicSubmissionState(PENDING_SUBMISSION);
render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(screen.getByText("Current status: undefined")).toBeInTheDocument());
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
});

it("shows success and calls onSuccess", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

setExtrinsicSubmissionState(SUCCESSFUL_SUBMISSION);
render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(screen.getByText("Submission successful.")).toBeInTheDocument());
expect(onSuccess).toBeCalledWith("extrinsicId");
expect(onError).not.toBeCalled();
});
});

interface SignAndSubmitMock {
signAndSubmit: SignAndSubmit,
setResult: React.Dispatch<React.SetStateAction<SignedTransaction | null>> | null,
setError: React.Dispatch<React.SetStateAction<any>> | null,
setResult: ((result: ISubmittableResult | null) => void) | null,
setError: ((error: unknown) => void) | null,
}

function buildSignAndSubmitMock(): SignAndSubmitMock {
Expand All @@ -56,63 +117,3 @@ function buildSignAndSubmitMock(): SignAndSubmitMock {
}
return result;
}

test("Submitter shows error and calls onError", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(result.setError).not.toBeNull());
act(() => result.setError!("error"));

await waitFor(() => expect(screen.getByText("Submission failed: error")).toBeInTheDocument());
expect(onSuccess).not.toBeCalled();
expect(onError).toBeCalled();
});

test("Submitter shows progress", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(result.setResult).not.toBeNull());
act(() => result.setResult!(mockSubmittableResult(false)));

await waitFor(() => expect(screen.getByText("Current status: undefined")).toBeInTheDocument());
expect(onSuccess).not.toBeCalled();
expect(onError).not.toBeCalled();
});

test("Submitter shows success and calls onSuccess", async () => {
const onSuccess = jest.fn();
const onError = jest.fn();
const result = buildSignAndSubmitMock();

render(<ExtrinsicSubmitter
id="extrinsicId"
signAndSubmit={ result.signAndSubmit }
onSuccess={ onSuccess }
onError={ onError }
/>);

await waitFor(() => expect(result.setResult).not.toBeNull());
act(() => result.setResult!(mockSubmittableResult(true)));

await waitFor(() => expect(screen.getByText("Submission successful.")).toBeInTheDocument());
expect(onSuccess).toBeCalledWith("extrinsicId");
expect(onError).not.toBeCalled();
});
Loading

0 comments on commit f341e22

Please sign in to comment.