diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..4288f8fae6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,24 @@ +// Babel config for NodeJS (server-side). Frontend Babel configuration is embed +// inside Webpack config. +{ + "presets": ["env", "react", "stage-2"], + "plugins": [ + ["css-modules-transform", { + "extensions": [".css", ".scss"], + "generateScopedName": "[path]___[name]__[local]___[hash:base64:5]" + }], + "inline-react-svg", + ["module-resolver", { + "extensions": [".js", ".jsx"], + "root": [ + "./src/shared", + "./src" + ] + }], + ["react-css-modules", { + "filetypes": { + ".scss": "postcss-scss" + } + }] + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..8867467b30 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +__coverage__/ +node_modules/ +.git/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..42dd1b1464 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +__coverage__ +build +node_modules \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..700bcd665c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": "airbnb", + "settings": { + "import/resolver": { + "babel-module": {} + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5148e527a7..22522a81b7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ pids lib-cov # Coverage directory used by tools like istanbul +__coverage__ coverage # nyc test coverage @@ -24,7 +25,7 @@ coverage .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +build # Dependency directories node_modules @@ -35,3 +36,14 @@ jspm_packages # Optional REPL history .node_repl_history + +# Elastic Beanstalk Files +.elasticbeanstalk/* +!.elasticbeanstalk/*.cfg.yml +!.elasticbeanstalk/*.global.yml + +# macOS system files +*.DS_Store + +# Misc files +.vscode diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..564952cbde --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v6.10.2 diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000000..ef22505871 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,8 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "selector-pseudo-class-no-unknown": [true, { + "ignorePseudoClasses": ["global"] + }] + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..5581f1c6ea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:6.10.2 +LABEL version="1.0" +LABEL description="Community App" + +# Create app directory +RUN mkdir -p /opt/app +ADD package.json /opt/app/package.json +WORKDIR /opt/app +RUN npm install + +ADD . /opt/app + +ARG BUILD_ENV=prod +ENV NODE_ENV=$BUILD_ENV +RUN npm run build + +EXPOSE 3000 + +CMD ["npm", "start"] diff --git a/README.md b/README.md index 5d0f73dd65..75b91af5fe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,103 @@ -# community-app -React webapp for serving Topcoder Community +![Dev Build Status](https://img.shields.io/circleci/project/github/topcoder-platform/community-app/develop.svg?label=develop) + +# Topcoder Community App +New version of Topcoder Community website. + +### Deployment and Execution + +*Disclaimer:* Current instructions are biased towards Ubuntu 16.04. Hovewer, similar recipies should work for other OS. Should you encounter and overcome any tricky issues on other OS, you are welcome to add notes/hints into this file. + +1. You should have NodeJS 6.10.0 (other recent versions should also work fine); + +2. Install dependencies with one of the following commands: + - `$ npm install` Installs all dependencies. Recommended for local development; + - `$ npm install --production` Installs only production dependencies. These include all you need to run linters & unit tests, to build & run production version of the App. Does not include additional development tools. + +3. Run linters and unit tests with following commands: + - `$ npm run lint:js` Runs ESLint (AirBnB style); + - `$ npm run lint:scss` Runs Stylelint (standard Stylelint style); + - `$ npm run lint` Runs both ESLint and Stylelint; + - `$ npm run jest` Runs unit tests; + - `$ npm run jest -- -u` Runs unit test with update of component snapshots; + - `$ npm test` Runs ESLint, Stylelint and unit tests. + +4. Set environment variables: + - `PORT` Specifies the port to run the App at. Defaults to 3000; + - `NODE_ENV` Specifies Topcoder backend to use. Should be either `development` either `production`. Defaults to `production`. + +5. To rebuild the App's frontend (initially, it is automatically build as a part of the install step) run one of (the result of build will be output into `/build` folder in both cases): + - `$ npm run build` To rebuild production frontend; + - `$ npm run build:dev` This command should only be used to test whether development build of the front end works. You don't have to execute this command to run development version of the App (the server will automatically build frontend in memory anyway). You can't successfully execute this command without installing dev dependencies. + +6. To run the App use: + - `$ npm start` To run the App in normal mode. The frontend will be served from `/build` folder. The Topcoder backend to use will be chosen depending on `NODE_ENV` value; + - `$ npm run dev` To run the App with development tools. In this case the frontend is build in memory by server and uses dev tools like redux-devtools. The Topcoder backend to use will be chosen depending on `NODE_ENV` value. This demands dev dependencies installed at the firts step. + +If you run the App locally against development Topcoder backend you should access the App as `local.topcoder-dev.com:3000`. Prior doing this you should add into your `/etc/hosts` the line `127.0.0.1 local.topcoder-dev.com:3000`. To login into development Topcoder backend use `accounts.topcoder-dev.com/members` to login. Log out at `www.topcoder-dev.com`, or just wipe out auth cookies. + +If you run the App locally against production Topcoder backend you should run it at the port 80 and access the App as `local.topcoder.com`. Prior doing this you should add into your `/etc/hosts` the line `127.0.0.1 local.topcoder.com`. The easiest way to allow the App to listen at the port 80 on Ubuntu 16.04 is (no guarantees, how safe is it): +- `$ sudo apt install libcap2-bin`; +- `$ which node` to figure out your `path/to/node`; +- `$ sudo setcap cap_net_bind_service=+ep /path/to/node`; +- Now you can run the App. +To login into production Topcoder backend use `accounts.topcoder.com/members` with your regular account, and to logout you can just wipe out cookies, or just log out at `www.topcoder.com`. + +Development dependencies include StyleFMT. You can execute `$ npm run fix:styles` to automatically correct you stylesheets to comply with Stylelint rules (but it can fail for some rules). +To automatically correct js files, you can use `npm run fix:js`. + +### Development Notes +- [Challenge Listing - Notes from winning submission](docs/challenge-listing-notes.md) +- [Leaderboard - Notes from the winning submission](docs/leaderboard-notes.md) +- [Wipro Community - Notes from the preliminary winning submission](docs/wipro-community.md) +- [Why Reducer Factories and How to Use Them?](docs/why-reducer-factories-and-how-to-use-them.md) +- [~~WYSIWYG Page Editor - Notes from the winning submission~~](docs/editor-notes.pdf) + +### Current Status + +*Note:* Server-side rendering is supported. It means, if you go to `/src/server/App.jsx` and remove the line `<_script type="application/javascript" src="/bundle.js">`, which loads JS bundle in the page, when you start the App and load any page, you'll still see a properly rendered page (without any interactivity). It means that loading of JS bundle and initialization of ReactJS do not block the proper rendering of the page. + +*Setup of this App is not finished yet. Here is a brief summary of current configuration and problems found on the way.* + +This App already contains: +- A high-level draft of isomorphic App structure; +- A dummy client App; +- A set of general Topcoder stylesheets in `/src/styles`; +- Autoprefixer; +- Babel with latest JS support both client- and server-side; +- ESLint (AirBnB style); +- Express server; +- Font loading (Roboto fonts are included into the repo); +- Hot Module Replacement for JS code and SCSS styles in dev environment; +- Isomorphic fetch and Topcoder API Auth; +- Loading of .svg assets as ReactJS components with babel-plugin-inline-react-svg +- Node-Config; +- React; +- React CSS Modules (via Babel plugin); +- [react-css-themr](https://github.com/javivelasco/react-css-themr); +- React Router; +- Redux with Flux Standard Actions, redux-promise middleware, support of server-side rendering, and DevTools for dev environment; +- SCSS support; +- CSS support for third party modules; +- StyleFMT; +- Stylelint for scss (standard Stylelint style); +- Unit testing with Jest; +- Various examples; +- Webpack; + +Pending low-priority stuff (these are important, but can be added along the way): +- Webpack Dashboard (https://github.com/FormidableLabs/webpack-dashboard); + +### CI / CD +Deploy scripts are setup to use AWS ECS + CircleCI. Make sure the following environment variables are setup in CircleCI: +* AWS_ECS_SERVICE +* AWS_REPOSITORY +* DEV_AWS_ACCESS_KEY_ID +* DEV_AWS_ACCOUNT_ID +* DEV_AWS_ECS_CLUSTER +* DEV_AWS_REGION +* DEV_AWS_SECRET_ACCESS_KEY +* PROD_AWS_ACCESS_KEY_ID +* PROD_AWS_ACCOUNT_ID +* PROD_AWS_ECS_CLUSTER +* PROD_AWS_REGION +* PROD_AWS_SECRET_ACCESS_KEY diff --git a/__mocks__/redux-devtools-dock-monitor.js b/__mocks__/redux-devtools-dock-monitor.js new file mode 100644 index 0000000000..d5be7b8fd9 --- /dev/null +++ b/__mocks__/redux-devtools-dock-monitor.js @@ -0,0 +1,9 @@ +/** + * Mock redux-devtools-dock-monitor module. + * Allows to test development-only code depending on that module even + * in production environment, where that module is not installed. + */ + +export default function ReduxDevtoolsDockMonitor() { + return null; +} diff --git a/__mocks__/redux-devtools-log-monitor.js b/__mocks__/redux-devtools-log-monitor.js new file mode 100644 index 0000000000..569b991ea8 --- /dev/null +++ b/__mocks__/redux-devtools-log-monitor.js @@ -0,0 +1,3 @@ +export default function ReduxDevtoolsLogMonitor() { + return null; +} diff --git a/__mocks__/redux-devtools.js b/__mocks__/redux-devtools.js new file mode 100644 index 0000000000..6f34af2801 --- /dev/null +++ b/__mocks__/redux-devtools.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; + +export function createDevTools(obj) { + const res = () => obj; + res.instrument = _.noop; + return res; +} + +export default undefined; diff --git a/__mocks__/webpack-dev-middleware.js b/__mocks__/webpack-dev-middleware.js new file mode 100644 index 0000000000..24f433453c --- /dev/null +++ b/__mocks__/webpack-dev-middleware.js @@ -0,0 +1,5 @@ +function webpackDevMiddleware(req, res, next) { + if (next) next(); +} + +module.exports = () => webpackDevMiddleware; diff --git a/__mocks__/webpack-hot-middleware.js b/__mocks__/webpack-hot-middleware.js new file mode 100644 index 0000000000..4cd128cbce --- /dev/null +++ b/__mocks__/webpack-hot-middleware.js @@ -0,0 +1,5 @@ +function webpackHotMiddleware(req, res, next) { + if (next) next(); +} + +module.exports = () => webpackHotMiddleware; diff --git a/__mocks__/webpack.js b/__mocks__/webpack.js new file mode 100644 index 0000000000..abcd7b5885 --- /dev/null +++ b/__mocks__/webpack.js @@ -0,0 +1,3 @@ +import _ from 'lodash'; + +module.exports = _.noop; diff --git a/__tests__/.eslintrc b/__tests__/.eslintrc new file mode 100644 index 0000000000..44fa9f88a8 --- /dev/null +++ b/__tests__/.eslintrc @@ -0,0 +1,12 @@ +{ + "env": { + "jest/globals": true + }, + "plugins": [ + "jest" + ], + "rules": { + "global-require": 0, + "import/no-dynamic-require": 0 + } +} diff --git a/__tests__/client/__snapshots__/index.jsx.snap b/__tests__/client/__snapshots__/index.jsx.snap new file mode 100644 index 0000000000..a4db6dff35 --- /dev/null +++ b/__tests__/client/__snapshots__/index.jsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Properly starts with process.env.FRONT_ENV evaluating true Renders proper code (matching snapshot) 1`] = ` +
+

