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

[Enterprise Search] Added a test helper for Kea tests #100134

Closed
wants to merge 8 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 @@ -85,6 +85,7 @@ export const setMockActions = (actions: object) => {
* });
*/
import { resetContext, LogicWrapper } from 'kea';
import { merge } from 'lodash';

type LogicFile = LogicWrapper<any>;

Expand All @@ -101,25 +102,82 @@ export class LogicMounter {
if (!values || !Object.keys(values).length) {
resetContext({});
} else {
let { path, key } = this.logicFile.inputs[0];

// For keyed logic files, both key and path should be functions
if (this.logicFile._isKeaWithKey) {
key = key(props);
path = path(key);
}

// Generate the correct nested defaults obj based on the file path
// example path: ['x', 'y', 'z']
// example defaults: { x: { y: { z: values } } }
const defaults = path.reduceRight(
(value: object, name: string) => ({ [name]: value }),
values
);
const defaults: object = this.createDefaultValuesObject(values, props);
resetContext({ defaults });
}
};

/**
* Based on the values passed into mount, turn them into properly nested objects that can
* be passed to kea's resetContext in order to set default values":
*
* ex.
*
* input:
*
* values: {
* schema: { foo: "text" },
* engineName: "engine1"
* }
*
* output:
*
* {
* enterprise_search: {
* app_search: {
* schema_logic: {
* schema: { foo: "text" }
* },
* engine_logic: {
* engineName: "engine1"
* }
* }
* }
* }
*/
private createDefaultValuesObject = (values: object, props?: object) => {
let { path, key } = this.logicFile.inputs[0];

// For keyed logic files, both key and path should be functions
if (this.logicFile._isKeaWithKey) {
key = key(props);
path = path(key);
}

// TODO Deal with this if and when we get there.
if (this.logicFile.inputs[0].connect?.values.length > 2) {
throw Error(
"This connected logic has more than 2 values in 'connect', implement handler logic for this in kea.mock.ts"
);
}

// If a logic includes values from another logic via the "connect" property, we need to make sure they're nested
// correctly under the correct path.
//
// For example, if the current logic under test is SchemaLogic connects values from EngineLogic also, then we need
// to make sure that values from SchemaLogic get nested under enterprise_search.app_search.schema_logic, and values
// from EngineLogic get nested under enterprise_search.app_search.engine_logic
if (this.logicFile.inputs[0].connect?.values[0]) {
const connectedPath = this.logicFile.inputs[0].connect.values[0].inputs[0].path;
const connectedValueKeys = this.logicFile.inputs[0].connect.values[1];

const primaryValues: Record<string, object> = {};
const connectedValues: Record<string, object> = {};

Object.entries(values).forEach(([k, v]) => {
if (connectedValueKeys.includes(k)) {
connectedValues[k] = v;
} else {
primaryValues[k] = v;
}
});

return merge(createDefaults(path, values), createDefaults(connectedPath, connectedValues));
} else {
return createDefaults(path, values);
}
};

