Skip to content

Commit c9d0a62

Browse files
Feature/initial (#1)
* Initial implementation * Add circleci configuration * Configure eslint and fix issues
1 parent ac4db99 commit c9d0a62

16 files changed

+1725
-5
lines changed

.babelrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"presets": [["es2015", {"modules": false}], "react", "stage-2"],
3+
"env": {
4+
"test": {
5+
"presets": [["es2015"], "react", "stage-2"]
6+
}
7+
}
8+
}

.circleci/config.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
version: 2
2+
jobs:
3+
build:
4+
docker:
5+
- image: circleci/node:8
6+
7+
working_directory: ~/repo
8+
9+
steps:
10+
- checkout
11+
12+
# Versions Check
13+
- run:
14+
name: Node version
15+
command: node -v
16+
- run:
17+
name: Yarn version
18+
command: yarn --version
19+
20+
- restore_cache:
21+
keys:
22+
- v1-dependencies-{{ checksum "package.json" }}
23+
- v1-dependencies-
24+
25+
- run:
26+
name: Install dependencies
27+
command: yarn install
28+
29+
- run:
30+
name: Run lint
31+
command: yarn lint
32+
33+
- run:
34+
name: Run tests
35+
command: yarn test
36+
37+
- save_cache:
38+
paths:
39+
- node_modules
40+
key: v1-dependencies-{{ checksum "package.json" }}

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
vendor

.eslintrc.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const OFF = 0;
4+
const ERROR = 2;
5+
6+
module.exports = {
7+
extends: 'fbjs',
8+
9+
// Stop ESLint from looking for a configuration file in parent folders
10+
root: true,
11+
12+
plugins: [
13+
'flowtype',
14+
'react',
15+
],
16+
17+
// We're stricter than the default config, mostly. We'll override a few rules
18+
// and then enable some React specific ones.
19+
rules: {
20+
'accessor-pairs': OFF,
21+
'brace-style': [ERROR, '1tbs'],
22+
'comma-dangle': [ERROR, 'always-multiline'],
23+
'consistent-return': OFF,
24+
'dot-location': [ERROR, 'property'],
25+
'dot-notation': ERROR,
26+
'eol-last': ERROR,
27+
'eqeqeq': [ERROR, 'allow-null'],
28+
'indent': OFF,
29+
'jsx-quotes': [ERROR, 'prefer-double'],
30+
'keyword-spacing': [ERROR, {after: true, before: true}],
31+
'no-bitwise': OFF,
32+
'no-inner-declarations': [ERROR, 'functions'],
33+
'no-multi-spaces': ERROR,
34+
'no-restricted-syntax': [ERROR, 'WithStatement'],
35+
'no-shadow': ERROR,
36+
'no-unused-expressions': ERROR,
37+
'no-unused-vars': [ERROR, {args: 'none'}],
38+
'no-useless-concat': OFF,
39+
'quotes': [ERROR, 'single', {avoidEscape: true, allowTemplateLiterals: true }],
40+
'space-before-blocks': ERROR,
41+
'space-before-function-paren': OFF,
42+
43+
// React & JSX
44+
// Our transforms set this automatically
45+
'react/jsx-boolean-value': [ERROR, 'always'],
46+
'react/jsx-no-undef': ERROR,
47+
// We don't care to do this
48+
'react/jsx-sort-prop-types': OFF,
49+
'react/jsx-tag-spacing': ERROR,
50+
'react/jsx-uses-react': ERROR,
51+
'react/no-is-mounted': OFF,
52+
// This isn't useful in our test code
53+
'react/react-in-jsx-scope': ERROR,
54+
'react/self-closing-comp': ERROR,
55+
// We don't care to do this
56+
'react/jsx-wrap-multilines': [ERROR, {declaration: false, assignment: false}]
57+
},
58+
59+
globals: {},
60+
};

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.log
33
*.map
44