+ Mock react-redux Provider +

+
+

+ Mock react-router-dom BrowserRouter +

+ Mock Browser History +
+ Application +
+
+
+`; diff --git a/__tests__/client/index.jsx b/__tests__/client/index.jsx new file mode 100644 index 0000000000..520c00db88 --- /dev/null +++ b/__tests__/client/index.jsx @@ -0,0 +1,198 @@ +/* eslint-env browser */ + +import _ from 'lodash'; +import PT from 'prop-types'; +import React from 'react'; +import renderer from 'react-test-renderer'; + +const SRC = '../../src'; +const MODULE = `${SRC}/client`; + +document.getElementById = id => + (id === 'react-view' ? 'REACT-VIEW' : undefined); + +window.CONFIG = { + ACCOUNTS_APP_CONNECT_URL: 'https://dummy.url', + COOKIES: { + MAXAGE: 7, + SECURE: false, + }, +}; + +window.ISTATE = 'Initial state of Redux store'; + +/* Mock of browser-cookies */ + +let tokenV2; +const mockCookies = { + erase: jest.fn(), + get: (name) => { + switch (name) { + case 'tcjwt': return tokenV2; + default: return undefined; + } + }, + set: () => {}, +}; +jest.setMock('browser-cookies', mockCookies); + +/* Mock of react-redux module */ + +function MockProvider(props) { + return ( +
+

Mock react-redux Provider

+ {props.children} +
+ ); +} + +MockProvider.propTypes = { + children: PT.node.isRequired, +}; + +jest.setMock('react-redux', { + Provider: MockProvider, +}); + +/* Mock of react-router-dom */ + +const mockBrowserHistory = 'Mock Browser History'; + +function MockBrowserRouter(props) { + return ( +
+

Mock react-router-dom BrowserRouter

+ {props.history} + {props.children} +
+ ); +} + +MockBrowserRouter.propTypes = { + children: PT.node.isRequired, + history: PT.string.isRequired, +}; + +jest.setMock('react-router-dom', { + browserHistory: mockBrowserHistory, + BrowserRouter: MockBrowserRouter, +}); + +/* Mock of tc-accounts */ + +let tokenV3; +const mockTcAccounts = { + configureConnector: () => undefined, + decodeToken: () => 'Decoded user object', + getFreshToken: () => Promise.resolve(tokenV3), +}; +jest.setMock('tc-accounts', mockTcAccounts); + +/* Mock auth actions */ + +const mockAuthActions = { + auth: { + loadProfile: jest.fn(), + setTcTokenV2: jest.fn(), + setTcTokenV3: jest.fn(), + }, +}; + +jest.setMock(`${SRC}/shared/actions/auth`, mockAuthActions); + +/* Mock of store factory */ + +const mockStoreFactory = jest.fn(() => Promise.resolve({ + dispatch: _.noop, + getState: () => ({ + auth: { + tokenV2: '12345', + tokenV3: '12345', + }, + }), +})); +jest.setMock(`${SRC}/shared/store-factory`, mockStoreFactory); + +/* Some other mocks */ + +jest.setMock(`${SRC}/shared`, { + default: () =>
Application
, +}); + +test('Fails to start with process.env.FRONT_END evaluating false', () => { + jest.resetModules(); + expect(process.env.FRONT_END).toBeUndefined(); + expect(() => require(MODULE)).toThrow(); +}); + +describe('Properly starts with process.env.FRONT_ENV evaluating true', () => { + /* NOTE: Before each test a promise is stored into this variable, which will + * resolve once the page is rendered. */ + let rendered; + + afterAll(() => delete process.env.FRONT_END); + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + process.env.FRONT_END = true; + + let resolve; + rendered = new Promise((r) => { resolve = r; }); + jest.setMock('react-dom', { + render: (code, target) => resolve({ code, target }), + }); + }); + + test('Constructs Redux store with proper initial state', () => { + require(MODULE); + expect(mockStoreFactory).toHaveBeenCalledWith(undefined, window.ISTATE); + }); + + test('Renders proper code (matching snapshot)', () => { + require(MODULE); + return rendered.then(({ code, target }) => { + expect(target).toBe('REACT-VIEW'); + const app = renderer.create(code).toJSON(); + expect(app).toMatchSnapshot(); + }); + }); + + test('Sets auth tokens when user is authorised', () => + new Promise((resolve) => { + tokenV2 = 'Token V2'; + tokenV3 = 'Token V3'; + require(MODULE); + + /* NOTE: We have mocked getFreshToken to return Promise.resolve(..), + * which resolves immediately. Thus, this call to setImmediate(..) is + * enough to wait until tokens are processed. */ + setImmediate(() => { + expect(mockAuthActions.auth.setTcTokenV2) + .toHaveBeenCalledWith('Token V2'); + expect(mockAuthActions.auth.setTcTokenV3) + .toHaveBeenCalledWith('Token V3'); + resolve(); + }); + }), + ); + + test('Does not write auth tokens to the state, when no need to', () => + new Promise((resolve) => { + tokenV2 = '12345'; + tokenV3 = '12345'; + require(MODULE); + + /* NOTE: We have mocked getFreshToken to return Promise.resolve(..), + * which resolves immediately. Thus, this call to setImmediate(..) is + * enough to wait until tokens are processed. */ + setImmediate(() => { + expect(mockAuthActions.auth.setTcTokenV2).not.toHaveBeenCalled(); + expect(mockAuthActions.auth.setTcTokenV3).not.toHaveBeenCalled(); + resolve(); + }); + }), + ); +}); + diff --git a/__tests__/server/.eslintrc b/__tests__/server/.eslintrc new file mode 100644 index 0000000000..1ac998e606 --- /dev/null +++ b/__tests__/server/.eslintrc @@ -0,0 +1,7 @@ +{ + "rules": { + // It is fine to use console server-side + // http://eslint.org/docs/rules/no-console + "no-console": 0 + } +} diff --git a/__tests__/server/index.js b/__tests__/server/index.js new file mode 100644 index 0000000000..433a7861c0 --- /dev/null +++ b/__tests__/server/index.js @@ -0,0 +1,72 @@ +import _ from 'lodash'; + +const SRC = '../../src'; + +/* Mock http */ + +const mockServer = { + listen: jest.fn(), + on: jest.fn(), +}; + +jest.setMock('http', { + createServer: jest.fn(() => mockServer), +}); + +jest.setMock(`${SRC}/server/server`, { + set: jest.fn(), +}); + +test('Should not throw', () => { + expect(() => require('server')).not.toThrow(); +}); + +describe('Successfully created server', () => { + beforeAll(() => { + jest.resetModules(); + jest.clearAllMocks(); + require('server'); + }); + + let onError; + test('A single error handler is created', () => { + onError = mockServer.on.mock.calls.filter(call => call[0] === 'error'); + expect(onError.length).toBe(1); + onError = onError[0][1]; + expect(_.isFunction(onError)).toBe(true); + }); + + test('onError throws for any syscall except of listen', () => { + const err = new Error(); + err.syscall = 'syscall'; + expect(() => onError(err)).toThrow(err); + }); + + test('onError throws for unknown errors', () => { + const err = new Error(); + err.syscall = 'listen'; + expect(() => onError(err)).toThrow(err); + }); + + test('onError handles EACCESS error as expected', () => { + const err = new Error(); + err.syscall = 'listen'; + err.code = 'EACCES'; + console.error = jest.fn(); + process.exit = jest.fn(); + onError(err); + expect(console.error).toHaveBeenCalledWith('Port 3000 requires elevated privileges'); + expect(process.exit).toHaveBeenCalledWith(1); + }); + + test('onError handles EADDRINUSE error as expected', () => { + const err = new Error(); + err.syscall = 'listen'; + err.code = 'EADDRINUSE'; + console.error = jest.fn(); + process.exit = jest.fn(); + onError(err); + expect(console.error).toHaveBeenCalledWith('Port 3000 is already in use'); + expect(process.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/__tests__/server/renderer.jsx b/__tests__/server/renderer.jsx new file mode 100644 index 0000000000..9da5409439 --- /dev/null +++ b/__tests__/server/renderer.jsx @@ -0,0 +1,15 @@ +jest.setMock('react-dom/server', { + renderToString: () => 'RENDER', +}); + +const renderer = require('server/renderer').default; + +test('should not throw errors', () => { + const req = { + url: '/', + }; + const res = { + send: () => {}, + }; + expect(() => renderer(req, res)).not.toThrow(); +}); diff --git a/__tests__/server/server.js b/__tests__/server/server.js new file mode 100644 index 0000000000..4da72c8533 --- /dev/null +++ b/__tests__/server/server.js @@ -0,0 +1,41 @@ +import _ from 'lodash'; + +const MODULE = require.resolve('server/server'); + +jest.setMock('../../config/webpack/development', { + output: { + publicPath: '', + }, +}); +jest.setMock(require.resolve('server/renderer'), _.noop); + +afterAll(() => { + delete process.env.DEV_TOOLS; + delete process.env.FRONT_END; +}); + +beforeEach(() => { + jest.resetModules(); +}); + +test('Throws when executed at front end', () => { + process.env.FRONT_END = true; + expect(() => require(MODULE)).toThrow(); + delete process.env.FRONT_END; +}); + +test('Does not throw when executed at the back end', () => { + expect(() => require(MODULE)).not.toThrow(); +}); + +test('Does not throw when executed with pipe name', () => { + process.env.PORT = 80; + expect(() => require(MODULE)).not.toThrow(); + delete process.env.PORT; +}); + +test('Does not throw when uses dev tools', () => { + process.env.DEV_TOOLS = true; + expect(() => require(MODULE)).not.toThrow(); + delete process.env.DEV_TOOLS; +}); diff --git a/__tests__/shared/__snapshots__/index.jsx.snap b/__tests__/shared/__snapshots__/index.jsx.snap new file mode 100644 index 0000000000..5d000a6192 --- /dev/null +++ b/__tests__/shared/__snapshots__/index.jsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snapshot match 1`] = ` +
+ +
+`; + +exports[`Snapshot match 2`] = ` +
+ +
+`; diff --git a/__tests__/shared/actions/auth.js b/__tests__/shared/actions/auth.js new file mode 100644 index 0000000000..fa6e79aedd --- /dev/null +++ b/__tests__/shared/actions/auth.js @@ -0,0 +1,57 @@ +jest.setMock('isomorphic-fetch', {}); + +const GROUPS_REQ_URL = + 'https://api.topcoder-dev.com/v3/groups?memberId=12345&membershipType=user'; +const PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v3/members/username12345'; + +global.fetch = jest.fn(url => Promise.resolve({ + json: () => { + let content; + switch (url) { + case GROUPS_REQ_URL: content = ['Group1', 'Group2']; break; + case PROFILE_REQ_URL: content = { userId: 12345 }; break; + default: throw new Error('Unexpected URL!'); + } + return { + result: { content, status: 200 }, + }; + }, +})); + +jest.setMock('tc-accounts', { + decodeToken: token => (token === 'token' ? { + handle: 'username12345', + userId: '12345', + } : undefined), +}); + +const actions = require('actions/auth').default; + +beforeEach(() => jest.clearAllMocks()); + +test('auth.loadProfile works as expected when authenticated', () => { + const action = actions.auth.loadProfile('token'); + expect(action.type).toBe('AUTH/LOAD_PROFILE'); + return action.payload.then((res) => { + expect(global.fetch).toHaveBeenCalledWith( + PROFILE_REQ_URL, { + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + }, + ); + expect(global.fetch).toHaveBeenCalledWith( + GROUPS_REQ_URL, { + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + }, + ); + expect(res).toEqual({ + groups: ['Group1', 'Group2'], + userId: 12345, + }); + }); +}); diff --git a/__tests__/shared/actions/challenge.js b/__tests__/shared/actions/challenge.js new file mode 100644 index 0000000000..b221f7c2fa --- /dev/null +++ b/__tests__/shared/actions/challenge.js @@ -0,0 +1,70 @@ +import actions from 'actions/challenge'; + +const mockFetch = resolvesTo => jest.fn(() => + Promise.resolve({ json: () => resolvesTo })); + +jest.mock('utils/config', () => ({ + API: { + V2: 'API-URL-V2', + V3: 'API-URL-V3', + }, +})); + +let originalFetch; + +beforeAll(() => { + originalFetch = global.fetch; +}); + +afterAll(() => { + global.fetch = originalFetch; +}); + +describe('challenge.fetchChallengeInit', () => { + const a = actions.fetchChallengeInit(); + + test('has expected type', () => { + expect(a.type).toBe('FETCH_CHALLENGE_INIT'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('challenge.fetchSubmissionsInit', () => { + const a = actions.fetchSubmissionsInit(); + + test('has expected type', () => { + expect(a.type).toBe('FETCH_SUBMISSIONS_INIT'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('challenge.fetchChallengeDone', () => { + global.fetch = mockFetch({ result: { content: ['DUMMY DATA'] } }); + + const a = actions.fetchChallengeDone({}); + + test('has expected type', () => { + expect(a.type).toBe('FETCH_CHALLENGE_DONE'); + }); + + test('payload is a promise which resolves to the expected object', () => + a.payload.then(res => expect(res).toEqual('DUMMY DATA'))); +}); + + +describe('challenge.fetchSubmissionsDone', () => { + global.fetch = mockFetch({ submissions: 'DUMMY DATA' }); + + const a = actions.fetchSubmissionsDone({}); + + test('has expected type', () => { + expect(a.type).toBe('FETCH_SUBMISSIONS_DONE'); + }); + + test('payload is a promise which resolves to the expected object', () => + a.payload.then(res => expect(res).toEqual('DUMMY DATA'))); +}); diff --git a/__tests__/shared/actions/examples/data-fetch.js b/__tests__/shared/actions/examples/data-fetch.js new file mode 100644 index 0000000000..0ee92c2358 --- /dev/null +++ b/__tests__/shared/actions/examples/data-fetch.js @@ -0,0 +1,32 @@ +import actions from 'actions/examples/data-fetch'; + +let originalFetch; + +beforeAll(() => { + originalFetch = global.fetch; +}); + +afterAll(() => { + global.fetch = originalFetch; +}); + +describe('examples.dataFetch.fetchDataDone', () => { + global.fetch = jest.fn(() => + new Promise(resolve => + setTimeout(() => resolve({ + json: () => ({ data: 'DUMMY DATA' }), + }), 1), + ), + ); + + const a = actions.examples.dataFetch.fetchDataDone(); + + test('has expected type', () => { + expect(a.type).toBe('EXAMPLES/DATA_FETCH/FETCH_DATA_DONE'); + }); + + /* NOTE: Starting from Jest 20.x.x you should use the more elegant way: + * expect(a.payload).resolves.toEqual({ data: 'DUMMY DATA' }); */ + test('payload is a promise which resolves to the expected object', () => + a.payload.then(res => expect(res).toEqual('DUMMY DATA'))); +}); diff --git a/__tests__/shared/actions/leaderboard.js b/__tests__/shared/actions/leaderboard.js new file mode 100644 index 0000000000..27dc1aae07 --- /dev/null +++ b/__tests__/shared/actions/leaderboard.js @@ -0,0 +1,38 @@ +import actions from 'actions/leaderboard'; + +const mockFetch = resolvesTo => jest.fn(() => + Promise.resolve({ json: () => resolvesTo })); + +let originalFetch; + +beforeAll(() => { + originalFetch = global.fetch; +}); + +afterAll(() => { + global.fetch = originalFetch; +}); + +describe('challenge.fetchLeaderboardInit', () => { + const a = actions.leaderboard.fetchLeaderboardInit(); + + test('has expected type', () => { + expect(a.type).toBe('LEADERBOARD/FETCH_LEADERBOARD_INIT'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('challenge.fetchLeaderboardDone', () => { + global.fetch = mockFetch([{ 'user.handle': 'fake.username' }]); + + const a = actions.leaderboard.fetchLeaderboardDone({}, ''); + + test('has expected type', () => { + expect(a.type).toBe('LEADERBOARD/FETCH_LEADERBOARD_DONE'); + }); + + test('payload is a promise which resolves to the expected object', () => + a.payload.then(res => expect(res.data[0]['user.handle']).toEqual('fake.username'))); +}); diff --git a/__tests__/shared/actions/smp.js b/__tests__/shared/actions/smp.js new file mode 100644 index 0000000000..76291b7d88 --- /dev/null +++ b/__tests__/shared/actions/smp.js @@ -0,0 +1,82 @@ +import actions from 'actions/smp'; + +jest.mock('utils/config', () => ({ + API: { + V2: 'https://api.topcoder-dev.com/v2', + V3: 'API-URL-V3', + }, +})); + +let originalFetch; + +beforeAll(() => { + originalFetch = global.fetch; +}); + +afterAll(() => { + global.fetch = originalFetch; +}); + +describe('smp.showDetails', () => { + const a = actions.smp.showDetails('PAYLOAD'); + + test('has expected type', () => { + expect(a.type).toBe('SMP/SHOW_DETAILS'); + }); + + test('payload is identity', () => + expect(a.payload).toEqual('PAYLOAD')); +}); + +describe('smp.cancelDelete', () => { + const a = actions.smp.cancelDelete('PAYLOAD'); + + test('has expected type', () => { + expect(a.type).toBe('SMP/CANCEL_DELETE'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('smp.confirmDelete', () => { + const a = actions.smp.confirmDelete('PAYLOAD'); + + test('has expected type', () => { + expect(a.type).toBe('SMP/CONFIRM_DELETE'); + }); + + test('payload is identity', () => + expect(a.payload).toEqual('PAYLOAD')); +}); + +describe('smp.deleteSubmissionDone', () => { + global.fetch = jest.fn(() => Promise.resolve()); + + const a = actions.smp.deleteSubmissionDone('Token V3', 'submissionId'); + + test('has expected type', () => { + expect(a.type).toBe('SMP/DELETE_SUBMISSION_DONE'); + }); + + test('Calls the correct endpoint', () => { + expect(global.fetch).toHaveBeenCalledWith( + 'API-URL-V3/submissions/submissionId', { + headers: { + Authorization: 'Bearer Token V3', + 'Content-Type': 'application/json', + }, + method: 'DELETE', + }); + }); + + test('payload be submissionId', () => + a.payload.then(res => expect(res).toEqual('submissionId'))); +}); + +describe('smp.downloadSubmission', () => { + test('does not throw', () => { + expect(() => + actions.smp.downloadSubmission({}, 'design', '12345')).not.toThrow(); + }); +}); diff --git a/__tests__/shared/actions/tc-communities/meta.js b/__tests__/shared/actions/tc-communities/meta.js new file mode 100644 index 0000000000..3288eb46ef --- /dev/null +++ b/__tests__/shared/actions/tc-communities/meta.js @@ -0,0 +1,56 @@ +import actions from 'actions/tc-communities/meta'; + +/* + As test are being run in server environment + we have to mock getCommunitiesMetadata function which is used in this case + by header actions + */ +jest.mock('utils/tc', () => ({ + getCommunitiesMetadata: communityId => ( + communityId !== 'someId404' + ? Promise.resolve({ communityId }) + : Promise.reject({ communityId, error: '404' }) + ), +})); + +describe('tcCommunities.header.mobileToggle', () => { + const a = actions.tcCommunities.meta.mobileToggle(); + + test('has expected type', () => { + expect(a.type).toBe('TC_COMMUNITIES/META/MOBILE_TOGGLE'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('tcCommunities.header.fetchDataInit', () => { + const a = actions.tcCommunities.meta.fetchDataInit(); + + test('has expected type', () => { + expect(a.type).toBe('TC_COMMUNITIES/META/FETCH_DATA_INIT'); + }); + + test('payload is undefined', () => + expect(a.payload).toBeUndefined()); +}); + +describe('tcCommunities.header.fetchDataDone', () => { + const a = actions.tcCommunities.meta.fetchDataDone('someId'); + + test('has expected type', () => { + expect(a.type).toBe('TC_COMMUNITIES/META/FETCH_DATA_DONE'); + }); + + test('payload is a promise which resolves to the expected object', () => + a.payload.then(res => expect(res).toEqual({ communityId: 'someId' }))); + + const a404 = actions.tcCommunities.meta.fetchDataDone('someId404'); + + test('has expected type', () => { + expect(a404.type).toBe('TC_COMMUNITIES/META/FETCH_DATA_DONE'); + }); + + test('payload is a promise which rejects to the expected object', () => + a404.payload.catch(err => expect(err).toEqual({ communityId: 'someId404', error: '404' }))); +}); diff --git a/__tests__/shared/actions/topcoder_header.js b/__tests__/shared/actions/topcoder_header.js new file mode 100644 index 0000000000..886b574609 --- /dev/null +++ b/__tests__/shared/actions/topcoder_header.js @@ -0,0 +1,24 @@ +import actions from 'actions/topcoder_header'; + +const mockNode = { + getBoundingClientRect: () => {}, +}; + +test('openMenu', () => { + const a = actions.topcoderHeader.openMenu('Menu', mockNode); + expect(a.type).toBe('TOPCODER_HEADER/OPEN_MENU'); + expect(a.payload).toEqual({ + menu: 'Menu', + trigger: { + }, + }); +}); + +test('openSearch', () => { + const a = actions.topcoderHeader.openSearch(mockNode); + expect(a.type).toBe('TOPCODER_HEADER/OPEN_SEARCH'); + expect(a.payload).toEqual({ + trigger: { + }, + }); +}); diff --git a/__tests__/shared/components/Avatar.jsx b/__tests__/shared/components/Avatar.jsx new file mode 100644 index 0000000000..b0f0cc332a --- /dev/null +++ b/__tests__/shared/components/Avatar.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import Renderer from 'react-test-renderer/shallow'; +import Avatar from 'components/Avatar'; + +test('Matches shallow shapshot', () => { + const renderer = new Renderer(); + + renderer.render(( + + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); + + renderer.render(( + + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); + + renderer.render(( + + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); + + renderer.render(( + + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/Button.jsx b/__tests__/shared/components/Button.jsx new file mode 100644 index 0000000000..c6f36c82ee --- /dev/null +++ b/__tests__/shared/components/Button.jsx @@ -0,0 +1,34 @@ +import Button from 'components/Button'; +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import TU from 'react-dom/test-utils'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render( +`; + +exports[`Snapshot match 2`] = ` + +`; + +exports[`Snapshot match 3`] = ` + +`; + +exports[`Snapshot match 4`] = ` + +`; + +exports[`Snapshot match 5`] = ` + +`; + +exports[`Snapshot match 6`] = ` + +`; diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/Submission.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/Submission.jsx.snap new file mode 100644 index 0000000000..03b581eb1f --- /dev/null +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/Submission.jsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snapshot match 1`] = ` + + + preview + + + + + Invalid date + + +
+ + + + + +
+ + +`; + +exports[`Snapshot match 2`] = ` + + + preview + + + 12345 + + + + Invalid date + + + + + +
+ + + + + +
+ + +`; diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionManagement.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionManagement.jsx.snap new file mode 100644 index 0000000000..44dca25c28 --- /dev/null +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionManagement.jsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+
+ +
+

+

+ + 0 + H + 0 + M +

+

+ left +

+
+
+
+
+

+ Manage your submissions +

+
+ +
+ +
+`; diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap new file mode 100644 index 0000000000..4bb946f306 --- /dev/null +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/SubmissionsTable.jsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+ + + + + + + + + + + + + + + + +
+ Preview + + ID + + Type + + Submission Date + + Actions +
+ +
+
+`; diff --git a/__tests__/shared/components/TopcoderFooter.jsx b/__tests__/shared/components/TopcoderFooter.jsx new file mode 100644 index 0000000000..e4fdbbb66e --- /dev/null +++ b/__tests__/shared/components/TopcoderFooter.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import Rnd from 'react-test-renderer'; +import TopcoderFooter from 'components/TopcoderFooter'; + +test('Matches shallow shapshot', () => { + const rnd = Rnd.create(); + expect(rnd.toJSON()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/TopcoderHeader/Auth.jsx b/__tests__/shared/components/TopcoderHeader/Auth.jsx new file mode 100644 index 0000000000..bfa7747b0a --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/Auth.jsx @@ -0,0 +1,16 @@ +import Auth from 'components/TopcoderHeader/Auth'; +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/TopcoderHeader/__snapshots__/Auth.jsx.snap b/__tests__/shared/components/TopcoderHeader/__snapshots__/Auth.jsx.snap new file mode 100644 index 0000000000..bc7bd9d5f4 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/__snapshots__/Auth.jsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snapshot match 1`] = ` + +`; + +exports[`Snapshot match 2`] = ` + +`; diff --git a/__tests__/shared/components/TopcoderHeader/__snapshots__/index.jsx.snap b/__tests__/shared/components/TopcoderHeader/__snapshots__/index.jsx.snap new file mode 100644 index 0000000000..07e3b90777 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/__snapshots__/index.jsx.snap @@ -0,0 +1,602 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Default render 1`] = ` +
+
+ + + +
    +
  • + Compete +
  • +
  • + Learn +
  • +
  • + Community +
  • +