// Automatically reset context & mount the logic file
public mount = (values?: object, props?: object) => {
this.resetContext(values, props);
Expand All @@ -132,6 +190,48 @@ export class LogicMounter {
// built logic instance with props, NOT the unmount fn
};

// Custom "jest-like" assertions
// ex.
// expectAction(() => {
// SomeLogic.actions.dataInitialized();
// }).toChangeState({
// from: { dataLoading: true },
// to: { dataLoading: false },
// });
//
// For keyed logic:
//
// ex.
// expectAction((logic) => {
// logic.actions.dataInitialized();
// }, PROPS).toChangeState({
// from: { dataLoading: true },
// to: { dataLoading: false },
// });
//

public expectAction = (action: (logic: LogicFile) => void, props: object = {}) => {
return {
// Mount state with "from" values and test that the specified "to" values are present in
// the updated state, and that no other values have changed.
toChangeState: ({ from, to, ignore }: { from: object; to: object; ignore?: string[] }) => {
const logic = this.mount(from, props);
const originalValues = {
...logic.values,
};
action(logic);
expect(logic.values).toEqual({
...originalValues,
...to,
...(ignore || []).reduce((acc: Record<string, object>, field: string) => {
acc[field] = expect.anything();
return acc;
}, {}),
});
},
};
};

// Also add unmount as a class method that can be destructured on init without becoming stale later
public unmount = () => {
this.unmountFn();
Expand Down Expand Up @@ -162,3 +262,10 @@ export class LogicMounter {
: listeners; // handles simpler logic files that just define listeners: { ... }
};
}

// Generate the correct nested defaults obj based on the file path
// example path: ['x', 'y', 'z']
// example defaults: { x: { y: { z: values } } }
const createDefaults = (path: string[], values: object) => {
return path.reduceRight((value: object, name: string) => ({ [name]: value }), values);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@

import { LogicMounter } from '../../../__mocks__';

import { Logic } from 'kea';

import { MultiInputRowsLogic } from './multi_input_rows_logic';

describe('MultiInputRowsLogic', () => {
const { mount } = new LogicMounter(MultiInputRowsLogic);
const { mount, expectAction } = new LogicMounter(MultiInputRowsLogic);

const MOCK_VALUES = ['a', 'b', 'c'];

Expand All @@ -37,67 +35,70 @@ describe('MultiInputRowsLogic', () => {
});

describe('actions', () => {
let logic: Logic;

beforeEach(() => {
logic = mount({}, DEFAULT_PROPS);
});

afterEach(() => {
// Should not mutate the original array
expect(logic.values.values).not.toBe(MOCK_VALUES); // Would fail if we did not clone a new array
});

describe('addValue', () => {
it('appends an empty string to the values array & sets addedNewRow to true', () => {
logic.actions.addValue();

expect(logic.values).toEqual({
...DEFAULT_VALUES,
addedNewRow: true,
hasEmptyValues: true,
values: ['a', 'b', 'c', ''],
expectAction((logic) => {
logic.actions.addValue();
}, DEFAULT_PROPS).toChangeState({
from: {
addedNewRow: false,
hasEmptyValues: false,
values: ['a', 'b', 'c'],
},
to: {
addedNewRow: true,
hasEmptyValues: true,
values: ['a', 'b', 'c', ''],
},
});
});
});

describe('deleteValue', () => {
it('deletes the value at the specified array index', () => {
logic.actions.deleteValue(1);

expect(logic.values).toEqual({
...DEFAULT_VALUES,
values: ['a', 'c'],
expectAction((logic) => {
logic.actions.deleteValue(1);
}, DEFAULT_PROPS).toChangeState({
from: {
values: ['a', 'b', 'c'],
},
to: {
values: ['a', 'c'],
},
});
});
});

describe('editValue', () => {
it('edits the value at the specified array index', () => {
logic.actions.editValue(2, 'z');

expect(logic.values).toEqual({
...DEFAULT_VALUES,
values: ['a', 'b', 'z'],
expectAction((logic) => {
logic.actions.editValue(2, 'z');
}, DEFAULT_PROPS).toChangeState({
from: {
values: ['a', 'b', 'c'],
},
to: {
values: ['a', 'b', 'z'],
},
});
});
});
});

describe('selectors', () => {
describe('hasEmptyValues', () => {
it('returns true if values has any empty strings', () => {
const logic = mount({}, { ...DEFAULT_PROPS, values: ['', '', ''] });
describe('selectors', () => {
describe('hasEmptyValues', () => {
it('returns true if values has any empty strings', () => {
const logic = mount({}, { ...DEFAULT_PROPS, values: ['', '', ''] });

expect(logic.values.hasEmptyValues).toEqual(true);
expect(logic.values.hasEmptyValues).toEqual(true);
});
});
});

describe('hasOnlyOneValue', () => {
it('returns true if values only has one item', () => {
const logic = mount({}, { ...DEFAULT_PROPS, values: ['test'] });
describe('hasOnlyOneValue', () => {
it('returns true if values only has one item', () => {
const logic = mount({}, { ...DEFAULT_PROPS, values: ['test'] });

expect(logic.values.hasOnlyOneValue).toEqual(true);
expect(logic.values.hasOnlyOneValue).toEqual(true);
});
});
});
});
Expand Down
Loading