[CCR] Client tests auto-follow pattern & follower index forms#34449
[CCR] Client tests auto-follow pattern & follower index forms#34449sebelga merged 19 commits intoelastic:masterfrom
Conversation
💔 Build Failed |
There was a bug introduced in the client side validation of the auto-follow patterns by mixing the type of error (object vs JSX element). The correct way to handle errors is to use an object with a "message" property. This way, we can add other properties to the error object (like "alwaysVisible") and give our validation UI all the flexibility needed.
💔 Build Failed |
💚 Build Succeeded |
|
Pinging @elastic/es-ui |
💚 Build Succeeded |
Clarify UX intentions with comments and use null to represent 'no error'.
|
@cjcenizal I updated the PR with your proposal. Can you have another look at it? I also added the tests for the follower index add & edit. cheers! |
💔 Build Failed |
💚 Build Succeeded |
| this.setState({ | ||
| isLoading: false, | ||
| remoteClusters: sortClusterByName(remoteClusters) | ||
| remoteClusters |
There was a problem hiding this comment.
Would you mind helping me understand why you made this change? The reason I ask is because this change adds a little complexity by creating an additional Promise (via the added then) and passes remoteClusters to sortClusterByName implicitly instead of explicitly, so the original code seems both simpler and more expressive to me.
There was a problem hiding this comment.
This is directly related to the video you shared to me: "first-class functions". The same way we don't need to be explicit (and use imperative programming) with map, and forEach. I must say that I'm a bit confused and don't understand why here you think being declarative adds complexity:
loadRemoteClusters()
.then(sortClusterByName)
.then((remoteClusters) => {
...
})but here no
await Promise.all(names.map(deleteCluster));There was a problem hiding this comment.
Sure, I can explain. I'm measuring complexity by how many "moving parts" are involved in an expression. In the example of piping through .then we've created an additional promise, which adds another moving part. By removing the .then we've removed that moving part which would be a reduction in complexity according to how I'm measuring it.
In this regard, I think your example of using Promise.all is as simple as you can get. It has the minimum number of moving parts required.
In terms of expressiveness, loadRemoteClusters().then(sortClusterByName) doesn't contain all of the information that names.map(deleteCluster) has. The latter example communicate that deleteCluster will accept name as its argument because that's what map does, but the first example implies that remoteClusters will be the argument passed to sortClusterByName because we assume that's what loadRemoteClusters resolves to.
Anyway, the bottom line is that I don't see the imperative programming style or functional programming style as inherently good or bad, I think they're both useful tools in various situations and I just wanted to share my perspective in this example -- just another way of thinking about code. :)
There was a problem hiding this comment.
Thanks for explaining your thoughts, I get it. Not saying I share your opinion on complexity but I understand better your idea. I don't have any problem of reverting the change if you prefer it the other way.
This is, in the end, the result of doing so much testing. From time to time you bump on some code and think: "oh, this would be more elegant written this way" (purely at the preference level 😊 )
| breadcrumbs: { set: () => {} }, | ||
| })); | ||
|
|
||
| jest.mock('ui/index_patterns', () => { |
There was a problem hiding this comment.
What do you think about adding some newlines so we don't need to disable the max-len rule? This will also make the code easier to read when the code editor is narrow. This comment applies to the other test files, too.
jest.mock('ui/index_patterns', () => {
const { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } =
jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/constants');
const { validateIndexPattern, ILLEGAL_CHARACTERS, CONTAINS_SPACES } =
jest.requireActual('../../../../../src/legacy/ui/public/index_patterns/validate/validate_index_pattern');
return {
INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE,
ILLEGAL_CHARACTERS,
CONTAINS_SPACES,
validateIndexPattern,
};
});| setLoadRemoteClusteresResponse(); | ||
|
|
||
| // Mock all HTTP Requests that have not been handled previously | ||
| server.respondWith([200, {}, '']); |
There was a problem hiding this comment.
Same with this mocked response. I think we can remove it?
There was a problem hiding this comment.
Nooooooooooo 😄 This is our safeguard against any not handled HTTP requests (that we don't care in our test but could break the CI at any point).
| } = registerHttpRequestMockHelpers(server)); | ||
|
|
||
| // Set "default" mock responses by not providing any arguments | ||
| setLoadRemoteClusteresResponse(); |
There was a problem hiding this comment.
Is this needed? I removed it and the tests seemed to run fine without it.
There was a problem hiding this comment.
Also I just noticed that this method name contains a typo -- "Clusters" is spelled "Clusteres", so it should be setLoadRemoteClustersResponse.
There was a problem hiding this comment.
😄 I must say that you make me smile here! So when I am explicit, you ask me not to be, when I am not, you ask me to be.
So this line is indeed not needed as we have a safeguard for HTTP requests not handled by any mock. To be even more explicit it should have been setLoadRemoteClusteresResponse([]); (with empty Array), but the default response is an empty array.
So even though it is not necessary, I decided to be explicit and make it clear that there is a request to load the remote clusters.
I am not sure though to what extent we should add comments to the test code to explain that the form depends on the remote clusters being loaded before displaying the component.
| expect(getFormErrorsMessages()).toContain(`Name can't begin with an underscore.`); | ||
| }); | ||
|
|
||
| test('should not allow a "," (coma)', () => { |
There was a problem hiding this comment.
Small typo: "coma" -> "comma"
| setLoadRemoteClusteresResponse(); | ||
|
|
||
| // Mock all HTTP Requests that have not been handled previously | ||
| server.respondWith([200, {}, '']); |
There was a problem hiding this comment.
Seems like both lines 56 and 59 are unnecessary, since the tests seem to execute normally when they're removed. This comment applies to follower_index_add.test.js, too.
| // The implementation of the remote cluster "Select" + validation is | ||
| // done inside the <RemoteClustersFormField /> component. The same component that we use in the <AutoFollowPatternAdd /> section. | ||
| // To avoid copy/pasting the same tests here, we simply make sure that both sections use the <RemoteClustersFormField /> | ||
| test('should use the same <RemoteClustersFormField /> component that in the <AutoFollowPatternAdd /> section', async () => { |
There was a problem hiding this comment.
I think "that in the" would read a bit more naturally if it was changed to "as the".
| await nextTick(550); // we need to wait as there is a debounce of 500ms on the http validation | ||
|
|
||
| expect(server.requests.length).toBe(totalRequests + 1); | ||
| expect(server.requests[totalRequests].url).toBe('/api/index_management/indices'); |
There was a problem hiding this comment.
Minor nit: I think this would be clearer if changed to:
expect(server.requests[server.requests.length - 1].url).toBe('/api/index_management/indices');Using server.requests.length - 1 more clearly communicates to me that we're looking at the most recent request, whereas referencing totalRequests is a bit more of a circuitous way of saying the same thing.
| * the "create" follower index, we won't test it again but simply make sure that | ||
| * the form component is indeed shared between the 2 app sections. | ||
| */ | ||
| test('should use the same Form component that the "<FollowerIndexAdd />" component', async () => { |
There was a problem hiding this comment.
CR + English class = ❤️ 😊
| expect(formAdd.length).toBe(1); | ||
| }); | ||
|
|
||
| test('should populate the form fields with the values from the follower index loaded', () => { |
There was a problem hiding this comment.
What do you think of using a data structure to dedupe some of these assertions and make it easier to compare inputs to expected values?
test('should populate the form fields with the values from the follower index loaded', () => {
const {
remoteCluster,
leaderIndex,
name,
maxReadRequestOperationCount,
maxOutstandingReadRequests,
maxReadRequestSize,
maxWriteRequestOperationCount,
maxWriteRequestSize,
maxOutstandingWriteRequests,
maxWriteBufferCount,
maxWriteBufferSize,
maxRetryDelay,
readPollTimeout,
} = FOLLOWER_INDEX;
const inputToValueMap = {
ccrRemoteClusterInput: remoteCluster,
ccrFollowerIndexFormLeaderIndexInput: leaderIndex,
ccrFollowerIndexFormFollowerIndexInput: name,
ccrFollowerIndexFormMaxReadRequestOperationCountInput: maxReadRequestOperationCount,
ccrFollowerIndexFormMaxOutstandingReadRequestsInput: maxOutstandingReadRequests,
ccrFollowerIndexFormMaxReadRequestSizeInput: maxReadRequestSize,
ccrFollowerIndexFormMaxWriteRequestOperationCountInput: maxWriteRequestOperationCount,
ccrFollowerIndexFormMaxWriteRequestSizeInput: maxWriteRequestSize,
ccrFollowerIndexFormMaxOutstandingWriteRequestsInput: maxOutstandingWriteRequests,
ccrFollowerIndexFormMaxWriteBufferCountInput: maxWriteBufferCount,
ccrFollowerIndexFormMaxWriteBufferSizeInput: maxWriteBufferSize,
ccrFollowerIndexFormMaxRetryDelayInput: maxRetryDelay,
ccrFollowerIndexFormReadPollTimeoutInput: readPollTimeout,
};
Object.keys(inputToValueMap).forEach(input => {
expect(find(input).props().value).toBe(inputToValueMap[input]);
});
});
💔 Build Failed |
💚 Build Succeeded |
|
Thanks for the review @cjcenizal ! I added your suggested changes. We can talk offline about the comments I made if you want. |
This PR adds the client integration tests for the
I also fixed a small bug in the client side validation of the auto-follow pattern input. It is better to have errors as objects with a "message" property. This way, we can add other properties to the error object (like "alwaysVisible") and give our validation UI all the flexibility needed.