+
+ +
+ +
+
+
+ +
+ +
+
, + "link": "https://www.topcoder-dev.com/challenges", + "title": "All Challenges", + }, + Object { + "icon": , + "link": "https://arena.topcoder-dev.com/", + "title": "Competitive Programming", + }, + ], + "title": "Compete", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/getting-started", + "title": "Getting Started", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/design", + "title": "Design", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/development", + "title": "Development", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/data-science/", + "title": "Data Science", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/competitive-programming", + "title": "Competitive Programming", + }, + ], + "title": "Learn", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/members", + "title": "Overview", + }, + Object { + "icon": , + "link": "https://www.topcoder.com/tco", + "title": "TCO", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/member-programs", + "title": "Programs", + }, + Object { + "icon": , + "link": "https://apps.topcoder-dev.com/forums", + "title": "Forums", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/statistics", + "title": "Statistics", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/events", + "title": "Events", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/blog", + "title": "Blog", + }, + ], + "title": "Community", + }, + ] + } + open={[Function]} + opened={false} + profile={null} + userMenu={null} + /> +
+`; + +exports[`Render with open menu 1`] = ` +
+
+ + + +
    +
  • + Compete +
  • +
  • + Learn +
  • +
  • + Community +
  • +
+
+ +
+ +
+
+
+ , + "link": "/link", + "title": "Title", + }, + ], + "title": "Menu Title", + } + } + trigger={null} + /> +
+ +
+
, + "link": "https://www.topcoder-dev.com/challenges", + "title": "All Challenges", + }, + Object { + "icon": , + "link": "https://arena.topcoder-dev.com/", + "title": "Competitive Programming", + }, + ], + "title": "Compete", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/getting-started", + "title": "Getting Started", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/design", + "title": "Design", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/development", + "title": "Development", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/data-science/", + "title": "Data Science", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/competitive-programming", + "title": "Competitive Programming", + }, + ], + "title": "Learn", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/members", + "title": "Overview", + }, + Object { + "icon": , + "link": "https://www.topcoder.com/tco", + "title": "TCO", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/member-programs", + "title": "Programs", + }, + Object { + "icon": , + "link": "https://apps.topcoder-dev.com/forums", + "title": "Forums", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/statistics", + "title": "Statistics", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/events", + "title": "Events", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/blog", + "title": "Blog", + }, + ], + "title": "Community", + }, + ] + } + open={[Function]} + opened={false} + profile={null} + userMenu={null} + /> +
+`; + +exports[`Render with specified profile 1`] = ` +
+
+ + + +
    +
  • + Compete +
  • +
  • + Learn +
  • +
  • + Community +
  • +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
, + "link": "https://www.topcoder-dev.com/challenges", + "title": "All Challenges", + }, + Object { + "icon": , + "link": "https://arena.topcoder-dev.com/", + "title": "Competitive Programming", + }, + ], + "title": "Compete", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/getting-started", + "title": "Getting Started", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/design", + "title": "Design", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/development", + "title": "Development", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/data-science/", + "title": "Data Science", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/competitive-programming", + "title": "Competitive Programming", + }, + ], + "title": "Learn", + }, + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/members", + "title": "Overview", + }, + Object { + "icon": , + "link": "https://www.topcoder.com/tco", + "title": "TCO", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/member-programs", + "title": "Programs", + }, + Object { + "icon": , + "link": "https://apps.topcoder-dev.com/forums", + "title": "Forums", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/statistics", + "title": "Statistics", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/community/events", + "title": "Events", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/blog", + "title": "Blog", + }, + ], + "title": "Community", + }, + ] + } + open={[Function]} + opened={false} + profile={Object {}} + userMenu={ + Object { + "items": Array [ + Object { + "icon": , + "link": "https://www.topcoder-dev.com/my-dashboard", + "title": "Dashboard", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/members/undefined", + "title": "My Profile", + }, + Object { + "icon": , + "link": "https://community.topcoder-dev.com/PactsMemberServlet?module=PaymentHistory&full_list=false", + "title": "Payments", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/settings/profile", + "title": "Settings", + }, + Object { + "icon": , + "link": "https://www.topcoder-dev.com/logout", + "title": "Log Out", + }, + ], + "title": "User", + } + } + /> +
+`; diff --git a/__tests__/shared/components/TopcoderHeader/index.jsx b/__tests__/shared/components/TopcoderHeader/index.jsx new file mode 100644 index 0000000000..caabc9b25f --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/index.jsx @@ -0,0 +1,212 @@ +/* eslint-env browser */ + +import _ from 'lodash'; +import React from 'react'; +import R from 'react-test-renderer/shallow'; +import TopcoderHeader from 'components/TopcoderHeader'; +import TU from 'react-dom/test-utils'; + +/* It is not possible to use functional components as arguments of + * TU.renderIntoDocument(..), hence this class-wrapper. */ +class Wrapper extends React.Component { + componentDidMount() {} + render() { + return ; + } +} + +const mockCloseMenu = jest.fn(); +const mockCloseSearch = jest.fn(); +const mockOpenMenu = jest.fn(); +const mockOpenSearch = jest.fn(); + +function styleNameMatch(item, styleName) { + return item && item.className && item.className.match(styleName); +} + +const r = new R(); + +test('Default render', () => { + r.render(( + + )); + expect(r.getRenderOutput()).toMatchSnapshot(); +}); + +test('Render with open menu', () => { + r.render(( + , + link: '/link', + title: 'Title', + }], + }} + openMobileMenu={_.noop} + openSearch={_.noop} + /> + )); + expect(r.getRenderOutput()).toMatchSnapshot(); +}); + +test('Render with specified profile', () => { + r.render(( + + )); + expect(r.getRenderOutput()).toMatchSnapshot(); +}); + +describe('User input handling', () => { + let page; + beforeAll(() => { + page = TU.renderIntoDocument(( + + )); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('main-menu-item opens sub-menu when hovered', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/main-menu-item/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseEnter(items[0]); + expect(mockOpenMenu).toHaveBeenCalled(); + }); + + test('main-menu-item closes sub-menu when mouse leaves downward', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/main-menu-item/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseLeave(items[0], { pageY: -1 }); + expect(mockCloseMenu).not.toHaveBeenCalled(); + }); + + test('main-menu-item closes sub-menu when mouse leaves not downards', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/main-menu-item/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseLeave(items[0], { pageY: -2 }); + expect(mockCloseMenu).toHaveBeenCalled(); + }); + + test('user-menu handle opens sub-menu when hovered', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/user-menu/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseEnter(items[0]); + expect(mockOpenMenu).toHaveBeenCalled(); + }); + + test('user-menu handle closes sub-menu when mouse leaves downwards', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/user-menu/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseLeave(items[0], { pageY: -1 }); + expect(mockCloseMenu).not.toHaveBeenCalled(); + }); + + test('user-menu closes sub-menu when mouse leaves not downards', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/user-menu/)); + expect(items.length).toBeGreaterThan(1); + TU.Simulate.mouseLeave(items[0], { pageY: -2 }); + expect(mockCloseMenu).toHaveBeenCalled(); + }); + + test('search-icon opens search when hovered', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/search-icon/)); + expect(items.length).toBe(1); + TU.Simulate.mouseEnter(items[0]); + expect(mockOpenSearch).toHaveBeenCalled(); + }); + + test('search-icon closes search when mouse leaves downwards', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/search-icon/)); + expect(items.length).toBe(1); + TU.Simulate.mouseLeave(items[0], { pageY: -1 }); + expect(mockCloseSearch).not.toHaveBeenCalled(); + }); + + test('search-icon closes search when mouse leaves not downards', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/search-icon/)); + expect(items.length).toBe(1); + TU.Simulate.mouseLeave(items[0], { pageY: -2 }); + expect(mockCloseSearch).toHaveBeenCalled(); + }); + + test('sub-menu closes when mouse leave downwards', () => { + const items = TU.findAllInRenderedTree(page, item => + styleNameMatch(item, 'closed-menu')); + expect(items.length).toBe(1); + TU.Simulate.mouseLeave(items[0], { pageY: 1 }); + expect(mockCloseMenu).toHaveBeenCalled(); + }); + + test('search-field closes when mouse leaves downwards', () => { + const items = TU.findAllInRenderedTree(page, item => + styleNameMatch(item, 'search-field')); + expect(items.length).toBe(1); + TU.Simulate.mouseLeave(items[0], { pageY: 1 }); + expect(mockCloseSearch).toHaveBeenCalled(); + }); + + test('Enter submits search field', () => { + const items = TU.findAllInRenderedTree(page, item => + styleNameMatch(item, 'search-field')); + expect(items.length).toBe(1); + expect(items[0].children.length).toBe(1); + const input = items[0].children[0]; + expect(input.tagName).toBe('INPUT'); + TU.Simulate.keyPress(input, { + key: 'Enter', + target: { + value: 'SEARCH', + }, + }); + /* TODO: The commented out string below does not work with jest: + * window.location comes from jsdom and works differently from + * browser's window.location. Should be investigated how to make + * this check properly. */ + // expect(window.location).toHaveBeenCalledWith('/search/members?q=SEARCH'); + }); +}); diff --git a/__tests__/shared/components/TopcoderHeader/mobile/Menu.jsx b/__tests__/shared/components/TopcoderHeader/mobile/Menu.jsx new file mode 100644 index 0000000000..75b1e61a83 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/Menu.jsx @@ -0,0 +1,23 @@ +import _ from 'lodash'; +import React from 'react'; +import R from 'react-test-renderer/shallow'; +import Menu from 'components/TopcoderHeader/mobile/Menu'; + +const renderer = new R(); + +test('Matches snapshot', () => { + renderer.render(( + , + link: '/LINK', + title: 'Item title', + }], + }]} + /> + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/TopcoderHeader/mobile/Search.jsx b/__tests__/shared/components/TopcoderHeader/mobile/Search.jsx new file mode 100644 index 0000000000..73cc3e316b --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/Search.jsx @@ -0,0 +1,42 @@ +import _ from 'lodash'; +import React from 'react'; +import Renderer from 'react-test-renderer/shallow'; +import Search from 'components/TopcoderHeader/mobile/Search'; +import TU from 'react-dom/test-utils'; + +class SearchWrapper extends React.Component { + componentDidMount() {} + + render() { + return ( + + ); + } +} + +test('Matches shallow shapshot', () => { + const renderer = new Renderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); + +const page = TU.renderIntoDocument(); + +test('Handles Enter key', () => { + const items = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match('search')); + const input = _.find(items[0].children, item => + item && item.tagName === 'INPUT'); + TU.Simulate.keyPress(input, { + key: 'Enter', + target: { + value: 'SEARCH', + }, + }); + /* TODO: We want to check here that /search/members?q=SEARCH + * was put into window.location, but inside jest test window.location + * comes from jsdom, and it is not really changed (at least, + * checking window.location returns a weird data structure, + * what is the way to access set url is not that clear). + * Should be investigated. */ +}); diff --git a/__tests__/shared/components/TopcoderHeader/mobile/SubMenu.jsx b/__tests__/shared/components/TopcoderHeader/mobile/SubMenu.jsx new file mode 100644 index 0000000000..5664fa9230 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/SubMenu.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import R from 'react-test-renderer/shallow'; +import SubMenu from 'components/TopcoderHeader/mobile/SubMenu'; + +const renderer = new R(); + +test('Matches snapshot', () => { + renderer.render(( + , + link: '/LINK', + title: 'Item title', + }], + }} + /> + )); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/TopcoderHeader/mobile/UserMenu.jsx b/__tests__/shared/components/TopcoderHeader/mobile/UserMenu.jsx new file mode 100644 index 0000000000..b1ad54d59f --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/UserMenu.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import UserMenu from 'components/TopcoderHeader/mobile/UserMenu'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + , + link: '/link', + title: 'Item title', + }], + }} + profile={{ + handle: 'username', + }} + /> + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Menu.jsx.snap b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Menu.jsx.snap new file mode 100644 index 0000000000..67be78ca81 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Menu.jsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches snapshot 1`] = ` +
+
+ + [ topcoder ] + + × + +
+ + +
+ , + "link": "/LINK", + "title": "Item title", + }, + ], + "title": "Menu title", + } + } + /> +
+`; diff --git a/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Search.jsx.snap b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Search.jsx.snap new file mode 100644 index 0000000000..b8d8a1a6dc --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/Search.jsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+ + +
+`; diff --git a/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/SubMenu.jsx.snap b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/SubMenu.jsx.snap new file mode 100644 index 0000000000..97bd8033a4 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/SubMenu.jsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches snapshot 1`] = ` + +`; diff --git a/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/UserMenu.jsx.snap b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/UserMenu.jsx.snap new file mode 100644 index 0000000000..a1acff7509 --- /dev/null +++ b/__tests__/shared/components/TopcoderHeader/mobile/__snapshots__/UserMenu.jsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snapshot match 1`] = ` +
+ + , + "link": "/link", + "title": "Item title", + }, + ], + "title": "Title", + } + } + /> +
+`; diff --git a/__tests__/shared/components/__snapshots__/Avatar.jsx.snap b/__tests__/shared/components/__snapshots__/Avatar.jsx.snap new file mode 100644 index 0000000000..c36c8fae40 --- /dev/null +++ b/__tests__/shared/components/__snapshots__/Avatar.jsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` + +`; + +exports[`Matches shallow shapshot 2`] = ` + +`; + +exports[`Matches shallow shapshot 3`] = ` + +`; + +exports[`Matches shallow shapshot 4`] = ` + +`; diff --git a/__tests__/shared/components/__snapshots__/Button.jsx.snap b/__tests__/shared/components/__snapshots__/Button.jsx.snap new file mode 100644 index 0000000000..7a252a7b84 --- /dev/null +++ b/__tests__/shared/components/__snapshots__/Button.jsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snapshot match 1`] = ` +
+`; diff --git a/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap b/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap new file mode 100644 index 0000000000..2b946706d4 --- /dev/null +++ b/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+ +
+

