Skip to content

Commit

Permalink
[KED-1611] Refactor node-list (#152)
Browse files Browse the repository at this point in the history
Improve the code for the node-list components by separating them into different files, and add more/better tests
  • Loading branch information
richardwestenra authored Apr 29, 2020
1 parent fcdb9e4 commit a2c92e4
Show file tree
Hide file tree
Showing 15 changed files with 697 additions and 492 deletions.
84 changes: 84 additions & 0 deletions src/components/node-list/filter-nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createSelector } from 'reselect';
import utils from '@quantumblack/kedro-ui/lib/utils';
const { escapeRegExp, getHighlightedText } = utils;

/**
* Get a list of IDs of the visible nodes
* @param {object} nodes Grouped nodes
* @return {array} List of node IDs
*/
export const getNodeIDs = nodes => {
const getNodeIDs = type => nodes[type].map(node => node.id);
const concatNodeIDs = (nodeIDs, type) => nodeIDs.concat(getNodeIDs(type));

return Object.keys(nodes).reduce(concatNodeIDs, []);
};

/**
* Add a new highlightedLabel field to each of the node objects
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} The grouped nodes with highlightedLabel fields added
*/
export const highlightMatch = (nodes, searchValue) => {
const addHighlightedLabel = node => ({
highlightedLabel: getHighlightedText(node.name, searchValue),
...node
});
const addLabelsToNodes = (newNodes, type) => ({
...newNodes,
[type]: nodes[type].map(addHighlightedLabel)
});

return Object.keys(nodes).reduce(addLabelsToNodes, {});
};

/**
* Check whether a name matches the search text
* @param {string} name
* @param {string} searchValue
* @return {boolean} True if match
*/
export const nodeMatchesSearch = (node, searchValue) => {
const valueRegex = searchValue
? new RegExp(escapeRegExp(searchValue), 'gi')
: '';
return Boolean(node.name.match(valueRegex));
};

/**
* Return only the results that match the search text
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} Grouped nodes
*/
export const filterNodes = (nodes, searchValue) => {
const filterNodesByType = type =>
nodes[type].filter(node => nodeMatchesSearch(node, searchValue));
const filterNodeLists = (newNodes, type) => ({
...newNodes,
[type]: filterNodesByType(type)
});

return Object.keys(nodes).reduce(filterNodeLists, {});
};

/**
* Return filtered/highlighted nodes, and filtered node IDs
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} Grouped nodes, and node IDs
*/
const getFilteredNodes = createSelector(
[state => state.nodes, state => state.searchValue],
(nodes, searchValue) => {
const filteredNodes = filterNodes(nodes, searchValue);

return {
filteredNodes: highlightMatch(filteredNodes, searchValue),
nodeIDs: getNodeIDs(filteredNodes)
};
}
);

export default getFilteredNodes;
154 changes: 154 additions & 0 deletions src/components/node-list/filter-nodes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import getFilteredNodes, {
getNodeIDs,
highlightMatch,
nodeMatchesSearch,
filterNodes
} from './filter-nodes';
import { mockState } from '../../utils/state.mock';
import { getGroupedNodes } from '../../selectors/nodes';

const ungroupNodes = groupedNodes =>
Object.keys(groupedNodes).reduce(
(names, key) => names.concat(groupedNodes[key]),
[]
);

