-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[KED-1611] Refactor node-list (#152)
Improve the code for the node-list components by separating them into different files, and add more/better tests
- Loading branch information
1 parent
fcdb9e4
commit a2c92e4
Showing
15 changed files
with
697 additions
and
492 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.