initial commit
This commit is contained in:
10
.dockerignore
Executable file
10
.dockerignore
Executable file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
LICENSE
|
||||
.babelrc
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.npmignore
|
||||
commitlint.config.js
|
||||
32
.env
Normal file
32
.env
Normal file
@@ -0,0 +1,32 @@
|
||||
NODE_ENV='production'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
DATA_API_BASE_URL=''
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
NEW_RELIC_APP_ID=''
|
||||
NEW_RELIC_LICENSE_KEY=''
|
||||
SITE_NAME=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL=''
|
||||
OPEN_SOURCE_URL=''
|
||||
TERMS_OF_SERVICE_URL=''
|
||||
PRIVACY_POLICY_URL=''
|
||||
FACEBOOK_URL=''
|
||||
TWITTER_URL=''
|
||||
YOU_TUBE_URL=''
|
||||
LINKED_IN_URL=''
|
||||
REDDIT_URL=''
|
||||
APPLE_APP_STORE_URL=''
|
||||
GOOGLE_PLAY_URL=''
|
||||
ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
38
.env.development
Normal file
38
.env.development
Normal file
@@ -0,0 +1,38 @@
|
||||
NODE_ENV='development'
|
||||
PORT=1994
|
||||
BASE_URL='localhost:1994'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SITE_NAME=localhost
|
||||
DATA_API_BASE_URL='http://localhost:8000'
|
||||
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
|
||||
LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
|
||||
FACEBOOK_URL='https://www.facebook.com'
|
||||
TWITTER_URL='https://twitter.com'
|
||||
YOU_TUBE_URL='https://www.youtube.com'
|
||||
LINKED_IN_URL='https://www.linkedin.com'
|
||||
REDDIT_URL='https://www.reddit.com'
|
||||
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
|
||||
GOOGLE_PLAY_URL='https://play.google.com/store'
|
||||
ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
5
.eslintignore
Executable file
5
.eslintignore
Executable file
@@ -0,0 +1,5 @@
|
||||
coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
paths: ["src", "node_modules"],
|
||||
extensions: [".js", ".jsx"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
23
.gitignore
vendored
Executable file
23
.gitignore
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
13
.npmignore
Executable file
13
.npmignore
Executable file
@@ -0,0 +1,13 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
config
|
||||
coverage
|
||||
node_modules
|
||||
public
|
||||
27
.releaserc
Normal file
27
.releaserc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
28
.travis.yml
Executable file
28
.travis.yml
Executable file
@@ -0,0 +1,28 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- masters-grades@edx.org
|
||||
on_success: never
|
||||
on_failure: always
|
||||
webhooks: https://www.travisbuddy.com/
|
||||
on_success: never
|
||||
before_install:
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
- npm run travis-deploy-once "npm run semantic-release"
|
||||
- npm run coveralls
|
||||
env:
|
||||
global:
|
||||
- secure: bBLQZVw1aVUxB7GFNXGrdKeztyFrCCJusVgFcSuej9S4qmj9/jrVsEc9dEcH+BMS+b49+SvILoxzd6ZYLaRygQLzevnO1/dX596DeCKVK48PTTZRsNyafaSMCkxNKqEmRcA9hYL52xJJ5GpKo7ViWsFy8VFgUfZEJxQi8/lYbfQ1vlXRpo2LJfJh09v85roSXdQmajyGJ1Dz6elcwUX5B+BgXmIHizJXUMfFci61xTEZmgKtfeCiwFQA5pCvVMHBQhgySqT2N3eRESzRt2jAfAdcRKBYXS0rwKymdlL1ZF349Jm8xwtqm19Fwsut21181Lnn6FmccMWhQ7man3WH1xfT0ahmHNs1KJMyZcwRJd/gDfbd6iD3LB9Pt9hEQ00Qh/m7MYeahMxTEL9bp2TyILi8cTP91jeBUHCExCdv2jRrUQEnUS5vZUYRdM8CR2DLoLmNh3APndKzwgr5U8rh6RdhbQBJp97Hb/YYVrBiP2atLJAaYPY/xEQHK/YoXelQgiZ6wHBMV+tF/L0ZRn7KyVWdkbBKWfbEjRKbEJD9WD+V7HayMR81tm5CSqlrG8mTvSy2boIGiX14GV11ZEfMj5bjb6W41BW+QGqQerZvmwk/4ywe304X85PD0OBhIYPRzeLIi0Gt6lD1aOpVxgm4M03tdgYQzCPWRPq32CB+1IA=
|
||||
- secure: w1d/E+cc4+Bf017Jpp9YsKBzLSZw9sqKZGeM2tNrO6eJZbMJqfKTmfUrRw8BoLh1Z8YRkHF7RADDy3ln7XEdeAX3j9OoC3Cz0zN6iDX6TPcI461NuOIscJYb4tyFcuWm6FhgVlBAlo/BI3q+zqKwjfWuDaORpk6+haacCmvTe5V0vWhY+MYT7M+LfnKeKVzhI4magGt8jPTE21oziIFwCqCCjJc4+AmsWoWTzU0Q7Db0DZiJnLXFfXybLbkedAgJmcSgEGZCSpaZIOkX0/Lbazsz1Ky4KASfkrYT1Z5iKQ8TE3skmx1IIu+1egN8iBbdrY+NhvV24RkT+rpUvD7TBIHTrjQ5JYLe0kGjN70vG7YlKgjNSyTjkrEd7fCKpuIol3DVjBRz3tV5aCl0t/A8mIPqKyNI94MamWsExpqsxgcb9vBVno5caZvD8ZXNrGNqanB3MSoLGxZTLKif9u+AZfLnB3xtjaiJg3/BNoWaOBPlp/M6BvGIGHElwvLrAhUvl8wzrwJcQQWpmRMh0b6enr6Y7ox/mGGs7NBCT+CNKEsWeCfY4thZzgi6/GocXyqdTpXMkNSI1PDoPmi+vKafBd+7aAYbcUlJBTU6TAxyncln0tF2JF+ghTZ0v8nNzEQ9VmV4ddyoOHx6YnHvEcenWZGMROQnMCVifyDbaHpPbPI=
|
||||
8
.tx/config
Normal file
8
.tx/config
Normal file
@@ -0,0 +1,8 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
65
Makefile
Executable file
65
Makefile
Executable file
@@ -0,0 +1,65 @@
|
||||
npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = ora-enhanced-staff-grader
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
.PHONY: test.npm.*
|
||||
test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
test -d node_modules || $(MAKE) requirements
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
rm -rf $(transifex_temp)
|
||||
npm run-script i18n_extract
|
||||
|
||||
i18n.concat:
|
||||
# Gathering JSON messages into one file...
|
||||
$(transifex_utils) $(transifex_temp) $(transifex_input)
|
||||
|
||||
extract_translations: | requirements i18n.extract i18n.concat
|
||||
|
||||
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
|
||||
detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
77
documentation/.travis.yml.md
Executable file
77
documentation/.travis.yml.md
Executable file
@@ -0,0 +1,77 @@
|
||||
# Travis Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.travis.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Caching node_modules
|
||||
|
||||
While [the `Travis` blog](https://blog.travis-ci.com/2016-11-21-travis-ci-now-supports-yarn) recommends
|
||||
|
||||
```yaml
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
```
|
||||
|
||||
this causes issues when testing different versions of `Node` because [`node_modules` will store the compiled native modules](https://stackoverflow.com/a/42523517/5225575).
|
||||
|
||||
Caching the `~/.npm` directory avoids storing these native modules.
|
||||
|
||||
## Notifications
|
||||
|
||||
This project uses a service called [`TravisBuddy`](https://www.travisbuddy.com/), which provides Travis build context within a PR via webhooks (configured only to add feedback for build failures).
|
||||
|
||||

|
||||
|
||||
## Installing `greenkeeper-lockfile`
|
||||
|
||||
As explained in [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#greenkeeper-step-by-step), `Greenkeeper` is a service that keeps track of your project's dependencies, and will, for example, automatically open PRs with an updated `package.json` file when the latest version of a dependency is a major version ahead of the existing dependency version in your `package.json` file.
|
||||
|
||||
This automated updating is great, but `Greenkeeper` does not update your `package-lock.json` file, just your `package.json` file. This makes sense, as the only way to update the `package-lock.json` file would be to run `npm install` when building your project, using the latest `package.json`, and then committing the updated `package-lock.json` file.
|
||||
|
||||
This is essentially what you have to do manually when `Greenkeeper` opens a PR - `git checkout` the branch, `npm install` locally, `git commit` the `package-lock.json` changes, and then `git push` those changes to the `Greenkeeper` branch on `origin`. It's fun probably only the first time, and even then it gets old, fast.
|
||||
|
||||
What [`greenkeeper-lockfile`](https://github.com/greenkeeperio/greenkeeper-lockfile) does is that it automates the previous steps as part of the build process.
|
||||
|
||||
It will
|
||||
* Check that the branch is a `Greenkeeper` branch
|
||||
* Update the lockfile
|
||||
* Push a commit with the updated lockfile back to the Greenkeeper branch
|
||||
|
||||
This is why it's important to install `greenkeeper-lockfile` in the `before_install` step, and since it's used exclusively only in the Travis Build, why it's not part of the package's dependencies.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in Travis *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a Travis build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [`Travis`'s GitHub pages configuration](https://docs.travis-ci.com/user/deployment/pages/).
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
15
jest.config.js
Normal file
15
jest.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
],
|
||||
});
|
||||
9
openedx.yaml
Normal file
9
openedx.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
tags:
|
||||
- frontend-app
|
||||
- masters
|
||||
oeps:
|
||||
oep-2: true # Repository metadata
|
||||
openedx-release: {ref: master}
|
||||
30693
package-lock.json
generated
Normal file
30693
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
package.json
Executable file
84
package.json
Executable file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-ora-enhanced-staff-grader",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-ora-enhanced-staff-grader.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"prepush": "npm run lint",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.1",
|
||||
"@edx/frontend-platform": "1.9.5",
|
||||
"@edx/paragon": "14.16.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||
"@redux-beacon/segment": "^1.0.0",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"email-prop-type": "^1.1.7",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux": "4.0.5",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"util": "^0.12.3",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.2",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"codecov": "^3.6.1",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "24.9.0",
|
||||
"react-dev-utils": "^5.0.3",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^17.2.3",
|
||||
"travis-deploy-once": "^5.0.11"
|
||||
}
|
||||
}
|
||||
12
public/index.html
Executable file
12
public/index.html
Executable file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>ORA Enhanced Staff Grader | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
34
src/App.jsx
Executable file
34
src/App.jsx
Executable file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import ListView from 'containers/ListView';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={ListView}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
13
src/App.scss
Executable file
13
src/App.scss
Executable file
@@ -0,0 +1,13 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
80
src/App.test.jsx
Normal file
80
src/App.test.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import App from './App';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: () => 'Provider',
|
||||
}));
|
||||
jest.mock('react-intl', () => ({
|
||||
IntlProvider: () => 'IntlProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0).childAt(0);
|
||||
});
|
||||
describe('IntlProvider', () => {
|
||||
test('outer-wrapper component', () => {
|
||||
expect(el.type()).toBe(IntlProvider);
|
||||
});
|
||||
test('"en" locale', () => {
|
||||
expect(el.props().locale).toEqual('en');
|
||||
});
|
||||
});
|
||||
describe('Provider, inside IntlProvider', () => {
|
||||
test('first child, passed the redux store props', () => {
|
||||
expect(el.childAt(0).type()).toBe(Provider);
|
||||
expect(el.childAt(0).props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('first child of Provider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={routePath} component={ListView} />
|
||||
</Switch>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
src/__snapshots__/App.test.jsx.snap
Normal file
26
src/__snapshots__/App.test.jsx.snap
Normal file
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<Provider
|
||||
store="testStore"
|
||||
>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="ListView"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
`;
|
||||
16
src/config/index.js
Normal file
16
src/config/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
// LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
// LOGIN_URL: process.env.LOGIN_URL,
|
||||
// LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
// SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
export { configuration, features };
|
||||
17
src/containers/ListView/index.jsx
Normal file
17
src/containers/ListView/index.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
/**
|
||||
* <ListView />
|
||||
*/
|
||||
export const ListView = () => (
|
||||
<div id="ora-esg-list-view" />
|
||||
);
|
||||
ListView.defaultProps = {};
|
||||
ListView.propTypes = {};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ListView);
|
||||
7
src/data/actions/app.js
Normal file
7
src/data/actions/app.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { StrictDict } from 'utils';
|
||||
// import { createActionFactory } from './utils';
|
||||
|
||||
// export const dataKey = 'app';
|
||||
// const createAction = createActionFactory(dataKey);
|
||||
|
||||
export default StrictDict({ });
|
||||
21
src/data/actions/index.js
Normal file
21
src/data/actions/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import app from './app';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import cohorts from './cohorts';
|
||||
import config from './config';
|
||||
import filters from './filters';
|
||||
import grades from './grades';
|
||||
import roles from './roles';
|
||||
import tracks from './tracks';
|
||||
|
||||
export default StrictDict({
|
||||
app,
|
||||
assignmentTypes,
|
||||
cohorts,
|
||||
config,
|
||||
filters,
|
||||
grades,
|
||||
roles,
|
||||
tracks,
|
||||
});
|
||||
48
src/data/actions/testUtils.js
Normal file
48
src/data/actions/testUtils.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* testActionTypes(actionTypes, dataKey)
|
||||
* Takes a list of actionTypes and a module dataKey, and verifies that
|
||||
* * all actionTypes are unique
|
||||
* * all actionTypes begin with the dataKey
|
||||
* @param {string[]} actionTypes - list of action types
|
||||
* @param {string} dataKey - module data key
|
||||
*/
|
||||
export const testActionTypes = (actionTypes, dataKey) => {
|
||||
test('all types are unique', () => {
|
||||
expect(actionTypes.length).toEqual((new Set(actionTypes)).size);
|
||||
});
|
||||
test('all types begin with the module dataKey', () => {
|
||||
actionTypes.forEach(type => {
|
||||
expect(type.startsWith(dataKey)).toEqual(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* testAction(action, args, expectedPayload)
|
||||
* Multi-purpose action creator test function.
|
||||
* If args/expectedPayload are passed, verifies that it produces the expected output when called
|
||||
* with the given args.
|
||||
* If none are passed, (for action creators with basic definition) it tests against a default
|
||||
* test payload.
|
||||
* @param {object} action - action creator object/method
|
||||
* @param {[object]} args - optional payload argument
|
||||
* @param {[object]} expectedPayload - optional expected payload.
|
||||
*/
|
||||
export const testAction = (action, args, expectedPayload) => {
|
||||
const type = action.toString();
|
||||
if (args) {
|
||||
if (Array.isArray(args)) {
|
||||
expect(action(...args)).toEqual({ type, payload: expectedPayload });
|
||||
} else {
|
||||
expect(action(args)).toEqual({ type, payload: expectedPayload });
|
||||
}
|
||||
} else {
|
||||
const payload = { test: 'PAYload' };
|
||||
expect(action(payload)).toEqual({ type, payload });
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
testAction,
|
||||
testActionTypes,
|
||||
};
|
||||
10
src/data/actions/utils.js
Normal file
10
src/data/actions/utils.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
const createActionFactory = (dataKey) => (actionKey, ...args) => (
|
||||
createAction(`${dataKey}/${actionKey}`, ...args)
|
||||
);
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
createActionFactory,
|
||||
};
|
||||
19
src/data/actions/utils.test.js
Normal file
19
src/data/actions/utils.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import * as utils from './utils';
|
||||
|
||||
jest.mock('@reduxjs/toolkit', () => ({
|
||||
createAction: (key, ...args) => ({ action: key, args }),
|
||||
}));
|
||||
|
||||
describe('redux action utils', () => {
|
||||
describe('createActionFactory', () => {
|
||||
it('returns an action creator with the data key', () => {
|
||||
const dataKey = 'part-of-the-model';
|
||||
const actionKey = 'an-action';
|
||||
const args = ['some', 'args'];
|
||||
expect(utils.createActionFactory(dataKey)(actionKey, ...args)).toEqual(
|
||||
createAction(`${dataKey}/${actionKey}`, ...args),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
4
src/data/constants/app.js
Normal file
4
src/data/constants/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
14
src/data/reducers/app.js
Normal file
14
src/data/reducers/app.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// import actions from '../actions/app';
|
||||
|
||||
const initialState = { };
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const app = (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default app;
|
||||
10
src/data/reducers/index.js
Executable file
10
src/data/reducers/index.js
Executable file
@@ -0,0 +1,10 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import app from './app';
|
||||
|
||||
/* istanbul ignore next */
|
||||
const rootReducer = combineReducers({
|
||||
app,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
3
src/data/selectors/app.js
Normal file
3
src/data/selectors/app.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export default StrictDict({});
|
||||
7
src/data/selectors/index.js
Normal file
7
src/data/selectors/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import app from './app';
|
||||
|
||||
export default StrictDict({
|
||||
app,
|
||||
});
|
||||
34
src/data/store.js
Executable file
34
src/data/store.js
Executable file
@@ -0,0 +1,34 @@
|
||||
import * as redux from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
|
||||
export const createStore = () => {
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const middleware = [thunkMiddleware, loggerMiddleware];
|
||||
|
||||
const store = redux.createStore(
|
||||
reducers,
|
||||
composeWithDevTools(redux.applyMiddleware(...middleware)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Dev tools for redux work
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.store = store;
|
||||
window.actions = actions;
|
||||
window.selectors = selectors;
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
const store = createStore();
|
||||
|
||||
export default store;
|
||||
65
src/data/store.test.js
Normal file
65
src/data/store.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { applyMiddleware } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
|
||||
import exportedStore, { createStore } from './store';
|
||||
|
||||
jest.mock('./reducers', () => 'REDUCER');
|
||||
jest.mock('./actions', () => 'ACTIONS');
|
||||
jest.mock('./selectors', () => 'SELECTORS');
|
||||
|
||||
jest.mock('redux-logger', () => ({
|
||||
createLogger: () => 'logger',
|
||||
}));
|
||||
jest.mock('redux-thunk', () => 'thunkMiddleware');
|
||||
jest.mock('redux', () => ({
|
||||
applyMiddleware: (...middleware) => ({ applied: middleware }),
|
||||
createStore: (reducer, middleware) => ({ reducer, middleware }),
|
||||
}));
|
||||
jest.mock('redux-devtools-extension/logOnlyInProduction', () => ({
|
||||
composeWithDevTools: (middleware) => ({ withDevTools: middleware }),
|
||||
}));
|
||||
|
||||
describe('store aggregator module', () => {
|
||||
describe('exported store', () => {
|
||||
it('is generated by createStore', () => {
|
||||
expect(exportedStore).toEqual(createStore());
|
||||
});
|
||||
it('creates store with connected reducers', () => {
|
||||
expect(createStore().reducer).toEqual(reducers);
|
||||
});
|
||||
describe('middleware', () => {
|
||||
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
|
||||
expect(createStore().middleware).toEqual(
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, createLogger())),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('dev exposed tools', () => {
|
||||
beforeEach(() => {
|
||||
window.store = undefined;
|
||||
window.actions = undefined;
|
||||
window.selectors = undefined;
|
||||
});
|
||||
it('exposes redux tools if in development env', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const store = createStore();
|
||||
expect(window.store).toEqual(store);
|
||||
expect(window.actions).toEqual(actions);
|
||||
expect(window.selectors).toEqual(selectors);
|
||||
});
|
||||
it('does not expose redux tools if in production env', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
createStore();
|
||||
expect(window.store).toEqual(undefined);
|
||||
expect(window.actions).toEqual(undefined);
|
||||
expect(window.selectors).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
8
src/data/thunkActions/app.js
Normal file
8
src/data/thunkActions/app.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const initialize = () => (dispatch) => {};
|
||||
|
||||
export default StrictDict({
|
||||
initialize,
|
||||
});
|
||||
6
src/data/thunkActions/index.js
Normal file
6
src/data/thunkActions/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import app from './app';
|
||||
|
||||
export default StrictDict({
|
||||
app,
|
||||
});
|
||||
53
src/data/thunkActions/testUtils.js
Normal file
53
src/data/thunkActions/testUtils.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
/** createTestFetcher(mockedMethod, thunkAction, args, onDispatch)
|
||||
* Creates a testFetch method, which will test a given thunkAction of the form:
|
||||
* ```
|
||||
* const <thunkAction> = (<args>) => (dispatch, getState) => {
|
||||
* ...
|
||||
* return <mockedMethod>.then().catch();
|
||||
* ```
|
||||
* The returned function will take a promise handler function, a list of expected actions
|
||||
* to have been dispatched (objects only), and an optional verifyFn method to be called after
|
||||
* the fetch has been completed.
|
||||
*
|
||||
* @param {fn} mockedMethod - already-mocked api method being exercised by the thunkAction.
|
||||
* @param {fn} thunkAction - thunkAction to call/test
|
||||
* @param {array} args - array of args to dispatch the thunkAction with
|
||||
* @param {[fn]} onDispatch - optional function to be called after dispatch
|
||||
*
|
||||
* @return {fn} testFetch method
|
||||
* @param {fn} resolveFn - promise handler of the form (resolve, reject) => {}.
|
||||
* should return a call to resolve or reject with response data.
|
||||
* @param {object[]} expectedActions - array of action objects expected to have been dispatched
|
||||
* will be verified after the thunkAction resolves
|
||||
* @param {[fn]} verifyFn - optional function to be called after dispatch
|
||||
*/
|
||||
export const createTestFetcher = (
|
||||
mockedMethod,
|
||||
thunkAction,
|
||||
args,
|
||||
onDispatch,
|
||||
) => (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockedMethod.mockReturnValue(new Promise(resolve => {
|
||||
resolve(new Promise(resolveFn));
|
||||
}));
|
||||
return store.dispatch(thunkAction(...args)).then(() => {
|
||||
onDispatch();
|
||||
if (expectedActions !== undefined) {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
createTestFetcher,
|
||||
};
|
||||
19
src/data/utils.js
Normal file
19
src/data/utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Simple selector factory.
|
||||
* Takes a list of string keys, and returns a simple slector for each.
|
||||
*
|
||||
* @function
|
||||
* @param {Object|string[]} keys - If passed as object, Object.keys(keys) is used.
|
||||
* @return {Object} - object of `{[key]: ({key}) => key}`
|
||||
*/
|
||||
const simpleSelectorFactory = (transformer, keys) => {
|
||||
const selKeys = Array.isArray(keys) ? keys : Object.keys(keys);
|
||||
return selKeys.reduce(
|
||||
(obj, key) => ({
|
||||
...obj, [key]: (state) => transformer(state)[key],
|
||||
}),
|
||||
{ root: (state) => transformer(state) },
|
||||
);
|
||||
};
|
||||
|
||||
export default simpleSelectorFactory;
|
||||
29
src/data/utils.test.js
Normal file
29
src/data/utils.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import simpleSelectorFactory from './utils';
|
||||
|
||||
describe('Redux utilities - creators', () => {
|
||||
describe('simpleSelectors', () => {
|
||||
const data = { a: 1, b: 2, c: 3 };
|
||||
const state = {
|
||||
testGroup: data,
|
||||
other: 'stuff',
|
||||
};
|
||||
const transformer = ({ testGroup }) => testGroup;
|
||||
|
||||
test('given a list of strings, returns a dict w/ a simple selector per string', () => {
|
||||
const keys = ['a', 'b'];
|
||||
const selectors = simpleSelectorFactory(transformer, keys);
|
||||
expect(Object.keys(selectors)).toEqual(['root', ...keys]);
|
||||
expect(selectors.root(state)).toEqual(data);
|
||||
expect(selectors.a(state)).toEqual(data.a);
|
||||
expect(selectors.b(state)).toEqual(data.b);
|
||||
});
|
||||
test('given an object for keys, returns a dict w/ simple selector per key', () => {
|
||||
const selectors = simpleSelectorFactory(transformer, data);
|
||||
expect(Object.keys(selectors)).toEqual(['root', ...Object.keys(data)]);
|
||||
expect(selectors.root(state)).toEqual(data);
|
||||
expect(selectors.a(state)).toEqual(data.a);
|
||||
expect(selectors.b(state)).toEqual(data.b);
|
||||
expect(selectors.c(state)).toEqual(data.c);
|
||||
});
|
||||
});
|
||||
});
|
||||
14
src/i18n/index.jsx
Normal file
14
src/i18n/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import arMessages from './messages/ar.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
};
|
||||
|
||||
export default messages;
|
||||
2
src/i18n/messages/ar.json
Normal file
2
src/i18n/messages/ar.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/es_419.json
Normal file
2
src/i18n/messages/es_419.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/fr.json
Normal file
2
src/i18n/messages/fr.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/zh_CN.json
Normal file
2
src/i18n/messages/zh_CN.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
27
src/index.jsx
Executable file
27
src/index.jsx
Executable file
@@ -0,0 +1,27 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
APP_READY,
|
||||
initialize,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import appMessages from './i18n';
|
||||
import App from './App';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
});
|
||||
|
||||
initialize({
|
||||
messages: [
|
||||
appMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: true,
|
||||
});
|
||||
51
src/index.test.jsx
Normal file
51
src/index.test.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
APP_READY,
|
||||
initialize,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import appMessages from './i18n';
|
||||
import App from './App';
|
||||
import '.';
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
render: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
APP_READY: 'app-is-ready-key',
|
||||
initialize: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
jest.mock('./App', () => 'App');
|
||||
|
||||
describe('app registry', () => {
|
||||
let getElement;
|
||||
|
||||
beforeEach(() => {
|
||||
getElement = window.document.getElementById;
|
||||
window.document.getElementById = jest.fn(id => ({ id }));
|
||||
});
|
||||
afterAll(() => {
|
||||
window.document.getElementById = getElement;
|
||||
});
|
||||
test('subscribe is called for APP_READY, linking App to root element', () => {
|
||||
const callArgs = subscribe.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(APP_READY);
|
||||
expect(callArgs[1]()).toEqual(
|
||||
ReactDOM.render(<App />, document.getElementById('root')),
|
||||
);
|
||||
});
|
||||
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
|
||||
expect(initialize).toHaveBeenCalledWith({
|
||||
messages: [appMessages, footerMessages],
|
||||
requireAuthenticatedUser: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
7
src/postcss.config.js
Normal file
7
src/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/* I'm here to allow autoprefixing in webpack.prod.config.js */
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')({ grid: true, browsers: ['>1%'] }),
|
||||
],
|
||||
};
|
||||
|
||||
85
src/segment.js
Normal file
85
src/segment.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// The code in this file is from Segment's website:
|
||||
// https://segment.com/docs/sources/website/analytics.js/quickstart/
|
||||
import { configuration } from './config';
|
||||
|
||||
(function () {
|
||||
// Create a queue, but don't obliterate an existing one!
|
||||
const analytics = window.analytics = window.analytics || [];
|
||||
|
||||
// If the real analytics.js is already on the page return.
|
||||
if (analytics.initialize) return;
|
||||
|
||||
// If the snippet was invoked already show an error.
|
||||
if (analytics.invoked) {
|
||||
if (window.console && console.error) {
|
||||
console.error('Segment snippet included twice.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoked flag, to make sure the snippet
|
||||
// is never invoked twice.
|
||||
analytics.invoked = true;
|
||||
|
||||
// A list of the methods in Analytics.js to stub.
|
||||
analytics.methods = [
|
||||
'trackSubmit',
|
||||
'trackClick',
|
||||
'trackLink',
|
||||
'trackForm',
|
||||
'pageview',
|
||||
'identify',
|
||||
'reset',
|
||||
'group',
|
||||
'track',
|
||||
'ready',
|
||||
'alias',
|
||||
'debug',
|
||||
'page',
|
||||
'once',
|
||||
'off',
|
||||
'on',
|
||||
];
|
||||
|
||||
// Define a factory to create stubs. These are placeholders
|
||||
// for methods in Analytics.js so that you never have to wait
|
||||
// for it to load to actually record data. The `method` is
|
||||
// stored as the first argument, so we can replay the data.
|
||||
analytics.factory = function (method) {
|
||||
return function () {
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
args.unshift(method);
|
||||
analytics.push(args);
|
||||
return analytics;
|
||||
};
|
||||
};
|
||||
|
||||
// For each of our methods, generate a queueing stub.
|
||||
for (let i = 0; i < analytics.methods.length; i++) {
|
||||
const key = analytics.methods[i];
|
||||
analytics[key] = analytics.factory(key);
|
||||
}
|
||||
|
||||
// Define a method to load Analytics.js from our CDN,
|
||||
// and that will be sure to only ever load it once.
|
||||
analytics.load = function (key, options) {
|
||||
// Create an async script element based on your key.
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.src = `https://cdn.segment.com/analytics.js/v1/${
|
||||
key}/analytics.min.js`;
|
||||
|
||||
// Insert our script next to the first script element.
|
||||
const first = document.getElementsByTagName('script')[0];
|
||||
first.parentNode.insertBefore(script, first);
|
||||
analytics._loadOptions = options;
|
||||
};
|
||||
|
||||
// Add a version to keep track of what's in the wild.
|
||||
analytics.SNIPPET_VERSION = '4.1.0';
|
||||
|
||||
// Load Analytics.js with your key, which will automatically
|
||||
// load the tools you've enabled for your account. Boosh!
|
||||
analytics.load(configuration.SEGMENT_KEY);
|
||||
}());
|
||||
23
src/setupTest.js
Executable file
23
src/setupTest.js
Executable file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
// These configuration values are usually set in webpack's EnvironmentPlugin however
|
||||
// Jest does not use webpack so we need to set these so for testing
|
||||
process.env.LMS_BASE_URL = 'http://localhost:18000';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
const PropTypes = jest.requireActual('prop-types');
|
||||
return {
|
||||
...i18n,
|
||||
intlShape: PropTypes.shape({
|
||||
formatMessage: jest.fn(msg => msg.defaultMessage),
|
||||
}),
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
};
|
||||
});
|
||||
20
src/utils/StrictDict.js
Normal file
20
src/utils/StrictDict.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable no-console */
|
||||
const strictGet = (target, name) => {
|
||||
if (name === Symbol.toStringTag) {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (name in target || name === '_reactFragment') {
|
||||
return target[name];
|
||||
}
|
||||
|
||||
console.log(name.toString());
|
||||
console.error({ target, name });
|
||||
const e = Error(`invalid property "${name.toString()}"`);
|
||||
console.error(e.stack);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const StrictDict = (dict) => new Proxy(dict, { get: strictGet });
|
||||
|
||||
export default StrictDict;
|
||||
62
src/utils/StrictDict.test.js
Normal file
62
src/utils/StrictDict.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import StrictDict from './StrictDict';
|
||||
|
||||
const value1 = 'valUE1';
|
||||
const value2 = 'vALue2';
|
||||
const key1 = 'Key1';
|
||||
const key2 = 'keY2';
|
||||
|
||||
jest.spyOn(window, 'Error').mockImplementation(error => ({ stack: error }));
|
||||
|
||||
describe('StrictDict', () => {
|
||||
let consoleError;
|
||||
let consoleLog;
|
||||
let windowError;
|
||||
beforeEach(() => {
|
||||
consoleError = window.console.error;
|
||||
consoleLog = window.console.lot;
|
||||
windowError = window.Error;
|
||||
window.console.error = jest.fn();
|
||||
window.console.log = jest.fn();
|
||||
window.Error = jest.fn(error => ({ stack: error }));
|
||||
});
|
||||
afterAll(() => {
|
||||
window.console.error = consoleError;
|
||||
window.console.log = consoleLog;
|
||||
window.Error = windowError;
|
||||
});
|
||||
const rawDict = {
|
||||
[key1]: value1,
|
||||
[key2]: value2,
|
||||
};
|
||||
const dict = StrictDict(rawDict);
|
||||
it('provides key access like a normal dict object', () => {
|
||||
expect(dict[key1]).toEqual(value1);
|
||||
});
|
||||
it('allows key listing', () => {
|
||||
expect(Object.keys(dict)).toEqual([key1, key2]);
|
||||
});
|
||||
it('allows item listing', () => {
|
||||
expect(Object.values(dict)).toEqual([value1, value2]);
|
||||
});
|
||||
it('allows stringification', () => {
|
||||
expect(dict.toString()).toEqual(rawDict.toString());
|
||||
expect({ ...dict }).toEqual({ ...rawDict });
|
||||
});
|
||||
it('allows entry listing', () => {
|
||||
expect(Object.entries(dict)).toEqual(Object.entries(rawDict));
|
||||
});
|
||||
describe('missing key', () => {
|
||||
it('logs error with target, name, and error stack', () => {
|
||||
// eslint-ignore-next-line no-unused-vars
|
||||
const callBadKey = () => dict.fakeKey;
|
||||
callBadKey();
|
||||
expect(window.console.error.mock.calls).toEqual([
|
||||
[{ target: dict, name: 'fakeKey' }],
|
||||
[Error('invalid property "fakeKey"').stack],
|
||||
]);
|
||||
});
|
||||
it('returns undefined', () => {
|
||||
expect(dict.fakeKey).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
2
src/utils/index.js
Normal file
2
src/utils/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as StrictDict } from './StrictDict';
|
||||
13
webpack.dev.config.js
Normal file
13
webpack.dev.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-dev');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
module.exports = config;
|
||||
13
webpack.prod.config.js
Normal file
13
webpack.prod.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const path = require('path');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-prod');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/;
|
||||
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user