Skip to content

Commit

Permalink
Signal Form Test (#626)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-union authored Oct 26, 2022
1 parent 32d48c9 commit 5257d15
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useEffect } from 'react';
import { history } from 'routes/history';
import { Routes } from 'routes/routes';
import t from './strings';
import { LaunchState } from './launchMachine';
import { LaunchState, TaskResumeContext } from './launchMachine';
import { useStyles } from './styles';
import { BaseInterpretedLaunchState, BaseLaunchService } from './types';

Expand Down Expand Up @@ -49,7 +49,12 @@ export const LaunchFormActions: React.FC<LaunchFormActionsProps> = ({
// id and navigate to the Execution Details page.
// if (state.matches({ submit: 'succeeded' })) {
if (newState.matches(LaunchState.SUBMIT_SUCCEEDED)) {
history.push(Routes.ExecutionDetails.makeUrl(newState.context.resultExecutionId));
if (newState.context.resultExecutionId) {
history.push(Routes.ExecutionDetails.makeUrl(newState.context.resultExecutionId));
}
if ((newState.context as TaskResumeContext).compiledNode) {
onCancel();
}
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { ThemeProvider } from '@material-ui/styles';
import { fireEvent, queryAllByRole, render, waitFor } from '@testing-library/react';
import { APIContext } from 'components/data/apiContext';
import { mockAPIContextValue } from 'components/data/__mocks__/apiContext';
import { muiTheme } from 'components/Theme/muiTheme';
import { SimpleType } from 'models/Common/types';
import { resumeSignalNode } from 'models/Execution/api';
import * as React from 'react';
import { NodeExecutionsByIdContext } from 'components/Executions/contexts';
import { dateToTimestamp } from 'common/utils';
import { NodeExecutionPhase } from 'models/Execution/enums';
import { createTestQueryClient } from 'test/utils';
import { mockWorkflowId } from 'mocks/data/fixtures/types';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Core } from 'flyteidl';
import { CompiledNode } from 'models/Node/types';
import { CompiledWorkflowClosure } from 'models/Workflow/types';
import { NodeExecutionDetailsContext } from 'components/Executions/contextProvider/NodeExecutionDetails';
import { signalInputName } from './constants';
import { ResumeSignalForm } from '../ResumeSignalForm';

const mockNodeExecutionId = 'n0';
const mockNodeId = 'node0';

const mockNodeExecutionsById = {
[mockNodeExecutionId]: {
closure: {
createdAt: dateToTimestamp(new Date()),
outputUri: '',
phase: NodeExecutionPhase.UNDEFINED,
},
id: {
executionId: { domain: 'domain', name: 'name', project: 'project' },
nodeId: mockNodeId,
},
inputUri: '',
scopedId: mockNodeExecutionId,
},
};

const createMockCompiledWorkflowClosure = (nodes: CompiledNode[]): CompiledWorkflowClosure => ({
primary: {
connections: {
downstream: {},
upstream: {},
},
template: {
id: mockWorkflowId,
nodes,
},
},
tasks: [],
});

const createMockCompiledNode = (type?: Core.ILiteralType): CompiledNode => ({
id: mockNodeExecutionId,
metadata: {
name: 'my-signal-name',
timeout: '3600s',
retries: {},
},
upstreamNodeIds: [],
gateNode: {
signal: {
signalId: 'my-signal-name',
type,
outputVariableName: 'o0',
},
},
});

describe('ResumeSignalForm', () => {
let onClose: jest.Mock;
let queryClient: QueryClient;
let mockResumeSignalNode: jest.Mock<ReturnType<typeof resumeSignalNode>>;

beforeEach(() => {
onClose = jest.fn();
queryClient = createTestQueryClient();
});

const renderForm = (type?: Core.ILiteralType) => {
const mockCompiledNode = createMockCompiledNode(type);
const mockCompiledWorkflowClosure = createMockCompiledWorkflowClosure([mockCompiledNode]);
return render(
<ThemeProvider theme={muiTheme}>
<QueryClientProvider client={queryClient}>
<APIContext.Provider
value={mockAPIContextValue({
resumeSignalNode: mockResumeSignalNode,
})}
>
<NodeExecutionDetailsContext.Provider
value={{
getNodeExecutionDetails: jest.fn(),
workflowId: mockWorkflowId,
compiledWorkflowClosure: mockCompiledWorkflowClosure,
}}
>
<NodeExecutionsByIdContext.Provider value={mockNodeExecutionsById}>
<ResumeSignalForm
onClose={onClose}
compiledNode={mockCompiledNode}
nodeId={mockNodeExecutionId}
/>
</NodeExecutionsByIdContext.Provider>
</NodeExecutionDetailsContext.Provider>
</APIContext.Provider>
</QueryClientProvider>
</ThemeProvider>,
);
};

const getSubmitButton = (container: HTMLElement) => {
const buttons = queryAllByRole(container, 'button').filter(
(el) => el.getAttribute('type') === 'submit',
);
expect(buttons.length).toBe(1);
return buttons[0];
};

describe('With inputs', () => {
beforeEach(() => {
mockResumeSignalNode = jest.fn();
});

it('should render the node id as a header title', async () => {
const { getByText } = renderForm();
expect(getByText('node0')).toBeInTheDocument();
});

it('should disable the submit button until the input is filled', async () => {
const { container } = renderForm();
const submitButton = await waitFor(() => getSubmitButton(container));
expect(submitButton).toBeDisabled();
});

it('should show disabled submit button if the value in input is invalid', async () => {
const { container, getByLabelText } = renderForm({ simple: SimpleType.INTEGER });
await waitFor(() => {});

const integerInput = await waitFor(() =>
getByLabelText(signalInputName, {
exact: false,
}),
);
const submitButton = getSubmitButton(container);
fireEvent.change(integerInput, { target: { value: 'abc' } });
fireEvent.click(getSubmitButton(container));
await waitFor(() => expect(submitButton).toBeDisabled());

fireEvent.change(integerInput, { target: { value: '123' } });
await waitFor(() => expect(submitButton).toBeEnabled());
});

it('should allow submission after fixing validation errors', async () => {
const { container, getByLabelText } = renderForm({ simple: SimpleType.INTEGER });
await waitFor(() => {});

const integerInput = await waitFor(() =>
getByLabelText(signalInputName, {
exact: false,
}),
);
const submitButton = getSubmitButton(container);
fireEvent.change(integerInput, { target: { value: 'abc' } });
await waitFor(() => expect(submitButton).toBeDisabled());

fireEvent.change(integerInput, { target: { value: '123' } });
await waitFor(() => expect(submitButton).toBeEnabled());
fireEvent.click(submitButton);
await waitFor(() => expect(mockResumeSignalNode).toHaveBeenCalled());
});

it('should show error when the submission fails', async () => {
const errorString = 'Something went wrong';
mockResumeSignalNode.mockRejectedValue(new Error(errorString));

const { container, getByText, getByLabelText } = renderForm({
simple: SimpleType.INTEGER,
});
const integerInput = await waitFor(() =>
getByLabelText(signalInputName, {
exact: false,
}),
);
const submitButton = getSubmitButton(container);
fireEvent.change(integerInput, { target: { value: '123' } });
await waitFor(() => expect(submitButton).toBeEnabled());

fireEvent.click(getSubmitButton(container));
await waitFor(() => expect(getByText(errorString)).toBeInTheDocument());
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const integerInputName = 'simpleInteger';
export const binaryInputName = 'simpleBinary';
export const errorInputName = 'simpleError';
export const mapInputName = 'simpleMap';
export const signalInputName = 'Signal Input';

export const iamRoleString = 'arn:aws:iam::12345678:role/defaultrole';
export const k8sServiceAccountString = 'default-service-account';
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ import {
import { validate as baseValidate } from './services';
import {
BaseLaunchFormProps,
InputType,
LaunchFormInputsRef,
ParsedInput,
ResumeFormState,
TaskInitialLaunchParameters,
} from './types';
import { getUnsupportedRequiredInputs } from './utils';
import { getInputDefintionForLiteralType, getUnsupportedRequiredInputs } from './utils';

interface ResumeFormProps extends BaseLaunchFormProps {
compiledNode: CompiledNode;
Expand All @@ -39,15 +38,10 @@ async function loadInputs({ compiledNode }: TaskResumeContext) {
const parsedInputs: ParsedInput[] = [
{
description: '',
label: '',
label: 'Signal Input',
name: 'signal',
required: true,
typeDefinition: {
type: InputType.Boolean,
literalType: {
simple: signalType.simple ?? undefined,
},
},
typeDefinition: getInputDefintionForLiteralType({ simple: signalType.simple ?? undefined }),
},
];

Expand Down

0 comments on commit 5257d15

Please sign in to comment.