+ Topcoder is also on +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ © 2017 Topcoder. All Rights Reserved +

+
+`; diff --git a/__tests__/shared/components/examples/Content.jsx b/__tests__/shared/components/examples/Content.jsx new file mode 100644 index 0000000000..92585cad01 --- /dev/null +++ b/__tests__/shared/components/examples/Content.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import Renderer from 'react-test-renderer/shallow'; +import Content from 'components/examples/Content'; + +test('Matches shallow shapshot', () => { + const renderer = new Renderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/examples/CssModules.jsx b/__tests__/shared/components/examples/CssModules.jsx new file mode 100644 index 0000000000..db67c08578 --- /dev/null +++ b/__tests__/shared/components/examples/CssModules.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import CssModules from 'components/examples/CssModules'; + +test('matches snapshots', () => { + const cmp = renderer.create(); + expect(cmp.toJSON()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/examples/DataFetch.jsx b/__tests__/shared/components/examples/DataFetch.jsx new file mode 100644 index 0000000000..81e0ea15b6 --- /dev/null +++ b/__tests__/shared/components/examples/DataFetch.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import Renderer from 'react-test-renderer/shallow'; +import DataFetch from 'components/examples/DataFetch'; + +test('Matches shallow shapshot', () => { + const renderer = new Renderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/examples/FontsTest.jsx b/__tests__/shared/components/examples/FontsTest.jsx new file mode 100644 index 0000000000..8ddba92d10 --- /dev/null +++ b/__tests__/shared/components/examples/FontsTest.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import FontsTest from 'components/examples/FontsTest'; + +test('matches snapshots', () => { + const cmp = renderer.create(); + expect(cmp.toJSON()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/examples/SvgLoading.jsx b/__tests__/shared/components/examples/SvgLoading.jsx new file mode 100644 index 0000000000..1d0c711d85 --- /dev/null +++ b/__tests__/shared/components/examples/SvgLoading.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import SvgLoading from 'components/examples/SvgLoading'; + +test('matches snapshots', () => { + const cmp = renderer.create(); + expect(cmp.toJSON()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/examples/__snapshots__/Content.jsx.snap b/__tests__/shared/components/examples/__snapshots__/Content.jsx.snap new file mode 100644 index 0000000000..a613f8cae5 --- /dev/null +++ b/__tests__/shared/components/examples/__snapshots__/Content.jsx.snap @@ -0,0 +1,308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+

+ Topcoder Community App +

+

+ Isomorphic ReactJS App for new version of Topcoder community website. Technological stack includes: +

+
    +
  • + Autoprefixer; +
  • +
  • + Babel with latest JS standard support both client- and server-side; +
  • +
  • + ESlint (AirBnB style, run with + + $ npm run lint:js + + ); +
  • +
  • + ExpressJS server; +
  • +
  • + Font loading (Roboto fonts are included into the repo); +
  • +
  • + General Topcoder styles (check + + /src/styles + + ); +
  • +
  • + Hot reload of JS code and SCSS styles in dev environment (start it with + + $ npm run dev + + ); +
  • +
  • + Loading of .svg assets as ReactJS components with + + babel-plugin-inline-react-svg + + ; +
  • +
  • + ReactJS; +
  • +
  • + React CSS Modules (with + + babel-plugin-react-css-modules + + ); +
  • +
  • + Redux with Flex Standard Actions, redux-promise middleware, and a custom pattern of server-side data fetching; +
  • +
  • + SCSS styles; +
  • +
  • + Topcoder API v2 and v3 service (see + + /src/shared/services/api.js + + ), with support of TC authentication (look for auth tokens either in + + store.auth + + of Redux store, or in + + v3jwt + + and + + tcjwt + + cookies of the front-end requests to the server); +
  • +
  • + Stylefmt; +
  • +
  • + Stylelint for SCSS (standard Stylelint style, run with + + $ npm run lint:scss + + ; +
  • +
  • + Webpack; +
  • +
+

+ New Topcoder Pages +

+
    +
  • + + Submission Management Page + + – New submission management page, is available at the endpoint + + /challenge/:challengeId/my-submissions + + . The link here leads to the test challenge. +
  • +
  • + + Community Challenge Listing Page + + – An example of community challenge list apge which shows only challenges with special criteria. In this case only challenges which has JavaScript technology tag. +
  • +
  • + + Leaderboard + + – Leaderboard page. +
  • +
  • + + Community header example + + – An example of cummunity header with default style. Also, there are examples of + + custom red theme + + , + + custom green theme + + and + + non-existent community page + + . +
  • +
  • + + Wipro Community Homepage + + – Example of community implementation. This community has three more pages: + + Learning & Certification + + , + + Challenges + + and + + Leaderboard + + . There are also examples of + + non-existent community page + + and + + non-existent community + + . +
  • +
  • + + Wipro 2 Community Homepage + + – Example of community implementation with new design. This community has three more pages: + + Learn + + , + + Challenges + + and + + Leaderboard + + . There is also an example of + + non-existent community page + + . +
  • +
+

+ Misc Examples +

+
    +
  • + + CSS Modules + + - Demo/test of CSS modules in action; +
  • +
  • + + Data Fetch + + - Demonstrates how data fetching should be implemented in isomorphic way, using Redux with Flux Standard Actions and promise; +
  • +
  • + + Fonts Test + + - A simple showcase of the fonts included into this repo, and the test of their proper inclusion into the bundle; +
  • +
  • + + SVG Loading + + - Shows how to load + + .svg + + assets with use of + + babel-plugin-inline-react-svg + + . +
  • +
  • + + Themr + + - Test/demo of react-css-themr. +
  • +
+
+`; diff --git a/__tests__/shared/components/examples/__snapshots__/CssModules.jsx.snap b/__tests__/shared/components/examples/__snapshots__/CssModules.jsx.snap new file mode 100644 index 0000000000..e37cd6b6b3 --- /dev/null +++ b/__tests__/shared/components/examples/__snapshots__/CssModules.jsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshots 1`] = ` +
+

