From eed3dc15fc50acf8c587d5be586d2cc1af3d3a24 Mon Sep 17 00:00:00 2001 From: johno Date: Wed, 2 Oct 2019 12:33:39 -0500 Subject: [PATCH 1/9] move example --- .../mobile-first-navigation-example/README.md | 11 +++ .../mobile-first-navigation-example.test.js | 7 ++ .../package.json | 54 ++++++++++++ .../mobile-first-navigation-example/server.js | 32 +++++++ .../src/adapters/queryStringAdapter.ts | 83 +++++++++++++++++++ .../src/examples/App.tsx | 0 .../src/examples/Routes.tsx | 2 +- .../src/examples/index.tsx | 0 .../src/examples/routes/Home.tsx | 0 .../src/examples/routes/Modal.tsx | 0 .../src/examples/routes/Page2.tsx | 0 .../src/examples/routes/Page2b.tsx | 0 .../src/examples/routes/Page3.tsx | 0 .../src/examples/store.ts | 2 +- .../tsconfig.json | 43 ++++++++++ .../wallaby.js | 49 +++++++++++ .../webpack.config.js | 27 ++++++ packages/mobile-first-router/package.json | 2 +- 18 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 packages/mobile-first-navigation-example/README.md create mode 100644 packages/mobile-first-navigation-example/__tests__/mobile-first-navigation-example.test.js create mode 100644 packages/mobile-first-navigation-example/package.json create mode 100644 packages/mobile-first-navigation-example/server.js create mode 100644 packages/mobile-first-navigation-example/src/adapters/queryStringAdapter.ts rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/App.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/Routes.tsx (97%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/index.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/routes/Home.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/routes/Modal.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/routes/Page2.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/routes/Page2b.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/routes/Page3.tsx (100%) rename packages/{mobile-first-router => mobile-first-navigation-example}/src/examples/store.ts (87%) create mode 100644 packages/mobile-first-navigation-example/tsconfig.json create mode 100644 packages/mobile-first-navigation-example/wallaby.js create mode 100644 packages/mobile-first-navigation-example/webpack.config.js diff --git a/packages/mobile-first-navigation-example/README.md b/packages/mobile-first-navigation-example/README.md new file mode 100644 index 0000000..4f233b8 --- /dev/null +++ b/packages/mobile-first-navigation-example/README.md @@ -0,0 +1,11 @@ +# `mobile-first-navigation-example` + +> TODO: description + +## Usage + +``` +const mobileFirstNavigationExample = require('mobile-first-navigation-example'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/mobile-first-navigation-example/__tests__/mobile-first-navigation-example.test.js b/packages/mobile-first-navigation-example/__tests__/mobile-first-navigation-example.test.js new file mode 100644 index 0000000..514f3ea --- /dev/null +++ b/packages/mobile-first-navigation-example/__tests__/mobile-first-navigation-example.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const mobileFirstNavigationExample = require('..'); + +describe('mobile-first-navigation-example', () => { + it('needs tests'); +}); diff --git a/packages/mobile-first-navigation-example/package.json b/packages/mobile-first-navigation-example/package.json new file mode 100644 index 0000000..cfbb3d3 --- /dev/null +++ b/packages/mobile-first-navigation-example/package.json @@ -0,0 +1,54 @@ +{ + "name": "mobile-first-navigation-example", + "version": "0.0.0", + "description": "> TODO: description", + "author": "johno ", + "homepage": "https://github.com/Aloompa/mobile-first-navigation#readme", + "license": "ISC", + "main": "lib/mobile-first-navigation-example.js", + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1", + "build": "node_modules/.bin/tsc", + "watch": "node_modules/.bin/tsc --watch", + "release": "npm publish", + "server": "node ./server" + }, + "dependencies": { + "@aloompa/mobile-first-components": "0.0.1", + "@aloompa/mobile-first-router": "0.0.1" + }, + "devDependencies": { + "express": "^4.16.4", + "ramda": "^0.25.0", + "react": "16.9.0", + "react-dom": "16.9.0", + "react-native": "0.61.1", + "react-native-web": "0.11.7", + "react-redux": "^5.0.7", + "redux": "^4.0.0", + "redux-actions": "^2.4.0", + "styled-components": "^3.3.3", + "ts-loader": "^4.4.2", + "typescript": "^2.9.2", + "webpack": "^4.16.1", + "webpack-dev-middleware": "^3.1.3", + "webpack-hot-middleware": "^2.22.3" + }, + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "publishConfig": { + "registry": "http://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Aloompa/mobile-first-navigation.git" + }, + "bugs": { + "url": "https://github.com/Aloompa/mobile-first-navigation/issues" + } +} diff --git a/packages/mobile-first-navigation-example/server.js b/packages/mobile-first-navigation-example/server.js new file mode 100644 index 0000000..68ca408 --- /dev/null +++ b/packages/mobile-first-navigation-example/server.js @@ -0,0 +1,32 @@ +const path = require('path'); +const express = require('express'); +const webpack = require('webpack'); +const config = require('./webpack.config'); + +const host = process.env.HOST || 'localhost'; + +const app = express(); +const compiler = webpack(config); +const port = process.env.PORT || 3001; + +app.use( + require('webpack-dev-middleware')(compiler, { + noInfo: true, + publicPath: config.output.publicPath + }) +); + +app.use(require('webpack-hot-middleware')(compiler)); + +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +app.listen(port, host, (err) => { + if (err) { + console.log(err); + return; + } + + console.log(`Listening at http://${host}:${port}`); +}); diff --git a/packages/mobile-first-navigation-example/src/adapters/queryStringAdapter.ts b/packages/mobile-first-navigation-example/src/adapters/queryStringAdapter.ts new file mode 100644 index 0000000..f81d877 --- /dev/null +++ b/packages/mobile-first-navigation-example/src/adapters/queryStringAdapter.ts @@ -0,0 +1,83 @@ +import { compose, fromPairs, map, path, split, tail } from 'ramda'; + +declare let global: any; + +const getQueryString = compose( + fromPairs, + map(split('=')), + split('&'), + tail +); + +const getUrlState = ({ route, params }) => { + const queryString = { + ...getQueryString(path(['document', 'location', 'search'], global)), + route, + params: urlencode(params) + }; + + if (queryString.route === 'Home') { + delete queryString.route; + } + + return Object.keys(queryString) + .filter((key) => queryString[key]) + .reduce((prev, key, i) => { + return `${prev}${i ? '&' : '?'}${key}=${queryString[key]}`; + }, `${window.location.origin}${window.location.pathname}`); +}; + +const urlencode = (params) => { + const url = btoa(JSON.stringify(params)); + + return url; +}; + +const setRoute = (action) => { + try { + window.history.replaceState(null, 'Router', getUrlState(action)); + } catch (_e) {} +}; + +const urldecode = (str) => { + try { + return atob(str); + } catch (_e) { + return str; + } +}; + +const getRoute = (initialRoute) => { + const queryString = getQueryString( + path(['document', 'location', 'search'], global) + ); + + if (!queryString.route || queryString.route === initialRoute) { + return []; + } + + const params = urldecode(queryString.params || '{}') + .replace(/%22/g, '"') + .replace(/%7B/g, '{') + .replace(/%7D/g, '}'); + + let queryParams = {}; + + try { + queryParams = JSON.parse(params); + } catch (_err) {} + + return [ + { + route: queryString.route, + params: queryParams + } + ]; +}; + +const queryStringAdapter = { + setRoute, + getRoute +}; + +export default queryStringAdapter; diff --git a/packages/mobile-first-router/src/examples/App.tsx b/packages/mobile-first-navigation-example/src/examples/App.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/App.tsx rename to packages/mobile-first-navigation-example/src/examples/App.tsx diff --git a/packages/mobile-first-router/src/examples/Routes.tsx b/packages/mobile-first-navigation-example/src/examples/Routes.tsx similarity index 97% rename from packages/mobile-first-router/src/examples/Routes.tsx rename to packages/mobile-first-navigation-example/src/examples/Routes.tsx index bfb7238..7536d09 100644 --- a/packages/mobile-first-router/src/examples/Routes.tsx +++ b/packages/mobile-first-navigation-example/src/examples/Routes.tsx @@ -5,7 +5,7 @@ import Page2 from './routes/Page2'; import Page2b from './routes/Page2b'; import Page3 from './routes/Page3'; import { always } from 'ramda'; -import { createRoutes } from '../index'; +import { createRoutes } from '@aloompa/mobile-first-router'; import { TabButton, TopNav } from '@aloompa/mobile-first-components'; const Routes = createRoutes({ diff --git a/packages/mobile-first-router/src/examples/index.tsx b/packages/mobile-first-navigation-example/src/examples/index.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/index.tsx rename to packages/mobile-first-navigation-example/src/examples/index.tsx diff --git a/packages/mobile-first-router/src/examples/routes/Home.tsx b/packages/mobile-first-navigation-example/src/examples/routes/Home.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/routes/Home.tsx rename to packages/mobile-first-navigation-example/src/examples/routes/Home.tsx diff --git a/packages/mobile-first-router/src/examples/routes/Modal.tsx b/packages/mobile-first-navigation-example/src/examples/routes/Modal.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/routes/Modal.tsx rename to packages/mobile-first-navigation-example/src/examples/routes/Modal.tsx diff --git a/packages/mobile-first-router/src/examples/routes/Page2.tsx b/packages/mobile-first-navigation-example/src/examples/routes/Page2.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/routes/Page2.tsx rename to packages/mobile-first-navigation-example/src/examples/routes/Page2.tsx diff --git a/packages/mobile-first-router/src/examples/routes/Page2b.tsx b/packages/mobile-first-navigation-example/src/examples/routes/Page2b.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/routes/Page2b.tsx rename to packages/mobile-first-navigation-example/src/examples/routes/Page2b.tsx diff --git a/packages/mobile-first-router/src/examples/routes/Page3.tsx b/packages/mobile-first-navigation-example/src/examples/routes/Page3.tsx similarity index 100% rename from packages/mobile-first-router/src/examples/routes/Page3.tsx rename to packages/mobile-first-navigation-example/src/examples/routes/Page3.tsx diff --git a/packages/mobile-first-router/src/examples/store.ts b/packages/mobile-first-navigation-example/src/examples/store.ts similarity index 87% rename from packages/mobile-first-router/src/examples/store.ts rename to packages/mobile-first-navigation-example/src/examples/store.ts index 7bec693..fd96e4f 100644 --- a/packages/mobile-first-router/src/examples/store.ts +++ b/packages/mobile-first-navigation-example/src/examples/store.ts @@ -1,7 +1,7 @@ import { combineReducers } from 'redux'; import { createStore } from 'redux'; import queryStringAdapter from '../adapters/queryStringAdapter'; -import { routerReducer } from '../index'; +import { routerReducer } from '@aloompa/mobile-first-router'; const combinedReducers = combineReducers({ router: routerReducer({ diff --git a/packages/mobile-first-navigation-example/tsconfig.json b/packages/mobile-first-navigation-example/tsconfig.json new file mode 100644 index 0000000..3442c90 --- /dev/null +++ b/packages/mobile-first-navigation-example/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "types": ["jest", "node"], + "jsx": "react", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "allowJs": true, + "allowSyntheticDefaultImports": false, + "alwaysStrict": true, + "experimentalDecorators": false, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "preserveConstEnums": false, + "pretty": true, + "outDir": "./lib", + "strictNullChecks": true, + "suppressExcessPropertyErrors": false, + "target": "es5", + "skipLibCheck": true, + "typeRoots": [ + "./node_modules/@types", + "./typings" + ], + "lib": [ + "dom", + "es2015", + "es5", + "es6" + ] + }, + "include": [ + "./src/**/*", + "!./src/**/__tests__/*", + "!./node_modules" + ] +} \ No newline at end of file diff --git a/packages/mobile-first-navigation-example/wallaby.js b/packages/mobile-first-navigation-example/wallaby.js new file mode 100644 index 0000000..98fc2cd --- /dev/null +++ b/packages/mobile-first-navigation-example/wallaby.js @@ -0,0 +1,49 @@ +module.exports = function(wallaby) { + const path = require('path'); + + process.env.NODE_PATH += + path.delimiter + path.join(__dirname, 'node_modules'); + + return { + hints: { + ignoreCoverage: /istanbul ignore next/ + }, + + files: [ + 'src/**/*.ts', + 'src/**/*.tsx', + '!src/**/__tests__/*.ts', + '!src/**/__tests__/*.tsx' + ], + + tests: ['src/**/__tests__/*.spec.ts', 'src/**/__tests__/*.spec.tsx'], + + env: { + type: 'node', + runner: 'node' + }, + + testFramework: 'jest', + setup: () => { + global.__DEV__ = true; + global.__TEST__ = true; + + global.window = global; + window.addEventListener = () => {}; + window.requestAnimationFrame = () => { + throw new Error('requestAnimationFrame is not supported in Node'); + }; + + wallaby.testFramework.configure({ + moduleNameMapper: { + '^react-native$': 'react-native-web' + } + }); + }, + + compilers: { + '**/*.ts': wallaby.compilers.typeScript(require('./tsconfig.json')), + '**/*.tsx': wallaby.compilers.typeScript(require('./tsconfig.json')) + } + }; +}; diff --git a/packages/mobile-first-navigation-example/webpack.config.js b/packages/mobile-first-navigation-example/webpack.config.js new file mode 100644 index 0000000..c3ef12f --- /dev/null +++ b/packages/mobile-first-navigation-example/webpack.config.js @@ -0,0 +1,27 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + mode: 'development', + entry: ['webpack-hot-middleware/client', './src/examples/index.tsx'], + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + alias: { + 'react-native': 'react-native-web' + } + }, + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist') + }, + plugins: [new webpack.HotModuleReplacementPlugin()] +}; diff --git a/packages/mobile-first-router/package.json b/packages/mobile-first-router/package.json index a2f56c6..d545fcd 100644 --- a/packages/mobile-first-router/package.json +++ b/packages/mobile-first-router/package.json @@ -1,6 +1,6 @@ { "name": "@aloompa/mobile-first-router", - "version": "0.0.0", + "version": "0.0.1", "description": "", "main": "index.js", "scripts": { From 391795ac9fee87a52ab03145352ad67edc35f524 Mon Sep 17 00:00:00 2001 From: johno Date: Wed, 2 Oct 2019 13:23:29 -0500 Subject: [PATCH 2/9] import router --- .../mobile-first-navigation-example/src/examples/Routes.tsx | 2 +- packages/mobile-first-navigation-example/src/examples/store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mobile-first-navigation-example/src/examples/Routes.tsx b/packages/mobile-first-navigation-example/src/examples/Routes.tsx index 7536d09..35d1e69 100644 --- a/packages/mobile-first-navigation-example/src/examples/Routes.tsx +++ b/packages/mobile-first-navigation-example/src/examples/Routes.tsx @@ -5,7 +5,7 @@ import Page2 from './routes/Page2'; import Page2b from './routes/Page2b'; import Page3 from './routes/Page3'; import { always } from 'ramda'; -import { createRoutes } from '@aloompa/mobile-first-router'; +import { createRoutes } from '@aloompa/mobile-first-router/lib'; import { TabButton, TopNav } from '@aloompa/mobile-first-components'; const Routes = createRoutes({ diff --git a/packages/mobile-first-navigation-example/src/examples/store.ts b/packages/mobile-first-navigation-example/src/examples/store.ts index fd96e4f..dad65e8 100644 --- a/packages/mobile-first-navigation-example/src/examples/store.ts +++ b/packages/mobile-first-navigation-example/src/examples/store.ts @@ -1,7 +1,7 @@ import { combineReducers } from 'redux'; import { createStore } from 'redux'; import queryStringAdapter from '../adapters/queryStringAdapter'; -import { routerReducer } from '@aloompa/mobile-first-router'; +import { routerReducer } from '@aloompa/mobile-first-router/lib'; const combinedReducers = combineReducers({ router: routerReducer({ From 98fd32f6c4e5112eec14f738989331b37c564489 Mon Sep 17 00:00:00 2001 From: John Ottenlips Date: Mon, 21 Oct 2019 08:55:30 -0500 Subject: [PATCH 3/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 140f044..a258fef 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # Mobile First Navigation Monorepo -A navigational router with an emphasis on being Mobile and Human friendly 📱 +A navigational router with an emphasis on being Mobile and Developer friendly 📱 From 7a86e16663d359b43b25683246a2814dbfc79b30 Mon Sep 17 00:00:00 2001 From: John Ottenlips Date: Mon, 21 Oct 2019 09:09:23 -0500 Subject: [PATCH 4/9] Update README.md --- README.md | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/README.md b/README.md index a258fef..1fec393 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,184 @@ # Mobile First Navigation Monorepo + + A navigational router with an emphasis on being Mobile and Developer friendly 📱 + +## Packages + +### Mobile First Router + +#### demo + +Coming soon... + +#### install + +`npm i @aloompa/mobile-first-router` + +#### configuration + +##### Standard + +```Routes.jsx +import Home from './routes/Home'; +import Modal from './routes/Modal'; +import Page2 from './routes/Page2'; +import Page2b from './routes/Page2b'; +import Page3 from './routes/Page3'; +import { always } from 'ramda'; +import { createRoutes } from '../index'; +import { TopNav } from '@aloompa/mobile-first-components'; + +export const routeConfig = { + initialRoute: 'Home', + routes: { + Home: { + route: Home, + getTitle: () => 'Home' + }, + Page2: { + route: Page2, + getTitle: () => 'Page 2' + }, + Page2b: { + route: Page2b, + getTitle: () => 'Page 2b' + }, + Page3: { + route: Page3, + getTitle: () => 'Page 3' + }, + Modal: { + route: Modal, + getTitle: () => 'Modal Page', + mode: 'modal' + } + }, + renderTopNav: TopNav +}; + +const Routes = createRoutes(routeConfig); + +export default Routes; +``` + +##### With Tabs + +```Routes.jsx +import * as React from 'react'; +import Home from './routes/Home'; +import Modal from './routes/Modal'; +import Page2 from './routes/Page2'; +import Page2b from './routes/Page2b'; +import Page3 from './routes/Page3'; +import { createRoutes } from '@aloompa/mobile-first-router'; +import { TabButton, TopNav } from '@aloompa/mobile-first-components'; + +export const routeConfig = { + initialActiveTab: 1, + initialRoute: 'Home', + tabs: [ + { + button: (isSelected: boolean, onPress: Function) => ( + + ), + initial: 'Home' + }, + { + button: (isSelected: boolean, onPress: Function) => ( + + ), + initial: 'Page2' + }, + { + button: (isSelected: boolean, onPress: Function) => ( + + ), + initial: 'Page3' + } + ], + routes: { + Home: { + route: Home, + getTitle: () => 'Home' + }, + Page2: { + route: Page2, + getTitle: () => 'Page 2' + }, + Page2b: { + route: Page2b, + getTitle: () => 'Page 2b' + }, + Page3: { + route: Page3, + getTitle: () => 'Page 3' + }, + Modal: { + route: Modal, + getTitle: () => 'Modal Page', + mode: 'modal' + } + }, + renderTopNav: TopNav +}; + +const Routes = createRoutes(routeConfig); + +export default Routes; +``` + +```App.jsx +import * as React from 'react'; + +import Routes from './Routes'; + +const App = () => ; + +export default App; +``` + +### Mobile First Components + +#### install + +`npm i @aloompa/mobile-first-components` + +### Mobile First Storybook + +#### live storybook demo and documentation + +Coming soon... From 9d32532f850f94d2f994ab9d185308834f2a5864 Mon Sep 17 00:00:00 2001 From: John Ottenlips Date: Mon, 21 Oct 2019 09:09:45 -0500 Subject: [PATCH 5/9] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1fec393..f39ea58 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ import Modal from './routes/Modal'; import Page2 from './routes/Page2'; import Page2b from './routes/Page2b'; import Page3 from './routes/Page3'; -import { always } from 'ramda'; import { createRoutes } from '../index'; import { TopNav } from '@aloompa/mobile-first-components'; From ffac22b511b1d9ddb0c4f298d14e4a2149d2adb0 Mon Sep 17 00:00:00 2001 From: Aloompa Date: Mon, 21 Oct 2019 09:10:49 -0500 Subject: [PATCH 6/9] Begin Tests Setup --- jest.config.js | 10 +++ packages/mobile-first-router/jest.config.js | 13 +--- .../mobile-first-router/package-lock.json | 15 ++++ packages/mobile-first-router/package.json | 1 + .../src/__tests__/routerReducer.spec.js | 43 +++++++++++ .../mobile-first-router/src/routerReducer.ts | 72 ++++++++++--------- yarn.lock | 12 ++++ 7 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 jest.config.js create mode 100644 packages/mobile-first-router/src/__tests__/routerReducer.spec.js diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5c91676 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + verbose: true, + moduleFileExtensions: ['ts', 'tsx', 'js'], + transform: { + '\\.(ts|tsx)$': '/node_modules/ts-jest/preprocessor.js' + }, + testRegex: '__tests__/.*\\.(ts|tsx)$', + collectCoverage: false, + projects: ['/packages/*/src'] +}; diff --git a/packages/mobile-first-router/jest.config.js b/packages/mobile-first-router/jest.config.js index 33f7164..c2bd932 100644 --- a/packages/mobile-first-router/jest.config.js +++ b/packages/mobile-first-router/jest.config.js @@ -1,16 +1,9 @@ -const path = require('path'); - -process.env.NODE_PATH += path.delimiter + path.join(__dirname, 'node_modules'); - module.exports = { - roots: ['src', 'util'], - - testEnvironment: 'jsdom', - - moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'], - + verbose: true, + moduleFileExtensions: ['ts', 'tsx', 'js'], transform: { '^.+\\.(ts|tsx)$': '/scripts/preprocessor.js' }, + collectCoverage: false, testMatch: ['**/__tests__/*.(ts|tsx|js)'] }; diff --git a/packages/mobile-first-router/package-lock.json b/packages/mobile-first-router/package-lock.json index 0d93bd5..6e4aca1 100644 --- a/packages/mobile-first-router/package-lock.json +++ b/packages/mobile-first-router/package-lock.json @@ -4366,6 +4366,12 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -5514,6 +5520,15 @@ "to-camel-case": "^1.0.0" } }, + "redux-mock-store": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz", + "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==", + "dev": true, + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, "regenerator-runtime": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", diff --git a/packages/mobile-first-router/package.json b/packages/mobile-first-router/package.json index 2ce3c33..edf86b6 100644 --- a/packages/mobile-first-router/package.json +++ b/packages/mobile-first-router/package.json @@ -38,6 +38,7 @@ "react-redux": "^5.0.7", "redux": "^4.0.0", "redux-actions": "^2.4.0", + "redux-mock-store": "1.5.3", "styled-components": "^3.3.3", "ts-loader": "^4.4.2", "typescript": "^2.9.2", diff --git a/packages/mobile-first-router/src/__tests__/routerReducer.spec.js b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js new file mode 100644 index 0000000..2c03b92 --- /dev/null +++ b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js @@ -0,0 +1,43 @@ +import routerReducer, { buildInitialState, setRoute } from '../routerReducer'; +import configureMockStore from 'redux-mock-store'; +const mockStore = configureMockStore([]); + +const routeConfig = { + initialActiveTab: 0, + initialRoute: 'BrowseHome', + tabs: [{}, {}, {}], + routes: { + BrowseHome: { + getTitle: () => 'Home' + }, + CategoryView: { + getTitle: () => 'Page2' + }, + ItemView: { + getTitle: () => 'Page3' + }, + ItemDetails: { + getTitle: () => 'Modal' + } + }, + renderTopNav: {}, + topNavHeight: 52 +}; + +describe('The router reducer', () => { + it('Should set route', () => { + const reducer = routerReducer(routeConfig); + const initialState = buildInitialState(routeConfig); + const store = mockStore(); + + setRoute.dispatch( + reducer({ + route: 'Page2' + }) + ); + + const state = reducer(initialState, store.getActions()[0]); + + expect(state.history[0].name).toBe('Page2'); + }); +}); diff --git a/packages/mobile-first-router/src/routerReducer.ts b/packages/mobile-first-router/src/routerReducer.ts index e97c18d..d58c8f1 100644 --- a/packages/mobile-first-router/src/routerReducer.ts +++ b/packages/mobile-first-router/src/routerReducer.ts @@ -48,38 +48,7 @@ const routerReducer: Function = (config: { getUrlState: Function; }; }) => { - const initialRoute: MFNavigationHistoryRoute = { - route: path(['routeConfig', 'initialRoute'], config) - }; - const tabs: Array = defaultTo( - [], - path(['routeConfig', 'tabs'], config) - ); - const tabRoutes: Array> = - tabs.length > 0 - ? tabs.map((tab: MFNavigationTab) => [{ route: tab.initial }]) - : [[initialRoute]]; - const queryInitialTab = getInitialTabQuery(config, initialRoute); - const activeTab = defaultTo( - 0, - queryInitialTab || path(['routeConfig', 'initialActiveTab'], config) - ); - - const history: Array = tabRoutes[activeTab]; - - const initialState = { - navbarHidden: false, - isNavigating: false, - destinations: [], - isNavigatingBack: false, - titleCache: {}, - routeToPop: '', - history, - poppedRoute: { route: '' }, - activeTab, - isModal: false, - tabRoutes - }; + const initialState = buildInitialState(config); return handleActions( { @@ -203,7 +172,42 @@ const routerReducer: Function = (config: { export default routerReducer; -function getInitialTabQuery(config, initialRoute) { +export const buildInitialState = (config) => { + const initialRoute: MFNavigationHistoryRoute = { + route: path(['routeConfig', 'initialRoute'], config) + }; + const tabs: Array = defaultTo( + [], + path(['routeConfig', 'tabs'], config) + ); + const tabRoutes: Array> = + tabs.length > 0 + ? tabs.map((tab: MFNavigationTab) => [{ route: tab.initial }]) + : [[initialRoute]]; + const queryInitialTab = getInitialTabQuery(config, initialRoute); + const activeTab = defaultTo( + 0, + queryInitialTab || path(['routeConfig', 'initialActiveTab'], config) + ); + + const history: Array = tabRoutes[activeTab]; + + return { + navbarHidden: false, + isNavigating: false, + destinations: [], + isNavigatingBack: false, + titleCache: {}, + routeToPop: '', + history, + poppedRoute: { route: '' }, + activeTab, + isModal: false, + tabRoutes + }; +}; + +const getInitialTabQuery = (config, initialRoute) => { if (!config.adapter) { return false; } @@ -211,4 +215,4 @@ function getInitialTabQuery(config, initialRoute) { const urlState = config.adapter.getUrlState(initialRoute); const queryString = config.adapter.getQueryString(urlState); return parseInt(queryString.tab) || false; -} +}; diff --git a/yarn.lock b/yarn.lock index f6af565..c33e05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9171,6 +9171,11 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -11898,6 +11903,13 @@ redux-actions@^2.4.0: reduce-reducers "^0.4.3" to-camel-case "^1.0.0" +redux-mock-store@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d" + integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA== + dependencies: + lodash.isplainobject "^4.0.6" + redux@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" From 6f6b89333fe049532551a3666872598b3f997378 Mon Sep 17 00:00:00 2001 From: John Ottenlips Date: Mon, 21 Oct 2019 15:48:30 -0500 Subject: [PATCH 7/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f39ea58..f9f2bbd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ import Modal from './routes/Modal'; import Page2 from './routes/Page2'; import Page2b from './routes/Page2b'; import Page3 from './routes/Page3'; -import { createRoutes } from '../index'; +import { createRoutes } from '@aloompa/mobile-first-router'; import { TopNav } from '@aloompa/mobile-first-components'; export const routeConfig = { From ec6f628dcafe5fc27dd3b7b0bc0a62f40a5a45ae Mon Sep 17 00:00:00 2001 From: Aloompa Date: Mon, 21 Oct 2019 16:03:19 -0500 Subject: [PATCH 8/9] Add Router Reducer Tests --- .../src/__mocks__/routerReducer.mock.js | 69 ++++++ .../src/__tests__/routerReducer.spec.js | 196 +++++++++++++++--- .../mobile-first-router/src/routerReducer.ts | 8 +- 3 files changed, 238 insertions(+), 35 deletions(-) create mode 100644 packages/mobile-first-router/src/__mocks__/routerReducer.mock.js diff --git a/packages/mobile-first-router/src/__mocks__/routerReducer.mock.js b/packages/mobile-first-router/src/__mocks__/routerReducer.mock.js new file mode 100644 index 0000000..cd9fbea --- /dev/null +++ b/packages/mobile-first-router/src/__mocks__/routerReducer.mock.js @@ -0,0 +1,69 @@ +import routerReducer, { buildInitialState } from '../routerReducer'; + +export const routeConfig = { + initialActiveTab: 0, + initialRoute: 'BrowseHome', + tabs: [{}, {}, {}], + routes: { + BrowseHome: { + getTitle: () => 'Home' + }, + CategoryView: { + getTitle: () => 'Page2' + }, + ItemView: { + getTitle: () => 'Page3' + }, + ItemDetails: { + getTitle: () => 'Modal' + } + }, + renderTopNav: {}, + topNavHeight: 52 +}; + +export const initialState = buildInitialState(routeConfig); + +export const navigatingState = { + ...initialState, + destinations: [{ route: 'Page2' }], + isNavigating: true +}; + +export const navigatedState = { + ...initialState, + history: [ + { + route: 'Home' + }, + { + route: 'Page2' + }, + { + route: 'Page3' + } + ] +}; + +export const navigatingBackState = { + ...navigatedState, + activeTab: 0, + isNavigatingBack: true, + routeToPop: { route: 'Page3' }, + tabRoutes: [[...navigatedState.history]] +}; + +export const navbarHiddenState = { + ...initialState, + navbarHidden: true +}; + +export const activeNavigatedTabState = { + ...initialState, + activeTab: 0, + history: [{ route: 'Home0' }, { route: 'Page20' }], + tabRoutes: [ + [{ route: 'Home0' }, { route: 'Page20' }], + [{ route: 'Home1' }, { route: 'Page21' }] + ] +}; diff --git a/packages/mobile-first-router/src/__tests__/routerReducer.spec.js b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js index 2c03b92..8903a9f 100644 --- a/packages/mobile-first-router/src/__tests__/routerReducer.spec.js +++ b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js @@ -1,43 +1,177 @@ -import routerReducer, { buildInitialState, setRoute } from '../routerReducer'; +import routerReducer, { + setRoute, + navigateBack, + navigateBackComplete, + navigateComplete, + resetNavigation, + setActiveTab, + setNavbarHidden, + setTitleCache +} from '../routerReducer'; import configureMockStore from 'redux-mock-store'; -const mockStore = configureMockStore([]); +import { + activeNavigatedTabState, + initialState, + navbarHiddenState, + navigatedState, + navigatingBackState, + navigatingState, + routeConfig +} from '../__mocks__/routerReducer.mock'; -const routeConfig = { - initialActiveTab: 0, - initialRoute: 'BrowseHome', - tabs: [{}, {}, {}], - routes: { - BrowseHome: { - getTitle: () => 'Home' - }, - CategoryView: { - getTitle: () => 'Page2' - }, - ItemView: { - getTitle: () => 'Page3' - }, - ItemDetails: { - getTitle: () => 'Modal' - } - }, - renderTopNav: {}, - topNavHeight: 52 -}; +const mockStore = configureMockStore([]); describe('The router reducer', () => { - it('Should set route', () => { - const reducer = routerReducer(routeConfig); - const initialState = buildInitialState(routeConfig); - const store = mockStore(); + const reducer = routerReducer(routeConfig); - setRoute.dispatch( - reducer({ + it('Should add a route stack to history and set navigating to true', () => { + const store = mockStore(); + store.dispatch( + setRoute({ route: 'Page2' }) ); - const state = reducer(initialState, store.getActions()[0]); + const currentState = { ...initialState }; + expect(currentState.history[1]).toBeFalsy(); + expect(currentState.isNavigating).toBe(false); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.history[1].route).toBe('Page2'); + expect(updatedState.isNavigating).toBe(true); + }); + + it('Should set navigating to false', () => { + const store = mockStore(); + store.dispatch(navigateComplete()); + + const currentState = { ...navigatingState }; + expect(currentState.isNavigating).toBe(true); + + const updatedState = updateState(reducer, currentState, store); + expect(currentState.destinations.length).toBeGreaterThan( + updatedState.destinations.length + ); + expect(updatedState.isNavigating).toBe(false); + }); + + it('Should reset navigation', () => { + const store = mockStore(); + store.dispatch(resetNavigation()); + + const currentState = { ...navigatedState }; + expect(currentState.isNavigatingBack).toBe(false); + expect(currentState.history.length).toBeGreaterThan(1); - expect(state.history[0].name).toBe('Page2'); + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.isNavigatingBack).toBe(true); + expect(updatedState.history.length).toBe(2); + expect(updatedState.routeToPop).toBe( + currentState.history[currentState.history.length - 1] + ); + }); + + it('Should set a route to be popped', () => { + const store = mockStore(); + store.dispatch(navigateBack()); + + const currentState = { ...navigatedState }; + expect(currentState.isNavigatingBack).toBe(false); + expect(currentState.history.length).toBe(3); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.isNavigatingBack).toBe(true); + expect(updatedState.routeToPop).toBe(currentState.history[2]); + }); + + it('Should end navigating back', () => { + const store = mockStore(); + store.dispatch(navigateBackComplete()); + + const currentState = { ...navigatingBackState }; + expect(currentState.isNavigatingBack).toBe(true); + expect(currentState.history.length).toBe(3); + expect(currentState.history[currentState.history.length - 1].route).toBe( + 'Page3' + ); + expect(currentState.routeToPop.route).toBe('Page3'); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.isNavigatingBack).toBe(false); + expect(updatedState.history.length).toBe(2); + expect(updatedState.history[updatedState.history.length - 1].route).toBe( + 'Page2' + ); + expect(updatedState.poppedRoute.route).toBe('Page3'); + expect(updatedState.history).toEqual( + updatedState.tabRoutes[updatedState.activeTab] + ); + }); + + it('Should set title cache', () => { + const store = mockStore(); + store.dispatch( + setTitleCache({ + title: 'Cache' + }) + ); + + const currentState = { ...initialState }; + expect(currentState.titleCache.title).toBeFalsy(); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.titleCache).toBeTruthy(); + expect(updatedState.titleCache.title).toBe('Cache'); + }); + + it('Should hide the navbar', () => { + const store = mockStore(); + store.dispatch(setNavbarHidden(true)); + + const currentState = { ...initialState }; + expect(currentState.navbarHidden).toBe(false); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.navbarHidden).toBe(true); + }); + + it('Should show the navbar', () => { + const store = mockStore(); + store.dispatch(setNavbarHidden(false)); + + const currentState = { ...navbarHiddenState }; + expect(currentState.navbarHidden).toBe(true); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.navbarHidden).toBe(false); + }); + + it(`Should changes tabs, setting the history to the new tab's history`, () => { + const store = mockStore(); + store.dispatch(setActiveTab(1)); + + const currentState = { ...activeNavigatedTabState }; + expect(currentState.activeTab).toBe(0); + expect(currentState.history[0].route).toBe('Home0'); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.activeTab).toBe(1); + expect(updatedState.history[0].route).toBe('Home1'); + }); + + it('Should not allow tab changing during navigation', () => { + const store = mockStore(); + store.dispatch(setActiveTab(1)); + + const currentState = { ...activeNavigatedTabState, isNavigating: true }; + expect(currentState.activeTab).toBe(0); + expect(currentState.history[0].route).toBe('Home0'); + expect(currentState.isNavigating).toBe(true); + + const updatedState = updateState(reducer, currentState, store); + expect(updatedState.activeTab).toBe(0); }); }); + +const updateState = (reducer, currentState, store) => + reducer(currentState, store.getActions()[0]); diff --git a/packages/mobile-first-router/src/routerReducer.ts b/packages/mobile-first-router/src/routerReducer.ts index f498e31..76003ab 100644 --- a/packages/mobile-first-router/src/routerReducer.ts +++ b/packages/mobile-first-router/src/routerReducer.ts @@ -2,7 +2,6 @@ import { createActions, handleActions } from 'redux-actions'; import { last, defaultTo, path } from 'ramda'; import { - MFNavigationConfig, MFNavigationHistoryRoute, MFNavigationTab, MFNavigationReducerConfig @@ -125,6 +124,7 @@ const routerReducer: Function = (config: MFNavigationReducerConfig) => { [RESET_NAVIGATION]: (state) => ({ ...state, history: [state.history[0], last(state.history)], + routeToPop: last(state.history), tabRoutes: state.tabRoutes.map((route, index) => index === state.activeTab ? [route[0], last(route)] : route ), @@ -193,9 +193,7 @@ const routerReducer: Function = (config: MFNavigationReducerConfig) => { ); }; -export default routerReducer; - -export const buildInitialState = (config) => { +export const buildInitialState = (config: MFNavigationReducerConfig) => { const initialRoute: MFNavigationHistoryRoute = { route: path(['routeConfig', 'initialRoute'], config) }; @@ -242,3 +240,5 @@ const getInitialTabQuery = ( const queryString = config.adapter.getQueryString(urlState); return parseInt(queryString.tab) || false; }; + +export default routerReducer; From 9598f9754ddb86ee036d274ff504b24c57fbfd5b Mon Sep 17 00:00:00 2001 From: Aloompa Date: Wed, 23 Oct 2019 12:52:15 -0500 Subject: [PATCH 9/9] Some Clean Up --- .../src/__tests__/routerReducer.spec.js | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/mobile-first-router/src/__tests__/routerReducer.spec.js b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js index 8903a9f..6674b9b 100644 --- a/packages/mobile-first-router/src/__tests__/routerReducer.spec.js +++ b/packages/mobile-first-router/src/__tests__/routerReducer.spec.js @@ -25,8 +25,7 @@ describe('The router reducer', () => { const reducer = routerReducer(routeConfig); it('Should add a route stack to history and set navigating to true', () => { - const store = mockStore(); - store.dispatch( + const store = mockStoreAndDispatch( setRoute({ route: 'Page2' }) @@ -42,8 +41,7 @@ describe('The router reducer', () => { }); it('Should set navigating to false', () => { - const store = mockStore(); - store.dispatch(navigateComplete()); + const store = mockStoreAndDispatch(navigateComplete()); const currentState = { ...navigatingState }; expect(currentState.isNavigating).toBe(true); @@ -56,8 +54,7 @@ describe('The router reducer', () => { }); it('Should reset navigation', () => { - const store = mockStore(); - store.dispatch(resetNavigation()); + const store = mockStoreAndDispatch(resetNavigation()); const currentState = { ...navigatedState }; expect(currentState.isNavigatingBack).toBe(false); @@ -72,8 +69,7 @@ describe('The router reducer', () => { }); it('Should set a route to be popped', () => { - const store = mockStore(); - store.dispatch(navigateBack()); + const store = mockStoreAndDispatch(navigateBack()); const currentState = { ...navigatedState }; expect(currentState.isNavigatingBack).toBe(false); @@ -85,8 +81,7 @@ describe('The router reducer', () => { }); it('Should end navigating back', () => { - const store = mockStore(); - store.dispatch(navigateBackComplete()); + const store = mockStoreAndDispatch(navigateBackComplete()); const currentState = { ...navigatingBackState }; expect(currentState.isNavigatingBack).toBe(true); @@ -109,8 +104,7 @@ describe('The router reducer', () => { }); it('Should set title cache', () => { - const store = mockStore(); - store.dispatch( + const store = mockStoreAndDispatch( setTitleCache({ title: 'Cache' }) @@ -125,8 +119,7 @@ describe('The router reducer', () => { }); it('Should hide the navbar', () => { - const store = mockStore(); - store.dispatch(setNavbarHidden(true)); + const store = mockStoreAndDispatch(setNavbarHidden(true)); const currentState = { ...initialState }; expect(currentState.navbarHidden).toBe(false); @@ -136,8 +129,7 @@ describe('The router reducer', () => { }); it('Should show the navbar', () => { - const store = mockStore(); - store.dispatch(setNavbarHidden(false)); + const store = mockStoreAndDispatch(setNavbarHidden(false)); const currentState = { ...navbarHiddenState }; expect(currentState.navbarHidden).toBe(true); @@ -147,8 +139,7 @@ describe('The router reducer', () => { }); it(`Should changes tabs, setting the history to the new tab's history`, () => { - const store = mockStore(); - store.dispatch(setActiveTab(1)); + const store = mockStoreAndDispatch(setActiveTab(1)); const currentState = { ...activeNavigatedTabState }; expect(currentState.activeTab).toBe(0); @@ -160,8 +151,7 @@ describe('The router reducer', () => { }); it('Should not allow tab changing during navigation', () => { - const store = mockStore(); - store.dispatch(setActiveTab(1)); + const store = mockStoreAndDispatch(setActiveTab(1)); const currentState = { ...activeNavigatedTabState, isNavigating: true }; expect(currentState.activeTab).toBe(0); @@ -173,5 +163,12 @@ describe('The router reducer', () => { }); }); +const mockStoreAndDispatch = (action) => { + const store = mockStore(); + store.dispatch(action); + + return store; +}; + const updateState = (reducer, currentState, store) => reducer(currentState, store.getActions()[0]);