5+
dist
56
node_modules
67
package-lock.json
78
yarn.lock

README.md

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SpatialNavigation from '../src/spatial-navigation';
2+
3+
describe('SpatialNavigation', () => {
4+
describe('initialize', () => {
5+
let setStateSpy;
6+
7+
beforeEach(() => {
8+
setStateSpy = jest.fn();
9+
SpatialNavigation.init(setStateSpy);
10+
});
11+
12+
it('listens to sn:focused event', () => {
13+
const event = new CustomEvent('sn:focused', {
14+
detail: { sectionId: 'focusPath' },
15+
});
16+
document.dispatchEvent(event);
17+
18+
expect(setStateSpy).toHaveBeenCalled();
19+
});
20+
21+
describe('when focusing the same focused element', () => {
22+
beforeEach(() => {
23+
SpatialNavigation.focused = 'focusPath';
24+
});
25+
26+
it('does nothing', () => {
27+
const event = new CustomEvent('sn:focused', {
28+
detail: { sectionId: 'focusPath' },
29+
});
30+
document.dispatchEvent(event);
31+
32+
expect(setStateSpy).not.toHaveBeenCalled();
33+
});
34+
});
35+
});
36+
37+
describe('destroy', () => {
38+
39+
});
40+
});

__tests__/with-focusable-test.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
3+
import Enzyme, { mount } from 'enzyme';
4+
import Adapter from 'enzyme-adapter-react-16';
5+
Enzyme.configure({ adapter: new Adapter() });
6+
7+
import SpatialNavigation from '../src/spatial-navigation';
8+
import withFocusable from '../src/with-focusable';
9+
10+
describe('withFocusable', () => {
11+
const Component = () => <div />;
12+
const renderComponent = ({
13+
focusPath,
14+
currentFocusPath,
15+
setFocus = jest.fn(),
16+
}) => {
17+
const EnhancedComponent = withFocusable({ focusPath })(Component);
18+
return mount(
19+
<EnhancedComponent />,
20+
{ context: { currentFocusPath, setFocus } }
21+
);
22+
};
23+
24+
let component;
25+
26+
it('injects focusPath as prop', () => {
27+
component = renderComponent({ focusPath: 'focusPath' });
28+
expect(component.find(Component).prop('focusPath')).toEqual('focusPath');
29+
});
30+
31+
describe('when element focusPath is the same as currentFocusPath', () => {
32+
it('injects focused prop as true', () => {
33+
const focusPath = 'focusPath1';
34+
component = renderComponent({ currentFocusPath: focusPath, focusPath });
35+
36+
expect(component.find(Component).prop('focused')).toBe(true);
37+
});
38+
});
39+
40+
describe('when element focusPath is different than currentFocusPath', () => {
41+
it('injects focused prop as true', () => {
42+
component = renderComponent({
43+
currentFocusPath: 'focusPath1',
44+
focusPath: 'focusPath2',
45+
});
46+
47+
expect(component.find(Component).prop('focused')).toBe(false);
48+
});
49+
});
50+
51+
describe('about setFocus injected prop', () => {
52+
it('injects function to children', () => {
53+
component = renderComponent({ focusPath: 'focusPath' });
54+
expect(component.find(Component).prop('setFocus')).not.toBeFalsy();
55+
});
56+
57+
it('binds configured focusPath as first parameter', () => {
58+
const setFocusSpy = jest.fn();
59+
component = renderComponent({
60+
focusPath: 'focusPath',
61+
setFocus: setFocusSpy,
62+
});
63+
64+
const setFocus = component.find(Component).prop('setFocus');
65+
setFocus();
66+
67+
expect(setFocusSpy).toHaveBeenCalledWith('focusPath');
68+
});
69+
70+
it('sends setFocus parameter as second parameter', () => {
71+
const setFocusSpy = jest.fn();
72+
component = renderComponent({
73+
focusPath: 'focusPath',
74+
setFocus: setFocusSpy,
75+
});
76+
77+
const setFocus = component.find(Component).prop('setFocus');
78+
setFocus('otherFocusPath');
79+
80+
expect(setFocusSpy).toHaveBeenCalledWith('focusPath', 'otherFocusPath');
81+
});
82+
});
83+
84+
describe('lifecycle', () => {
85+
beforeEach(() => {
86+
spyOn(SpatialNavigation, 'addFocusable');
87+
spyOn(SpatialNavigation, 'removeFocusable');
88+
});
89+
90+
it('adds focusable after component mounts', () => {
91+
component = renderComponent({ focusPath: 'focusPath' });
92+
expect(SpatialNavigation.addFocusable).toHaveBeenCalledWith('focusPath');
93+
});
94+
95+
it('removes focusable after component unmounts', () => {
96+
component = renderComponent({ focusPath: 'focusPath' });
97+
98+
component.unmount();
99+
expect(SpatialNavigation.removeFocusable)
100+
.toHaveBeenCalledWith('focusPath');
101+
});
102+
});
103+
});

