Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-refreshes the run details page #722

Merged
merged 3 commits into from
Jan 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/mock-backend/fixed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ const runs: ApiRunDetail[] = [
relationship: ApiRelationship.OWNER,
}],
scheduled_at: new Date('2018-04-17T21:00:00.000Z'),
status: 'Succeeded',
status: 'Error',
},
},
{
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/SideNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ describe('SideNav', () => {
localStorageIsCollapsedSpy.mockImplementation(() => false);
});

afterEach(() => {
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.resetAllMocks();
tree.unmount();
(window as any).innerWidth = wideWidth;
});

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/UploadPipelineDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import TestUtils from '../TestUtils';
describe('UploadPipelineDialog', () => {
let tree: ReactWrapper | ShallowWrapper;

afterEach(() => {
tree.unmount();
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('renders closed', () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/Compare.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ describe('Compare', () => {
getRunSpy.mockImplementation((id: string) => runs.find((r) => r.run!.id === id));
});

afterEach(() => {
tree.unmount();
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('clears banner upon initial load', () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/ExperimentDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ describe('ExperimentDetails', () => {
await mockNRuns(0);
});

afterEach(() => {
tree.unmount();
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('renders a page with no runs or recurring runs', async () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/ExperimentList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('ExperimentList', () => {
const historyPushSpy = jest.fn();
const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment');
const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');
// We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
// We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
// test enviroments
const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/NewRun.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,11 @@ describe('NewRun', () => {
MOCK_RUN_WITH_EMBEDDED_PIPELINE = newMockRunWithEmbeddedPipeline();
});

afterEach(() => {
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.resetAllMocks();
tree.unmount();
});

it('renders the new run page', async () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/PipelineDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,11 @@ describe('PipelineDetails', () => {
createGraphSpy.mockImplementation(() => new graphlib.Graph());
});

afterEach(() => {
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.resetAllMocks();
tree.unmount();
});

it('shows empty pipeline details with no graph', async () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/PipelineList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ describe('PipelineList', () => {
jest.clearAllMocks();
});

afterEach(() => {
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.resetAllMocks();
tree.unmount();
});

it('renders an empty list with empty state message', () => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/pages/ResourceSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ describe('ResourceSelector', () => {
selectionChangedCbSpy.mockReset();
});

afterEach(() => {
tree.unmount();
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 () => {
Expand Down
114 changes: 112 additions & 2 deletions frontend/src/pages/RunDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { PlotType } from '../components/viewers/Viewer';
import { RouteParams, RoutePage, QUERY_PARAMS } from '../components/Router';
import { Workflow } from 'third_party/argo-ui/argo_template';
import { shallow, ShallowWrapper } from 'enzyme';
import { NodePhase } from './Status';

describe('RunDetails', () => {
const updateBannerSpy = jest.fn();
Expand Down Expand Up @@ -66,6 +67,9 @@ describe('RunDetails', () => {
beforeAll(() => jest.spyOn(console, 'error').mockImplementation());

beforeEach(() => {
// The RunDetails page uses timers to periodically refresh
jest.useFakeTimers();

testRun = {
pipeline_runtime: {
workflow_manifest: '{}',
Expand All @@ -92,9 +96,11 @@ describe('RunDetails', () => {
jest.clearAllMocks();
});

afterEach(() => {
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.resetAllMocks();
tree.unmount();
});

it('shows success run status in page title', async () => {
Expand Down Expand Up @@ -661,4 +667,108 @@ describe('RunDetails', () => {
await refreshBtn!.action();
expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined);
});

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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice test!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to test the refresh method getting/not getting called rather than just checking setInterval and clearInterval? We're testing the implementation at this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you're wrong; the interval is just a thing to make sure it refreshes, but refresh is imprecise as it can be called elsewhere, and testing it involves fake timers.

Sort of a side-note, I was previously calling refresh() on focus even if the run had completed to avoid having the slightly more complex code below, although it's technically more correct since it cuts out unnecessary refreshes.

What do you think?

private async _startAutoRefresh(): Promise<void> {
  // If the run was not finished last time we checked, check again in case anything changed
  // before proceeding to set auto-refresh interval
  if (!this.state.runFinished) {
    // refresh() updates runFinished's value
    await this.refresh();
  }

  // Only set interval if run has not finished, and verify that the interval is undefined to
  // avoid setting multiple intervals
  if (!this.state.runFinished && this._interval === undefined) {
    this._interval = setInterval(
      () => this.refresh(),
      this.AUTO_REFRESH_INTERVAL
    );
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both ways are fine.

// 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);
});
});
});
Loading