describe('filter-nodes', () => {
describe('getFilteredNodes', () => {
const nodes = getGroupedNodes(mockState.lorem);
const searchValue = 'e';
const { filteredNodes, nodeIDs } = getFilteredNodes({ nodes, searchValue });
const nodeList = ungroupNodes(filteredNodes);

describe('filteredNodes', () => {
test.each(nodeList.map(node => node.name))(
`node name "%s" contains search term "${searchValue}"`,
name => {
expect(name).toEqual(expect.stringMatching(searchValue));
}
);

test.each(nodeList.map(node => node.highlightedLabel))(
`node label "%s" contains highlighted search term "<b>${searchValue}</b>"`,
name => {
expect(name).toEqual(expect.stringMatching(`<b>${searchValue}</b>`));
}
);
});

describe('nodeIDs', () => {
test.each(nodeIDs)(
`node ID "%s" contains search term "${searchValue}"`,
nodeID => {
expect(nodeID).toEqual(expect.stringMatching(searchValue));
}
);
});
});

describe('getNodeIDs', () => {
const generateNodes = (type, count) =>
Array.from(new Array(count)).map((d, i) => ({
id: type + i
}));

const nodes = {
data: generateNodes('data', 10),
task: generateNodes('task', 10),
parameters: generateNodes('parameters', 10)
};

it('returns a list of node IDs', () => {
const nodeIDs = getNodeIDs(nodes);
expect(nodeIDs).toHaveLength(30);
expect(nodeIDs).toEqual(expect.arrayContaining([expect.any(String)]));
expect(nodeIDs).toContain('data0');
expect(nodeIDs).toContain('task1');
expect(nodeIDs).toContain('parameters1');
});
});

describe('highlightMatch', () => {
const nodes = getGroupedNodes(mockState.animals);
const searchValue = 'e';
const formattedNodes = highlightMatch(nodes, searchValue);
const nodeList = ungroupNodes(formattedNodes);

describe(`nodes which match the search term "${searchValue}"`, () => {
const matchingNodeList = nodeList.filter(node =>
node.name.includes(searchValue)
);
test.each(matchingNodeList.map(node => node.highlightedLabel))(
`node label "%s" contains highlighted search term "<b>${searchValue}</b>"`,
label => {
expect(label).toEqual(expect.stringMatching(`<b>${searchValue}</b>`));
}
);
});

describe(`nodes which do not match the search term "${searchValue}"`, () => {
const notMatchingNodeList = nodeList.filter(
node => !node.name.includes(searchValue)
);
test.each(notMatchingNodeList.map(node => node.highlightedLabel))(
`node label "%s" does not contain "<b>"`,
label => {
expect(label).not.toEqual(expect.stringMatching(`<b>`));
}
);
});
});

describe('nodeMatchesSearch', () => {
const node = { name: 'qwertyuiop' };

it('returns true if the node name matches the search', () => {
expect(nodeMatchesSearch(node, 'qwertyuiop')).toBe(true);
expect(nodeMatchesSearch(node, 'qwe')).toBe(true);
expect(nodeMatchesSearch(node, 'p')).toBe(true);
});

it('returns true if the search is falsey', () => {
expect(nodeMatchesSearch(node, '')).toBe(true);
expect(nodeMatchesSearch(node, null)).toBe(true);
expect(nodeMatchesSearch(node, undefined)).toBe(true);
});

it('returns false if the node name does not match the search', () => {
expect(nodeMatchesSearch(node, 'a')).toBe(false);
expect(nodeMatchesSearch(node, 'qwe ')).toBe(false);
expect(nodeMatchesSearch(node, ' ')).toBe(false);
expect(nodeMatchesSearch(node, '_')).toBe(false);
});
});

describe('filterNodes', () => {
const nodes = getGroupedNodes(mockState.animals);
const searchValue = 'a';
const filteredNodes = filterNodes(nodes, searchValue);
const nodeList = ungroupNodes(filteredNodes);
const notMatchingNodeList = ungroupNodes(nodes).filter(
node => !node.name.includes(searchValue)
);

describe('nodes which match the search term', () => {
test.each(nodeList.map(node => node.name))(
`node name "%s" should contain search term "${searchValue}"`,
name => {
expect(name).toEqual(expect.stringMatching(searchValue));
}
);
});

describe('nodes which do not match the search term', () => {
test.each(notMatchingNodeList.map(node => node.id))(
`filtered node list should not contain a node with id "%s"`,
nodeID => {
expect(nodeList.map(node => node.id)).not.toContain(
expect.stringMatching(searchValue)
);
}
);
});
});
});
Loading

0 comments on commit a2c92e4

Please sign in to comment.