-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Curation] Add promoted/hidden documents section & logic + Restore defaults button #94769
Merged
Merged
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
303a281
Set up promoted & hidden documents logic
cee-chen 1f2617c
Set up result utility for converting CurationResult to Result
cee-chen df49b74
Set up AddResultButton in documents sections
cee-chen 8788bb3
Add HiddenDocuments section
cee-chen 942cbe8
Add PromotedDocuments section w/ draggable results
cee-chen 8ca1efe
Update OrganicDocuments results with promote/hide actions
cee-chen f2b6446
Add the Restore Defaults button+logic
cee-chen b02a63e
Merge branch 'master' into curation-3
kibanamachine b0ae5fe
PR feedback: key ID
constancecchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
...applications/app_search/components/curations/curation/documents/hidden_documents.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { setMockValues, setMockActions } from '../../../../../__mocks__'; | ||
|
||
import React from 'react'; | ||
|
||
import { shallow } from 'enzyme'; | ||
|
||
import { EuiEmptyPrompt, EuiButtonEmpty } from '@elastic/eui'; | ||
|
||
import { DataPanel } from '../../../data_panel'; | ||
import { CurationResult } from '../results'; | ||
|
||
import { HiddenDocuments } from './'; | ||
|
||
describe('HiddenDocuments', () => { | ||
const values = { | ||
curation: { | ||
hidden: [ | ||
{ id: 'mock-document-1' }, | ||
{ id: 'mock-document-2' }, | ||
{ id: 'mock-document-3' }, | ||
{ id: 'mock-document-4' }, | ||
{ id: 'mock-document-5' }, | ||
], | ||
}, | ||
hiddenDocumentsLoading: false, | ||
}; | ||
const actions = { | ||
removeHiddenId: jest.fn(), | ||
clearHiddenIds: jest.fn(), | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
setMockValues(values); | ||
setMockActions(actions); | ||
}); | ||
|
||
it('renders a list of hidden documents', () => { | ||
const wrapper = shallow(<HiddenDocuments />); | ||
|
||
expect(wrapper.find(CurationResult)).toHaveLength(5); | ||
}); | ||
|
||
it('renders an empty state & hides the panel actions when empty', () => { | ||
setMockValues({ ...values, curation: { hidden: [] } }); | ||
const wrapper = shallow(<HiddenDocuments />); | ||
|
||
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); | ||
expect(wrapper.find(DataPanel).prop('action')).toBe(false); | ||
}); | ||
|
||
it('renders a loading state', () => { | ||
setMockValues({ ...values, hiddenDocumentsLoading: true }); | ||
const wrapper = shallow(<HiddenDocuments />); | ||
|
||
expect(wrapper.find(DataPanel).prop('isLoading')).toEqual(true); | ||
}); | ||
|
||
describe('actions', () => { | ||
it('renders results with an action button that un-hides the result', () => { | ||
const wrapper = shallow(<HiddenDocuments />); | ||
const result = wrapper.find(CurationResult).last(); | ||
result.prop('actions')[0].onClick(); | ||
|
||
expect(actions.removeHiddenId).toHaveBeenCalledWith('mock-document-5'); | ||
}); | ||
|
||
it('renders a restore all button that un-hides all hidden results', () => { | ||
const wrapper = shallow(<HiddenDocuments />); | ||
const panelActions = shallow(wrapper.find(DataPanel).prop('action') as React.ReactElement); | ||
|
||
panelActions.find(EuiButtonEmpty).simulate('click'); | ||
expect(actions.clearHiddenIds).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
99 changes: 99 additions & 0 deletions
99
...blic/applications/app_search/components/curations/curation/documents/hidden_documents.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import React from 'react'; | ||
|
||
import { useValues, useActions } from 'kea'; | ||
|
||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
|
||
import { DataPanel } from '../../../data_panel'; | ||
|
||
import { SHOW_DOCUMENT_ACTION } from '../../constants'; | ||
import { CurationLogic } from '../curation_logic'; | ||
import { AddResultButton, CurationResult, convertToResultFormat } from '../results'; | ||
|
||
export const HiddenDocuments: React.FC = () => { | ||
const { clearHiddenIds, removeHiddenId } = useActions(CurationLogic); | ||
const { curation, hiddenDocumentsLoading } = useValues(CurationLogic); | ||
|
||
const documents = curation.hidden; | ||
const hasDocuments = documents.length > 0; | ||
|
||
return ( | ||
<DataPanel | ||
filled | ||
iconType="eyeClosed" | ||
title={ | ||
<h2> | ||
{i18n.translate( | ||
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.title', | ||
{ defaultMessage: 'Hidden documents' } | ||
)} | ||
</h2> | ||
} | ||
subtitle={i18n.translate( | ||
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.description', | ||
{ defaultMessage: 'Hidden documents will not appear in organic results.' } | ||
)} | ||
action={ | ||
hasDocuments && ( | ||
<EuiFlexGroup gutterSize="s"> | ||
<EuiFlexItem> | ||
<AddResultButton /> | ||
</EuiFlexItem> | ||
<EuiFlexItem> | ||
<EuiButtonEmpty onClick={clearHiddenIds} iconType="menuUp" size="s"> | ||
{i18n.translate( | ||
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.removeAllButtonLabel', | ||
{ defaultMessage: 'Restore all' } | ||
)} | ||
</EuiButtonEmpty> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
) | ||
} | ||
isLoading={hiddenDocumentsLoading} | ||
> | ||
{hasDocuments ? ( | ||
documents.map((document) => ( | ||
<CurationResult | ||
key={document.id} | ||
result={convertToResultFormat(document)} | ||
actions={[ | ||
{ | ||
...SHOW_DOCUMENT_ACTION, | ||
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. Nice, I like how you group these together in a constant and then spread them here 💯 |
||
onClick: () => removeHiddenId(document.id), | ||
}, | ||
]} | ||
/> | ||
)) | ||
) : ( | ||
<EuiEmptyPrompt | ||
titleSize="s" | ||
title={ | ||
<h3> | ||
{i18n.translate( | ||
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle', | ||
{ defaultMessage: 'No documents are being hidden for this query' } | ||
)} | ||
</h3> | ||
} | ||
body={i18n.translate( | ||
'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyDescription', | ||
{ | ||
defaultMessage: | ||
'Hide documents by clicking the eye icon on the organic results above, or search and hide a result manually.', | ||
} | ||
)} | ||
actions={<AddResultButton />} | ||
/> | ||
)} | ||
</DataPanel> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ | |
*/ | ||
|
||
export { OrganicDocuments } from './organic_documents'; | ||
export { HiddenDocuments } from './hidden_documents'; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Is it worth writing as assertion in your tests that confirm that the result is converted before passed to
CurationResult
?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.
IMO, no - that's the point of unit tests and having split
convertToResultFormat
to its own file/set of tests - it gets tested separately and doesn't muddy this test (and is also much easier to remove if/when we no longer need the helper).And of course in an ideal world we would have E2E tests as well which would crash with Javascript type errors if the
<Result>
component wasn't getting the correct data structure it expects, but for now we can QA that manually 😄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.
I know when we get into testing philosophy the waters can get a bit muddy. Some counter points:
A key part of unit testing is being able to refactor a unit with confidence that it is still working as expected. A key thing that this unit does is format a result and pass it to CurationResult. As it is, you could lose that key behavior in a refactor, right? Which means you cannot refactor with confidence.
If unit tests are supposed to be living documentation, isn't it important to document this behavior?
IMO, that's only half the equation. You test the behavior of convertToResultFormat separately, and here, you would mock it and confirm that it is called. It's the same concept with shallow rendering, right? You mock the components that you are rendering, since they are tested elsewhere, but you still assert that they have been called / rendered.
If this rendered a wrapper component that performed
convertToResultFormat
automatically on Result, like<CurationResultFromDocument result={document} />
, then I would say you might not have to assert anything. You would almost certainly write an assertion on theCurationResultFromDocument
test to confirm that it callsconvertToResultFormat
and formats the result before passing it toCurationResult
. You're losing that here, right? As it is, you're basically composing these two functions together ... CurationResult and convertToResultFormat to get a certain effect, but you're not actually testing that that effect is happening.I am just thinking out loud. I don't expect you to make any changes here. I just wanted to play devil's advocate a bit since we've already opened this can of worms.
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.
There's a lot going on here, so I'm going to split up my responses separately to try to help with readability.
That's correct! And that's how unit testing should work. Testing that components work together is the point of integration, E2E testing, or manual testing.
Let me give you an example (that I've actually run into in the past with previous developers and have had to do my best to try and dissuade people from doing). Let's say I'm doing this instead:
So basically, I'm using a 3rd party fn instead of my 1st party
convertToResultFormat
fn.In my unit test, do you think it's worth the time of either the developer writing the tests or reading the tests to get the
result
prop and check that each key/value pair has correctly been inverted? Or how aboutcloneDeep
as an example instead - should I iterate into each nested object to check for mutation? That seems like a pretty big waste of time, right?So the answer is no, because that is not the responsibility of my unit. We have to trust that the lodash author(s) correctly wrote their helper functions and have their own set of unit tests that covers the
invert
orcloneDeep
fns. We can reaffirm that the library is integrating as expected with out code via integration/E2E tests, but we absolutely do not want to get into the business of unit testing other people's code for them.The same principle applies to
convertToResultFormat
- the only difference is that that helper/fn belongs to us instead of some 3rd party. However, if we have correctly written separate unit tests for our helper, we can now refer to those unit tests and trust thatconvertToResultFormat
will give us the format we need (by reading the tests and confirming the returned response). We then verify the connection/integration with manual or E2E tests.I hope that makes sense - let me know if not. I'll address some of your other comments (refactoring with confidence, shallow rendering) separately in a follow up comment here.
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.
On refactoring with confidence:
Absolutely I could. Let's say I'm refactoring
convertToResultFormat
itself. Let's say I switched my.forEach
to a plainfor
loop because I'm feeling old school. All the unit tests within utils.test.ts should still pass (if I haven't messed up the indexes in my for loop because I haven't written one in a million years 😅 ). Refactoring the unit should be easily assertable by the unit test.I'm not 100% sure I'm reading this correctly here, but if the refactor you're referring to is the upcoming one where we fix our data structure and remove the need for
convertToResultFormat
entirely, then I'd say that's a bit of a false equivalency. Removing this utility (the eventual goal) is not a refactor, it's straight up an intentional behavior change.Even so, we could make that change with near 100% confidence still. All it takes is deleting the entire helper file, removing the
convertToResultFormat(document)
and just passing inresult={document}
. If we made a mistake along the way:document
is not the correct type thatresult
expectsI start to worry that because we're (currently) only writing unit tests, we're relying too much on them & not seeing the bigger picture of letting more responsibility lie with a full and robust set of tests & tools (unit, integration, E2E, visual diffs, typescript/linters, etc.). I think we should only feel fully confident once we have a wider variety of tests, and that's why I've deliberately booked time for us post-MVP to write E2E tests - it's not a nice to have, it's essential, and it will absolutely save us time on unit tests attempting to be integration tests.
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.
I don't think this is a test trying to be an integration test, I think this is just a test trying to assert that the unit under test is functioning normally.
Let me ask you this ... if instead of HiddenDocument, you were writing this component:
How would you test this?
Would you simply write the following and call it a day?
I'm guessing not, I'm guessing you'd probably write this:
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.
Like if you just wrote the prior, you'd be glossing over a key behavior of this unit.
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.
On deciding what to assert on:
I LOVE this question because I don't have a super good answer for this. Absolutely I could have added an
expect(convertToResultFormat).toHaveBeenCalledWith(someDocument);
assertion (and I'd definitely still be down for it, if y'all think it helps understanding).
Here's the key though, right, so I'm writing these unit tests to help others understand the code - the design of it and the intention of it. And I'm writing it through the lens of my understanding of the code. And React components are particularly difficult, 1. because anytime you're attempting to test UI it's a clusterfuck, and 2. because they're not really true "units", and you kind of have to go in manually and discern the different functionalities and purposes of various components and onClicks and so on.
So in this particular case when I was writing tests for the
<HiddenDocuments>
component, I was like - okay, what's the main purpose of this UI? And I decided it was 1. showing a list of hidden results, and 2. providing the ability to un-hide results. So I focused on that, and then from there I used code coverage reports to cover any remaining lines.While I was doing that, I didn't think much about the use of
convertToResultFormat
here, I think for a few reasons:<OrganicDocuments>
view).That being said, maybe my interpretation was wrong and it's useful for other developers to see in the unit test assertions that the function is even there.
And honestly that's one of the joys and difficulties of writing unit tests for me - is that because we're writing it with other people in mind and not computers, it's has a decent amount of room for subjectivity. I don't have a silver bullet answer for this, and I definitely think that's what the code review process is for, but I do have a couple general paths I personally tend to follow that I'd love to get thoughts on:
convertToResultFormat()
is being called without any conditionals or any extra logic around what's being passed to it. So in the same way I wouldn't necessarily check that acloneDeep
got called in the middle of a busy function - I assume it works and I prefer to assert on the beginning/end.On unit testing React components/views:
As I've alluded to earlier, unit testing React views is tricky. I'm definitely not confident my approach is 100% the right approach either to be clear, I've kind of settled on this while trying to strike a sane and readable balance to myself/my future self/other developers. I'm definitely open to talking/diving more into how we approach component testing as a team and discussing potential changes (e.g.
jest.snapshot
instead of manual assertions, etc.)This is actually a really great analogy and plays into what I'm saying above, because wait! We don't always. Take the
<FlashMessages />
component in the Curation view:kibana/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx
Line 72 in f4da063
It's there - did I assert on it? No, and we don't assert on it in almost any other view either. Because it's incidental to this view, it's not the primary purpose of it, and it doesn't have any complex logic around it - it's a very basic pass-through that handles its own logic & has its own unit tests, and that is typically better tested for in this view via non-unit tests (either E2E testing or visual diff testing for example).
We definitely should assert on views that are either conditional (to cover logic branches) or crucial to the view (for example, if the view is a flyout, I'd definitely check that an
EuiFlyout
is being rendered) - but it's the same principle I mentioned above, where it's absolutely possible for there to be too much noise to be helpful to others, and as such we're constantly making subjective decisions about what's important to highlight in our unit test "documentation" and what's not.I hope that makes more sense now as to my reasoning and I'm definitely open to adding an assertion if you think it helps your understanding - because that's definitely the goal of unit tests for me.
And that's my last set of comments!! Thanks for sticking with me, I had a lot of fun writing this out and I hope you similarly enjoyed (or at least didn't hate) reading this!
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.
Your answer is on-point. 100% agree. Thank you for writing this out. I think of the things the same way, and I think my original question was just wondering out loud if this fell into the category of "things that are crucial to the view".
Yes, I agree!
Anyway, just leave this as is, now that we've talked about it I'd agree that it's not necessarily crucial to the view.
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.
Oh shit sorry Jason, I swear I like hard refreshed the page and GitHub wasn't showing me any new responses until like a second ago 😱 I super did not mean to just carry on like I was ignoring your responses lol, very sorry 🙇♀️
Ahhhh!! Yeah!!! Now I look like either an idiot or a genius with my 3rd comment (probably more idiot). Totally on the same page as you now, apologies for taking 2 overly-long comments to get there lol. 🤦♀️
I had a lot of fun writing too much and appreciate you giving me the chance to do so! Love having discussions like these on subjective shenanigans and syncing up on them. Thanks a million!