diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index f244a6481fdd3cd..f5d4a279f1c0f8e 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -11,13 +11,12 @@ * order to be correct, the code that the `declare` declares needs to be available on import as well. */ export {}; + declare global { /* eslint-disable @typescript-eslint/no-namespace */ namespace jest { interface Matchers { - toSometimesYieldEqualTo( - expectedYield: T extends AsyncIterable ? E : never - ): Promise; + toYieldEqualTo(expectedYield: T extends AsyncIterable ? E : never): Promise; } } } @@ -28,7 +27,7 @@ expect.extend({ * If any yielded value deep-equals the expected value, the matcher will pass. * If the generator ends with none of the yielded values matching, it will fail. */ - async toSometimesYieldEqualTo( + async toYieldEqualTo( this: jest.MatcherContext, receivedIterable: AsyncIterable, expected: T @@ -41,17 +40,17 @@ expect.extend({ promise: this.promise, }; // The last value received: Used in printing the message - let lastReceived: T | undefined; + const received: T[] = []; // Set to true if the test passes. let pass: boolean = false; // Async iterate over the iterable - for await (const received of receivedIterable) { - // keep track of the last value. Used in both pass and fail messages - lastReceived = received; + for await (const next of receivedIterable) { + // keep track of all received values. Used in pass and fail messages + received.push(next); // Use deep equals to compare the value to the expected value - if (this.equals(received, expected)) { + if (this.equals(next, expected)) { // If the value is equal, break pass = true; break; @@ -64,23 +63,25 @@ expect.extend({ ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ - this.utils.stringify(expected) !== this.utils.stringify(lastReceived!) - ? `Received: ${this.utils.printReceived(lastReceived)}` + this.utils.stringify(expected) !== this.utils.stringify(received!) + ? `Received: ${this.utils.printReceived(received)}` : '' }` : () => - `${this.utils.matcherHint( - matcherName, - undefined, - undefined, - options - )}\n\n${this.utils.printDiffOrStringify( - expected, - lastReceived, - 'Expected', - 'Received', - this.expand - )}`; + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\nCompared ${ + received.length + } yields.\n\n${received + .map( + (next, index) => + `yield ${index + 1}:\n\n${this.utils.printDiffOrStringify( + expected, + next, + 'Expected', + 'Received', + this.expand + )}` + ) + .join(`\n\n`)}`; return { message, pass }; }, diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts new file mode 100644 index 000000000000000..40267d83c30f840 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactWrapper } from 'enzyme'; + +/** + * Return a collection of attribute 'entries'. + * The 'entries' are attributeName-attributeValue tuples. + */ +export function attributeEntries(wrapper: ReactWrapper): Array<[string, string]> { + return Array.prototype.slice + .call(wrapper.getDOMNode().attributes) + .map(({ name, value }) => [name, value]); +} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts index 4e70740df9025ec..45730531cf46729 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts @@ -25,25 +25,29 @@ export const spyMiddlewareFactory: () => SpyMiddleware = () => { return { middleware: (api) => (next) => (action: ResolverAction) => { + // handle the action first so we get the state after the reducer + next(action); + const state = api.getState(); + + // Resolving these promises may cause code to await the next result. That will add more resolve functions to `resolvers`. + // For this reason, copy all the existing resolvers to an array and clear the set. const oldResolvers = [...resolvers]; resolvers.clear(); for (const resolve of oldResolvers) { resolve({ action, state }); } - - next(action); }, actions, debugActions() { let stop: boolean = false; (async () => { - for await (const action of actions()) { + for await (const actionStatePair of actions()) { if (stop) { break; } // eslint-disable-next-line no-console - console.log('action', action); + console.log('action', actionStatePair.action, 'state', actionStatePair.state); } })(); return () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 321fb3a458561e7..9cb900736677e06 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -45,7 +45,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( graphLoadingElements: simulator.graphLoadingElement().length, graphErrorElements: simulator.graphErrorElement().length, })) - ).toSometimesYieldEqualTo({ + ).toYieldEqualTo({ // it should have 1 graph element, an no error or loading elements. graphElements: 1, graphLoadingElements: 0, @@ -85,7 +85,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( originLooksUnselected: simulator.processNodeElementLooksUnselected(entityIDs.origin), }; }) - ).toSometimesYieldEqualTo({ + ).toYieldEqualTo({ // Just the second child should be marked as selected in the query string queryStringSelectedNode: [entityIDs.secondChild], // The second child is rendered and has `[aria-selected]` diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 0c7ddc9c108fb90..24de45ee894dcb8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -316,7 +316,7 @@ const UnstyledProcessEventDot = React.memo( () => { handleFocus(); handleClick(); - } /* a11y note: this is strictly an alternate to the button, so no tabindex is necessary*/ + } /* a11y note: this is strictly an alternate to the button, so no tabindex is necessary*/ } role="img" aria-labelledby={labelHTMLID} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 3b28a9ba490435e..c5e5ff98ed7ae6d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -72,25 +72,12 @@ export const ResolverWithoutProviders = React.memo( const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); - const { - cleanUpQueryParams, - queryParams: { crumbId }, - pushToQueryParams, - } = useResolverQueryParams(); + const { cleanUpQueryParams } = useResolverQueryParams(); useEffectOnce(() => { return () => cleanUpQueryParams(); }); - useEffect(() => { - // When you refresh the page after selecting a process in the table view (not the timeline view) - // The old crumbId still exists in the query string even though a resolver is no longer visible - // This just makes sure the activeDescendant and crumbId are in sync on load for that view as well as the timeline - if (activeDescendantId && crumbId !== activeDescendantId) { - pushToQueryParams({ crumbId: activeDescendantId, crumbEvent: '' }); - } - }, [crumbId, activeDescendantId, pushToQueryParams]); - return ( {isLoading ? (