+ CSS Modules +

+

+ Here is a simple demo/test of CSS modules in action. +

+
+ If CSS modules work fine this text should be red, otherwise - green. +
+
+ This text should be green in all cases. +
+
+`; diff --git a/__tests__/shared/components/examples/__snapshots__/DataFetch.jsx.snap b/__tests__/shared/components/examples/__snapshots__/DataFetch.jsx.snap new file mode 100644 index 0000000000..9db1e919c8 --- /dev/null +++ b/__tests__/shared/components/examples/__snapshots__/DataFetch.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matches shallow shapshot 1`] = ` +
+

+ Data Fetch +

+

+ This is a simple example of how to fetch data from a remote API into the Redux store, using actions and reducers. Two pages available via the links below are implemented with the same code / components, the only difference is that on one route the data are fetched at server side and injected into the page during server-side rendering, thus enhancing page loading time and hence the user experience. At the other route the data are not fetched at the server, thus the code falls back to fetching them at the client-side. Which works fine, but slower. +

+

+ + Server-side data fetch. + +

+

+ + Client-side data fetch. + +

+
+`; diff --git a/__tests__/shared/components/examples/__snapshots__/FontsTest.jsx.snap b/__tests__/shared/components/examples/__snapshots__/FontsTest.jsx.snap new file mode 100644 index 0000000000..d35cf20521 --- /dev/null +++ b/__tests__/shared/components/examples/__snapshots__/FontsTest.jsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshots 1`] = ` +
+

