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 9fc7af38beb42a..df8f32d15a7ab6 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 @@ -17,6 +17,7 @@ declare global { namespace jest { interface Matchers { toYieldEqualTo(expectedYield: T extends AsyncIterable ? E : never): Promise; + toYieldObjectEqualTo(expectedYield: unknown): Promise; } } } @@ -57,6 +58,70 @@ expect.extend({ } } + // Use `pass` as set in the above loop (or initialized to `false`) + // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils + const message = pass + ? () => + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + + `Expected: not ${this.utils.printExpected(expected)}\n${ + this.utils.stringify(expected) !== this.utils.stringify(received[received.length - 1]!) + ? `Received: ${this.utils.printReceived(received[received.length - 1])}` + : '' + }` + : () => + `${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 }; + }, + /** + * A custom matcher that takes an async generator and compares each value it yields to an expected value. + * This uses the same equality logic as `toMatchObject`. + * If any yielded value equals the expected value, the matcher will pass. + * If the generator ends with none of the yielded values matching, it will fail. + */ + async toYieldObjectEqualTo( + this: jest.MatcherContext, + receivedIterable: AsyncIterable, + expected: T + ): Promise<{ pass: boolean; message: () => string }> { + // Used in printing out the pass or fail message + const matcherName = 'toSometimesYieldEqualTo'; + const options: jest.MatcherHintOptions = { + comment: 'deep equality with any yielded value', + isNot: this.isNot, + promise: this.promise, + }; + // The last value received: Used in printing the message + const received: T[] = []; + + // Set to true if the test passes. + let pass: boolean = false; + + // Async iterate over the iterable + 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(next, expected), [this.utils.iterableEquality, this.utils.subsetEquality])) { + // If the value is equal, break + pass = true; + break; + } + } + // Use `pass` as set in the above loop (or initialized to `false`) // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils const message = pass diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 85b283a76cfa9b..6607056b2a4bcc 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -370,7 +370,7 @@ export class Simulator { public async resolveWrapper( wrapperFactory: () => ReactWrapper, predicate: (wrapper: ReactWrapper) => boolean = (wrapper) => wrapper.length > 0 - ): Promise { + ): Promise { for await (const wrapper of this.map(wrapperFactory)) { if (predicate(wrapper)) { return wrapper; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index 9c484d936f58cb..3b66f6af87cb48 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -8,14 +8,22 @@ import { Simulator } from '../test_utilities/simulator'; import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; import { nudgeAnimationDuration } from '../store/camera/scaling_constants'; import '../test_utilities/extend_jest'; -import { ReactWrapper } from 'enzyme'; -describe('graph controls', () => { +describe('graph controls: when relsover is loaded with an origin node', () => { let simulator: Simulator; let originEntityID: string; - let originNode: ReactWrapper; + let originNodeStyle: () => AsyncIterable; const resolverComponentInstanceID = 'graph-controls-test'; + const originalPositionStyle: Readonly<{ left: string; top: string }> = { + left: '746.93132px', + top: '535.5792px', + }; + const originalSizeStyle: Readonly<{ width: string; height: string }> = { + width: '360px', + height: '120px', + }; + beforeEach(async () => { const { metadata: { databaseDocumentID, entityIDs }, @@ -28,179 +36,185 @@ describe('graph controls', () => { resolverComponentInstanceID, }); originEntityID = entityIDs.origin; - }); - - describe('when the graph controls load', () => { - it('should display all cardinal panning buttons and the center button', async () => { - await expect( - simulator.map(() => ({ - westPanButton: simulator.westPanElement().length, - southPanButton: simulator.southPanElement().length, - eastPanButton: simulator.eastPanElement().length, - northPanButton: simulator.northPanElement().length, - centerButton: simulator.centerPanElement().length, - })) - ).toYieldEqualTo({ - westPanButton: 1, - southPanButton: 1, - eastPanButton: 1, - northPanButton: 1, - centerButton: 1, - }); - }); - it('should display the zoom buttons and slider', async () => { - await expect( - simulator.map(() => ({ - zoomInButton: simulator.zoomInElement().length, - zoomOutButton: simulator.zoomOutElement().length, - zoomSlider: simulator.zoomSliderElement().length, - })) - ).toYieldEqualTo({ - zoomInButton: 1, - zoomOutButton: 1, - zoomSlider: 1, + originNodeStyle = () => + simulator.map(() => { + const wrapper = simulator.processNodeElements({ entityID: originEntityID }); + // `getDOMNode` can only be called on a wrapper of a single node: https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/getDOMNode.html + if (wrapper.length === 1) { + return wrapper.getDOMNode().style; + } + return null; }); - }); }); - describe('panning', () => { - const originalPositionStyle = { left: '746.93132px', top: '535.5792px' }; - beforeEach(() => { - originNode = simulator.processNodeElements({ entityID: originEntityID }); + it('should display all cardinal panning buttons and the center button', async () => { + await expect( + simulator.map(() => ({ + westPanButton: simulator.westPanElement().length, + southPanButton: simulator.southPanElement().length, + eastPanButton: simulator.eastPanElement().length, + northPanButton: simulator.northPanElement().length, + centerButton: simulator.centerPanElement().length, + })) + ).toYieldEqualTo({ + westPanButton: 1, + southPanButton: 1, + eastPanButton: 1, + northPanButton: 1, + centerButton: 1, }); + }); - describe('when the user has not interacted with panning yet', () => { - it("should show the origin node in it's original position", () => { - expect(originNode.getDOMNode()).toHaveStyle(originalPositionStyle); - }); + it('should display the zoom buttons and slider', async () => { + await expect( + simulator.map(() => ({ + zoomInButton: simulator.zoomInElement().length, + zoomOutButton: simulator.zoomOutElement().length, + zoomSlider: simulator.zoomSliderElement().length, + })) + ).toYieldEqualTo({ + zoomInButton: 1, + zoomOutButton: 1, + zoomSlider: 1, }); + }); - describe('when the user clicks the west panning button', () => { - beforeEach(() => { - simulator.westPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); + it("should show the origin node in it's original position", async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); + }); - it('should show the origin node further left on the screen', async () => { - expect(originNode.getDOMNode()).toHaveStyle({ left: '796.93132px', top: '535.5792px' }); - }); + describe('when the user clicks the west panning button', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.westPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); }); - describe('when the user clicks the south panning button', () => { - beforeEach(() => { - simulator.southPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); - - it('should show the origin node lower on the screen', async () => { - expect(originNode.getDOMNode()).toHaveStyle({ left: '746.93132px', top: '485.5792px' }); + it('should show the origin node further left on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '796.93132px', + top: '535.5792px', }); }); + }); - describe('when the user clicks the east panning button', () => { - beforeEach(() => { - simulator.eastPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); + describe('when the user clicks the south panning button', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.southPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it('should show the origin node further right on the screen', async () => { - expect(originNode.getDOMNode()).toHaveStyle({ left: '696.93132px', top: '535.5792px' }); + it('should show the origin node lower on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '746.93132px', + top: '485.5792px', }); }); + }); - describe('when the user clicks the north panning button', () => { - beforeEach(() => { - simulator.northPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); + describe('when the user clicks the east panning button', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.eastPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it('should show the origin node higher on the screen', async () => { - expect(originNode.getDOMNode()).toHaveStyle({ left: '746.93132px', top: '585.5792px' }); + it('should show the origin node further right on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '696.93132px', + top: '535.5792px', }); }); + }); - describe('when the user clicks the center panning button', () => { - beforeEach(() => { - simulator.northPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - simulator.centerPanElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); + describe('when the user clicks the north panning button', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.northPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it("should return the origin node to it's original position", async () => { - expect(originNode.getDOMNode()).toHaveStyle(originalPositionStyle); + it('should show the origin node higher on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + left: '746.93132px', + top: '585.5792px', }); }); }); - describe('zooming', () => { - const originalSizeStyle = { width: '360px', height: '120px' }; - beforeEach(() => { - originNode = simulator.processNodeElements({ entityID: originEntityID }); + describe('when the user clicks the center panning button', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.northPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + (await simulator.resolveWrapper(() => simulator.centerPanElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); }); - describe('when the user has not interacted with the zoom buttons or slider yet', () => { - it('should show the origin node as larger on the screen', () => { - expect(originNode.getDOMNode()).toHaveStyle(originalSizeStyle); - }); + it("should return the origin node to it's original position", async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalPositionStyle); }); + }); - describe('when the zoom in button is clicked', () => { - beforeEach(() => { - simulator.zoomInElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); - }); + it('should show the origin node as larger on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalSizeStyle); + }); - it('should show the origin node as larger on the screen', () => { - expect(originNode.getDOMNode()).toHaveStyle({ - width: '427.7538290724795px', - height: '142.5846096908265px', - }); - }); + describe('when the zoom in button is clicked', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.zoomInElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); }); - describe('when the zoom out button is clicked', () => { - beforeEach(() => { - simulator.zoomOutElement().simulate('click'); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + it('should show the origin node as larger on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '427.7538290724795px', + height: '142.5846096908265px', }); + }); + }); + + describe('when the zoom out button is clicked', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.zoomOutElement()))!.simulate('click'); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it('should show the origin node as smaller on the screen', () => { - expect(originNode.getDOMNode()).toHaveStyle({ - width: '303.0461709275204px', - height: '101.01539030917347px', - }); + it('should show the origin node as smaller on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '303.0461709275204px', + height: '101.01539030917347px', }); }); + }); - describe('when the slider is moved upwards', () => { - beforeEach(() => { - expect(originNode.getDOMNode()).toHaveStyle(originalSizeStyle); + describe('when the slider is moved upwards', () => { + beforeEach(async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo(originalSizeStyle); - simulator.zoomSliderElement().simulate('change', { target: { value: 0.8 } }); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + (await simulator.resolveWrapper(() => simulator.zoomSliderElement()))!.simulate('change', { + target: { value: 0.8 }, }); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it('should show the origin node as large on the screen', () => { - expect(originNode.getDOMNode()).toHaveStyle({ - width: '525.6000000000001px', - height: '175.20000000000005px', - }); + it('should show the origin node as large on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '525.6000000000001px', + height: '175.20000000000005px', }); }); + }); - describe('when the slider is moved downwards', () => { - beforeEach(() => { - simulator.zoomSliderElement().simulate('change', { target: { value: 0.2 } }); - simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + describe('when the slider is moved downwards', () => { + beforeEach(async () => { + (await simulator.resolveWrapper(() => simulator.zoomSliderElement()))!.simulate('change', { + target: { value: 0.2 }, }); + simulator.runAnimationFramesTimeFromNow(nudgeAnimationDuration); + }); - it('should show the origin node as smaller on the screen', () => { - expect(originNode.getDOMNode()).toHaveStyle({ - width: '201.60000000000002px', - height: '67.2px', - }); + it('should show the origin node as smaller on the screen', async () => { + await expect(originNodeStyle()).toYieldObjectEqualTo({ + width: '201.60000000000002px', + height: '67.2px', }); }); });