"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.Simulator = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _enzyme = require("enzyme"); var _history = require("history"); var _mocks = require("@kbn/core/public/mocks"); var _store = require("../../../common/store/store"); var _spy_middleware_factory = require("../spy_middleware_factory"); var _middleware = require("../../store/middleware"); var _mock_resolver = require("./mock_resolver"); var _side_effect_simulator_factory = require("../../view/side_effect_simulator_factory"); var _ui_setting = require("../../mocks/ui_setting"); var _helpers = require("../../store/helpers"); var _mock = require("../../../common/mock"); var _actions = require("../../store/actions"); /* * 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. */ /** * Test a Resolver instance using jest, enzyme, and a mock data layer. */ class Simulator { /** * The redux store, creating in the constructor using the `dataAccessLayer`. * This code subscribes to state transitions. */ /** * A fake 'History' API used with `react-router` to simulate a browser history. */ /** * The 'wrapper' returned by `enzyme` that contains the rendered Resolver react code. */ /** * A `redux` middleware that exposes all actions dispatched (along with the state at that point.) * This is used by `debugActions`. */ /** * Simulator which allows you to explicitly simulate resize events and trigger animation frames */ /** * An `enzyme` supported CSS selector for process node elements. */ static nodeElementSelector({ entityID, selected = false } = {}) { let selector = baseNodeElementSelector; if (entityID !== undefined) { selector += `[data-test-resolver-node-id="${entityID}"]`; } if (selected) { selector += '[aria-selected="true"]'; } return selector; } /** * The simulator returns enzyme `ReactWrapper`s from various methods. Use this predicate to determine if they are DOM nodes. */ static isDOM(wrapper) { return typeof wrapper.type() === 'string'; } constructor({ dataAccessLayer, resolverComponentInstanceID, databaseDocumentID, indices, history, filters, shouldUpdate }) { (0, _defineProperty2.default)(this, "store", void 0); (0, _defineProperty2.default)(this, "history", void 0); (0, _defineProperty2.default)(this, "wrapper", void 0); (0, _defineProperty2.default)(this, "spyMiddleware", void 0); (0, _defineProperty2.default)(this, "sideEffectSimulator", void 0); // create the spy middleware (for debugging tests) this.spyMiddleware = (0, _spy_middleware_factory.spyMiddlewareFactory)(); // Create a redux store w/ the top level Resolver reducer and the enhancer that includes the Resolver middleware and the `spyMiddleware` const { storage } = (0, _mock.createSecuritySolutionStorageMock)(); this.store = (0, _store.createStore)({ ..._mock.mockGlobalState, analyzer: { [resolverComponentInstanceID]: _helpers.EMPTY_RESOLVER } }, _mock.SUB_PLUGINS_REDUCER, _mock.kibanaObservable, storage, [(0, _middleware.resolverMiddlewareFactory)(dataAccessLayer), this.spyMiddleware.middleware]); // If needed, create a fake 'history' instance. // Resolver will use to read and write query string values. this.history = history !== null && history !== void 0 ? history : (0, _history.createMemoryHistory)(); // Used for `KibanaContextProvider` const coreStart = _mocks.coreMock.createStart(); coreStart.settings.client.get.mockImplementation(_ui_setting.uiSetting); this.sideEffectSimulator = (0, _side_effect_simulator_factory.sideEffectSimulatorFactory)(); // Render Resolver via the `MockResolver` component, using `enzyme`. this.wrapper = (0, _enzyme.mount)( /*#__PURE__*/_react.default.createElement(_mock_resolver.MockResolver, { resolverComponentInstanceID: resolverComponentInstanceID, history: this.history, sideEffectSimulator: this.sideEffectSimulator, store: this.store, coreStart: coreStart, databaseDocumentID: databaseDocumentID, indices: indices, filters: filters, shouldUpdate: shouldUpdate })); } /** * Unmount the Resolver component. Use this to test what happens when code that uses Resolver unmounts it. */ unmount() { this.wrapper.unmount(); } /** * Get the component instance ID from the component. */ get resolverComponentInstanceID() { return this.wrapper.prop('resolverComponentInstanceID'); } /** * Change the component instance ID (updates the React component props.) */ set resolverComponentInstanceID(value) { this.store.dispatch((0, _actions.createResolver)({ id: value })); this.wrapper.setProps({ resolverComponentInstanceID: value }); } /** * Change the indices (updates the React component props.) */ set indices(value) { this.wrapper.setProps({ indices: value }); } /** * Get the indices (updates the React component props.) */ get indices() { return this.wrapper.prop('indices'); } /** * Change the shouldUpdate prop (updates the React component props.) */ set shouldUpdate(value) { this.wrapper.setProps({ shouldUpdate: value }); } get shouldUpdate() { return this.wrapper.prop('shouldUpdate'); } set filters(value) { this.wrapper.setProps({ filters: value }); } get filters() { return this.wrapper.prop('filters'); } /** * Call this to console.log actions (and state). Use this to debug your tests. * State and actions aren't exposed otherwise because the tests using this simulator should * assert stuff about the DOM instead of internal state. Use selector/middleware/reducer * unit tests to test that stuff. */ debugActions() { return this.spyMiddleware.debugActions(); } /** * EUI uses a component called `AutoSizer` that won't render its children unless it has sufficient size. * This forces any `AutoSizer` instances to have a large size. */ forceAutoSizerOpen() { this.wrapper.find('AutoSizer').forEach(wrapper => wrapper.setState({ width: 10000, height: 10000 })); } /** * Yield the result of `mapper` over and over, once per event-loop cycle. * After 10 times, quit. * Use this to continually check a value. See `toYieldEqualTo`. */ async *map(mapper) { let timeoutCount = 0; while (timeoutCount < 10) { timeoutCount++; yield mapper(); await new Promise(resolve => { setTimeout(() => { this.forceAutoSizerOpen(); this.wrapper.update(); resolve(); }, 0); }); } } /** * Find a process node element. Takes options supported by `resolverNodeSelector`. * returns a `ReactWrapper` even if nothing is found, as that is how `enzyme` does things. */ processNodeElements(options = {}) { return this.domNodes(Simulator.nodeElementSelector(options)); } /** * The button that opens a node's submenu. */ processNodeSubmenuButton( /** nodeID for the related node */entityID) { return this.domNodes(`[data-test-subj="resolver:submenu:button"][data-test-resolver-node-id="${entityID}"]`); } /** * The primary button (used to select a node) which contains a label for the node as its content. */ processNodePrimaryButton( /** nodeID for the related node */entityID) { return this.domNodes(`[data-test-subj="resolver:node:primary-button"][data-test-resolver-node-id="${entityID}"]`); } /** * Return the node element with the given `entityID`. */ selectedProcessNode(entityID) { return this.processNodeElements({ entityID, selected: true }); } /** * Return the node element with the given `entityID`. It will only be returned if it is not selected. */ unselectedProcessNode(entityID) { return this.processNodeElements({ entityID }).not(Simulator.nodeElementSelector({ entityID, selected: true })); } /** * Dump all contents of the outer ReactWrapper (to be `console.log`ged as appropriate) * This will include both DOM (div, span, etc.) and React/JSX (MyComponent, MyGrid, etc.) */ debugWrapper() { return this.wrapper.debug(); } /** * This manually runs the animation frames tied to a configurable timestamp in the future. */ runAnimationFramesTimeFromNow(time = 0) { this.sideEffectSimulator.controls.time = time; this.sideEffectSimulator.controls.provideAnimationFrame(); } /** * The last value written to the clipboard via the `SideEffectors`. */ get clipboardText() { return this.sideEffectSimulator.controls.clipboardText; } /** * Call this to resolve the promise returned by the `SideEffectors` `writeText` method (which in production points to `navigator.clipboard.writeText`. */ confirmTextWrittenToClipboard() { this.sideEffectSimulator.controls.confirmTextWrittenToClipboard(); } /** * The 'search' part of the URL. */ get historyLocationSearch() { // Wrap the `search` value from the MemoryHistory using `URLSearchParams` in order to standardize it. return new URLSearchParams(this.history.location.search).toString(); } /** * Given a 'data-test-subj' value, it will resolve the react wrapper or undefined if not found */ async resolve(selector) { return this.resolveWrapper(() => this.domNodes(`[data-test-subj="${selector}"]`)); } /** * Given a `role`, return DOM nodes that have it. Use this to assert that ARIA roles are present as expected. */ domNodesWithRole(role) { return this.domNodes(`[role="${role}"]`); } /** * Given a 'data-test-subj' selector, it will return the domNode */ testSubject(selector) { return this.domNodes(`[data-test-subj="${selector}"]`); } /** * Given a `ReactWrapper`, returns a wrapper containing immediately following `dd` siblings. * `subject` must contain just 1 element. */ descriptionDetails(subject) { // find the associated DOM nodes, then return an enzyme wrapper that only contains those. const subjectNode = subject.getDOMNode(); let current = subjectNode.nextElementSibling; const associated = new Set(); // Multiple `dt`s can be associated with a set of `dd`s. Skip immediately following `dt`s. while (current !== null && current.nodeName === 'DT') { current = current.nextElementSibling; } while (current !== null && current.nodeName === 'DD') { associated.add(current); current = current.nextElementSibling; } return subject.closest('dl').find('dd').filterWhere(candidate => { return associated.has(candidate.getDOMNode()); }); } /** * Return DOM nodes that match `enzymeSelector`. */ domNodes(enzymeSelector) { return this.wrapper.find(enzymeSelector).filterWhere(Simulator.isDOM); } /** * The titles and descriptions (as text) from the node detail panel. */ nodeDetailDescriptionListEntries() { /** * The details of the selected node are shown in a description list. This returns the title elements of the description list. */ const titles = this.domNodes('[data-test-subj="resolver:node-detail:entry-title"]'); /** * The details of the selected node are shown in a description list. This returns the description elements of the description list. */ const descriptions = this.domNodes('[data-test-subj="resolver:node-detail:entry-description"]'); const entries = []; for (let index = 0; index < Math.min(titles.length, descriptions.length); index++) { const title = titles.at(index).text(); const description = descriptions.at(index).text(); entries.push([title, description]); } return entries; } /** * Resolve the wrapper returned by `wrapperFactory` only once it has at least 1 element in it. */ async resolveWrapper(wrapperFactory, predicate = wrapper => wrapper.length > 0) { for await (const wrapper of this.map(wrapperFactory)) { if (predicate(wrapper)) { return wrapper; } } } } exports.Simulator = Simulator; const baseNodeElementSelector = '[data-test-subj="resolver:node"]';