+ Fonts Test +

+

+ This is a simple showcase of the fonts included into this repo, and a test of their proper packing into the bundle. +

+
+

+ Roboto Thin +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas semper consectetur dui, nec scelerisque lectus hendrerit a. Curabitur eget imperdiet orci. Sed non tincidunt turpis, laoreet fringilla nisl. Suspendisse tincidunt ligula arcu, nec hendrerit erat ultricies et. Nam elit nisl, pharetra in leo a, posuere facilisis lacus. Vivamus mollis est ac justo pulvinar iaculis sed ac ex. Cras et maximus enim, eget posuere ante. Cras in viverra quam. Sed lacinia rutrum semper. Praesent mollis turpis elit, vel feugiat nisl sollicitudin nec. Cras ex quam, facilisis eu placerat ac, vehicula a nulla. Vivamus et tellus in est hendrerit rhoncus et a leo. Donec dui lorem, laoreet nec malesuada sed, dictum ut elit. Nullam pretium augue vel odio ultrices, ut commodo lacus imperdiet. Integer maximus imperdiet odio, non dignissim lorem sollicitudin nec. Sed sed cursus metus. +
+
+

+ Roboto Thin Italic +

+ Praesent quam arcu, ultricies et dolor sed, interdum gravida nibh. Proin scelerisque porttitor nibh, nec finibus nibh interdum sit amet. Duis luctus sapien nec velit sollicitudin convallis. Ut eget neque vel nibh lacinia commodo. In ut lorem id quam molestie blandit. Integer in nunc cursus, suscipit sem id, accumsan mi. Sed luctus, quam sit amet fringilla feugiat, nisl lacus pretium nunc, et elementum est odio et risus. Praesent quis cursus urna. Ut orci elit, rutrum id accumsan luctus, cursus sed dui. Phasellus lorem urna, mattis et mauris sit amet, tristique hendrerit sapien. +
+
+

