enzyme#ReactWrapper TypeScript Examples

The following examples show how to use enzyme#ReactWrapper. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: test-utils.tsx    From react-component-library with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
findByTestId = (id: string, wrapper: ShallowWrapper | ReactWrapper): any =>
    wrapper.find(`[data-test="${id}"]`).hostNodes()
Example #2
Source File: BarGaugePanel.test.tsx    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
function createBarGaugePanelWithData(data: PanelData): ReactWrapper<PanelProps<BarGaugeOptions>> {
  const timeRange = createTimeRange();

  const options: BarGaugeOptions = {
    displayMode: BarGaugeDisplayMode.Lcd,
    fieldOptions: {
      calcs: ['mean'],
      defaults: {},
      values: false,
      overrides: [],
    },
    orientation: VizOrientation.Horizontal,
    showUnfilled: true,
  };

  return mount<BarGaugePanel>(
    <BarGaugePanel
      id={1}
      data={data}
      timeRange={timeRange}
      timeZone={'utc'}
      options={options}
      onOptionsChange={() => {}}
      onChangeTimeRange={() => {}}
      replaceVariables={s => s}
      renderCounter={0}
      width={532}
      transparent={false}
      height={250}
    />
  );
}
Example #3
Source File: NamedColorsPalette.test.tsx    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
describe('NamedColorsPalette', () => {
  const BasicGreen = getColorDefinitionByName('green');

  describe('theme support for named colors', () => {
    let wrapper: ReactWrapper, selectedSwatch;

    afterEach(() => {
      wrapper.unmount();
    });

    it('should render provided color variant specific for theme', () => {
      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme()} onChange={() => {}} />);
      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);

      wrapper.unmount();
      wrapper = mount(
        <NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />
      );
      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
    });

    it('should render dar variant of provided color when theme not provided', () => {
      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
    });
  });
});
Example #4
Source File: TestUtils.tsx    From kfp-tekton-backend with Apache License 2.0 6 votes vote down vote up
/**
   * Mounts the given component with a fake router and returns the mounted tree
   */
  // tslint:disable-next-line:variable-name
  public static mountWithRouter(component: React.ReactElement<any>): ReactWrapper {
    const childContextTypes = {
      router: object,
    };
    const context = createRouterContext();
    const tree = mount(component, { context, childContextTypes });
    return tree;
  }
