From c47a4dddbde78e7a15d9e7570a16d82a032c52be Mon Sep 17 00:00:00 2001 From: bendtherules Date: Fri, 20 Sep 2019 11:27:55 +0530 Subject: [PATCH] match - Add css match functions with basic test cases --- src/index.ts | 4 ++ src/match.ts | 93 +++++++++++++++++++++++----- test/matchGenerator-basic.spec.tsx | 98 ++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 test/matchGenerator-basic.spec.tsx diff --git a/src/index.ts b/src/index.ts index b26bb20..6e26bef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { findNodeByComponentRef, findNodeByComponentName } from "./findNode"; +import { matchGenerator, matchAll, matchFirst } from "./match"; import * as Utils from "./utils"; export { @@ -12,5 +13,8 @@ export { findNodeByComponent, findNodeByComponentRef, findNodeByComponentName, + matchGenerator, + matchAll, + matchFirst, Utils }; diff --git a/src/match.ts b/src/match.ts index 6b4d576..08fd867 100644 --- a/src/match.ts +++ b/src/match.ts @@ -1,20 +1,22 @@ -import CSSwhat from "css-what"; +import * as CSSWhat from "css-what"; import { traverseGenerator } from "./traverse"; import { FiberNode } from "./mocked-types"; import { isNodeNotHtmlLike } from "./utils"; function* matchGenerator( node: FiberNode, - match: string | CSSwhat.Selector[][] + match: string | CSSWhat.Selector[][] ): IterableIterator { // Either parse match string or allow parsed match object as it is - let matchParsed: CSSwhat.Selector[][]; + let matchParsed: CSSWhat.Selector[][]; if (typeof match === "string") { - matchParsed = CSSwhat.parse(match); + matchParsed = CSSWhat.parse(match, { + lowerCaseTags: false, + lowerCaseAttributeNames: false + }); } else { matchParsed = match; } - // If selector is a combination of multiple basic selectors (a, b), // pass them separately to matchGenerator and combine their results one after another const selectorsCount = matchParsed.length; @@ -35,20 +37,46 @@ function* matchGenerator( let currentMatchingNodes: FiberNode[] = [node]; let currentMatchingSelectorPartIndex = 0; - let currentMatchingSelectorPart: CSSwhat.Selector = - parsedSelector[currentMatchingSelectorPartIndex]; - let lastRelationshipSelectorPart: CSSwhat.Selector | undefined = undefined; + let lastRelationshipSelectorPart: CSSWhat.Selector | undefined = undefined; while (currentMatchingSelectorPartIndex < parsedSelector.length) { const nextMatchingNodes: FiberNode[] = []; for (const currentNode of currentMatchingNodes) { + const currentMatchingSelectorPart: CSSWhat.Selector = + parsedSelector[currentMatchingSelectorPartIndex]; if (["tag"].includes(currentMatchingSelectorPart.type)) { - const traverseIterator = traverseGenerator(currentNode); + const startParams = { + skipSelfForStartNode: true, + skipSiblingForStartNode: true + }; + + let nextParams: { + skipChild?: boolean; + skipSibling?: boolean; + } = {}; + if ( + lastRelationshipSelectorPart === undefined || + lastRelationshipSelectorPart.type === "descendant" + ) { + nextParams = { skipChild: false, skipSibling: false }; + } else if (lastRelationshipSelectorPart.type === "child") { + // visit only first level of child + nextParams = { skipChild: true }; + } else if (lastRelationshipSelectorPart.type === "sibling") { + // visit only siblings of start node + nextParams = { skipChild: true, skipSibling: true }; + startParams.skipSiblingForStartNode = false; + } + + const traverseIterator = traverseGenerator(currentNode, startParams); // Handle supported non-traversal parts here if (currentMatchingSelectorPart.type == "tag") { - for (const tmpNode of traverseIterator) { + let tmpNode: FiberNode; + while ( + !({ value: tmpNode } = traverseIterator.next(nextParams)).done + ) { if ( isNodeNotHtmlLike(tmpNode) && tmpNode.type.name === currentMatchingSelectorPart.name @@ -57,11 +85,15 @@ function* matchGenerator( } } } - traverseIterator.throw && - traverseIterator.throw(new Error("cleanup")); - } else if (["descendant"].includes(currentMatchingSelectorPart.type)) { - // Handle traversal parts here + // traverseIterator.throw && + // traverseIterator.throw(new Error("cleanup")); + } else if ( + ["descendant", "child"].includes(currentMatchingSelectorPart.type) + ) { + // Handle traversal parts here - Save for look back in next part lastRelationshipSelectorPart = currentMatchingSelectorPart; + // Preserve currentMatchingNodes + nextMatchingNodes.push(...currentMatchingNodes); } else { // For unhandled parts lastRelationshipSelectorPart = undefined; @@ -69,10 +101,41 @@ function* matchGenerator( } currentMatchingNodes = nextMatchingNodes; + currentMatchingSelectorPartIndex += 1; + } + + for (const tmpNode of currentMatchingNodes) { + yield tmpNode; } return; } } -export { matchGenerator }; +function matchAll( + node: FiberNode, + match: string | CSSWhat.Selector[][] +): Array { + return [...matchGenerator(node, match)]; +} + +function matchFirst( + node: FiberNode, + match: string | CSSWhat.Selector[][] +): FiberNode | null { + const matchIterator = matchGenerator(node, match); + const firstResult = matchIterator.next(); + + // Cancel generator + matchIterator.throw && matchIterator.throw(new Error("Cleanup")); + + // If match found, return that + if (!firstResult.done) { + return firstResult.value; + } + + // Else return null + return null; +} + +export { matchGenerator, matchAll, matchFirst }; diff --git a/test/matchGenerator-basic.spec.tsx b/test/matchGenerator-basic.spec.tsx new file mode 100644 index 0000000..e4ff808 --- /dev/null +++ b/test/matchGenerator-basic.spec.tsx @@ -0,0 +1,98 @@ +import * as React from "react"; + +// Import stuff from src +import { + // matchGenerator, + matchAll + // matchFirst +} from "../src"; + +import { + FiberNodeForComponentClass + // FiberNodeForInstrinsicElement, + // FiberNodeForFunctionComponent +} from "../src/mocked-types"; + +// Import test helpers and sample components +import { mountAndGetRootNode } from "./utils/mountInEnzyme"; +import { + // createClassComponents, + // createFunctionComponents, + createClassComponent, + createClassComponents +} from "./utils/createComponent"; + +describe("matchGenerator", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.body.appendChild(document.createElement("div")); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + describe("basic", () => { + it("should work for depth=1 class", () => { + const C1 = createClassComponent("C1"); + function CRoot() { + return ; + } + const rootNode = mountAndGetRootNode(CRoot, container); + + const nodes = matchAll(rootNode, "C1"); + + // Can't be zero, in any case + expect(nodes.length).not.toBe(0); + // Check that only one node is yielded + expect(nodes.length).toBe(1); + // Check that type is correct + expect((nodes[0] as FiberNodeForComponentClass).type).toBe(C1); + }); + + it("should work for depth=2 class", () => { + const [C1, C2] = createClassComponents(["C1", "C2"]); + function CRoot() { + return ( + + + + ); + } + const rootNode = mountAndGetRootNode(CRoot, container); + + const nodes = matchAll(rootNode, "C2"); + + // Can't be zero, in any case + expect(nodes.length).not.toBe(0); + // Check that only one node is yielded + expect(nodes.length).toBe(1); + // Check that type is correct + expect((nodes[0] as FiberNodeForComponentClass).type).toBe(C2); + expect((nodes[0] as FiberNodeForComponentClass).type).not.toBe(C1); + }); + + it("should work for depth=2 class with descendant selector", () => { + const [C1, C2] = createClassComponents(["C1", "C2"]); + function CRoot() { + return ( + + + + ); + } + const rootNode = mountAndGetRootNode(CRoot, container); + + const nodes = matchAll(rootNode, "C1 C2"); + + // Can't be zero, in any case + expect(nodes.length).not.toBe(0); + // Check that only one node is yielded + expect(nodes.length).toBe(1); + // Check that type is correct + expect((nodes[0] as FiberNodeForComponentClass).type).toBe(C2); + expect((nodes[0] as FiberNodeForComponentClass).type).not.toBe(C1); + }); + }); +});