+ Roboto Light +

+ Sed nec dolor blandit, commodo arcu in, tincidunt nulla. In in odio id arcu luctus aliquet non in mi. Ut efficitur, lorem eget mollis tempus, ligula purus varius massa, a malesuada diam urna placerat leo. Sed quis diam ullamcorper, fringilla augue sed, hendrerit felis. Sed eget felis ac nulla feugiat gravida. Donec a sem lobortis, pulvinar nibh eu, convallis magna. Suspendisse tempus tincidunt dolor, id blandit est lacinia eu. Nam fermentum, sapien at dictum consectetur, felis neque mollis libero, at auctor nunc nunc eu tellus. Maecenas ultrices at neque eget tincidunt. Nullam vel consequat nunc, eget efficitur quam. Nam nec elit vitae metus cursus eleifend semper aliquet diam. +
+
+

+ Roboto Light Italic +

+ Nulla suscipit dui et placerat vulputate. Nunc et tempus neque, eget elementum elit. Integer vitae dignissim tellus, et venenatis nulla. Vivamus non lacus et ipsum imperdiet interdum tempus ullamcorper leo. Phasellus tempus magna imperdiet sagittis viverra. Curabitur varius elementum auctor. Nullam quam nisl, vestibulum et magna pharetra, vestibulum placerat leo. Aliquam faucibus maximus urna, sed mattis ex pretium in. Nam eu enim vitae massa vestibulum iaculis. Quisque nec risus varius, eleifend urna non, ornare est. Curabitur gravida tempus eros, posuere pellentesque magna euismod sed. Donec sed justo ut dolor accumsan gravida vitae nec neque. Proin ac tellus dui. Integer ac euismod massa. +
+
+

+ Roboto Regular +