__tests__/with-navigation-test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
3+
import Enzyme, { mount } from 'enzyme';
4+
import Adapter from 'enzyme-adapter-react-16';
5+
Enzyme.configure({ adapter: new Adapter() });
6+
7+
import SpatialNavigation from '../src/spatial-navigation';
8+
import withNavigation from '../src/with-navigation';
9+
10+
describe('withNavigation', () => {
11+
const Component = () => <div />;
12+
const EnhancedComponent = withNavigation(Component);
13+
const renderComponent = () => mount(<EnhancedComponent />);
14+
15+
let component;
16+
17+
describe('#setFocus', () => {
18+
beforeEach(() => {
19+
spyOn(SpatialNavigation, 'setCurrentFocusedPath').and.callThrough();
20+
component = renderComponent();
21+
});
22+
23+
describe('for the same focusPath', () => {
24+
it('does nothing', () => {
25+
component.setState({ currentFocusPath: 'focusPath' });
26+
component.children().props().setFocus('focusPath');
27+
expect(SpatialNavigation.setCurrentFocusedPath).not.toHaveBeenCalled();
28+
});
29+
});
30+
31+
describe('for a different focusPath', () => {
32+
it('updates navigation currentFocusPath', () => {
33+
component.setState({ currentFocusPath: 'focusPath' });
34+
component.children().props().setFocus('anotherFocusPath');
35+
expect(SpatialNavigation.getCurrentFocusedPath())
36+
.toEqual('anotherFocusPath');
37+
});
38+
39+
it('updates currentFocusPath state', () => {
40+
component.setState({ currentFocusPath: 'focusPath' });
41+
component.children().props().setFocus('anotherFocusPath');
42+
expect(component.state().currentFocusPath).toEqual('anotherFocusPath');
43+
});
44+
});
45+
});
46+
47+
describe('lifecycle', () => {
48+
beforeEach(() => {
49+
spyOn(SpatialNavigation, 'init');
50+
spyOn(SpatialNavigation, 'destroy');
51+
});
52+
53+
it('initializes after component mounts', () => {
54+
component = renderComponent();
55+
expect(SpatialNavigation.init).toHaveBeenCalled();
56+
});
57+
58+
it('initializes after component updates', () => {
59+
component = renderComponent();
60+
61+
SpatialNavigation.init.calls.reset();
62+
expect(SpatialNavigation.init).not.toHaveBeenCalled();
63+
component.setProps({ prop: 'test' });
64+
expect(SpatialNavigation.init).toHaveBeenCalled();
65+
});
66+
67+
it('destroys after component unmounts', () => {
68+
component = renderComponent();
69+
70+
component.unmount();
71+
expect(SpatialNavigation.destroy).toHaveBeenCalled();
72+
});
73+
});
74+
});

0 commit comments

Comments
 (0)