-
Notifications
You must be signed in to change notification settings - Fork 29
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
Add empty and loading states to Plots webview with tests #1000
Changes from 3 commits
d3ea0c5
14af633
19a1b3a
78d7d7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
import React from 'react' | ||
import { | ||
render, | ||
cleanup, | ||
waitFor, | ||
screen, | ||
fireEvent | ||
} from '@testing-library/react' | ||
import '@testing-library/jest-dom/extend-expect' | ||
import complexLivePlotsData from 'dvc/src/test/fixtures/complex-live-plots-example' | ||
import { | ||
MessageFromWebviewType, | ||
MessageToWebviewType | ||
} from 'dvc/src/webview/contract' | ||
import { App } from './App' | ||
|
||
import { vsCodeApi } from '../util/vscode' | ||
|
||
jest.mock('../util/vscode') | ||
|
||
const { postMessage: mockPostMessage } = vsCodeApi | ||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
afterEach(() => { | ||
cleanup() | ||
}) | ||
|
||
describe('App', () => { | ||
it('Sends the initialized message on first render', () => { | ||
render(<App />) | ||
expect(mockPostMessage).toHaveBeenCalledWith({ | ||
type: MessageFromWebviewType.initialized | ||
}) | ||
expect(mockPostMessage).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('Renders the loading state when given no data', async () => { | ||
render(<App />) | ||
const loadingState = await waitFor(() => | ||
screen.getByText('Loading plots data...') | ||
) | ||
expect(loadingState).toBeInTheDocument() | ||
}) | ||
|
||
it('Renders the empty state when given data with no experiments', async () => { | ||
const dataMessageWithoutPlots = new MessageEvent('message', { | ||
data: { | ||
data: [], | ||
type: MessageToWebviewType.setData | ||
} | ||
}) | ||
render(<App />) | ||
fireEvent(window, dataMessageWithoutPlots) | ||
const emptyState = await waitFor(() => | ||
screen.getByText('There are no experiments to make plots from.') | ||
) | ||
expect(emptyState).toBeInTheDocument() | ||
}) | ||
|
||
it('Renders plots when given a message with plots data', () => { | ||
const dataMessageWithPlots = new MessageEvent('message', { | ||
data: { | ||
data: complexLivePlotsData, | ||
type: MessageToWebviewType.setData | ||
} | ||
}) | ||
render(<App />) | ||
fireEvent(window, dataMessageWithPlots) | ||
|
||
const emptyState = screen.queryByText('Loading plots data...') | ||
expect(emptyState).not.toBeInTheDocument() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,26 +2,24 @@ import React, { useEffect, useState } from 'react' | |
import { PlotsData } from 'dvc/src/plots/webview/contract' | ||
import { | ||
MessageFromWebviewType, | ||
MessageToWebview, | ||
MessageToWebviewType | ||
} from 'dvc/src/webview/contract' | ||
import Plots from './Plots' | ||
import { InternalVsCodeApi } from '../../shared/api' | ||
|
||
declare global { | ||
function acquireVsCodeApi(): InternalVsCodeApi | ||
} | ||
|
||
const vsCodeApi = acquireVsCodeApi() | ||
import { vsCodeApi } from '../util/vscode' | ||
|
||
const signalInitialized = () => | ||
vsCodeApi.postMessage({ type: MessageFromWebviewType.initialized }) | ||
|
||
const App = () => { | ||
export const App = () => { | ||
const [plotsData, setPlotsData] = useState<PlotsData>() | ||
const [dvcRoot, setDvcRoot] = useState() | ||
const [dvcRoot, setDvcRoot] = useState<string>() | ||
useEffect(() => { | ||
signalInitialized() | ||
window.addEventListener('message', ({ data }) => { | ||
const messageListener = ({ | ||
data | ||
}: { | ||
data: MessageToWebview<PlotsData> | ||
}) => { | ||
switch (data.type) { | ||
case MessageToWebviewType.setData: { | ||
setPlotsData(data.data) | ||
|
@@ -31,7 +29,10 @@ const App = () => { | |
setDvcRoot(data.dvcRoot) | ||
} | ||
} | ||
}) | ||
} | ||
window.addEventListener('message', messageListener) | ||
signalInitialized() | ||
return () => window.removeEventListener('message', messageListener) | ||
Comment on lines
+32
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cleaning up a memory leak that was exposed by tests re-creating the |
||
}, []) | ||
useEffect(() => { | ||
vsCodeApi.setState({ | ||
|
@@ -41,5 +42,3 @@ const App = () => { | |
}, [plotsData, dvcRoot]) | ||
return <Plots plotsData={plotsData} /> | ||
} | ||
|
||
export default App |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -111,7 +111,10 @@ const Plot = ({ | |
} | ||
|
||
const Plots = ({ plotsData }: { plotsData?: PlotsData }) => { | ||
return ( | ||
if (!plotsData) { | ||
return <p>Loading plots data...</p> | ||
} | ||
return plotsData.length > 0 ? ( | ||
<> | ||
{plotsData?.map(plotData => ( | ||
<Plot | ||
|
@@ -121,6 +124,13 @@ const Plots = ({ plotsData }: { plotsData?: PlotsData }) => { | |
/> | ||
))} | ||
</> | ||
) : ( | ||
<> | ||
<p>There are no experiments to make plots from.</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! I did the same thing we do for the table with no data, but the ugliness is more apparent here since this state can happen much more often. |
||
<p> | ||
<a href="command:dvc.runExperiment">Run an Experiment</a> to add some! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [I] This is not necessarily true (no checkpoints). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also need to pass |
||
</p> | ||
</> | ||
) | ||
} | ||
export default Plots |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const vsCodeApi = { | ||
getState: jest.fn(), | ||
postMessage: jest.fn(), | ||
setState: jest.fn() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { InternalVsCodeApi } from '../../shared/api' | ||
|
||
declare global { | ||
function acquireVsCodeApi(): InternalVsCodeApi | ||
} | ||
|
||
export const vsCodeApi = acquireVsCodeApi() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't find a way to mock this global directly, so wrapping it is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Q] Did you see that we mock this for experiments? See There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also mock a wrapper there, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not reuse the code? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function
App
has 29 lines of code (exceeds 25 allowed). Consider refactoring.