+ Morbi a urna maximus, imperdiet ante id, rutrum sem. Nunc fermentum ante sodales convallis placerat. Donec eleifend, metus eget congue semper, lorem nibh vehicula velit, eu sollicitudin mi orci eget purus. Pellentesque accumsan fermentum arcu et hendrerit. Donec non porta purus. Vivamus eu venenatis sapien. Nullam et mi at eros finibus ultrices eget sit amet est. Curabitur non diam ornare est dapibus tempor a ut turpis. Nulla ut nibh metus. Vivamus hendrerit turpis nisl, eget fermentum nulla egestas quis. +
+
+

+ Roboto Regular Italic +

+ Nam vel ligula in ipsum condimentum sodales. Praesent id lorem tortor. In vel condimentum leo, nec rhoncus elit. Sed accumsan metus vitae diam ultricies, eu vestibulum metus pretium. Nullam congue, purus a tempor venenatis, leo dui blandit nibh, nec fermentum ante eros ac ipsum. Maecenas id neque ligula. Ut vitae faucibus lectus, vel tempus ipsum. Donec ut erat lobortis, vestibulum enim vel, scelerisque turpis. Aliquam ornare velit at elementum euismod. +
+
+

+ Roboto Medium +

+ Ut laoreet rhoncus vulputate. Quisque elementum quam justo, ac eleifend mauris viverra eget. Nunc sit amet commodo est. Nullam scelerisque elit ac porttitor finibus. Sed laoreet urna non enim molestie, iaculis suscipit felis commodo. Vestibulum gravida ante porttitor urna hendrerit, quis dapibus sem viverra. Praesent consectetur risus ac finibus varius. Ut commodo felis vel laoreet ornare. Donec imperdiet sagittis efficitur. Nunc in dui id ligula blandit vehicula a id leo. In pulvinar felis eget tortor pretium pulvinar. Integer a mi a justo sagittis finibus ac eget nisl. In eu dictum lectus, eu accumsan purus. +
+
+

+ Roboto Medium Italic +

+ Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum eget eros malesuada lacus porta scelerisque. Integer in dolor et metus dictum faucibus sit amet a mauris. Vivamus eget volutpat nulla, non posuere sapien. Vivamus mattis vehicula justo eu faucibus. Nunc eleifend mollis ultricies. Integer elementum ipsum eu nisi sodales, eget ornare quam posuere. Maecenas sit amet sem mattis, porttitor neque ut, molestie velit. +
+
+

+ Roboto Bold +

+ Nulla quis cursus orci. Mauris metus enim, volutpat id diam ac, fermentum dapibus augue. Donec mi elit, volutpat eget rutrum non, lobortis ac enim. In tempus iaculis turpis, vitae facilisis quam vehicula eget. Ut blandit, elit at porta vulputate, orci ipsum fermentum nunc, non dignissim lectus metus a velit. In hac habitasse platea dictumst. Mauris tincidunt, sem quis interdum ullamcorper, erat velit interdum lacus, eu tincidunt eros lacus vitae libero. +
+
+

+ Roboto Bold Italic +

+ Donec luctus ligula id augue blandit porta. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras sem eros, iaculis pulvinar gravida vitae, interdum eu lacus. Praesent rutrum sem a dolor viverra aliquet. Vestibulum dictum tempus fringilla. Pellentesque eu eros elit. Integer fringilla ipsum sed hendrerit rhoncus. +
+
+

+ Roboto Black +

+ In varius nibh elit. Nam nec pretium erat. Duis euismod mi vel massa scelerisque, ut tincidunt urna viverra. Praesent vel libero eros. Etiam a accumsan nulla. Nulla consequat venenatis risus quis accumsan. Etiam placerat pretium faucibus. Proin consequat in ante hendrerit lobortis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut eget est tempus, aliquam enim quis, pellentesque erat. Nulla dapibus diam interdum vehicula dignissim. Donec orci velit, varius sed nisi semper, lobortis bibendum neque. +
+
+

+ Roboto Black Italic +

+ Proin felis velit, suscipit sit amet consequat id, consectetur et lectus. Donec porttitor sollicitudin lorem sed laoreet. Fusce rhoncus mi id nulla cursus mollis. Sed scelerisque et sem id eleifend. Maecenas quis nisi non diam tempor mattis at ut tortor. Ut auctor est odio, id scelerisque massa facilisis in. Suspendisse sollicitudin rutrum porta. Sed at purus eget lacus finibus sagittis. Sed nulla ligula, sagittis quis ipsum vel, finibus feugiat eros. Fusce non enim a lectus imperdiet auctor. Aliquam mattis molestie ante vel dignissim. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. +
+
+`; diff --git a/__tests__/shared/components/examples/__snapshots__/SvgLoading.jsx.snap b/__tests__/shared/components/examples/__snapshots__/SvgLoading.jsx.snap new file mode 100644 index 0000000000..f7e9080c41 --- /dev/null +++ b/__tests__/shared/components/examples/__snapshots__/SvgLoading.jsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches snapshots 1`] = ` +
+

+ SVG Loading +

+

+ This component show how to load + + .svg + + assets with use of + + babel-plugin-inline-react-svg + + . +

+ + + + + A Sample SVG asset :) + + + +
+`; diff --git a/__tests__/shared/components/tc-communities/Accordion.jsx b/__tests__/shared/components/tc-communities/Accordion.jsx new file mode 100644 index 0000000000..fb3d893312 --- /dev/null +++ b/__tests__/shared/components/tc-communities/Accordion.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import Accordion from 'components/tc-communities/Accordion/Accordion'; +import AccordionItem from 'components/tc-communities/Accordion/AccordionItem'; +import TU from 'react-dom/test-utils'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + + +
Test content
+
+
+ )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); + +class Wrapper extends React.Component { + componentDidMount() {} + render() { + return ( +
+ + +
Test content 1
+
+ +
Test content 2
+
+
+
+ ); + } +} + +const page = TU.renderIntoDocument(( + +)); + +describe('Rendered title list', () => { + beforeEach(() => jest.clearAllMocks()); + + test('Rendered title list', () => { + const titleListItem = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/__titleListItem___/)); + expect(titleListItem.length).toBe(2); + }); +}); diff --git a/__tests__/shared/components/tc-communities/AccordionItem.jsx b/__tests__/shared/components/tc-communities/AccordionItem.jsx new file mode 100644 index 0000000000..4bf65398f7 --- /dev/null +++ b/__tests__/shared/components/tc-communities/AccordionItem.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import AccordionItem from 'components/tc-communities/Accordion/AccordionItem'; +import TU from 'react-dom/test-utils'; + +const mockOnTitleClick = jest.fn(); + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + +
Test content
+
+ )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); + + rnd.render(( + +
Test content
+
+ )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); + +class Wrapper extends React.Component { + componentDidMount() {} + render() { + return ( +
+ +
Test content
+
+
+ ); + } +} + +const page = TU.renderIntoDocument(( + +)); + +describe('Click on title', () => { + beforeEach(() => jest.clearAllMocks()); + + test('onTitleClick', () => { + const btn = TU.findAllInRenderedTree(page, item => + item && item.className && item.className.match(/__title___/)); + expect(btn.length).toBe(1); + TU.Simulate.click(btn[0]); + expect(mockOnTitleClick).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/shared/components/tc-communities/ArticleCard.jsx b/__tests__/shared/components/tc-communities/ArticleCard.jsx new file mode 100644 index 0000000000..3e9541616c --- /dev/null +++ b/__tests__/shared/components/tc-communities/ArticleCard.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import ArticleCard from 'components/tc-communities/ArticleCard'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); + + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/tc-communities/Banner.jsx b/__tests__/shared/components/tc-communities/Banner.jsx new file mode 100644 index 0000000000..77a85c966c --- /dev/null +++ b/__tests__/shared/components/tc-communities/Banner.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import Banner from 'components/tc-communities/Banner'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); + + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/tc-communities/Dropdown.jsx b/__tests__/shared/components/tc-communities/Dropdown.jsx new file mode 100644 index 0000000000..a25b5b8850 --- /dev/null +++ b/__tests__/shared/components/tc-communities/Dropdown.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import Dropdown from 'components/tc-communities/Dropdown'; + +const rnd = new Rnd(); + +const dropdownOptions = [ + { + label: 'iOS Community', + value: '1', + }, { + label: 'Predix Topcoder', + value: '2', + }, { + label: 'Cognitive Topcoder', + value: '3', + }, { + label: 'Android Community', + value: '4', + }, +]; + +test('Snapshot match', () => { + rnd.render(( + + )); + expect(rnd.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/__tests__/shared/components/tc-communities/Footer.jsx b/__tests__/shared/components/tc-communities/Footer.jsx new file mode 100644 index 0000000000..ae5fd51676 --- /dev/null +++ b/__tests__/shared/components/tc-communities/Footer.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import Rnd from 'react-test-renderer/shallow'; +import Footer from 'components/tc-communities/Footer'; + +const rnd = new Rnd(); + +test('Snapshot match', () => { + rnd.render(( +