Skip to content
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

Graph control tests more #74884

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare global {
namespace jest {
interface Matchers<R, T> {
toYieldEqualTo(expectedYield: T extends AsyncIterable<infer E> ? E : never): Promise<R>;
toYieldObjectEqualTo(expectedYield: unknown): Promise<R>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Not sure about making this even more complicated. What does it do that toYieldEqualTo doesn't? Would be helpful to read in a comment here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

}
}
}
Expand Down Expand Up @@ -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<T>(
this: jest.MatcherContext,
receivedIterable: AsyncIterable<T>,
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { spyMiddlewareFactory } from '../spy_middleware_factory';
import { resolverMiddlewareFactory } from '../../store/middleware';
import { resolverReducer } from '../../store/reducer';
import { MockResolver } from './mock_resolver';
import { ResolverState, DataAccessLayer, SpyMiddleware } from '../../types';
import { ResolverState, DataAccessLayer, SpyMiddleware, SideEffectSimulator } from '../../types';
import { ResolverAction } from '../../store/actions';
import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory';

/**
* Test a Resolver instance using jest, enzyme, and a mock data layer.
Expand Down Expand Up @@ -43,6 +44,11 @@ export class Simulator {
* This is used by `debugActions`.
*/
private readonly spyMiddleware: SpyMiddleware;
/**
* Simulator which allows you to explicitly simulate resize events and trigger animation frames
*/
private readonly sideEffectSimulator: SideEffectSimulator;

constructor({
dataAccessLayer,
resolverComponentInstanceID,
Expand Down Expand Up @@ -87,11 +93,14 @@ export class Simulator {
// Used for `KibanaContextProvider`
const coreStart: CoreStart = coreMock.createStart();

this.sideEffectSimulator = sideEffectSimulatorFactory();

// Render Resolver via the `MockResolver` component, using `enzyme`.
this.wrapper = mount(
<MockResolver
resolverComponentInstanceID={this.resolverComponentInstanceID}
history={this.history}
sideEffectSimulator={this.sideEffectSimulator}
store={this.store}
coreStart={coreStart}
databaseDocumentID={databaseDocumentID}
Expand Down Expand Up @@ -173,6 +182,14 @@ export class Simulator {
return this.wrapper.debug();
}

/**
* This manually runs the animation frames tied to a configurable timestamp in the future
*/
public runAnimationFramesTimeFromNow(time: number = 0) {
this.sideEffectSimulator.controls.time = time;
this.sideEffectSimulator.controls.provideAnimationFrame();
}

/**
* Return an Enzyme ReactWrapper that includes the Related Events host button for a given process node
*
Expand Down Expand Up @@ -251,7 +268,63 @@ export class Simulator {
}

/**
* The icon element for the node detail title.
* Wrapper for the west panning button
*/
public westPanElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:west-button"]');
}

/**
* Wrapper for the south panning button
*/
public southPanElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:south-button"]');
}

/**
* Wrapper for the east panning button
*/
public eastPanElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:east-button"]');
}

/**
* Wrapper for the north panning button
*/
public northPanElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:north-button"]');
}

/**
* Wrapper for the center panning button
*/
public centerPanElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:center-button"]');
}

/**
* Wrapper for the zoom in button
*/
public zoomInElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:zoom-in"]');
}

/**
* Wrapper for the zoom out button
*/
public zoomOutElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:zoom-out"]');
}

/**
* Wrapper for the zoom slider
*/
public zoomSliderElement(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:graph-controls:zoom-slider"]');
}

/**
* The details of the selected node are shown in a description list. This returns the description elements of the description list.
*/
public nodeDetailViewTitleIcon(): ReactWrapper {
return this.domNodes('[data-test-subj="resolver:node-detail:title-icon"]');
Expand Down Expand Up @@ -297,7 +370,7 @@ export class Simulator {
public async resolveWrapper(
wrapperFactory: () => ReactWrapper,
predicate: (wrapper: ReactWrapper) => boolean = (wrapper) => wrapper.length > 0
): Promise<ReactWrapper | void> {
): Promise<ReactWrapper | undefined> {
for await (const wrapper of this.map(wrapperFactory)) {
if (predicate(wrapper)) {
return wrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/* eslint-disable react/display-name */

import React, { useMemo, useEffect, useState, useCallback } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { Router } from 'react-router-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { Provider } from 'react-redux';
Expand All @@ -17,7 +17,6 @@ import { ResolverState, SideEffectSimulator, ResolverProps } from '../../types';
import { ResolverAction } from '../../store/actions';
import { ResolverWithoutProviders } from '../../view/resolver_without_providers';
import { SideEffectContext } from '../../view/side_effect_context';
import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory';

type MockResolverProps = {
/**
Expand All @@ -38,6 +37,10 @@ type MockResolverProps = {
history: React.ComponentProps<typeof Router>['history'];
/** Pass a resolver store. See `storeFactory` and `mockDataAccessLayer` */
store: Store<ResolverState, ResolverAction>;
/**
* Pass the side effect simulator which handles animations and resizing. See `sideEffectSimulatorFactory`
*/
sideEffectSimulator: SideEffectSimulator;
/**
* All the props from `ResolverWithoutStore` can be passed. These aren't defaulted to anything (you might want to test what happens when they aren't present.)
*/
Expand Down Expand Up @@ -66,8 +69,6 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
setResolverElement(element);
}, []);

const simulator: SideEffectSimulator = useMemo(() => sideEffectSimulatorFactory(), []);

// Resize the Resolver element to match the passed in props. Resolver is size dependent.
useEffect(() => {
if (resolverElement) {
Expand All @@ -84,15 +85,15 @@ export const MockResolver = React.memo((props: MockResolverProps) => {
return this;
},
};
simulator.controls.simulateElementResize(resolverElement, size);
props.sideEffectSimulator.controls.simulateElementResize(resolverElement, size);
}
}, [props.rasterWidth, props.rasterHeight, simulator.controls, resolverElement]);
}, [props.rasterWidth, props.rasterHeight, props.sideEffectSimulator.controls, resolverElement]);

return (
<I18nProvider>
<Router history={props.history}>
<KibanaContextProvider services={props.coreStart}>
<SideEffectContext.Provider value={simulator.mock}>
<SideEffectContext.Provider value={props.sideEffectSimulator.mock}>
<Provider store={props.store}>
<ResolverWithoutProviders
ref={resolverRef}
Expand Down
Loading