Example #5
Source File: utils.ts    From geist-ui with MIT License 6 votes vote down vote up
updateWrapper = async (wrapper: ReactWrapper, time: number = 0) => {
  await act(async () => {
    await sleep(time)
    wrapper.update()
  })
}
Example #6
Source File: utils.tsx    From yforms with MIT License 6 votes vote down vote up
export async function waitForComponentToPaint<P = any>(wrapper: ReactWrapper<P>, amount = 0) {
  await act(async () => {
    await new Promise((resolve) => setTimeout(resolve, amount));
    wrapper.update();
  });
}
Example #7
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectPopoverIsShow = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.inner').length).not.toBe(0)
}
Example #8
Source File: setupTests.ts    From ke with MIT License 5 votes vote down vote up
waitForComponentToPaint = async (wrapper: ReactWrapper): Promise<any> => {
  await act(async () => {
    await new Promise((resolve) => setTimeout(resolve))
    wrapper.update()
  })
}
Example #9
Source File: SelectBase.test.tsx    From grafana-chinese with Apache License 2.0 5 votes vote down vote up
findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' })
Example #10
Source File: ColorPickerPopover.test.tsx    From grafana-chinese with Apache License 2.0 5 votes vote down vote up
describe('ColorPickerPopover', () => {
  const BasicGreen = getColorDefinitionByName('green');
  const BasicBlue = getColorDefinitionByName('blue');

  describe('rendering', () => {
    it('should render provided color as selected if color provided by name', () => {
      const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
      const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
      const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);

      expect(selectedSwatch.length).toBe(1);
      expect(notSelectedSwatches.length).toBe(allColors.length - 1);
      expect(selectedSwatch.prop('isSelected')).toBe(true);
    });

    it('should render provided color as selected if color provided by hex', () => {
      const wrapper = mount(
        <ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />
      );
      const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
      const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);

      expect(selectedSwatch.length).toBe(1);
      expect(notSelectedSwatches.length).toBe(allColors.length - 1);
      expect(selectedSwatch.prop('isSelected')).toBe(true);
    });
  });

  describe('named colors support', () => {
    const onChangeSpy = jest.fn();
    let wrapper: ReactWrapper;

    afterEach(() => {
      wrapper.unmount();
      onChangeSpy.mockClear();
    });

    it('should pass hex color value to onChange prop by default', () => {
      wrapper = mount(
        <ColorPickerPopover
          color={BasicGreen.variants.dark}
          onChange={onChangeSpy}
          theme={getTheme(GrafanaThemeType.Light)}
        />
      );
      const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);

      basicBlueSwatch.simulate('click');

      expect(onChangeSpy).toBeCalledTimes(1);
      expect(onChangeSpy).toBeCalledWith(BasicBlue.variants.light);
    });

    it('should pass color name to onChange prop when named colors enabled', () => {
      wrapper = mount(
        <ColorPickerPopover
          enableNamedColors
          color={BasicGreen.variants.dark}
          onChange={onChangeSpy}
          theme={getTheme(GrafanaThemeType.Light)}
        />
      );
      const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);

      basicBlueSwatch.simulate('click');

      expect(onChangeSpy).toBeCalledTimes(1);
      expect(onChangeSpy).toBeCalledWith(BasicBlue.name);
    });
  });
});
Example #11
Source File: TestComponent.tsx    From use-long-press with MIT License 5 votes vote down vote up
export function createMountedTestComponent<Target = Element>(
  props: TestComponentProps
): ReactWrapper<Required<TestComponentProps & HTMLAttributes<Target>>> {
  return mount<Component<Required<TestComponentProps & HTMLAttributes<Target>>>>(<TestComponent {...props} />);
}
Example #12
Source File: use-modal.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectDrawerIsOpened = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.position').length).not.toBe(0)
}
Example #13
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectToastIsHidden = (wrapper: ReactWrapper) => {
  const toast = wrapper.find('.toasts').find('.toast')
  expect(toast.length).toBe(0)
}
Example #14
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectToastIsShow = (wrapper: ReactWrapper) => {
  const toast = wrapper.find('.toasts').find('.toast')
  expect(toast.length).not.toBe(0)
}
Example #15
Source File: use-modal.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectModalIsClosed = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.position').length).toBe(0)
}
Example #16
Source File: use-modal.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectDrawerIsClosed = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.position').length).toBe(0)
}
Example #17
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectPopoverIsHidden = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.inner').length).toBe(0)
}
Example #18
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectToggleIsChecked = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.checked').length).not.toBe(0)
}
Example #19
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectToggleIsUnChecked = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.checked').length).toBe(0)
}
Example #20
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectTooltipIsShow = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.inner').length).not.toBe(0)
}
Example #21
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
triggerToast = (wrapper: ReactWrapper, params = {}) => {
  wrapper.find('#btn').simulate('click', {
    ...nativeEvent,
    target: params,
  })
}
Example #22
Source File: use-modal.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectModalIsOpened = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.position').length).not.toBe(0)
}
Example #23
Source File: index.test.tsx    From geist-ui with MIT License 5 votes vote down vote up
expectTooltipIsHidden = (wrapper: ReactWrapper) => {
  expect(wrapper.find('.inner').length).toBe(0)
}
Example #24
Source File: PipelineList.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('PipelineList', () => {
  let tree: ReactWrapper | ShallowWrapper;

  let updateBannerSpy: jest.Mock<{}>;
  let updateDialogSpy: jest.Mock<{}>;
  let updateSnackbarSpy: jest.Mock<{}>;
  let updateToolbarSpy: jest.Mock<{}>;
  let listPipelinesSpy: jest.SpyInstance<{}>;
  let listPipelineVersionsSpy: jest.SpyInstance<{}>;
  let deletePipelineSpy: jest.SpyInstance<{}>;
  let deletePipelineVersionSpy: jest.SpyInstance<{}>;

  function spyInit() {
    updateBannerSpy = jest.fn();
    updateDialogSpy = jest.fn();
    updateSnackbarSpy = jest.fn();
    updateToolbarSpy = jest.fn();
    listPipelinesSpy = jest.spyOn(Apis.pipelineServiceApi, 'listPipelines');
    listPipelineVersionsSpy = jest.spyOn(Apis.pipelineServiceApi, 'listPipelineVersions');
    deletePipelineSpy = jest.spyOn(Apis.pipelineServiceApi, 'deletePipeline');
    deletePipelineVersionSpy = jest.spyOn(Apis.pipelineServiceApi, 'deletePipelineVersion');
  }

  function generateProps(): PageProps {
    return TestUtils.generatePageProps(
      PipelineList,
      '' as any,
      '' as any,
      null,
      updateBannerSpy,
      updateDialogSpy,
      updateToolbarSpy,
      updateSnackbarSpy,
    );
  }

  async function mountWithNPipelines(n: number): Promise<ReactWrapper> {
    listPipelinesSpy.mockImplementation(() => ({
      pipelines: range(n).map(i => ({
        id: 'test-pipeline-id' + i,
        name: 'test pipeline name' + i,
        defaultVersion: {
          id: 'test-pipeline-id' + i + '_default_version',
          name: 'test-pipeline-id' + i + '_default_version_name',
        },
      })),
    }));
    tree = TestUtils.mountWithRouter(<PipelineList {...generateProps()} />);
    await listPipelinesSpy;
    await TestUtils.flushPromises();
    tree.update(); // Make sure the tree is updated before returning it
    return tree;
  }

  beforeEach(() => {
    spyInit();
    jest.clearAllMocks();
  });

  afterEach(async () => {
    // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
    // depends on mocks/spies
    await tree.unmount();
    jest.restoreAllMocks();
  });

  it('renders an empty list with empty state message', () => {
    tree = shallow(<PipelineList {...generateProps()} />);
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one pipeline', async () => {
    tree = shallow(<PipelineList {...generateProps()} />);
    tree.setState({
      displayPipelines: [
        {
          created_at: new Date(2018, 8, 22, 11, 5, 48),
          description: 'test pipeline description',
          name: 'pipeline1',
          parameters: [],
        },
      ],
    });
    await listPipelinesSpy;
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one pipeline with no description or created date', async () => {
    tree = shallow(<PipelineList {...generateProps()} />);
    tree.setState({
      displayPipelines: [
        {
          name: 'pipeline1',
          parameters: [],
        },
      ],
    });
    await listPipelinesSpy;
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one pipeline with error', async () => {
    tree = shallow(<PipelineList {...generateProps()} />);
    tree.setState({
      displayPipelines: [
        {
          created_at: new Date(2018, 8, 22, 11, 5, 48),
          description: 'test pipeline description',
          error: 'oops! could not load pipeline',
          name: 'pipeline1',
          parameters: [],
        },
      ],
    });
    await listPipelinesSpy;
    expect(tree).toMatchSnapshot();
  });

  it('calls Apis to list pipelines, sorted by creation time in descending order', async () => {
    listPipelinesSpy.mockImplementationOnce(() => ({ pipelines: [{ name: 'pipeline1' }] }));
    tree = TestUtils.mountWithRouter(<PipelineList {...generateProps()} />);
    await listPipelinesSpy;
    expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', '');
    expect(tree.state()).toHaveProperty('displayPipelines', [
      { expandState: 0, name: 'pipeline1' },
    ]);
  });

  it('has a Refresh button, clicking it refreshes the pipeline list', async () => {
    tree = await mountWithNPipelines(1);
    const instance = tree.instance() as PipelineList;
    expect(listPipelinesSpy.mock.calls.length).toBe(1);
    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    expect(refreshBtn).toBeDefined();
    await refreshBtn!.action();
    expect(listPipelinesSpy.mock.calls.length).toBe(2);
    expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', '');
    expect(updateBannerSpy).toHaveBeenLastCalledWith({});
  });

  it('shows error banner when listing pipelines fails', async () => {
    TestUtils.makeErrorResponseOnce(listPipelinesSpy, 'bad stuff happened');
    tree = TestUtils.mountWithRouter(<PipelineList {...generateProps()} />);
    await listPipelinesSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message: 'Error: failed to retrieve list of pipelines. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('shows error banner when listing pipelines fails after refresh', async () => {
    tree = TestUtils.mountWithRouter(<PipelineList {...generateProps()} />);
    const instance = tree.instance() as PipelineList;
    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    expect(refreshBtn).toBeDefined();
    TestUtils.makeErrorResponseOnce(listPipelinesSpy, 'bad stuff happened');
    await refreshBtn!.action();
    expect(listPipelinesSpy.mock.calls.length).toBe(2);
    expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', '');
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message: 'Error: failed to retrieve list of pipelines. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('hides error banner when listing pipelines fails then succeeds', async () => {
    TestUtils.makeErrorResponseOnce(listPipelinesSpy, 'bad stuff happened');
    tree = TestUtils.mountWithRouter(<PipelineList {...generateProps()} />);
    const instance = tree.instance() as PipelineList;
    await listPipelinesSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message: 'Error: failed to retrieve list of pipelines. Click Details for more information.',
        mode: 'error',
      }),
    );
    updateBannerSpy.mockReset();

    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    listPipelinesSpy.mockImplementationOnce(() => ({ pipelines: [{ name: 'pipeline1' }] }));
    await refreshBtn!.action();
    expect(listPipelinesSpy.mock.calls.length).toBe(2);
    expect(updateBannerSpy).toHaveBeenLastCalledWith({});
  });

  it('renders pipeline names as links to their details pages', async () => {
    tree = await mountWithNPipelines(1);
    const link = tree.find('a[children="test pipeline name0"]');
    expect(link).toHaveLength(1);
    expect(link.prop('href')).toBe(
      RoutePage.PIPELINE_DETAILS_NO_VERSION.replace(
        ':' + RouteParams.pipelineId + '?',
        'test-pipeline-id0',
      ),
    );
  });

  it('always has upload pipeline button enabled', async () => {
    tree = await mountWithNPipelines(1);
    const calls = updateToolbarSpy.mock.calls[0];
    expect(calls[0].actions[ButtonKeys.NEW_PIPELINE_VERSION]).not.toHaveProperty('disabled');
  });

  it('enables delete button when one pipeline is selected', async () => {
    tree = await mountWithNPipelines(1);
    tree.find('.tableRow').simulate('click');
    expect(updateToolbarSpy.mock.calls).toHaveLength(2); // Initial call, then selection update
    const calls = updateToolbarSpy.mock.calls[1];
    expect(calls[0].actions[ButtonKeys.DELETE_RUN]).toHaveProperty('disabled', false);
  });

  it('enables delete button when two pipelines are selected', async () => {
    tree = await mountWithNPipelines(2);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(1)
      .simulate('click');
    expect(updateToolbarSpy.mock.calls).toHaveLength(3); // Initial call, then selection updates
    const calls = updateToolbarSpy.mock.calls[2];
    expect(calls[0].actions[ButtonKeys.DELETE_RUN]).toHaveProperty('disabled', false);
  });

  it('re-disables delete button pipelines are unselected', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    expect(updateToolbarSpy.mock.calls).toHaveLength(3); // Initial call, then selection updates
    const calls = updateToolbarSpy.mock.calls[2];
    expect(calls[0].actions[ButtonKeys.DELETE_RUN]).toHaveProperty('disabled', true);
  });

  it('shows delete dialog when delete button is clicked', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    expect(call).toHaveProperty('title', 'Delete 1 pipeline?');
  });

  it('shows delete dialog when delete button is clicked, indicating several pipelines to delete', async () => {
    tree = await mountWithNPipelines(5);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(2)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(3)
      .simulate('click');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    expect(call).toHaveProperty('title', 'Delete 3 pipelines?');
  });

  it('does not call delete API for selected pipeline when delete dialog is canceled', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const cancelBtn = call.buttons.find((b: any) => b.text === 'Cancel');
    await cancelBtn.onClick();
    expect(deletePipelineSpy).not.toHaveBeenCalled();
  });

  it('calls delete API for selected pipeline after delete dialog is confirmed', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    expect(deletePipelineSpy).toHaveBeenLastCalledWith('test-pipeline-id0');
  });

  it('updates the selected indices after a pipeline is deleted', async () => {
    tree = await mountWithNPipelines(5);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    expect(tree.state()).toHaveProperty('selectedIds', ['test-pipeline-id0']);
    deletePipelineSpy.mockImplementation(() => Promise.resolve());
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    expect(tree.state()).toHaveProperty('selectedIds', []);
  });

  it('updates the selected indices after multiple pipelines are deleted', async () => {
    tree = await mountWithNPipelines(5);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(3)
      .simulate('click');
    expect(tree.state()).toHaveProperty('selectedIds', ['test-pipeline-id0', 'test-pipeline-id3']);
    deletePipelineSpy.mockImplementation(() => Promise.resolve());
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    expect(tree.state()).toHaveProperty('selectedIds', []);
  });

  it('calls delete API for all selected pipelines after delete dialog is confirmed', async () => {
    tree = await mountWithNPipelines(5);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(1)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(4)
      .simulate('click');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    expect(deletePipelineSpy).toHaveBeenCalledTimes(3);
    expect(deletePipelineSpy).toHaveBeenCalledWith('test-pipeline-id0');
    expect(deletePipelineSpy).toHaveBeenCalledWith('test-pipeline-id1');
    expect(deletePipelineSpy).toHaveBeenCalledWith('test-pipeline-id4');
  });

  it('shows snackbar confirmation after pipeline is deleted', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    deletePipelineSpy.mockImplementation(() => Promise.resolve());
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    expect(updateSnackbarSpy).toHaveBeenLastCalledWith({
      message: 'Deletion succeeded for 1 pipeline',
      open: true,
    });
  });

  it('shows error dialog when pipeline deletion fails', async () => {
    tree = await mountWithNPipelines(1);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    TestUtils.makeErrorResponseOnce(deletePipelineSpy, 'woops, failed');
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    const lastCall = updateDialogSpy.mock.calls[1][0];
    expect(lastCall).toMatchObject({
      content: 'Failed to delete pipeline: test-pipeline-id0 with error: "woops, failed"',
      title: 'Failed to delete some pipelines and/or some pipeline versions',
    });
  });

  it('shows error dialog when multiple pipeline deletions fail', async () => {
    tree = await mountWithNPipelines(5);
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(2)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(1)
      .simulate('click');
    tree
      .find('.tableRow')
      .at(3)
      .simulate('click');
    deletePipelineSpy.mockImplementation(id => {
      if (id.indexOf(3) === -1 && id.indexOf(2) === -1) {
        // eslint-disable-next-line no-throw-literal
        throw {
          text: () => Promise.resolve('woops, failed!'),
        };
      }
    });
    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();
    // Should show only one error dialog for both pipelines (plus once for confirmation)
    expect(updateDialogSpy).toHaveBeenCalledTimes(2);
    const lastCall = updateDialogSpy.mock.calls[1][0];
    expect(lastCall).toMatchObject({
      content:
        'Failed to delete pipeline: test-pipeline-id0 with error: "woops, failed!"\n\n' +
        'Failed to delete pipeline: test-pipeline-id1 with error: "woops, failed!"',
      title: 'Failed to delete some pipelines and/or some pipeline versions',
    });

    // Should show snackbar for the one successful deletion
    expect(updateSnackbarSpy).toHaveBeenLastCalledWith({
      message: 'Deletion succeeded for 2 pipelines',
      open: true,
    });
  });

  it("delete a pipeline and some other pipeline's version together", async () => {
    deletePipelineSpy.mockImplementation(() => Promise.resolve());
    deletePipelineVersionSpy.mockImplementation(() => Promise.resolve());
    listPipelineVersionsSpy.mockImplementation(() => ({
      versions: [
        {
          id: 'test-pipeline-id1_default_version',
          name: 'test-pipeline-id1_default_version_name',
        },
      ],
    }));

    tree = await mountWithNPipelines(2);
    tree
      .find('button[aria-label="Expand"]')
      .at(1)
      .simulate('click');
    await listPipelineVersionsSpy;
    tree.update();

    // select pipeline of id 'test-pipeline-id0'
    tree
      .find('.tableRow')
      .at(0)
      .simulate('click');
    // select pipeline version of id 'test-pipeline-id1_default_version' under pipeline 'test-pipeline-id1'
    tree
      .find('.tableRow')
      .at(2)
      .simulate('click');

    expect(tree.state()).toHaveProperty('selectedIds', ['test-pipeline-id0']);
    expect(tree.state()).toHaveProperty('selectedVersionIds', {
      'test-pipeline-id1': ['test-pipeline-id1_default_version'],
    });

    const deleteBtn = (tree.instance() as PipelineList).getInitialToolbarState().actions[
      ButtonKeys.DELETE_RUN
    ];
    await deleteBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
    await confirmBtn.onClick();

    await deletePipelineSpy;
    await deletePipelineVersionSpy;

    expect(deletePipelineSpy).toHaveBeenCalledTimes(1);
    expect(deletePipelineSpy).toHaveBeenCalledWith('test-pipeline-id0');

    expect(deletePipelineVersionSpy).toHaveBeenCalledTimes(1);
    expect(deletePipelineVersionSpy).toHaveBeenCalledWith('test-pipeline-id1_default_version');

    expect(tree.state()).toHaveProperty('selectedIds', []);
    expect(tree.state()).toHaveProperty('selectedVersionIds', { 'test-pipeline-id1': [] });

    // Should show snackbar for the one successful deletion
    expect(updateSnackbarSpy).toHaveBeenLastCalledWith({
      message: 'Deletion succeeded for 1 pipeline and 1 pipeline version',
      open: true,
    });
  });
});
Example #25
Source File: SubMenuFilter.spec.tsx    From next-basics with GNU General Public License v3.0 4 votes vote down vote up
describe("SubMenuFilter", () => {
  const props: SubMenuFilterProps = {
    multiple: true,
    defaultOpenKeys: ["diy"],
    defaultSelectedKeys: ["C++"],
    inlineIndent: 12,
    menuItems: [
      {
        type: "item",
        title: "全部",
        key: "All",
        count: 100,
        icon: {
          lib: "fa",
          icon: "cube",
        },
      },
      {
        type: "group",
        title: "内置模板",
        key: "built-in-template",
        items: [
          {
            title: "Java",
            key: "Java",
            count: 60,
            icon: {
              lib: "easyops",
              category: "model",
              icon: "java",
            },
          },
          {
            title: "Go",
            key: "Go",
            count: 10,
            icon: {
              lib: "easyops",
              category: "model",
              icon: "go",
            },
          },
          {
            title: "Cc",
            key: "cc",
            count: 10,
            icon: {
              lib: "easyops",
              category: "model",
              icon: "go",
            },
          },
          {
            title: "Python",
            key: "Python",
            count: 10,
            icon: {
              lib: "fa",
              icon: "cube",
            },
          },
        ],
      },
      {
        type: "group",
        title: "自定义模板",
        key: "customTemplate",
        items: [
          {
            type: "subMenu",
            title: "标准模板",
            key: "standard",
            icon: {
              lib: "easyops",
              category: "model",
              icon: "app",
            },
            items: [
              {
                title: "C++",
                key: "C++",
                count: 5,
                icon: {
                  lib: "easyops",
                  category: "model",
                  icon: "cpp",
                },
              },
              {
                title: "C",
                key: "C",
                count: 5,
                icon: {
                  lib: "easyops",
                  category: "model",
                  icon: "c",
                },
              },
              {
                title: "b",
                key: "b",
                count: 5,
                icon: {
                  lib: "fa",
                  icon: "cube",
                },
              },
            ],
          },
          {
            type: "subMenu",
            title: "个性化模板c",
            key: "diy",
            icon: {
              lib: "fa",
              icon: "cube",
            },
            items: [
              {
                title: "易语言",
                key: "iyuyan",
                count: 10,
                icon: {
                  lib: "fa",
                  icon: "cube",
                },
              },
            ],
          },
        ],
      },
    ],
    onSearch: jest.fn(),
    onSelect: jest.fn(),
  };
  let wrapper: ReactWrapper;

  beforeEach(() => {
    wrapper = mount(<SubMenuFilter {...props} />);
  });

  it("should work", () => {
    expect(wrapper.find(".transparentBackground").length).toBe(0);
    wrapper.setProps({
      transparentBackground: true,
      suffixBrick: {
        useBrick: {
          brick: "div",
          transform: {
            titleContent: "@{count}",
          },
        },
      },
    });
    wrapper.update();

    expect(wrapper.find(".transparentBackground").length).toBe(1);
    expect(wrapper.find(Menu.ItemGroup).length).toBe(2);
    expect(wrapper.find(Menu.SubMenu).length).toBe(2);
    expect(wrapper.find(Menu.Item).length).toBe(6);
    expect(wrapper.find(Menu).prop("inlineIndent")).toBe(12);
    expect(wrapper.find(Menu).prop("openKeys")).toEqual(["diy"]);

    expect(
      wrapper
        .find("div")
        .map((node) => node)
        .filter((node) => node.text() === "FakeBrickAsComponent").length
    ).toBe(6);
  });

  it("should call onSearch ", () => {
    wrapper.setProps({ placeholder: "fakePlaceholder" });
    wrapper.update();
    const searchInput = wrapper.find(Input.Search);
    expect(searchInput.prop("placeholder")).toBe("fakePlaceholder");

    searchInput.invoke("onSearch")("c");
    expect(props.onSearch).toHaveBeenLastCalledWith("c");

    searchInput.invoke("onSearch")("");
    const menu = wrapper.find(Menu);
    expect(menu.prop("openKeys")).toEqual(props.defaultOpenKeys);

    menu.invoke("onOpenChange")(["diy", "standard"]);
    wrapper.update();
    expect(wrapper.find(Menu).prop("openKeys")).toEqual(["diy", "standard"]);
  });

  it("should call onSelect", async () => {
    wrapper.setProps({
      selectable: true,
    });
    wrapper.update();
    const menu = wrapper.find(Menu);
    menu.invoke("onSelect")({ key: "cc" });
    await (global as any).flushPromises();
    expect(props.onSelect).toHaveBeenCalledWith([
      {
        title: "C++",
        key: "C++",
        count: 5,
        icon: {
          lib: "easyops",
          category: "model",
          icon: "cpp",
        },
      },
      {
        title: "Cc",
        key: "cc",
        count: 10,
        icon: {
          lib: "easyops",
          category: "model",
          icon: "go",
        },
      },
    ]);
    // call deselect function
    menu.invoke("onDeselect")({ key: "Cc" });
    await (global as any).flushPromises();
    expect(props.onSelect).toHaveBeenLastCalledWith([
      {
        title: "C++",
        key: "C++",
        count: 5,
        icon: {
          lib: "easyops",
          category: "model",
          icon: "cpp",
        },
      },
    ]);
  });
});
Example #26
Source File: ExperimentList.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('ExperimentList', () => {
  let tree: ShallowWrapper | ReactWrapper;

  const onErrorSpy = jest.fn();
  const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment');
  const getExperimentSpy = jest.spyOn(Apis.experimentServiceApi, 'getExperiment');
  // We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
  // test enviroments
  const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');
  const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');

  function generateProps(): ExperimentListProps {
    return {
      history: {} as any,
      location: { search: '' } as any,
      match: '' as any,
      onError: onErrorSpy,
    };
  }

  function mockNExperiments(n: number): void {
    getExperimentSpy.mockImplementation(id =>
      Promise.resolve({
        id: 'testexperiment' + id,
        name: 'experiment with id: testexperiment' + id,
      }),
    );
    listExperimentsSpy.mockImplementation(() =>
      Promise.resolve({
        experiments: range(1, n + 1).map(i => {
          return {
            id: 'testexperiment' + i,
            name: 'experiment with id: testexperiment' + i,
          };
        }),
      }),
    );
  }

  beforeEach(() => {
    formatDateStringSpy.mockImplementation((date?: Date) => {
      return date ? '1/2/2019, 12:34:56 PM' : '-';
    });
    onErrorSpy.mockClear();
    listExperimentsSpy.mockClear();
    getExperimentSpy.mockClear();
  });

  afterEach(async () => {
    // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
    // depends on mocks/spies
    if (tree) {
      await tree.unmount();
    }
    jest.resetAllMocks();
  });

  it('renders the empty experience', () => {
    expect(shallow(<ExperimentList {...generateProps()} />)).toMatchSnapshot();
  });

  it('renders the empty experience in ARCHIVED state', () => {
    const props = generateProps();
    props.storageState = ExperimentStorageState.ARCHIVED;
    expect(shallow(<ExperimentList {...props} />)).toMatchSnapshot();
  });

  it('loads experiments whose storage state is not ARCHIVED when storage state equals AVAILABLE', async () => {
    mockNExperiments(1);
    const props = generateProps();
    props.storageState = ExperimentStorageState.AVAILABLE;
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'storage_state',
              op: PredicateOp.NOTEQUALS,
              string_value: ExperimentStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
      undefined,
      undefined,
    );
  });

  it('loads experiments whose storage state is ARCHIVED when storage state equals ARCHIVED', async () => {
    mockNExperiments(1);
    const props = generateProps();
    props.storageState = ExperimentStorageState.ARCHIVED;
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'storage_state',
              op: PredicateOp.EQUALS,
              string_value: ExperimentStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
      undefined,
      undefined,
    );
  });

  it('augments request filter with storage state predicates', async () => {
    mockNExperiments(1);
    const props = generateProps();
    props.storageState = ExperimentStorageState.ARCHIVED;
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({
      filter: encodeURIComponent(
        JSON.stringify({
          predicates: [{ key: 'k', op: 'op', string_value: 'val' }],
        }),
      ),
    });
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'k',
              op: 'op',
              string_value: 'val',
            },
            {
              key: 'storage_state',
              op: PredicateOp.EQUALS,
              string_value: ExperimentStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
      undefined,
      undefined,
    );
  });

  it('loads one experiment', async () => {
    mockNExperiments(1);
    const props = generateProps();
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('reloads the experiment when refresh is called', async () => {
    mockNExperiments(0);
    const props = generateProps();
    tree = TestUtils.mountWithRouter(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentList).refresh();
    tree.update();
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenCalledTimes(2);
    expect(Apis.experimentServiceApi.listExperiment).toHaveBeenLastCalledWith(
      '',
      10,
      ExperimentSortKeys.CREATED_AT + ' desc',
      '',
      undefined,
      undefined,
    );
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('loads multiple experiments', async () => {
    mockNExperiments(5);
    const props = generateProps();
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('calls error callback when loading experiment fails', async () => {
    TestUtils.makeErrorResponseOnce(
      jest.spyOn(Apis.experimentServiceApi, 'listExperiment'),
      'bad stuff happened',
    );
    const props = generateProps();
    tree = shallow(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    expect(props.onError).toHaveBeenLastCalledWith(
      'Error: failed to list experiments: ',
      new Error('bad stuff happened'),
    );
  });

  it('loads runs for a given experiment id when it is expanded', async () => {
    listRunsSpy.mockImplementation(() => {});
    mockNExperiments(1);
    const props = generateProps();
    tree = TestUtils.mountWithRouter(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    tree.update();
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.COLLAPSED,
        id: 'testexperiment1',
        name: 'experiment with id: testexperiment1',
      },
    ]);
    // Expand the first experiment
    tree
      .find('button[aria-label="Expand"]')
      .at(0)
      .simulate('click');
    await listRunsSpy;
    tree.update();
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.EXPANDED,
        id: 'testexperiment1',
        name: 'experiment with id: testexperiment1',
      },
    ]);
    expect(Apis.runServiceApi.listRuns).toHaveBeenCalledTimes(1);
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      '',
      10,
      'created_at desc',
      'EXPERIMENT',
      'testexperiment1',
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'storage_state',
              op: PredicateOp.NOTEQUALS,
              string_value: RunStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
    );
  });

  it('loads runs for a given experiment id with augumented storage state when it is expanded', async () => {
    listRunsSpy.mockImplementation(() => {});
    mockNExperiments(1);
    const props = generateProps();
    props.storageState = ExperimentStorageState.ARCHIVED;
    tree = TestUtils.mountWithRouter(<ExperimentList {...props} />);
    await (tree.instance() as ExperimentListTest)._loadExperiments({});
    tree.update();
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.COLLAPSED,
        id: 'testexperiment1',
        name: 'experiment with id: testexperiment1',
      },
    ]);
    // Expand the first experiment
    tree
      .find('button[aria-label="Expand"]')
      .at(0)
      .simulate('click');
    await listRunsSpy;
    tree.update();
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.EXPANDED,
        id: 'testexperiment1',
        name: 'experiment with id: testexperiment1',
      },
    ]);
    expect(Apis.runServiceApi.listRuns).toHaveBeenCalledTimes(1);
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      '',
      10,
      'created_at desc',
      'EXPERIMENT',
      'testexperiment1',
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'storage_state',
              op: PredicateOp.EQUALS,
              string_value: RunStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
    );
  });
});
Example #27
Source File: RunList.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('RunList', () => {
  let tree: ShallowWrapper | ReactWrapper;

  const onErrorSpy = jest.fn();
  const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');
  const getRunSpy = jest.spyOn(Apis.runServiceApi, 'getRun');
  const getPipelineSpy = jest.spyOn(Apis.pipelineServiceApi, 'getPipeline');
  const getExperimentSpy = jest.spyOn(Apis.experimentServiceApi, 'getExperiment');
  // We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
  // test enviroments
  const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');

  function generateProps(): RunListProps {
    return {
      history: {} as any,
      location: { search: '' } as any,
      match: '' as any,
      onError: onErrorSpy,
    };
  }

  function mockNRuns(n: number, runTemplate: Partial<ApiRunDetail>): void {
    getRunSpy.mockImplementation(id =>
      Promise.resolve(
        produce(runTemplate, draft => {
          draft.run = draft.run || {};
          draft.run.id = id;
          draft.run.name = 'run with id: ' + id;
        }),
      ),
    );

    listRunsSpy.mockImplementation(() =>
      Promise.resolve({
        runs: range(1, n + 1).map(i => {
          if (runTemplate.run) {
            return produce(runTemplate.run as Partial<ApiRun>, draft => {
              draft.id = 'testrun' + i;
              draft.name = 'run with id: testrun' + i;
            });
          }
          return {
            id: 'testrun' + i,
            name: 'run with id: testrun' + i,
          } as ApiRun;
        }),
      }),
    );

    getPipelineSpy.mockImplementation(() => ({ name: 'some pipeline' }));
    getExperimentSpy.mockImplementation(() => ({ name: 'some experiment' }));
  }

  function getMountedInstance(): RunList {
    tree = TestUtils.mountWithRouter(<RunList {...generateProps()} />);
    return tree.instance() as RunList;
  }

  function getShallowInstance(): RunList {
    tree = shallow(<RunList {...generateProps()} />);
    return tree.instance() as RunList;
  }

  beforeEach(() => {
    formatDateStringSpy.mockImplementation((date?: Date) => {
      return date ? '1/2/2019, 12:34:56 PM' : '-';
    });
    onErrorSpy.mockClear();
    listRunsSpy.mockClear();
    getRunSpy.mockClear();
    getPipelineSpy.mockClear();
    getExperimentSpy.mockClear();
  });

  afterEach(async () => {
    // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
    // depends on mocks/spies
    if (tree) {
      await tree.unmount();
    }
    jest.resetAllMocks();
  });

  it('renders the empty experience', () => {
    expect(shallow(<RunList {...generateProps()} />)).toMatchSnapshot();
  });

  describe('in archived state', () => {
    it('renders the empty experience', () => {
      const props = generateProps();
      props.storageState = RunStorageState.ARCHIVED;
      expect(shallow(<RunList {...props} />)).toMatchSnapshot();
    });

    it('loads runs whose storage state is not ARCHIVED when storage state equals AVAILABLE', async () => {
      mockNRuns(1, {});
      const props = generateProps();
      props.storageState = RunStorageState.AVAILABLE;
      tree = shallow(<RunList {...props} />);
      await (tree.instance() as RunListTest)._loadRuns({});
      expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        encodeURIComponent(
          JSON.stringify({
            predicates: [
              {
                key: 'storage_state',
                op: PredicateOp.NOTEQUALS,
                string_value: RunStorageState.ARCHIVED.toString(),
              },
            ],
          } as ApiFilter),
        ),
      );
    });

    it('loads runs whose storage state is ARCHIVED when storage state equals ARCHIVED', async () => {
      mockNRuns(1, {});
      const props = generateProps();
      props.storageState = RunStorageState.ARCHIVED;
      tree = shallow(<RunList {...props} />);
      await (tree.instance() as RunListTest)._loadRuns({});
      expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        encodeURIComponent(
          JSON.stringify({
            predicates: [
              {
                key: 'storage_state',
                op: PredicateOp.EQUALS,
                string_value: RunStorageState.ARCHIVED.toString(),
              },
            ],
          } as ApiFilter),
        ),
      );
    });

    it('augments request filter with storage state predicates', async () => {
      mockNRuns(1, {});
      const props = generateProps();
      props.storageState = RunStorageState.ARCHIVED;
      tree = shallow(<RunList {...props} />);
      await (tree.instance() as RunListTest)._loadRuns({
        filter: encodeURIComponent(
          JSON.stringify({
            predicates: [{ key: 'k', op: 'op', string_value: 'val' }],
          }),
        ),
      });
      expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        encodeURIComponent(
          JSON.stringify({
            predicates: [
              {
                key: 'k',
                op: 'op',
                string_value: 'val',
              },
              {
                key: 'storage_state',
                op: PredicateOp.EQUALS,
                string_value: RunStorageState.ARCHIVED.toString(),
              },
            ],
          } as ApiFilter),
        ),
      );
    });
  });

  it('loads one run', async () => {
    mockNRuns(1, {});
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('reloads the run when refresh is called', async () => {
    mockNRuns(0, {});
    const props = generateProps();
    tree = TestUtils.mountWithRouter(<RunList {...props} />);
    await (tree.instance() as RunList).refresh();
    tree.update();
    expect(Apis.runServiceApi.listRuns).toHaveBeenCalledTimes(2);
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      '',
      10,
      RunSortKeys.CREATED_AT + ' desc',
      undefined,
      undefined,
      '',
    );
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('loads multiple runs', async () => {
    mockNRuns(5, {});
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('calls error callback when loading runs fails', async () => {
    TestUtils.makeErrorResponseOnce(
      jest.spyOn(Apis.runServiceApi, 'listRuns'),
      'bad stuff happened',
    );
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).toHaveBeenLastCalledWith(
      'Error: failed to fetch runs.',
      new Error('bad stuff happened'),
    );
  });

  it('displays error in run row if pipeline could not be fetched', async () => {
    mockNRuns(1, { run: { pipeline_spec: { pipeline_id: 'test-pipeline-id' } } });
    TestUtils.makeErrorResponseOnce(getPipelineSpy, 'bad stuff happened');
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(tree).toMatchSnapshot();
  });

  it('displays error in run row if experiment could not be fetched', async () => {
    mockNRuns(1, {
      run: {
        resource_references: [
          {
            key: { id: 'test-experiment-id', type: ApiResourceType.EXPERIMENT },
          },
        ],
      },
    });
    TestUtils.makeErrorResponseOnce(getExperimentSpy, 'bad stuff happened');
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(tree).toMatchSnapshot();
  });

  it('displays error in run row if it failed to parse (run list mask)', async () => {
    TestUtils.makeErrorResponseOnce(jest.spyOn(Apis.runServiceApi, 'getRun'), 'bad stuff happened');
    const props = generateProps();
    props.runIdListMask = ['testrun1', 'testrun2'];
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(tree).toMatchSnapshot();
  });

  it('shows run time for each run', async () => {
    mockNRuns(1, {
      run: {
        created_at: new Date(2018, 10, 10, 10, 10, 10),
        finished_at: new Date(2018, 10, 10, 11, 11, 11),
        status: 'Succeeded',
      },
    });
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('loads runs for a given experiment id', async () => {
    mockNRuns(1, {});
    const props = generateProps();
    props.experimentIdMask = 'experiment1';
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      'EXPERIMENT',
      'experiment1',
      undefined,
    );
  });

  it('loads runs for a given namespace', async () => {
    mockNRuns(1, {});
    const props = generateProps();
    props.namespaceMask = 'namespace1';
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(
      undefined,
      undefined,
      undefined,
      'NAMESPACE',
      'namespace1',
      undefined,
    );
  });

  it('loads given list of runs only', async () => {
    mockNRuns(5, {});
    const props = generateProps();
    props.runIdListMask = ['run1', 'run2'];
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(Apis.runServiceApi.listRuns).not.toHaveBeenCalled();
    expect(Apis.runServiceApi.getRun).toHaveBeenCalledTimes(2);
    expect(Apis.runServiceApi.getRun).toHaveBeenCalledWith('run1');
    expect(Apis.runServiceApi.getRun).toHaveBeenCalledWith('run2');
  });

  it('adds metrics columns', async () => {
    mockNRuns(2, {
      run: {
        metrics: [
          { name: 'metric1', number_value: 5 },
          { name: 'metric2', number_value: 10 },
        ],
        status: 'Succeeded',
      },
    });
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('shows pipeline name', async () => {
    mockNRuns(1, {
      run: { pipeline_spec: { pipeline_id: 'test-pipeline-id', pipeline_name: 'pipeline name' } },
    });
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('retrieves pipeline from backend to display name if not in spec', async () => {
    mockNRuns(1, {
      run: { pipeline_spec: { pipeline_id: 'test-pipeline-id' /* no pipeline_name */ } },
    });
    getPipelineSpy.mockImplementationOnce(() => ({ name: 'test pipeline' }));
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('shows link to recurring run config', async () => {
    mockNRuns(1, {
      run: {
        resource_references: [
          {
            key: { id: 'test-recurring-run-id', type: ApiResourceType.JOB },
          },
        ],
      },
    });
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('shows experiment name', async () => {
    mockNRuns(1, {
      run: {
        resource_references: [
          {
            key: { id: 'test-experiment-id', type: ApiResourceType.EXPERIMENT },
          },
        ],
      },
    });
    getExperimentSpy.mockImplementationOnce(() => ({ name: 'test experiment' }));
    const props = generateProps();
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('hides experiment name if instructed', async () => {
    mockNRuns(1, {
      run: {
        resource_references: [
          {
            key: { id: 'test-experiment-id', type: ApiResourceType.EXPERIMENT },
          },
        ],
      },
    });
    getExperimentSpy.mockImplementationOnce(() => ({ name: 'test experiment' }));
    const props = generateProps();
    props.hideExperimentColumn = true;
    tree = shallow(<RunList {...props} />);
    await (tree.instance() as RunListTest)._loadRuns({});
    expect(props.onError).not.toHaveBeenCalled();
    expect(tree).toMatchSnapshot();
  });

  it('renders run name as link to its details page', () => {
    expect(
      getMountedInstance()._nameCustomRenderer({ value: 'test run', id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders pipeline name as link to its details page', () => {
    expect(
      getMountedInstance()._pipelineVersionCustomRenderer({
        id: 'run-id',
        value: { displayName: 'test pipeline', pipelineId: 'pipeline-id', usePlaceholder: false },
      }),
    ).toMatchSnapshot();
  });

  it('handles no pipeline id given', () => {
    expect(
      getMountedInstance()._pipelineVersionCustomRenderer({
        id: 'run-id',
        value: { displayName: 'test pipeline', usePlaceholder: false },
      }),
    ).toMatchSnapshot();
  });

  it('shows "View pipeline" button if pipeline is embedded in run', () => {
    expect(
      getMountedInstance()._pipelineVersionCustomRenderer({
        id: 'run-id',
        value: { displayName: 'test pipeline', pipelineId: 'pipeline-id', usePlaceholder: true },
      }),
    ).toMatchSnapshot();
  });

  it('handles no pipeline name', () => {
    expect(
      getMountedInstance()._pipelineVersionCustomRenderer({
        id: 'run-id',
        value: { /* no displayName */ usePlaceholder: true },
      }),
    ).toMatchSnapshot();
  });

  it('renders pipeline name as link to its details page', () => {
    expect(
      getMountedInstance()._recurringRunCustomRenderer({
        id: 'run-id',
        value: { id: 'recurring-run-id' },
      }),
    ).toMatchSnapshot();
  });

  it('renders experiment name as link to its details page', () => {
    expect(
      getMountedInstance()._experimentCustomRenderer({
        id: 'run-id',
        value: { displayName: 'test experiment', id: 'experiment-id' },
      }),
    ).toMatchSnapshot();
  });

  it('renders no experiment name', () => {
    expect(
      getMountedInstance()._experimentCustomRenderer({
        id: 'run-id',
        value: { /* no displayName */ id: 'experiment-id' },
      }),
    ).toMatchSnapshot();
  });

  it('renders status as icon', () => {
    expect(
      getShallowInstance()._statusCustomRenderer({ value: NodePhase.SUCCEEDED, id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders metric buffer', () => {
    expect(
      getShallowInstance()._metricBufferCustomRenderer({ value: {}, id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders an empty metric when there is no metric', () => {
    expect(
      getShallowInstance()._metricCustomRenderer({ value: undefined, id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders an empty metric when metric is empty', () => {
    expect(
      getShallowInstance()._metricCustomRenderer({ value: {}, id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders an empty metric when metric value is empty', () => {
    expect(
      getShallowInstance()._metricCustomRenderer({ value: { metric: {} }, id: 'run-id' }),
    ).toMatchSnapshot();
  });

  it('renders percentage metric', () => {
    expect(
      getShallowInstance()._metricCustomRenderer({
        id: 'run-id',
        value: { metric: { number_value: 0.3, format: RunMetricFormat.PERCENTAGE } },
      }),
    ).toMatchSnapshot();
  });

  it('renders raw metric', () => {
    expect(
      getShallowInstance()._metricCustomRenderer({
        id: 'run-id',
        value: {
          metadata: { count: 1, maxValue: 100, minValue: 10 } as MetricMetadata,
          metric: { number_value: 55 } as ApiRunMetric,
        },
      }),
    ).toMatchSnapshot();
  });

  it('renders pipeline version name as link to its details page', () => {
    expect(
      getMountedInstance()._pipelineVersionCustomRenderer({
        id: 'run-id',
        value: {
          displayName: 'test pipeline version',
          pipelineId: 'pipeline-id',
          usePlaceholder: false,
          versionId: 'version-id',
        },
      }),
    ).toMatchSnapshot();
  });
});
Example #28
Source File: RunDetails.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('RunDetails', () => {
  let updateBannerSpy: any;
  let updateDialogSpy: any;
  let updateSnackbarSpy: any;
  let updateToolbarSpy: any;
  let historyPushSpy: any;
  let getRunSpy: any;
  let getExperimentSpy: any;
  let isCustomVisualizationsAllowedSpy: any;
  let getPodLogsSpy: any;
  let pathsParser: any;
  let pathsWithStepsParser: any;
  let loaderSpy: any;
  let retryRunSpy: any;
  let terminateRunSpy: any;
  let artifactTypesSpy: any;
  let formatDateStringSpy: any;

  let testRun: ApiRunDetail = {};
  let tree: ShallowWrapper | ReactWrapper;

  function generateProps(): RunDetailsInternalProps & PageProps {
    const pageProps: PageProps = {
      history: { push: historyPushSpy } as any,
      location: '' as any,
      match: { params: { [RouteParams.runId]: testRun.run!.id }, isExact: true, path: '', url: '' },
      toolbarProps: { actions: {}, breadcrumbs: [], pageTitle: '' },
      updateBanner: updateBannerSpy,
      updateDialog: updateDialogSpy,
      updateSnackbar: updateSnackbarSpy,
      updateToolbar: updateToolbarSpy,
    };
    return Object.assign(pageProps, {
      toolbarProps: new RunDetails(pageProps).getInitialToolbarState(),
      gkeMetadata: {},
    });
  }

  beforeEach(() => {
    // The RunDetails page uses timers to periodically refresh
    jest.useFakeTimers();
    // TODO: mute error only for tests that are expected to have error
    jest.spyOn(console, 'error').mockImplementation(() => null);

    testRun = {
      pipeline_runtime: {
        workflow_manifest: '{}',
      },
      run: {
        created_at: new Date(2018, 8, 5, 4, 3, 2),
        description: 'test run description',
        id: 'test-run-id',
        name: 'test run',
        pipeline_spec: {
          parameters: [{ name: 'param1', value: 'value1' }],
          pipeline_id: 'some-pipeline-id',
        },
        status: 'Succeeded',
      },
    };
    updateBannerSpy = jest.fn();
    updateDialogSpy = jest.fn();
    updateSnackbarSpy = jest.fn();
    updateToolbarSpy = jest.fn();
    historyPushSpy = jest.fn();
    getRunSpy = jest.spyOn(Apis.runServiceApi, 'getRun');
    getExperimentSpy = jest.spyOn(Apis.experimentServiceApi, 'getExperiment');
    isCustomVisualizationsAllowedSpy = jest.spyOn(Apis, 'areCustomVisualizationsAllowed');
    getPodLogsSpy = jest.spyOn(Apis, 'getPodLogs');
    pathsParser = jest.spyOn(WorkflowParser, 'loadNodeOutputPaths');
    pathsWithStepsParser = jest.spyOn(WorkflowParser, 'loadAllOutputPathsWithStepNames');
    loaderSpy = jest.spyOn(OutputArtifactLoader, 'load');
    retryRunSpy = jest.spyOn(Apis.runServiceApi, 'retryRun');
    terminateRunSpy = jest.spyOn(Apis.runServiceApi, 'terminateRun');
    artifactTypesSpy = jest.spyOn(Api.getInstance().metadataStoreService, 'getArtifactTypes');
    // We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
    // test environments
    formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');

    getRunSpy.mockImplementation(() => Promise.resolve(testRun));
    getExperimentSpy.mockImplementation(() =>
      Promise.resolve({ id: 'some-experiment-id', name: 'some experiment' }),
    );
    isCustomVisualizationsAllowedSpy.mockImplementation(() => Promise.resolve(false));
    getPodLogsSpy.mockImplementation(() => 'test logs');
    pathsParser.mockImplementation(() => []);
    pathsWithStepsParser.mockImplementation(() => []);
    loaderSpy.mockImplementation(() => Promise.resolve([]));
    formatDateStringSpy.mockImplementation(() => '1/2/2019, 12:34:56 PM');
    artifactTypesSpy.mockImplementation(() => {
      // TODO: This is temporary workaround to let tfx artifact resolving logic fail early.
      // We should add proper testing for those cases later too.
      const response = new GetArtifactTypesResponse();
      response.setArtifactTypesList([]);
      return response;
    });
  });

  afterEach(async () => {
    if (tree && tree.exists()) {
      // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
      // depends on mocks/spies
      await tree.unmount();
    }
    jest.resetAllMocks();
    jest.restoreAllMocks();
  });

  it('shows success run status in page title', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const lastCall = updateToolbarSpy.mock.calls[2][0];
    expect(lastCall.pageTitle).toMatchSnapshot();
  });

  it('shows failure run status in page title', async () => {
    testRun.run!.status = 'Failed';
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const lastCall = updateToolbarSpy.mock.calls[2][0];
    expect(lastCall.pageTitle).toMatchSnapshot();
  });

  it('has a clone button, clicking it navigates to new run page', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const cloneBtn = instance.getInitialToolbarState().actions[ButtonKeys.CLONE_RUN];
    expect(cloneBtn).toBeDefined();
    await cloneBtn!.action();
    expect(historyPushSpy).toHaveBeenCalledTimes(1);
    expect(historyPushSpy).toHaveBeenLastCalledWith(
      RoutePage.NEW_RUN + `?${QUERY_PARAMS.cloneFromRun}=${testRun.run!.id}`,
    );
  });

  it('clicking the clone button when the page is half-loaded navigates to new run page with run id', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    // Intentionally don't wait until all network requests finish.
    const instance = tree.instance() as RunDetails;
    const cloneBtn = instance.getInitialToolbarState().actions[ButtonKeys.CLONE_RUN];
    expect(cloneBtn).toBeDefined();
    await cloneBtn!.action();
    expect(historyPushSpy).toHaveBeenCalledTimes(1);
    expect(historyPushSpy).toHaveBeenLastCalledWith(
      RoutePage.NEW_RUN + `?${QUERY_PARAMS.cloneFromRun}=${testRun.run!.id}`,
    );
  });

  it('has a retry button', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    expect(retryBtn).toBeDefined();
  });

  it('shows retry confirmation dialog when retry button is clicked', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    await retryBtn!.action();
    expect(updateDialogSpy).toHaveBeenCalledTimes(1);
    expect(updateDialogSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        title: 'Retry this run?',
      }),
    );
  });

  it('does not call retry API for selected run when retry dialog is canceled', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    await retryBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const cancelBtn = call.buttons.find((b: any) => b.text === 'Cancel');
    await cancelBtn.onClick();
    expect(retryRunSpy).not.toHaveBeenCalled();
  });

  it('calls retry API when retry dialog is confirmed', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    await retryBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Retry');
    await confirmBtn.onClick();
    expect(retryRunSpy).toHaveBeenCalledTimes(1);
    expect(retryRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
  });

  it('calls retry API when retry dialog is confirmed and page is half-loaded', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    // Intentionally don't wait until all network requests finish.
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    await retryBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Retry');
    await confirmBtn.onClick();
    expect(retryRunSpy).toHaveBeenCalledTimes(1);
    expect(retryRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
  });

  it('shows an error dialog when retry API fails', async () => {
    retryRunSpy.mockImplementation(() => Promise.reject('mocked error'));

    tree = mount(<RunDetails {...generateProps()} />);
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const retryBtn = instance.getInitialToolbarState().actions[ButtonKeys.RETRY];
    await retryBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Retry');
    await confirmBtn.onClick();
    expect(updateDialogSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        content: 'Failed to retry run: test-run-id with error: ""mocked error""',
      }),
    );
    // There shouldn't be a snackbar on error.
    expect(updateSnackbarSpy).not.toHaveBeenCalled();
  });

  it('has a terminate button', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const terminateBtn = instance.getInitialToolbarState().actions[ButtonKeys.TERMINATE_RUN];
    expect(terminateBtn).toBeDefined();
  });

  it('shows terminate confirmation dialog when terminate button is clicked', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    const instance = tree.instance() as RunDetails;
    const terminateBtn = instance.getInitialToolbarState().actions[ButtonKeys.TERMINATE_RUN];
    await terminateBtn!.action();
    expect(updateDialogSpy).toHaveBeenCalledTimes(1);
    expect(updateDialogSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        title: 'Terminate this run?',
      }),
    );
  });

  it('does not call terminate API for selected run when terminate dialog is canceled', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    const instance = tree.instance() as RunDetails;
    const terminateBtn = instance.getInitialToolbarState().actions[ButtonKeys.TERMINATE_RUN];
    await terminateBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const cancelBtn = call.buttons.find((b: any) => b.text === 'Cancel');
    await cancelBtn.onClick();
    expect(terminateRunSpy).not.toHaveBeenCalled();
  });

  it('calls terminate API when terminate dialog is confirmed', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    const instance = tree.instance() as RunDetails;
    const terminateBtn = instance.getInitialToolbarState().actions[ButtonKeys.TERMINATE_RUN];
    await terminateBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Terminate');
    await confirmBtn.onClick();
    expect(terminateRunSpy).toHaveBeenCalledTimes(1);
    expect(terminateRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
  });

  it('calls terminate API when terminate dialog is confirmed and page is half-loaded', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    // Intentionally don't wait until all network requests finish.
    const instance = tree.instance() as RunDetails;
    const terminateBtn = instance.getInitialToolbarState().actions[ButtonKeys.TERMINATE_RUN];
    await terminateBtn!.action();
    const call = updateDialogSpy.mock.calls[0][0];
    const confirmBtn = call.buttons.find((b: any) => b.text === 'Terminate');
    await confirmBtn.onClick();
    expect(terminateRunSpy).toHaveBeenCalledTimes(1);
    expect(terminateRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
  });

  it('has an Archive button if the run is not archived', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE)).toBeDefined();
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE)).toBeUndefined();
  });

  it('shows "All runs" in breadcrumbs if the run is not archived', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateToolbarSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        breadcrumbs: [{ displayName: 'All runs', href: RoutePage.RUNS }],
      }),
    );
  });

  it('shows experiment name in breadcrumbs if the run is not archived', async () => {
    testRun.run!.resource_references = [
      { key: { id: 'some-experiment-id', type: ApiResourceType.EXPERIMENT } },
    ];
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateToolbarSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        breadcrumbs: [
          { displayName: 'Experiments', href: RoutePage.EXPERIMENTS },
          {
            displayName: 'some experiment',
            href: RoutePage.EXPERIMENT_DETAILS.replace(
              ':' + RouteParams.experimentId,
              'some-experiment-id',
            ),
          },
        ],
      }),
    );
  });

  it('has a Restore button if the run is archived', async () => {
    testRun.run!.storage_state = RunStorageState.ARCHIVED;
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE)).toBeDefined();
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE)).toBeUndefined();
  });

  it('shows Archive in breadcrumbs if the run is archived', async () => {
    testRun.run!.storage_state = RunStorageState.ARCHIVED;
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateToolbarSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        breadcrumbs: [{ displayName: 'Archive', href: RoutePage.ARCHIVED_RUNS }],
      }),
    );
  });

  it('renders an empty run', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await TestUtils.flushPromises();
    expect(tree).toMatchSnapshot();
  });

  it('calls the get run API once to load it', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(getRunSpy).toHaveBeenCalledTimes(1);
    expect(getRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
  });

  it('shows an error banner if get run API fails', async () => {
    TestUtils.makeErrorResponseOnce(getRunSpy, 'woops');
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once initially to clear
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'woops',
        message:
          'Error: failed to retrieve run: ' +
          testRun.run!.id +
          '. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('shows an error banner if get experiment API fails', async () => {
    testRun.run!.resource_references = [
      { key: { id: 'experiment1', type: ApiResourceType.EXPERIMENT } },
    ];
    TestUtils.makeErrorResponseOnce(getExperimentSpy, 'woops');
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once initially to clear
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'woops',
        message:
          'Error: failed to retrieve run: ' +
          testRun.run!.id +
          '. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('calls the get experiment API once to load it if the run has its reference', async () => {
    testRun.run!.resource_references = [
      { key: { id: 'experiment1', type: ApiResourceType.EXPERIMENT } },
    ];
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(getRunSpy).toHaveBeenCalledTimes(1);
    expect(getRunSpy).toHaveBeenLastCalledWith(testRun.run!.id);
    expect(getExperimentSpy).toHaveBeenCalledTimes(1);
    expect(getExperimentSpy).toHaveBeenLastCalledWith('experiment1');
  });

  it('shows workflow errors as page error', async () => {
    jest
      .spyOn(WorkflowParser, 'getWorkflowError')
      .mockImplementationOnce(() => 'some error message');
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once to clear on init, once for error
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'some error message',
        message:
          'Error: found errors when executing run: ' +
          testRun.run!.id +
          '. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('switches to run output tab, shows empty message', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 1);
    expect(tree.state('selectedTab')).toBe(1);
    await TestUtils.flushPromises();
    expect(tree).toMatchSnapshot();
  });

  it("loads the run's outputs in the output tab", async () => {
    pathsWithStepsParser.mockImplementation(() => [
      { stepName: 'step1', path: { source: 'gcs', bucket: 'somebucket', key: 'somekey' } },
    ]);
    pathsParser.mockImplementation(() => [{ source: 'gcs', bucket: 'somebucket', key: 'somekey' }]);
    loaderSpy.mockImplementation(() =>
      Promise.resolve([{ type: PlotType.TENSORBOARD, url: 'some url' }]),
    );
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 1);
    expect(tree.state('selectedTab')).toBe(1);
    await TestUtils.flushPromises();
    expect(tree).toMatchSnapshot();
  });

  it('switches to config tab', async () => {
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 2);
    expect(tree.state('selectedTab')).toBe(2);
    expect(tree).toMatchSnapshot();
  });

  it('shows run config fields', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      metadata: {
        creationTimestamp: new Date(2018, 6, 5, 4, 3, 2).toISOString(),
      },
      spec: {
        arguments: {
          parameters: [
            {
              name: 'param1',
              value: 'value1',
            },
            {
              name: 'param2',
              value: 'value2',
            },
          ],
        },
      },
      status: {
        finishedAt: new Date(2018, 6, 6, 5, 4, 3).toISOString(),
        phase: 'Skipped',
        startedAt: new Date(2018, 6, 5, 4, 3, 2).toISOString(),
      },
    } as Workflow);
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 2);
    expect(tree.state('selectedTab')).toBe(2);
    expect(tree).toMatchSnapshot();
  });

  it('shows run config fields - handles no description', async () => {
    delete testRun.run!.description;
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      metadata: {
        creationTimestamp: new Date(2018, 6, 5, 4, 3, 2).toISOString(),
      },
      status: {
        finishedAt: new Date(2018, 6, 6, 5, 4, 3).toISOString(),
        phase: 'Skipped',
        startedAt: new Date(2018, 6, 5, 4, 3, 2).toISOString(),
      },
    } as Workflow);
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 2);
    expect(tree.state('selectedTab')).toBe(2);
    expect(tree).toMatchSnapshot();
  });

  it('shows run config fields - handles no metadata', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        finishedAt: new Date(2018, 6, 6, 5, 4, 3).toISOString(),
        phase: 'Skipped',
        startedAt: new Date(2018, 6, 5, 4, 3, 2).toISOString(),
      },
    } as Workflow);
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    tree.find('MD2Tabs').simulate('switch', 2);
    expect(tree.state('selectedTab')).toBe(2);
    expect(tree).toMatchSnapshot();
  });

  it('shows a one-node graph', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    expect(tree).toMatchSnapshot();
  });

  it('opens side panel when graph node is clicked', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree).toMatchSnapshot();
  });

  it('shows clicked node message in side panel', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        nodes: {
          node1: {
            id: 'node1',
            message: 'some test message',
            phase: 'Succeeded',
          },
        },
      },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    expect(tree.state('selectedNodeDetails')).toHaveProperty(
      'phaseMessage',
      'This step is in ' + testRun.run!.status + ' state with this message: some test message',
    );
    expect(tree.find('Banner')).toMatchInlineSnapshot(`
      <Banner
        message="This step is in Succeeded state with this message: some test message"
        mode="warning"
      />
    `);
  });

  it('shows clicked node output in side pane', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    pathsWithStepsParser.mockImplementation(() => [
      { stepName: 'step1', path: { source: 'gcs', bucket: 'somebucket', key: 'somekey' } },
    ]);
    pathsParser.mockImplementation(() => [{ source: 'gcs', bucket: 'somebucket', key: 'somekey' }]);
    loaderSpy.mockImplementation(() =>
      Promise.resolve([{ type: PlotType.TENSORBOARD, url: 'some url' }]),
    );
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    await pathsParser;
    await pathsWithStepsParser;
    await loaderSpy;
    await artifactTypesSpy;
    await TestUtils.flushPromises();

    // TODO: fix this test and write additional tests for the ArtifactTabContent component.
    // expect(pathsWithStepsParser).toHaveBeenCalledTimes(1); // Loading output list
    // expect(pathsParser).toHaveBeenCalledTimes(1);
    // expect(pathsParser).toHaveBeenLastCalledWith({ id: 'node1' });
    // expect(loaderSpy).toHaveBeenCalledTimes(2);
    // expect(loaderSpy).toHaveBeenLastCalledWith({
    //   bucket: 'somebucket',
    //   key: 'somekey',
    //   source: 'gcs',
    // });
    // expect(tree.state('selectedNodeDetails')).toMatchObject({ id: 'node1' });
    // expect(tree).toMatchSnapshot();
  });

  it('switches to inputs/outputs tab in side pane', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        nodes: {
          node1: {
            id: 'node1',
            inputs: {
              parameters: [{ name: 'input1', value: 'val1' }],
            },
            name: 'node1',
            outputs: {
              parameters: [
                { name: 'output1', value: 'val1' },
                { name: 'output2', value: 'value2' },
              ],
            },
            phase: 'Succeeded',
          },
        },
      },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.INPUT_OUTPUT);
    await TestUtils.flushPromises();
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.INPUT_OUTPUT);
    expect(tree).toMatchSnapshot();
  });

  it('switches to volumes tab in side pane', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.VOLUMES);
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.VOLUMES);
    expect(tree).toMatchSnapshot();
  });

  it('switches to manifest tab in side pane', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.MANIFEST);
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.MANIFEST);
    expect(tree).toMatchSnapshot();
  });

  it('closes side panel when close button is clicked', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    await TestUtils.flushPromises();
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    tree.find('SidePanel').simulate('close');
    expect(tree.state('selectedNodeDetails')).toBeNull();
    await TestUtils.flushPromises();
    expect(tree).toMatchSnapshot();
  });

  it('keeps side pane open and on same tab when page is refreshed', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.LOGS);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);

    await (tree.instance() as RunDetails).refresh();
    expect(getRunSpy).toHaveBeenCalledTimes(2);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);
  });

  it('keeps side pane open and on same tab when more nodes are added after refresh', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        nodes: {
          node1: { id: 'node1' },
          node2: { id: 'node2' },
        },
      },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.LOGS);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);

    await (tree.instance() as RunDetails).refresh();
    expect(getRunSpy).toHaveBeenCalledTimes(2);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);
  });

  it('keeps side pane open and on same tab when run status changes, shows new status', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.LOGS);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
    expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);
    expect(updateToolbarSpy).toHaveBeenCalledTimes(3);

    const thirdCall = updateToolbarSpy.mock.calls[2][0];
    expect(thirdCall.pageTitle).toMatchSnapshot();

    testRun.run!.status = 'Failed';
    await (tree.instance() as RunDetails).refresh();
    const fourthCall = updateToolbarSpy.mock.calls[3][0];
    expect(fourthCall.pageTitle).toMatchSnapshot();
  });

  it('shows node message banner if node receives message after refresh', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1', phase: 'Succeeded', message: '' } } },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.LOGS);
    expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined);

    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        nodes: { node1: { id: 'node1', phase: 'Succeeded', message: 'some node message' } },
      },
    });
    await (tree.instance() as RunDetails).refresh();
    expect(tree.state('selectedNodeDetails')).toHaveProperty(
      'phaseMessage',
      'This step is in Succeeded state with this message: some node message',
    );
  });

  it('dismisses node message banner if node loses message after refresh', async () => {
    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: {
        nodes: { node1: { id: 'node1', phase: 'Succeeded', message: 'some node message' } },
      },
    });
    tree = shallow(<RunDetails {...generateProps()} />);
    await getRunSpy;
    await TestUtils.flushPromises();
    clickGraphNode(tree, 'node1');
    tree
      .find('MD2Tabs')
      .at(1)
      .simulate('switch', STEP_TABS.LOGS);
    expect(tree.state('selectedNodeDetails')).toHaveProperty(
      'phaseMessage',
      'This step is in Succeeded state with this message: some node message',
    );

    testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
      status: { nodes: { node1: { id: 'node1' } } },
    });
    await (tree.instance() as RunDetails).refresh();
    expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined);
  });

  [NodePhase.RUNNING, NodePhase.PENDING, NodePhase.UNKNOWN].forEach(unfinishedStatus => {
    it(`displays a spinner if graph is not defined and run has status: ${unfinishedStatus}`, async () => {
      const unfinishedRun = {
        pipeline_runtime: {
          // No graph
          workflow_manifest: '{}',
        },
        run: {
          id: 'test-run-id',
          name: 'test run',
          status: unfinishedStatus,
        },
      };
      getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun));

      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();

      expect(tree).toMatchSnapshot();
    });
  });

  [NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(
    finishedStatus => {
      it(`displays a message indicating there is no graph if graph is not defined and run has status: ${finishedStatus}`, async () => {
        const unfinishedRun = {
          pipeline_runtime: {
            // No graph
            workflow_manifest: '{}',
          },
          run: {
            id: 'test-run-id',
            name: 'test run',
            status: finishedStatus,
          },
        };
        getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun));

        tree = shallow(<RunDetails {...generateProps()} />);
        await getRunSpy;
        await TestUtils.flushPromises();

        expect(tree).toMatchSnapshot();
      });
    },
  );

  it('shows an error banner if the custom visualizations state API fails', async () => {
    TestUtils.makeErrorResponseOnce(isCustomVisualizationsAllowedSpy, 'woops');
    tree = shallow(<RunDetails {...generateProps()} />);
    await isCustomVisualizationsAllowedSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenCalledTimes(2); // Once initially to clear
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'woops',
        message:
          'Error: Unable to enable custom visualizations. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  describe('logs tab', () => {
    it('switches to logs tab in side pane', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);
      expect(tree).toMatchSnapshot();
    });

    it('loads and shows logs in side pane', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      expect(getPodLogsSpy).toHaveBeenCalledTimes(1);
      expect(getPodLogsSpy).toHaveBeenLastCalledWith('node1', undefined);
      expect(tree).toMatchSnapshot();
    });

    it('shows stackdriver link next to logs in GKE', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      tree = shallow(
        <RunDetails
          {...generateProps()}
          gkeMetadata={{ projectId: 'test-project-id', clusterName: 'test-cluster-name' }}
        />,
      );
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      await TestUtils.flushPromises();
      expect(tree.find(NODE_DETAILS_SELECTOR)).toMatchInlineSnapshot(`
        <div
          className="page"
          data-testid="run-details-node-details"
        >
          <div
            className="page"
          >
            <div
              className=""
            >
              Logs can also be viewed in
               
              <a
                className="link unstyled"
                href="https://console.cloud.google.com/logs/viewer?project=test-project-id&interval=NO_LIMIT&advancedFilter=resource.type%3D\\"k8s_container\\"%0Aresource.labels.cluster_name:\\"test-cluster-name\\"%0Aresource.labels.pod_name:\\"node1\\""
                rel="noopener noreferrer"
                target="_blank"
              >
                Stackdriver Kubernetes Monitoring
              </a>
              .
            </div>
            <div
              className="pageOverflowHidden"
            >
              <LogViewer
                logLines={
                  Array [
                    "test logs",
                  ]
                }
              />
            </div>
          </div>
        </div>
      `);
    });

    it("loads logs in run's namespace", async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        metadata: { namespace: 'username' },
        status: { nodes: { node1: { id: 'node1' } } },
      });
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      expect(getPodLogsSpy).toHaveBeenCalledTimes(1);
      expect(getPodLogsSpy).toHaveBeenLastCalledWith('node1', 'username');
    });

    it('shows warning banner and link to Stackdriver in logs area if fetching logs failed and cluster is in GKE', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      TestUtils.makeErrorResponseOnce(getPodLogsSpy, 'getting logs failed');
      tree = shallow(
        <RunDetails
          {...generateProps()}
          gkeMetadata={{ projectId: 'test-project-id', clusterName: 'test-cluster-name' }}
        />,
      );
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      await TestUtils.flushPromises();
      expect(tree.find(NODE_DETAILS_SELECTOR)).toMatchInlineSnapshot(`
        <div
          className="page"
          data-testid="run-details-node-details"
        >
          <div
            className="page"
          >
            <Banner
              additionalInfo="getting logs failed"
              message="Warning: failed to retrieve pod logs. Possible reasons include cluster autoscaling or pod preemption"
              mode="warning"
              refresh={[Function]}
            />
            <div
              className=""
            >
              Logs can also be viewed in
               
              <a
                className="link unstyled"
                href="https://console.cloud.google.com/logs/viewer?project=test-project-id&interval=NO_LIMIT&advancedFilter=resource.type%3D\\"k8s_container\\"%0Aresource.labels.cluster_name:\\"test-cluster-name\\"%0Aresource.labels.pod_name:\\"node1\\""
                rel="noopener noreferrer"
                target="_blank"
              >
                Stackdriver Kubernetes Monitoring
              </a>
              .
            </div>
          </div>
        </div>
      `);
    });

    it('shows warning banner without stackdriver link in logs area if fetching logs failed and cluster is not in GKE', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      TestUtils.makeErrorResponseOnce(getPodLogsSpy, 'getting logs failed');
      tree = shallow(<RunDetails {...generateProps()} gkeMetadata={{}} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      await TestUtils.flushPromises();
      expect(tree.find('[data-testid="run-details-node-details"]')).toMatchInlineSnapshot(`
        <div
          className="page"
          data-testid="run-details-node-details"
        >
          <div
            className="page"
          >
            <Banner
              additionalInfo="getting logs failed"
              message="Warning: failed to retrieve pod logs. Possible reasons include cluster autoscaling or pod preemption"
              mode="warning"
              refresh={[Function]}
            />
          </div>
        </div>
      `);
    });

    it('does not load logs if clicked node status is skipped', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: {
          nodes: {
            node1: {
              id: 'node1',
              phase: 'Skipped',
            },
          },
        },
      });
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      await TestUtils.flushPromises();
      expect(getPodLogsSpy).not.toHaveBeenCalled();
      expect(tree.state()).toMatchObject({
        logsBannerAdditionalInfo: '',
        logsBannerMessage: '',
      });
      expect(tree).toMatchSnapshot();
    });

    it('keeps side pane open and on same tab when logs change after refresh', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      expect(tree.state('selectedNodeDetails')).toHaveProperty('id', 'node1');
      expect(tree.state('sidepanelSelectedTab')).toEqual(STEP_TABS.LOGS);

      getPodLogsSpy.mockImplementationOnce(() => 'new test logs');
      await (tree.instance() as RunDetails).refresh();
      expect(tree).toMatchSnapshot();
    });

    it('dismisses log failure warning banner when logs can be fetched after refresh', async () => {
      testRun.pipeline_runtime!.workflow_manifest = JSON.stringify({
        status: { nodes: { node1: { id: 'node1' } } },
      });
      TestUtils.makeErrorResponseOnce(getPodLogsSpy, 'getting logs failed');
      tree = shallow(<RunDetails {...generateProps()} />);
      await getRunSpy;
      await TestUtils.flushPromises();
      clickGraphNode(tree, 'node1');
      tree
        .find('MD2Tabs')
        .at(1)
        .simulate('switch', STEP_TABS.LOGS);
      await getPodLogsSpy;
      await TestUtils.flushPromises();
      expect(tree.state()).toMatchObject({
        logsBannerAdditionalInfo: 'getting logs failed',
        logsBannerMessage:
          'Warning: failed to retrieve pod logs. Possible reasons include cluster autoscaling or pod preemption',
        logsBannerMode: 'warning',
      });

      testRun.run!.status = 'Failed';
      await (tree.instance() as RunDetails).refresh();
      expect(tree.state()).toMatchObject({
        logsBannerAdditionalInfo: '',
        logsBannerMessage: '',
      });
    });
  });

  describe('auto refresh', () => {
    beforeEach(() => {
      testRun.run!.status = NodePhase.PENDING;
    });

    it('starts an interval of 5 seconds to auto refresh the page', async () => {
      tree = shallow(<RunDetails {...generateProps()} />);
      await TestUtils.flushPromises();

      expect(setInterval).toHaveBeenCalledTimes(1);
      expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
    });

    it('refreshes after each interval', async () => {
      tree = shallow(<RunDetails {...generateProps()} />);
      await TestUtils.flushPromises();

      const refreshSpy = jest.spyOn(tree.instance() as RunDetails, 'refresh');

      expect(refreshSpy).toHaveBeenCalledTimes(0);

      jest.runOnlyPendingTimers();
      expect(refreshSpy).toHaveBeenCalledTimes(1);
      await TestUtils.flushPromises();
    }, 10000);

    [NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(status => {
      it(`sets 'runFinished' to true if run has status: ${status}`, async () => {
        testRun.run!.status = status;
        tree = shallow(<RunDetails {...generateProps()} />);
        await TestUtils.flushPromises();

        expect(tree.state('runFinished')).toBe(true);
      });
    });

    [NodePhase.PENDING, NodePhase.RUNNING, NodePhase.UNKNOWN].forEach(status => {
      it(`leaves 'runFinished' false if run has status: ${status}`, async () => {
        testRun.run!.status = status;
        tree = shallow(<RunDetails {...generateProps()} />);
        await TestUtils.flushPromises();

        expect(tree.state('runFinished')).toBe(false);
      });
    });

    it('pauses auto refreshing if window loses focus', async () => {
      tree = shallow(<RunDetails {...generateProps()} />);
      await TestUtils.flushPromises();

      expect(setInterval).toHaveBeenCalledTimes(1);
      expect(clearInterval).toHaveBeenCalledTimes(0);

      window.dispatchEvent(new Event('blur'));
      await TestUtils.flushPromises();

      expect(clearInterval).toHaveBeenCalledTimes(1);
    });

    it('resumes auto refreshing if window loses focus and then regains it', async () => {
      // Declare that the run has not finished
      testRun.run!.status = NodePhase.PENDING;
      tree = shallow(<RunDetails {...generateProps()} />);
      await TestUtils.flushPromises();

      expect(tree.state('runFinished')).toBe(false);
      expect(setInterval).toHaveBeenCalledTimes(1);
      expect(clearInterval).toHaveBeenCalledTimes(0);

      window.dispatchEvent(new Event('blur'));
      await TestUtils.flushPromises();

      expect(clearInterval).toHaveBeenCalledTimes(1);

      window.dispatchEvent(new Event('focus'));
      await TestUtils.flushPromises();

      expect(setInterval).toHaveBeenCalledTimes(2);
    });

    it('does not resume auto refreshing if window loses focus and then regains it but run is finished', async () => {
      // Declare that the run has finished
      testRun.run!.status = NodePhase.SUCCEEDED;
      tree = shallow(<RunDetails {...generateProps()} />);
      await TestUtils.flushPromises();

      expect(tree.state('runFinished')).toBe(true);
      expect(setInterval).toHaveBeenCalledTimes(0);
      expect(clearInterval).toHaveBeenCalledTimes(0);

      window.dispatchEvent(new Event('blur'));
      await TestUtils.flushPromises();

      // We expect 0 calls because the interval was never set, so it doesn't need to be cleared
      expect(clearInterval).toHaveBeenCalledTimes(0);

      window.dispatchEvent(new Event('focus'));
      await TestUtils.flushPromises();

      expect(setInterval).toHaveBeenCalledTimes(0);
    });
  });

  describe('EnhancedRunDetails', () => {
    it('redirects to experiments page when namespace changes', () => {
      const history = createMemoryHistory({
        initialEntries: ['/does-not-matter'],
      });
      const { rerender } = render(
        <Router history={history}>
          <NamespaceContext.Provider value='ns1'>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).not.toEqual('/experiments');
      rerender(
        <Router history={history}>
          <NamespaceContext.Provider value='ns2'>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).toEqual('/experiments');
    });

    it('does not redirect when namespace stays the same', () => {
      const history = createMemoryHistory({
        initialEntries: ['/initial-path'],
      });
      const { rerender } = render(
        <Router history={history}>
          <NamespaceContext.Provider value='ns1'>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).toEqual('/initial-path');
      rerender(
        <Router history={history}>
          <NamespaceContext.Provider value='ns1'>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).toEqual('/initial-path');
    });

    it('does not redirect when namespace initializes', () => {
      const history = createMemoryHistory({
        initialEntries: ['/initial-path'],
      });
      const { rerender } = render(
        <Router history={history}>
          <NamespaceContext.Provider value={undefined}>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).toEqual('/initial-path');
      rerender(
        <Router history={history}>
          <NamespaceContext.Provider value='ns1'>
            <EnhancedRunDetails {...generateProps()} />
          </NamespaceContext.Provider>
        </Router>,
      );
      expect(history.location.pathname).toEqual('/initial-path');
    });
  });
});
Example #29
Source File: ResourceSelector.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('ResourceSelector', () => {
  let tree: ReactWrapper | ShallowWrapper;

  const updateDialogSpy = jest.fn();
  const selectionChangedCbSpy = jest.fn();
  const listResourceSpy = jest.fn();
  const RESOURCES: BaseResource[] = [
    {
      created_at: new Date(2018, 1, 2, 3, 4, 5),
      description: 'test-1 description',
      id: 'some-id-1',
      name: 'test-1 name',
    },
    {
      created_at: new Date(2018, 10, 9, 8, 7, 6),
      description: 'test-2 description',
      id: 'some-2-id',
      name: 'test-2 name',
    },
  ];

  const selectorColumns = [
    { label: 'Resource name', flex: 1, sortKey: 'name' },
    { label: 'Description', flex: 1.5 },
    { label: 'Uploaded on', flex: 1, sortKey: 'created_at' },
  ];

  const testEmptyMessage = 'Test - Sorry, no resources.';
  const testTitle = 'A test selector';

  function generateProps(): ResourceSelectorProps {
    return {
      columns: selectorColumns,
      emptyMessage: testEmptyMessage,
      filterLabel: 'test filter label',
      history: {} as any,
      initialSortColumn: 'created_at',
      listApi: listResourceSpy as any,
      location: '' as any,
      match: {} as any,
      selectionChanged: selectionChangedCbSpy,
      title: testTitle,
      updateDialog: updateDialogSpy,
    };
  }

  beforeEach(() => {
    listResourceSpy.mockReset();
    listResourceSpy.mockImplementation(() => ({
      nextPageToken: 'test-next-page-token',
      resources: RESOURCES,
    }));
    updateDialogSpy.mockReset();
    selectionChangedCbSpy.mockReset();
  });

  afterEach(async () => {
    // unmount() should be called before resetAllMocks() in case any part of the unmount life cycle
    // depends on mocks/spies
    await tree.unmount();
  });

  it('displays resource selector', async () => {
    tree = shallow(<TestResourceSelector {...generateProps()} />);
    await (tree.instance() as TestResourceSelector)._load({});

    expect(listResourceSpy).toHaveBeenCalledTimes(1);
    expect(listResourceSpy).toHaveBeenLastCalledWith(undefined, undefined, undefined, undefined);
    expect(tree.state('resources')).toEqual(RESOURCES);
    expect(tree).toMatchSnapshot();
  });

  it('converts resources into a table rows', async () => {
    const props = generateProps();
    const resources: BaseResource[] = [
      {
        created_at: new Date(2018, 1, 2, 3, 4, 5),
        description: 'a description',
        id: 'an-id',
        name: 'a name',
      },
    ];
    listResourceSpy.mockImplementationOnce(() => ({ resources, nextPageToken: '' }));
    props.listApi = listResourceSpy as any;

    tree = shallow(<TestResourceSelector {...props} />);
    await (tree.instance() as TestResourceSelector)._load({});

    expect(tree.state('rows')).toEqual([
      {
        id: 'an-id',
        otherFields: ['a name', 'a description', '2/2/2018, 3:04:05 AM'],
      },
    ]);
  });

  it('shows error dialog if listing fails', async () => {
    TestUtils.makeErrorResponseOnce(listResourceSpy, 'woops!');
    jest.spyOn(console, 'error').mockImplementation();

    tree = shallow(<TestResourceSelector {...generateProps()} />);
    await (tree.instance() as TestResourceSelector)._load({});

    expect(listResourceSpy).toHaveBeenCalledTimes(1);
    expect(updateDialogSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        content: 'List request failed with:\nwoops!',
        title: 'Error retrieving resources',
      }),
    );
    expect(tree.state('resources')).toEqual([]);
  });

  it('calls selection callback when a resource is selected', async () => {
    tree = shallow(<TestResourceSelector {...generateProps()} />);
    await (tree.instance() as TestResourceSelector)._load({});

    expect(tree.state('selectedIds')).toEqual([]);
    (tree.instance() as TestResourceSelector)._selectionChanged([RESOURCES[1].id!]);
    expect(selectionChangedCbSpy).toHaveBeenLastCalledWith(RESOURCES[1]);
    expect(tree.state('selectedIds')).toEqual([RESOURCES[1].id]);
  });

  it('logs error if more than one resource is selected', async () => {
    tree = shallow(<TestResourceSelector {...generateProps()} />);
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
    await (tree.instance() as TestResourceSelector)._load({});

    expect(tree.state('selectedIds')).toEqual([]);

    (tree.instance() as TestResourceSelector)._selectionChanged([
      RESOURCES[0].id!,
      RESOURCES[1].id!,
    ]);

    expect(selectionChangedCbSpy).not.toHaveBeenCalled();
    expect(tree.state('selectedIds')).toEqual([]);
    expect(consoleSpy).toHaveBeenLastCalledWith('2 resources were selected somehow', [
      RESOURCES[0].id,
      RESOURCES[1].id,
    ]);
  });

  it('logs error if selected resource ID is not found in list', async () => {
    tree = shallow(<TestResourceSelector {...generateProps()} />);
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
    await (tree.instance() as TestResourceSelector)._load({});

    expect(tree.state('selectedIds')).toEqual([]);

    (tree.instance() as TestResourceSelector)._selectionChanged(['id-not-in-list']);

    expect(selectionChangedCbSpy).not.toHaveBeenCalled();
    expect(tree.state('selectedIds')).toEqual([]);
    expect(consoleSpy).toHaveBeenLastCalledWith(
      'Somehow no resource was found with ID: id-not-in-list',
    );
  });
});