Tests for searchFolderTree part I.

folders
Rick Carlino 2019-12-11 16:44:54 -06:00
parent eeb199b816
commit 32bbe8ad96
6 changed files with 176 additions and 140 deletions

View File

@ -4,27 +4,28 @@ import { collapseAll } from "../actions";
import { sample } from "lodash";
import { cloneAndClimb, climb } from "../climb";
// Folder structure used in tests:
// ├─ One
// ├─ Two
// │ └─ Three
// ├─ Four
// │ └─ Five
// ├─ Six
// │ └─ Seven
// │ ├─ Eight
// │ └─ Nine
// ├─ Ten
// │ ├─ Eleven
// │ └─ Twelve
// │ └─ Thirteen
// └─ Fourteen
// ├─ Fifteen
// └─ Sixteen
// ├─ Seventeen
// └─ Eighteen
const FOLDERS: FolderNode[] = [
/** A set of fake Folder resources used exclusively for testing purposes.
```
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
``` */
const TEST_FOLDERS: FolderNode[] = [
{ id: 1, parent_id: undefined, color: "blue", name: "One" },
{ id: 2, parent_id: undefined, color: "blue", name: "Two" },
{ id: 3, parent_id: 2, color: "blue", name: "Three" },
@ -45,8 +46,30 @@ const FOLDERS: FolderNode[] = [
{ id: 18, parent_id: 16, color: "blue", name: "Eighteen" }
];
const GRAPH = ingest({
folders: FOLDERS,
/**
```
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Eleven
Twelve
Thirteen
Fourteen
Fifteen
Sixteen
Seventeen
Eighteen
```
*/
export const TEST_GRAPH = ingest({
folders: TEST_FOLDERS,
localMetaAttributes: {}
});
@ -55,7 +78,7 @@ describe("deletion of folders", () => {
});
describe("expand/collapse all", () => {
const halfOpen = cloneAndClimb(GRAPH, (node) => {
const halfOpen = cloneAndClimb(TEST_GRAPH, (node) => {
node.open = !sample([true, false]);
});

View File

@ -0,0 +1,17 @@
import { TEST_GRAPH } from "./actions_test";
import { searchFolderTree } from "../search_folder_tree";
describe("searchFolderTree", () => {
it("Reutrns an empty result set when no match is found.", () => {
const before = JSON.stringify(TEST_GRAPH);
const results = searchFolderTree({
references: {},
input: "foo",
root: TEST_GRAPH
});
const after = JSON.stringify(TEST_GRAPH);
expect(results).toBeTruthy();
expect(results.length).toEqual(0);
expect(before).toEqual(after); // Prevent mutation of original data.
});
});

View File

@ -4,13 +4,8 @@ import {
FolderNodeTerminal,
RootFolderNode,
FolderMeta,
FolderUnion,
} from "./constants";
import { sortBy } from "lodash";
import {
TaggedResource,
TaggedSequence
} from "farmbot/dist/resources/tagged_resource";
type FoldersIndexedByParentId = Record<number, FolderNode[]>;
@ -82,109 +77,3 @@ export const ingest: IngestFn = ({ folders, localMetaAttributes }) => {
return output;
};
interface FolderSearchProps {
references: Record<string, TaggedResource | undefined>;
input: string;
root: RootFolderNode;
}
const isSearchMatchSeq =
(searchTerm: string, s?: TaggedResource): s is TaggedSequence => {
if (s && s.kind == "Sequence") {
const name = s.body.name.toLowerCase();
return name.includes(searchTerm);
} else {
return false;
}
};
const isSearchMatchFolder = (searchTerm: string, f: FolderUnion) => {
if (f.name.toLowerCase().includes(searchTerm)) {
return true;
}
return false;
};
/** Given an input search term, returns folder IDs (number) and Sequence UUIDs
* that match */
export const searchFoldersAndSequencesForTerm = (props: FolderSearchProps) => {
// A sequence is included if:
// * CASE 1: The name is a search match
// * CASE 2: The containing folder is a search match.
// A folder is included if:
// * CASE 3: The name is a search match
// * CASE 4: It contains a sequence that is a match.
// * CASE 5: It has a child that has a search match.
const searchTerm = props.input.toLowerCase();
const sequenceSet = new Set<string>();
const folderSet = new Set<FolderUnion>();
props.root.folders.map(level1 => {
level1.content.map(level1Sequence => { // ========= Level 1
if (isSearchMatchSeq(searchTerm, props.references[level1Sequence])) {
// CASE 1:
sequenceSet.add(level1Sequence);
// CASE 4:
folderSet.add(level1);
}
});
if (isSearchMatchFolder(searchTerm, level1)) {
// CASE 2
level1.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level1);
}
level1.children.map(level2 => { // ================ LEVEL 2
if (isSearchMatchFolder(searchTerm, level2)) {
// CASE 2
level2.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
level2.content.map(level2Sequence => {
if (isSearchMatchSeq(searchTerm, props.references[level2Sequence])) {
// CASE 1:
sequenceSet.add(level2Sequence);
// CASE 4:
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
});
level2.children.map(level3 => { // ============== LEVEL 3
if (isSearchMatchFolder(searchTerm, level3)) {
// CASE 2
level3.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level3);
// CASE 5
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
level3.content.map(level3Sequence => {
if (isSearchMatchSeq(searchTerm, props.references[level3Sequence])) {
// CASE 1:
sequenceSet.add(level3Sequence);
// CASE 3
folderSet.add(level3);
// CASE 5
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
});
});
});
});
return Array.from(folderSet);
};

View File

@ -0,0 +1,109 @@
import { TaggedResource, TaggedSequence } from "farmbot";
import { RootFolderNode, FolderUnion } from "./constants";
interface FolderSearchProps {
references: Record<string, TaggedResource | undefined>;
input: string;
root: RootFolderNode;
}
const isSearchMatchSeq =
(searchTerm: string, s?: TaggedResource): s is TaggedSequence => {
if (s && s.kind == "Sequence") {
const name = s.body.name.toLowerCase();
return name.includes(searchTerm);
} else {
return false;
}
};
const isSearchMatchFolder = (searchTerm: string, f: FolderUnion) => {
if (f.name.toLowerCase().includes(searchTerm)) {
return true;
}
return false;
};
/** Given an input search term, returns folder IDs (number) and Sequence UUIDs
* that match */
export const searchFolderTree = (props: FolderSearchProps): FolderUnion[] => {
// A sequence is included if:
// * CASE 1: The name is a search match
// * CASE 2: The containing folder is a search match.
// A folder is included if:
// * CASE 3: The name is a search match
// * CASE 4: It contains a sequence that is a match.
// * CASE 5: It has a child that has a search match.
const searchTerm = props.input.toLowerCase();
const sequenceSet = new Set<string>();
const folderSet = new Set<FolderUnion>();
props.root.folders.map(level1 => {
level1.content.map(level1Sequence => { // ========= Level 1
if (isSearchMatchSeq(searchTerm, props.references[level1Sequence])) {
// CASE 1:
sequenceSet.add(level1Sequence);
// CASE 4:
folderSet.add(level1);
}
});
if (isSearchMatchFolder(searchTerm, level1)) {
// CASE 2
level1.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level1);
}
level1.children.map(level2 => { // ================ LEVEL 2
if (isSearchMatchFolder(searchTerm, level2)) {
// CASE 2
level2.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
level2.content.map(level2Sequence => {
if (isSearchMatchSeq(searchTerm, props.references[level2Sequence])) {
// CASE 1:
sequenceSet.add(level2Sequence);
// CASE 4:
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
});
level2.children.map(level3 => { // ============== LEVEL 3
if (isSearchMatchFolder(searchTerm, level3)) {
// CASE 2
level3.content.map(uuid => sequenceSet.add(uuid));
// CASE 3
folderSet.add(level3);
// CASE 5
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
level3.content.map(level3Sequence => {
if (isSearchMatchSeq(searchTerm, props.references[level3Sequence])) {
// CASE 1:
sequenceSet.add(level3Sequence);
// CASE 3
folderSet.add(level3);
// CASE 5
folderSet.add(level2);
// CASE 5
folderSet.add(level1);
}
});
});
});
});
return Array.from(folderSet);
};

View File

@ -24,10 +24,8 @@ import { farmwareState } from "../farmware/reducer";
import { initialState as regimenState } from "../regimens/reducer";
import { initialState as sequenceState } from "../sequences/reducer";
import { initialState as alertState } from "../messages/reducer";
import {
ingest,
searchFoldersAndSequencesForTerm
} from "../folders/data_transfer";
import { ingest } from "../folders/data_transfer";
import { searchFolderTree } from "../folders/search_folder_tree";
export const emptyState = (): RestResources => {
return {
@ -190,7 +188,7 @@ export const resourceReducer =
.add<string | undefined>(Actions.FOLDER_SEARCH, (s, { payload }) => {
s.index.sequenceFolders.searchTerm = payload;
if (payload) {
const folders = searchFoldersAndSequencesForTerm({
const folders = searchFolderTree({
references: s.index.references,
input: payload,
root: s.index.sequenceFolders.folders