Skip to content

Commit

Permalink
Added unit tests for injectHookVariableNames
Browse files Browse the repository at this point in the history
  • Loading branch information
vibhorgupta-gh committed Dec 15, 2020
1 parent ff7b6c3 commit 8efb506
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import {
checkNodeLocation,
isConfirmedHookDeclaration,
getFilteredHookASTNodes,
filterMemberWithHookVariableName,
} from 'react-devtools-shared/src/utils';

describe('injectHookVariableNamesFunction', () => {

it('should identify variable names in destructed syntax', async done => {
const jsxCode = `
const componentSnippet = `
const Example = () => {
const [count, setCount] = React.useState(1);
return count;
};
`;

const ast = parse(jsxCode, {
const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
Expand All @@ -35,14 +37,14 @@ describe('injectHookVariableNamesFunction', () => {
});

it('should identify variable names in direct assignment', async done => {
const jsxCode = `
const componentSnippet = `
const Example = () => {
const count = React.useState(1);
return count;
};
`;

const ast = parse(jsxCode, {
const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
Expand All @@ -57,19 +59,21 @@ describe('injectHookVariableNamesFunction', () => {
});

it('should identify variable names in case of destructured assignment', async done => {
const jsxCode = `
const componentSnippet = `
const Example = () => {
const count = React.useState(1);
const [x, setX] = count;
return count;
const countState = React.useState(1);
const [count, setCount] = countState;
return countState;
};
`;

const ast = parse(jsxCode, {
const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
// hookAstNodes captures lines of interest: 3 & 4
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(2);
// This line number corresponds to where the hook is present
const lineNumber = 3;

Expand All @@ -78,7 +82,7 @@ describe('injectHookVariableNamesFunction', () => {
node =>
checkNodeLocation(node, lineNumber) && isConfirmedHookDeclaration(node),
);
// Find the nodes that are associated with the React Hook found - in this case we obtain the [x, setX] line
// Find the nodes that are associated with the React Hook found - in this case we obtain the [count, setCount] line
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hookAstNodes,
Expand All @@ -90,24 +94,242 @@ describe('injectHookVariableNamesFunction', () => {
expect(nodesAssociatedWithReactHookASTNode).toHaveLength(1);
const relatedNode = nodesAssociatedWithReactHookASTNode[0];

// The [x,setX] destructuring is on line 4
// The [count, setCount] destructuring is on line 4
expect(relatedNode.node.loc.start.line).toBe(4);

const hookName = getHookVariableName(relatedNode);
expect(hookName).toBe('x');
expect(hookName).toBe('count');
done();
});

it('should identify variable names for multiple hooks in one app', async done => {
const jsxCode = `
const Example = () => {
const count = React.useState(1);
const [x, setX] = count;
const [count1, setCount1] = useState(0);
return count;
it('should identify variable names in case of assignment from object members', async done => {
const componentSnippet = `
const Example = () => {
const countState = useState(1);
const count = countState[0];
const setCount = countState[1];
return countState;
};
`;

const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
// hookAstNodes captures lines of interest: 3, 4 & 5
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(3);
// This line number corresponds to where the hook is present
const lineNumber = 3;

// Isolate the Hook AST Node
const potentialReactHookASTNode = hookAstNodes.find(
node =>
checkNodeLocation(node, lineNumber) && isConfirmedHookDeclaration(node),
);
// Find the nodes that are associated with the React Hook found - in this case we obtain the lines
// -> const count = countState[0];
// -> const setCount = countState[1];
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hookAstNodes,
'example-app',
new Map(),
);

// Two nodes should be found here
expect(nodesAssociatedWithReactHookASTNode).toHaveLength(2);
const nodeAssociatedWithReactHookASTNode = nodesAssociatedWithReactHookASTNode.filter(
hookPath => filterMemberWithHookVariableName(hookPath)
);

// Node containing the variable name should be isolated here
expect(nodeAssociatedWithReactHookASTNode).toHaveLength(1);
const relatedNode = nodeAssociatedWithReactHookASTNode[0];

// The const count = countState[0] assignment is on line 4
expect(relatedNode.node.loc.start.line).toBe(4);

const hookName = getHookVariableName(relatedNode);
expect(hookName).toBe('count');
done();
})

it('should identify variable names in case of inline assignment from object members', async done => {
const componentSnippet = `
const Example = () => {
const countState = useState(1);
const count = countState[0], setCount = countState[1];
return countState;
};
`;

const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
// hookAstNodes captures lines of interest: 3 & 4
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(3);
// This line number corresponds to where the hook is present
const lineNumber = 3;

// Isolate the Hook AST Node
const potentialReactHookASTNode = hookAstNodes.find(
node =>
checkNodeLocation(node, lineNumber) && isConfirmedHookDeclaration(node),
);
// Find the nodes that are associated with the React Hook found - in this case we obtain the line
// -> const count = countState[0], setCount = countState[1];
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hookAstNodes,
'example-app',
new Map(),
);

// Two nodes should be found here
expect(nodesAssociatedWithReactHookASTNode).toHaveLength(2);
const nodeAssociatedWithReactHookASTNode = nodesAssociatedWithReactHookASTNode.filter(
hookPath => filterMemberWithHookVariableName(hookPath)
);

// Node containing the variable name should be isolated here
expect(nodeAssociatedWithReactHookASTNode).toHaveLength(1);
const relatedNode = nodeAssociatedWithReactHookASTNode[0];

// The const count = countState[0] assignment is on line 4
expect(relatedNode.node.loc.start.line).toBe(4);

const hookName = getHookVariableName(relatedNode);
expect(hookName).toBe('count');
done();
})

it('should default to original variable name in case of repeated references', async done => {
const componentSnippet = `
const Example = () => {
const countState = useState(1);
const count = countState[0];
const setCount = countState[1];
const [anotherCount, setAnotherCount] = countState;
return countState;
};
`;

const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
// hookAstNodes captures lines of interest: 3, 4, 5 & 6
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(4);
// This line number corresponds to where the hook is present
const lineNumber = 3;

// Isolate the Hook AST Node
const potentialReactHookASTNode = hookAstNodes.find(
node =>
checkNodeLocation(node, lineNumber) && isConfirmedHookDeclaration(node),
);
// Find the nodes that are associated with the React Hook found - in this case we obtain the lines
// -> const count = countState[0];
// -> const setCount = countState[1];
// -> const [anotherCount, setAnotherCount] = countState;
let nodesAssociatedWithReactHookASTNode = [];
nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hookAstNodes,
'example-app',
new Map(),
);

// Three nodes should be found here
expect(nodesAssociatedWithReactHookASTNode).toHaveLength(3);

// More than 2 nodes indicate there are multiple references of a hook assignment
// In such cases we default to the statement const countState = useState(1);
const hookName = getHookVariableName(potentialReactHookASTNode);
expect(hookName).toBe('countState');
done();
})

it('should default to original variable name in case of no found references', async done => {
const componentSnippet = `
const Example = () => {
const countState = useState(1);
return countState;
};
`;

const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
// hookAstNodes captures lines of interest: 3
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(1);

// Only one node of interest found
const hookName = getHookVariableName(hookAstNodes[0]);
expect(hookName).toBe('countState');
done();
})

it('should ignore non declarative primitive hooks', async done => {
const componentSnippet = `
const Example = (props, ref) => {
const [flag, toggleFlag] = useState(false);
const inputRef = useRef();
useDebugValue(flag ? 'Set' : 'Reset');
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
useEffect(() => {
toggleFlag(true);
}, []);
useLayoutEffect(() => {
console.log(flag)
}, []);
return <input ref={inputRef} />;
};
`;

const ast = parse(componentSnippet, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});

// hookAstNodes captures lines of interest: 3 & 4
// Should not capture any of the non declarative primitive hooks
const hookAstNodes = getPotentialHookDeclarationsFromAST(ast);
expect(hookAstNodes).toHaveLength(2);
done();
})

it('should identify variable names for multiple hooks in one app', async done => {
const componentSnippet = `
const Example = () => {
const countState = React.useState(() => 1);
const [count, setCount] = countState;
const [toggle, setToggle] = useState(false);
return [count, toggle];
};
`;
done();
});

it('should identify variable names for multiple hooks declared inline in one app', async done => {
const componentSnippet = `
const Example = () => {
const ref = React.useRef(null), [ticker, setTicker] = useState(() => 0);
return [ref, ticker];
};
`;
done();
});

// custom hook tests
});
4 changes: 2 additions & 2 deletions packages/react-devtools-shared/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,7 @@ function getHookNodeWithInjectedVariableName(originalHook: HooksNode, nodesAssoc
// const stateVariable = someState[0]
// const setStateVariable = someState[1]
//
// const [number2, setNumber2] = state
// const [number2, setNumber2] = someState
//
// We assign the state variable for 'someState' to multiple variables,
// and hence cannot isolate a unique variable name. In such cases,
Expand Down Expand Up @@ -1344,7 +1344,7 @@ function injectSubHooksWithVariableNames(hook: HooksTree, sourceMaps: Downloaded
* @param {NodePath} hook The AST Node Path for the concerned hook
* @return {boolean}
*/
function filterMemberWithHookVariableName(hook: NodePath): boolean {
export function filterMemberWithHookVariableName(hook: NodePath): boolean {
return hook.node.init.property.type === AST_NODE_TYPES.NUMERIC_LITERAL &&
hook.node.init.property.value === 0;
}
Expand Down

0 comments on commit 8efb506

Please sign in to comment.