Skip to content

Commit

Permalink
[Frontend] snapshot diff setup (kubeflow#3166)
Browse files Browse the repository at this point in the history
* [Frontend] snapshot diff setup

* Rename

* Fix test

* Update README about recommended setup
  • Loading branch information
Bobgy authored and Jeffwan committed Dec 9, 2020
1 parent 3aea9ab commit cf531a0
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 196 deletions.
9 changes: 6 additions & 3 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,16 @@ To understand more what prettier is: [What is Prettier](https://prettier.io/docs
this project's config automatically.
Recommend setting the following in [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-file-locations) for vscode to autoformat on save.
```
"[typescript]": {
"editor.formatOnSave": true,
"[typescript]": {
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": false,
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": false,
},
```
Also, vscode builtin trailing whitespace [conflicts with jest inline snapshot](https://github.com/Microsoft/vscode/issues/52711), so recommend disabling it.
- For others, refer to https://prettier.io/docs/en/editors.html

### Format Code Manually
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
],
"snapshotSerializers": [
"./src/__serializers__/mock-function",
"snapshot-diff/serializer.js",
"enzyme-to-json/serializer"
]
},
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/TestUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { match } from 'react-router';
import { mount, ReactWrapper } from 'enzyme';
import { object } from 'prop-types';
import { format } from 'prettier';
import snapshotDiff from 'snapshot-diff';

export default class TestUtils {
/**
Expand Down Expand Up @@ -110,3 +111,44 @@ export default class TestUtils {
export function formatHTML(html: string): string {
return format(html, { parser: 'html' });
}

/**
* Generate diff text for two HTML strings.
* Recommend providing base and update annotations to clarify context in the diff directly.
*/
export function diffHTML({
base,
update,
baseAnnotation,
updateAnnotation,
}: {
base: string;
baseAnnotation?: string;
update: string;
updateAnnotation?: string;
}) {
return diff({
base: formatHTML(base),
update: formatHTML(update),
baseAnnotation,
updateAnnotation,
});
}

export function diff({
base,
update,
baseAnnotation,
updateAnnotation,
}: {
base: string;
baseAnnotation?: string;
update: string;
updateAnnotation?: string;
}) {
return snapshotDiff(base, update, {
stablePatchmarks: true, // Avoid line numbers in diff, so that diffs are stable against irrelevant changes
aAnnotation: baseAnnotation,
bAnnotation: updateAnnotation,
});
}
33 changes: 29 additions & 4 deletions frontend/src/components/SideNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
import * as React from 'react';

import SideNav, { css } from './SideNav';
import TestUtils from '../TestUtils';
import TestUtils, { diff } from '../TestUtils';
import { Apis } from '../lib/Apis';
import { LocalStorage } from '../lib/LocalStorage';
import { ReactWrapper, ShallowWrapper, shallow } from 'enzyme';
import { RoutePage } from './Router';
import { RouterProps } from 'react-router';
import snapshotDiff from 'snapshot-diff';

const wideWidth = 1000;
const narrowWidth = 200;
Expand Down Expand Up @@ -259,10 +258,36 @@ describe('SideNav', () => {
buildInfoSpy.mockImplementationOnce(() => Promise.reject('Error when fetching build info'));

tree = shallow(<SideNav page={RoutePage.PIPELINES} {...routerProps} />);
const baseTree = tree.debug();
const base = tree.debug();
await TestUtils.flushPromises();
tree.update();
expect(snapshotDiff(baseTree, tree.debug())).toMatchSnapshot();
expect(diff({ base, update: tree.debug() })).toMatchInlineSnapshot(`
Snapshot Diff:
- Expected
+ Received
@@ --- --- @@
<WithStyles(IconButton) className="chevron" onClick={[Function: bound _toggleNavClicked]}>
<pure(ChevronLeftIcon) />
</WithStyles(IconButton)>
</div>
<div className="infoVisible">
+ <WithStyles(Tooltip) title="Cluster name: some-cluster-name, Project ID: some-project-id" enterDelay={300} placement="top-start">
+ <div className="envMetadata">
+ <span>
+ Cluster name:
+ </span>
+ <a href="https://console.cloud.google.com/kubernetes/list?project=some-project-id&filter=name:some-cluster-name" className="link unstyled" rel="noopener" target="_blank">
+ some-cluster-name
+ </a>
+ </div>
+ </WithStyles(Tooltip)>
<WithStyles(Tooltip) title="Report an Issue" enterDelay={300} placement="top-start">
<div className="envMetadata">
<a href="https://github.com/kubeflow/pipelines/issues/new?template=BUG_REPORT.md" className="link unstyled" rel="noopener" target="_blank">
Report an Issue
</a>
`);
});

it('displays the frontend commit hash if the api server hash is not returned', async () => {
Expand Down
28 changes: 0 additions & 28 deletions frontend/src/components/__snapshots__/SideNav.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,33 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SideNav populates the cluster information using the response from the system endpoints 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -64,10 +64,20 @@
<WithStyles(IconButton) className=\\"chevron\\" onClick={[Function: bound _toggleNavClicked]}>
<pure(ChevronLeftIcon) />
</WithStyles(IconButton)>
</div>
<div className=\\"infoVisible\\">
+ <WithStyles(Tooltip) title=\\"Cluster name: some-cluster-name, Project ID: some-project-id\\" enterDelay={300} placement=\\"top-start\\">
+ <div className=\\"envMetadata\\">
+ <span>
+ Cluster name:
+ </span>
+ <a href=\\"https://console.cloud.google.com/kubernetes/list?project=some-project-id&filter=name:some-cluster-name\\" className=\\"link unstyled\\" rel=\\"noopener\\" target=\\"_blank\\">
+ some-cluster-name
+ </a>
+ </div>
+ </WithStyles(Tooltip)>
<WithStyles(Tooltip) title=\\"Report an Issue\\" enterDelay={300} placement=\\"top-start\\">
<div className=\\"envMetadata\\">
<a href=\\"https://github.com/kubeflow/pipelines/issues/new?template=BUG_REPORT.md\\" className=\\"link unstyled\\" rel=\\"noopener\\" target=\\"_blank\\">
Report an Issue
</a>"
`;
exports[`SideNav populates the display build information using the response from the healthz endpoint 1`] = `
<div
className="root flexColumn noShrink"
Expand Down
63 changes: 55 additions & 8 deletions frontend/src/components/viewers/Tensorboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@

import * as React from 'react';
import TensorboardViewer from './Tensorboard';
import TestUtils from '../../TestUtils';
import TestUtils, { diff } from '../../TestUtils';
import { Apis } from '../../lib/Apis';
import { PlotType } from './Viewer';
import { ReactWrapper, ShallowWrapper, shallow, mount } from 'enzyme';
import snapshotDiff from 'snapshot-diff';

describe('Tensorboard', () => {
let tree: ReactWrapper | ShallowWrapper;
Expand Down Expand Up @@ -50,21 +49,53 @@ describe('Tensorboard', () => {
const getAppMock = () => Promise.resolve({ podAddress: '', tfVersion: '' });
jest.spyOn(Apis, 'getTensorboardApp').mockImplementation(getAppMock);
tree = shallow(<TensorboardViewer configs={[]} />);
const baseTree = tree.debug();
const base = tree.debug();

await TestUtils.flushPromises();
expect(snapshotDiff(tree.debug(), baseTree)).toMatchSnapshot();
expect(diff({ base, update: tree.debug() })).toMatchInlineSnapshot(`
Snapshot Diff:
- Expected
+ Received
@@ --- --- @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={true} title="Start Tensorboard" />
+ <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={false} title="Start Tensorboard" />
</div>
</div>
</div>
`);
});

it('does not break on empty data', async () => {
const getAppMock = () => Promise.resolve({ podAddress: '', tfVersion: '' });
jest.spyOn(Apis, 'getTensorboardApp').mockImplementation(getAppMock);
const config = { type: PlotType.TENSORBOARD, url: '' };
tree = shallow(<TensorboardViewer configs={[config]} />);
const baseTree = tree.debug();
const base = tree.debug();

await TestUtils.flushPromises();
expect(snapshotDiff(tree.debug(), baseTree)).toMatchSnapshot();
expect(diff({ base, update: tree.debug() })).toMatchInlineSnapshot(`
Snapshot Diff:
- Expected
+ Received
@@ --- --- @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={true} title="Start Tensorboard" />
+ <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={false} title="Start Tensorboard" />
</div>
</div>
</div>
`);
});

it('shows a link to the tensorboard instance if exists', async () => {
Expand All @@ -82,10 +113,26 @@ describe('Tensorboard', () => {
const getAppMock = () => Promise.resolve({ podAddress: '', tfVersion: '' });
const spy = jest.spyOn(Apis, 'getTensorboardApp').mockImplementation(getAppMock);
tree = shallow(<TensorboardViewer configs={[config]} />);
const baseTree = tree.debug();
const base = tree.debug();

await TestUtils.flushPromises();
expect(snapshotDiff(tree.debug(), baseTree)).toMatchSnapshot();
expect(diff({ base, update: tree.debug() })).toMatchInlineSnapshot(`
Snapshot Diff:
- Expected
+ Received
@@ --- --- @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={true} title="Start Tensorboard" />
+ <BusyButton className="buttonAction" disabled={false} onClick={[Function]} busy={false} title="Start Tensorboard" />
</div>
</div>
</div>
`);
expect(spy).toHaveBeenCalledWith(config.url);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,42 +111,6 @@ exports[`Tensorboard base component snapshot 1`] = `
</div>
`;

exports[`Tensorboard does not break on empty data 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -53,9 +53,9 @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={false} title=\\"Start Tensorboard\\" />
+ <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={true} title=\\"Start Tensorboard\\" />
</div>
</div>
</div>"
`;

exports[`Tensorboard does not break on no config 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -53,9 +53,9 @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={false} title=\\"Start Tensorboard\\" />
+ <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={true} title=\\"Start Tensorboard\\" />
</div>
</div>
</div>"
`;

exports[`Tensorboard shows a link to the tensorboard instance if exists 1`] = `
<div>
<div>
Expand Down Expand Up @@ -218,21 +182,3 @@ exports[`Tensorboard shows a link to the tensorboard instance if exists 1`] = `
</div>
</div>
`;

exports[`Tensorboard shows start button if no instance exists 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -53,9 +53,9 @@
</WithStyles(MenuItem)>
</WithStyles(WithFormControlContext(Select))>
</WithStyles(FormControl)>
</div>
<div>
- <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={false} title=\\"Start Tensorboard\\" />
+ <BusyButton className=\\"buttonAction\\" disabled={false} onClick={[Function]} busy={true} title=\\"Start Tensorboard\\" />
</div>
</div>
</div>"
`;
Loading

0 comments on commit cf531a0

Please sign in to comment.