Merge branch 'develop' into comments-reducer

This commit is contained in:
Paul Kaplan 2021-03-24 14:35:12 -04:00
commit 9252d56376
24 changed files with 1250 additions and 416 deletions

View file

@ -3,7 +3,7 @@ version: 2.1
aliases:
- &defaults
docker:
- image: circleci/node:12-browsers
- image: cimg/node:12.20.1-browsers
auth:
username: $DOCKERHUB_USERNAME
password: $DOCKERHUB_PASSWORD
@ -27,7 +27,6 @@ aliases:
restore_cache:
keys:
- v1-npm-{{ checksum "package-lock.json" }}
- v1-npm-
- &save_build_cache
save_cache:
paths:
@ -45,15 +44,33 @@ aliases:
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: "Run npm test to build"
name: "setup"
command: |
npm --production=false install
WWW_VERSION=${CIRCLE_SHA1:0:5} npm run test
npm --production=false ci
mkdir ./test/results
- run:
name: "run lint tests"
command: |
npm run test:lint:ci
- run:
name: "run npm build"
command: |
WWW_VERSION=${CIRCLE_SHA1:0:5} npm run build
- run:
name: "Run unit tests"
command: |
JEST_JUNIT_OUTPUT_NAME=unit-jest-results.xml npm run test:unit:jest:unit -- --reporters=jest-junit
JEST_JUNIT_OUTPUT_NAME=localization-jest-results.xml npm run test:unit:jest:localization -- --reporters=jest-junit
npm run test:unit:tap -- --output-file ./test/results/unit-raw.tap
npm run test:unit:convertReportToXunit
- *save_npm_cache
- *save_git_cache
- *save_build_cache
- store_test_results:
path: test/results
- store_artifacts:
path: build
- &deploy
<<: *defaults
steps:
@ -61,41 +78,63 @@ aliases:
- checkout
- *restore_npm_cache
- *restore_build_cache
- run:
name: "setup python"
command: |
curl https://bootstrap.pypa.io/3.5/get-pip.py -o get-pip.py
python3 get-pip.py pip==21.0.1
pip install s3cmd==2.1.0
- run:
name: "deploy to staging"
command: |
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py
pip install -r requirements.txt
npm run deploy
- &integration
- &integration_jest
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: "integration tests"
name: "integration tests with Jest"
command: |
npm run test:integration:remote
JEST_JUNIT_OUTPUT_NAME=integration-jest-results.xml npm run test:integration:jest:remote -- --reporters=jest-junit
- store_test_results:
path: test/results
- &integration_tap
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: "integration tests with Tap"
command: |
mkdir ./test/results
npm run test:smoke:sauce -- --output-file ./test/results/integration-raw-tap.tap
npm run test:smoke:convertReportToXunit
- store_test_results:
path: test/results
jobs:
build-staging:
<<: *build
build-production:
# <<: *build
<<: *build
deploy-staging:
<<: *deploy
deploy-production:
# <<: *deploy
integration-staging:
<<: *integration
integration-production:
# <<: *integration
integration-staging-jest:
<<: *integration_jest
integration-staging-tap:
<<: *integration_tap
integration-production-jest:
# <<: *integration_jest
integration-production-tap:
# <<: *integration_tap
workflows:
build-test-deploy:
build-staging-production: # build-test-deploy
jobs:
- build-staging:
context:
@ -107,6 +146,14 @@ workflows:
- develop
- /^hotfix\/.*/
- /^release\/.*/
- build-production:
context:
- scratch-www-all
- scratch-www-production
filters:
branches:
only:
- master
# - deploy-staging:
# context:
# - scratch-www-all
@ -119,7 +166,8 @@ workflows:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - integration-staging:
# - circleCI-configure-tests
# - integration-staging-jest:
# context:
# - scratch-www-all
# - scratch-www-staging
@ -131,3 +179,17 @@ workflows:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests
# - integration-staging-tap:
# context:
# - scratch-www-all
# - scratch-www-staging
# requires:
# - deploy-staging
# filters:
# branches:
# only:
# - develop
# - /^hotfix\/.*/
# - /^release\/.*/
# - circleCI-configure-tests

1
.gitignore vendored
View file

@ -20,6 +20,7 @@ deploy.zip
ENV
# Test
/test/results/*
/.nyc_output
/coverage
/bin/lib/localized-urls.json

524
package-lock.json generated
View file

@ -226,9 +226,9 @@
}
},
"@babel/compat-data": {
"version": "7.13.11",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.11.tgz",
"integrity": "sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz",
"integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==",
"dev": true
},
"@babel/core": {
@ -316,9 +316,9 @@
}
},
"@babel/parser": {
"version": "7.13.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz",
"integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"dev": true
},
"@babel/template": {
@ -350,9 +350,9 @@
}
},
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -519,9 +519,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.691",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.691.tgz",
"integrity": "sha512-ZqiO69KImmOGCyoH0icQPU3SndJiW93juEvf63gQngyhODO6SpQIPMTOHldtCs5DS5GMKvAkquk230E2zt2vpw==",
"version": "1.3.697",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.697.tgz",
"integrity": "sha512-VTAS+IWwGlfaL7VtfUMzFeV55PT/HglNFqQ6eW9E3PfjvPqhZfqJj+8dd9zrqrJYcouUfCgQw0OIse85Dz9V9Q==",
"dev": true
},
"semver": {
@ -553,18 +553,18 @@
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz",
"integrity": "sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz",
"integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==",
"dev": true,
"requires": {
"@babel/types": "^7.13.0"
"@babel/types": "^7.13.12"
},
"dependencies": {
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -587,18 +587,18 @@
}
},
"@babel/helper-module-imports": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz",
"integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz",
"integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
},
"dependencies": {
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -621,20 +621,19 @@
}
},
"@babel/helper-module-transforms": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz",
"integrity": "sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz",
"integrity": "sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-replace-supers": "^7.13.0",
"@babel/helper-simple-access": "^7.12.13",
"@babel/helper-module-imports": "^7.13.12",
"@babel/helper-replace-supers": "^7.13.12",
"@babel/helper-simple-access": "^7.13.12",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/helper-validator-identifier": "^7.12.11",
"@babel/template": "^7.12.13",
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.0",
"lodash": "^4.17.19"
"@babel/types": "^7.13.12"
},
"dependencies": {
"@babel/code-frame": {
@ -698,9 +697,9 @@
}
},
"@babel/parser": {
"version": "7.13.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz",
"integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"dev": true
},
"@babel/template": {
@ -732,9 +731,9 @@
}
},
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -828,9 +827,9 @@
},
"dependencies": {
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -859,15 +858,15 @@
"dev": true
},
"@babel/helper-replace-supers": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz",
"integrity": "sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
"integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.13.0",
"@babel/helper-member-expression-to-functions": "^7.13.12",
"@babel/helper-optimise-call-expression": "^7.12.13",
"@babel/traverse": "^7.13.0",
"@babel/types": "^7.13.0"
"@babel/types": "^7.13.12"
},
"dependencies": {
"@babel/code-frame": {
@ -931,9 +930,9 @@
}
},
"@babel/parser": {
"version": "7.13.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz",
"integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"dev": true
},
"@babel/template": {
@ -965,9 +964,9 @@
}
},
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -1052,18 +1051,18 @@
}
},
"@babel/helper-simple-access": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz",
"integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz",
"integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
"@babel/types": "^7.13.12"
},
"dependencies": {
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -1178,9 +1177,9 @@
}
},
"@babel/parser": {
"version": "7.13.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz",
"integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.12.tgz",
"integrity": "sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==",
"dev": true
},
"@babel/template": {
@ -1212,9 +1211,9 @@
}
},
"@babel/types": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz",
"integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==",
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.12.tgz",
"integrity": "sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
@ -2117,9 +2116,9 @@
"dev": true
},
"@types/babel__core": {
"version": "7.1.13",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.13.tgz",
"integrity": "sha512-CC6amBNND16pTk4K3ZqKIaba6VGKAQs3gMjEY17FVd56oI/ZWt9OhS6riYiWv9s8ENbYUi7p8lgqb0QHQvUKQQ==",
"version": "7.1.14",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz",
"integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==",
"dev": true,
"requires": {
"@babel/parser": "^7.1.0",
@ -2933,24 +2932,6 @@
"dev": true,
"requires": {
"source-map-support": "^0.5.11"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
}
}
},
"async-limiter": {
@ -4143,9 +4124,9 @@
"dev": true
},
"bind-obj-methods": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz",
"integrity": "sha512-3/qRXczDi2Cdbz6jE+W3IflJOutRVica8frpBn14de1mBOkzDo+6tY33kNhvkw54Kn3PzRRD2VnGbGPcTAk4sw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-2.0.1.tgz",
"integrity": "sha512-kKzUyCuc+jsWH4C2nW5KB2nh+rQRbQcdphfo9UN3j1uwIFGZ3JB8njtRZOiUAQCkxazH0nDQPN6x/zhvFcbZIw==",
"dev": true
},
"bit-twiddle": {
@ -5731,9 +5712,9 @@
},
"dependencies": {
"js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@ -5745,56 +5726,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dev": true
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
}
},
@ -6591,11 +6522,6 @@
"strip-bom": "^3.0.0"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@ -7669,9 +7595,9 @@
}
},
"eslint-config-scratch": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-scratch/-/eslint-config-scratch-6.0.0.tgz",
"integrity": "sha512-2aW7EVWhjTrwaM54hjAZeRETh/habWeQ5xzTbPO2dG//RtixBztUAmdxqAHKH8MEtmAMsUhf3mRjO1BstO4dGg==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-scratch/-/eslint-config-scratch-7.0.0.tgz",
"integrity": "sha512-8Ko7aaC+Gv09lyz3x7+EaVZ68ga/4mnfGgwH1Jz/MnR4UJyJyYtUOtlWxsPspPOlPZzDAVL5CKEcxmCX24SrJA==",
"dev": true,
"requires": {
"eslint-plugin-react": ">=7.14.2"
@ -8932,18 +8858,18 @@
"dev": true
},
"flow-parser": {
"version": "0.129.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.129.0.tgz",
"integrity": "sha512-kzxyoEl8vG0JF0/h/u0UjALXmsGvwU2NBfKczCSNO/It2fKb8hz1gMt05OuZAlMLYXcvgjntWJadIABeKGPK4g==",
"version": "0.147.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.147.0.tgz",
"integrity": "sha512-z+b/pgp2QLvsWJkzhKXU8yC5TmaNyXGRmHac3x0Swmn9uQESRXhNIJq9TPHKPPeWgFym33OLO+5BlIdy/tXRCQ==",
"dev": true
},
"flow-remove-types": {
"version": "2.129.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.129.0.tgz",
"integrity": "sha512-ucESHZUDQvEFzjRKstZMFBVIciRvXtKpVyPsJT+poIyOIxuPoCLiU/8HHnMBN9XHDWSJ2YJ91mv97n17NmI1Bg==",
"version": "2.147.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.147.0.tgz",
"integrity": "sha512-ppZdtui3daAhBe0dkghBdtdIdSRbc/p/IEXw3t6vtlbS2KNpY8G3cu5RupXT3pGvFQbbXHP7mwNCEKSEYGU6Aw==",
"dev": true,
"requires": {
"flow-parser": "^0.129.0",
"flow-parser": "^0.147.0",
"pirates": "^3.0.2",
"vlq": "^0.2.1"
}
@ -12404,12 +12330,6 @@
"semver": "^5.6.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@ -13063,6 +12983,47 @@
}
}
},
"jest-junit": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-12.0.0.tgz",
"integrity": "sha512-+8K35LlboWiPuCnXSyiid7rFdxNlpCWWM20WEYe6IZH6psfUWKZmSpSRQ5tk0C0cBeDsvsnIzcef5mYhyJsbug==",
"dev": true,
"requires": {
"mkdirp": "^1.0.4",
"strip-ansi": "^5.2.0",
"uuid": "^3.3.3",
"xml": "^1.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"jest-leak-detector": {
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz",
@ -16126,12 +16087,6 @@
"color-convert": "^1.9.0"
}
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@ -16184,9 +16139,9 @@
}
},
"js-yaml": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@ -16281,12 +16236,6 @@
"ansi-regex": "^4.1.0"
}
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
@ -16299,9 +16248,9 @@
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
@ -16321,16 +16270,6 @@
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
@ -16517,9 +16456,9 @@
}
},
"opener": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"dev": true
},
"optical-properties": {
@ -20970,9 +20909,9 @@
}
},
"scratch-blocks": {
"version": "0.1.0-prerelease.20210318033822",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210318033822.tgz",
"integrity": "sha512-nKOMgQwrvH/VKH7mHliwpQyddD/wM68UaA/Vh37/XkknKkY8g6qX2Jj5XgxziMIZQTGgNsu+LEkbyNOhqp0QOg==",
"version": "0.1.0-prerelease.20210324033606",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.20210324033606.tgz",
"integrity": "sha512-zCf7mN64RLME1tA9t2HcDEnf5h5+ziMyksbQj3gsWOUylYLrrYksMBw7wprVEMdPBJwz+4HhpcpkkrCQV1NVnw==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@ -20980,9 +20919,9 @@
}
},
"scratch-gui": {
"version": "0.1.0-prerelease.20210318040059",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210318040059.tgz",
"integrity": "sha512-dGrtJutjsAMqq7sotMYwhAAmJQbyRmi3b7vKlLQYTqV15cY5oa2GrVAso7PBrWxRLcd1XDbVUrcEpvgKaWZNJg==",
"version": "0.1.0-prerelease.20210324120840",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210324120840.tgz",
"integrity": "sha512-6KLZfZdJLAMLBDJo/LC6f0ckUg0CefE941NvBiNmV2XOIAoMPe5gq/eX3C1JT6BDLAFw4ogw4+K1KmQX/PYibw==",
"dev": true,
"requires": {
"arraybuffer-loader": "^1.0.6",
@ -21033,14 +20972,14 @@
"redux": "3.7.2",
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20210318033822",
"scratch-l10n": "3.11.20210318031539",
"scratch-paint": "0.2.0-prerelease.20210308034725",
"scratch-blocks": "0.1.0-prerelease.20210324033606",
"scratch-l10n": "3.11.20210324031512",
"scratch-paint": "0.2.0-prerelease.20210319222931",
"scratch-render": "0.1.0-prerelease.20210317200605",
"scratch-render-fonts": "1.0.0-prerelease.20200507182347",
"scratch-storage": "1.3.3",
"scratch-svg-renderer": "0.2.0-prerelease.20210317184701",
"scratch-vm": "0.2.0-prerelease.20210317111523",
"scratch-vm": "0.2.0-prerelease.20210324111836",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
"text-encoding": "0.7.0",
@ -21203,9 +21142,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.691",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.691.tgz",
"integrity": "sha512-ZqiO69KImmOGCyoH0icQPU3SndJiW93juEvf63gQngyhODO6SpQIPMTOHldtCs5DS5GMKvAkquk230E2zt2vpw==",
"version": "1.3.698",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.698.tgz",
"integrity": "sha512-VEXDzYblnlT+g8Q3gedwzgKOso1evkeJzV8lih7lV8mL8eAnGVnKyC3KsFT6S+R5PQO4ffdr1PI16/ElibY/kQ==",
"dev": true
},
"has-flag": {
@ -21492,9 +21431,9 @@
}
},
"scratch-l10n": {
"version": "3.11.20210318031539",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210318031539.tgz",
"integrity": "sha512-uxOUzuP4J1DwVP7yySzQkMhKeuti1ecNWAgeypL8lW5HAWPlIOvBeC4oT05hRua2zF5i/+ozeZpKh0stW+1aeg==",
"version": "3.11.20210324031512",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210324031512.tgz",
"integrity": "sha512-dyv2cjNWVHrL78XpW64uF2azYaUhMKLjfJVH6vSCPJxm3EKCvO6EoaUSlIAwhsGoMwgfxWZ8D74+IaifGJfnCQ==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@ -21505,9 +21444,9 @@
}
},
"scratch-paint": {
"version": "0.2.0-prerelease.20210308034725",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20210308034725.tgz",
"integrity": "sha512-DBhOVZ1Q7TpCcUJXAUMQACWWOZMFJyBmXvOf11ifP6opiQgjLbSBvUqYySPQckgkj38KDqDED90zAHVFga//eA==",
"version": "0.2.0-prerelease.20210319222931",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20210319222931.tgz",
"integrity": "sha512-O5tIIQ6ECsLZ3iol2S3OV3yaVyM1i6Kk0YVAR219t6GnSv1ux6RCD3qs7g8HCa+G3f+PHKBcCIk4DCR8Qv59Uw==",
"dev": true,
"requires": {
"@scratch/paper": "0.11.20200728195508",
@ -21742,9 +21681,9 @@
"dev": true
},
"scratch-vm": {
"version": "0.2.0-prerelease.20210317111523",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210317111523.tgz",
"integrity": "sha512-TVlh0hmvl8hUeZfvfmTn8KeBPAW2zYkAWZm32EI+sJUC7dmG7J3+7YrlqnvtubPv1q/bVeAE9dkYe/EmktbKOg==",
"version": "0.2.0-prerelease.20210324111836",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20210324111836.tgz",
"integrity": "sha512-vOayLHJJ3ZYS2XUIVPnIMZmaW/JuazC9x32Lc/i64ZxPsQjO8pANgeUN/8A5LdEc5ZO2YG2I3JWRBLnK6o+LEw==",
"dev": true,
"requires": {
"@vernier/godirect": "1.5.0",
@ -22479,6 +22418,24 @@
"urix": "^0.1.0"
}
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"source-map-url": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
@ -23395,9 +23352,9 @@
}
},
"tap": {
"version": "14.10.8",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.10.8.tgz",
"integrity": "sha512-aamkWefJ0G8GGf9t5LWFtrNF5tfVd8ut/tDUianLF6N4621ERITIl0qkocWCVEnsM6hZnaMKa+SggSAaBlC2tA==",
"version": "14.11.0",
"resolved": "https://registry.npmjs.org/tap/-/tap-14.11.0.tgz",
"integrity": "sha512-z8qnNFVyIjLh/bNoTLFRkEk09XZUDAZbCkz/BjvHHly3ao5H+y60gPnedALfheEjA6dA4tpp/mrKq2NWlMuq0A==",
"dev": true,
"requires": {
"@types/react": "^16.9.16",
@ -23430,7 +23387,7 @@
"rimraf": "^2.7.1",
"signal-exit": "^3.0.0",
"source-map-support": "^0.5.16",
"stack-utils": "^1.0.2",
"stack-utils": "^1.0.3",
"tap-mocha-reporter": "^5.0.0",
"tap-parser": "^10.0.1",
"tap-yaml": "^1.0.0",
@ -23812,9 +23769,9 @@
"dev": true
},
"binary-extensions": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"braces": {
@ -23867,19 +23824,19 @@
}
},
"chokidar": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz",
"integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.4.0"
"readdirp": "~3.5.0"
}
},
"ci-info": {
@ -23938,12 +23895,11 @@
"dev": true
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"version": "4.1.1",
"bundled": true,
"dev": true,
"requires": {
"ms": "2.1.2"
"ms": "^2.1.1"
}
},
"diff": {
@ -23982,9 +23938,9 @@
}
},
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
@ -24008,9 +23964,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@ -24357,17 +24313,6 @@
"bundled": true,
"dev": true
},
"react": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
"integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "16.13.1",
"bundled": true,
@ -24385,9 +24330,9 @@
}
},
"readdirp": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
@ -24485,20 +24430,21 @@
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"stack-utils": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz",
"integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
"escape-string-regexp": "^2.0.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true
}
}
},
"string-length": {
@ -24794,12 +24740,6 @@
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
@ -25006,9 +24946,9 @@
}
},
"tcompare": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/tcompare/-/tcompare-3.0.4.tgz",
"integrity": "sha512-Q3TitMVK59NyKgQyFh+857wTAUE329IzLDehuPgU4nF5e8g+EUQ+yUbjUy1/6ugiNnXztphT+NnqlCXolv9P3A==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/tcompare/-/tcompare-3.0.5.tgz",
"integrity": "sha512-+tmloQj1buaShBX+LP1i1NF5riJm110Yr0flIJAEoKf01tFVoMZvW2jq1JLqaW8fspOUVPm5NKKW5qLwT0ETDQ==",
"dev": true,
"requires": {
"diff-frag": "^1.0.1"
@ -25802,22 +25742,6 @@
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
}
}
},
@ -25930,9 +25854,9 @@
}
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
"dev": true
},
"ua-parser-js": {
@ -28201,6 +28125,12 @@
}
}
},
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=",
"dev": true
},
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
@ -28241,9 +28171,9 @@
"dev": true
},
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true
},
"yapool": {

View file

@ -6,17 +6,22 @@
"start": "node ./dev-server/index.js",
"test": "npm run test:lint && npm run build && npm run test:unit",
"test:lint": "eslint . --ext .js,.jsx,.json",
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml",
"test:integration": "npm run test:integration:jest && npm run test:smoke",
"test:integration:jest": "jest ./test/integration/*.test.js",
"test:integration:jest": "jest ./test/integration/*.test.js --reporters=default",
"test:integration:remote": "npm run test:integration:jest:remote && npm run test:smoke:sauce",
"test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js",
"test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default",
"test:smoke": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
"test:smoke:verbose": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
"test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration-legacy/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
"test:smoke:convertReportToXunit": "tap ./test/results/integration-raw-tap.tap --no-coverage -R xunit > ./test/results/integration-tap-results.xml",
"test:unit": "npm run test:unit:jest && npm run test:unit:tap",
"test:unit:jest": "jest ./test/unit/ && jest ./test/localization/*.test.js",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy} --no-coverage -R classic",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy} --coverage --coverage-report=lcov",
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
"test:unit:jest:unit": "jest ./test/unit/ --reporters=default",
"test:unit:jest:localization": "jest ./test/localization/*.test.js --reporters=default",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/*.js --no-coverage -R classic",
"test:unit:convertReportToXunit": "tap ./test/results/unit-raw.tap --no-coverage -R xunit > ./test/results/unit-tap-results.xml",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/*.js --coverage --coverage-report=lcov",
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
@ -77,7 +82,7 @@
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0",
"eslint-config-scratch": "6.0.0",
"eslint-config-scratch": "7.0.0",
"eslint-plugin-json": "2.0.1",
"eslint-plugin-react": "7.14.2",
"fastly": "1.2.1",
@ -90,6 +95,7 @@
"html-webpack-plugin": "^3.2.0",
"iso-3166-2": "0.4.0",
"jest": "^23.6.0",
"jest-junit": "12.0.0",
"keymirror": "0.1.1",
"lodash.bindall": "4.4.0",
"lodash.defaultsdeep": "4.6.1",
@ -119,12 +125,12 @@
"redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210318040059",
"scratch-gui": "0.1.0-prerelease.20210324120840",
"scratch-l10n": "latest",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",
"style-loader": "0.12.3",
"tap": "14.10.8",
"tap": "14.11.0",
"url-loader": "2.3.0",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.4.0",
@ -139,7 +145,14 @@
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "<rootDir>/test/__mocks__/styleMock.js"
}
},
"reporters": [
"default",
"jest-junit"
]
},
"jest-junit": {
"outputDirectory": "./test/results"
},
"nyc": {
"include": [

View file

@ -4,47 +4,53 @@
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
},
{
"id": 2,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
},
{
"id": 3,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
},
{
"id": 4,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
},
{
"id": 5,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
},
{
"id": 6,
"type": "project",
"title": "Project",
"thumbnailUrl": "",
"creator": "",
"href": "#"
"author": "",
"href": "#",
"stats": {"loves": 0, "remixes": 0}
}
]

View file

@ -327,8 +327,6 @@
"comments.isBad": "Hmm...the bad word detector thinks there is a problem with your comment. Please change it and remember to be respectful.",
"comments.hasChatSite": "Uh oh! The comment contains a link to a website with unmoderated chat. For safety reasons, please do not link to these sites!",
"comments.isSpam": "Hmm, seems like you've posted the same comment a bunch of times. Please don't spam.",
"comments.isMuted": "Hmm, the filterbot is pretty sure your recent comments weren't ok for Scratch, so your account has been muted for the rest of the day. :/",
"comments.isUnconstructive": "Hmm, the filterbot thinks your comment may be mean or disrespectful. Remember, most projects on Scratch are made by people who are just learning how to program.",
"comments.isDisallowed": "Hmm, it looks like comments have been turned off for this page. :/",
"comments.isIPMuted": "Sorry, the Scratch Team had to prevent your network from sharing comments or projects because it was used to break our community guidelines too many times. You can still share comments and projects from another network. If you'd like to appeal this block, you can contact appeals@scratch.mit.edu and reference Case Number {appealId}.",
"comments.isTooLong": "That comment is too long! Please find a way to shorten your text.",

127
src/redux/infinite-list.js Normal file
View file

@ -0,0 +1,127 @@
/**
* @typedef ReduxModule
* A redux "module" for reusable functionality. The module exports
* a reducer function, a set of action creators and a selector
* that are all scoped to the given "key". This allows us to reuse
* this reducer multiple times in the same redux store.
*
* @property {string} key The key to use when registering this
* modules reducer in the redux state tree.
* @property {function} selector Function called with the full
* state tree to select only this modules slice of the state.
* @property {object} actions An object of action creator functions
* to call to make changes to the data in this reducer.
* @property {function} reducer A redux reducer that takes an action
* from the action creators and the current state and returns
* the next state.
*/
/**
* @typedef {function} InfiniteListFetcher
* A function to call that returns more data for the InfiniteList
* loadMore action. It must resolve to {items: [], moreToLoad} or
* reject with the error {statusCode}.
* @returns {Promise<{items:[], moreToLoad:boolean}>}
*/
/**
* A redux module to create a list of items where more items can be loaded
* using an API. Additionally, there are actions for prepending items
* to the list, removing items and handling load errors.
*
* @param {string} key - used to scope action names and the selector
* This key must be unique among other instances of this module.
* @returns {ReduxModule} the redux module
*/
const InfiniteList = key => {
const initialState = {
items: [],
offset: 0,
error: null,
loading: true,
moreToLoad: false
};
const reducer = (state, action) => {
if (typeof state === 'undefined') {
state = initialState;
}
switch (action.type) {
case `${key}_LOADING`:
return {
...state,
error: null,
loading: true
};
case `${key}_APPEND`:
return {
...state,
items: state.items.concat(action.items),
loading: false,
error: null,
moreToLoad: action.moreToLoad
};
case `${key}_REPLACE`:
return {
...state,
items: state.items.map((item, i) => {
if (i === action.index) return action.item;
return item;
})
};
case `${key}_REMOVE`:
return {
...state,
items: state.items.filter((_, i) => i !== action.index)
};
case `${key}_PREPEND`:
return {
...state,
items: [action.item].concat(state.items)
};
case `${key}_ERROR`:
return {
...state,
error: action.error,
loading: false,
moreToLoad: false
};
default:
return state;
}
};
const actions = {
create: item => ({type: `${key}_PREPEND`, item}),
remove: index => ({type: `${key}_REMOVE`, index}),
replace: (index, item) => ({type: `${key}_REPLACE`, index, item}),
error: error => ({type: `${key}_ERROR`, error}),
loading: () => ({type: `${key}_LOADING`}),
append: (items, moreToLoad) => ({type: `${key}_APPEND`, items, moreToLoad}),
/**
* Load more action returns a thunk. It takes a function to call to get more items.
* It will call the LOADING action before calling the fetcher, and call
* APPEND with the results or call ERROR.
* @param {InfiniteListFetcher} fetcher - function that returns a promise
* which must resolve to {items: [], moreToLoad}.
* @returns {function} a thunk that sequences the load and dispatches
*/
loadMore: fetcher => (dispatch => {
dispatch(actions.loading());
return fetcher()
.then(({items, moreToLoad}) => dispatch(actions.append(items, moreToLoad)))
.catch(error => dispatch(actions.error(error)));
})
};
const selector = state => state[key];
return {
key, actions, reducer, selector
};
};
export default InfiniteList;

120
src/redux/studio.js Normal file
View file

@ -0,0 +1,120 @@
const keyMirror = require('keymirror');
const api = require('../lib/api');
const log = require('../lib/log');
const Status = keyMirror({
FETCHED: null,
NOT_FETCHED: null,
FETCHING: null,
ERROR: null
});
const getInitialState = () => ({
infoStatus: Status.NOT_FETCHED,
title: '',
description: '',
openToAll: false,
commentingAllowed: false,
thumbnail: '',
followers: 0,
rolesStatus: Status.NOT_FETCHED,
manager: false,
curator: false,
follower: false,
invited: false
});
const studioReducer = (state, action) => {
if (typeof state === 'undefined') {
state = getInitialState();
}
switch (action.type) {
case 'SET_INFO':
return {
...state,
...action.info
};
case 'SET_ROLES':
return {
...state,
...action.roles
};
case 'SET_FETCH_STATUS':
if (action.error) {
log.error(action.error);
}
return {
...state,
[action.fetchType]: action.fetchStatus
};
default:
return state;
}
};
const setFetchStatus = (fetchType, fetchStatus, error) => ({
type: 'SET_FETCH_STATUS',
fetchType,
fetchStatus,
error
});
const setInfo = info => ({
type: 'SET_INFO',
info: info
});
const setRoles = roles => ({
type: 'SET_ROLES',
roles: roles
});
const getInfo = studioId => (dispatch => {
dispatch(setFetchStatus('infoStatus', Status.FETCHING));
api({uri: `/studios/${studioId}`}, (err, body, res) => {
if (err || typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(setFetchStatus('infoStatus', Status.ERROR, err));
return;
}
dispatch(setFetchStatus('infoStatus', Status.FETCHED));
dispatch(setInfo({
title: body.title,
description: body.description,
openToAll: body.open_to_all,
commentingAllowed: body.commenting_allowed,
updated: new Date(body.history.modified),
followers: body.stats.followers
}));
});
});
const getRoles = (studioId, username, token) => (dispatch => {
dispatch(setFetchStatus('rolesStatus', Status.FETCHING));
api({
uri: `/studios/${studioId}/users/${username}`,
authentication: token
}, (err, body, res) => {
if (err || typeof body === 'undefined' || res.statusCode !== 200) {
dispatch(setFetchStatus('rolesStatus', Status.ERROR, err));
return;
}
dispatch(setFetchStatus('rolesStatus', Status.FETCHED));
dispatch(setRoles({
manager: body.manager,
curator: body.curator,
following: body.following,
invited: body.invited
}));
});
});
module.exports = {
getInitialState,
studioReducer,
Status,
getInfo,
getRoles
};

View file

@ -5,6 +5,7 @@ const Page = require('../../components/page/www/page.jsx');
const Box = require('../../components/box/box.jsx');
const Button = require('../../components/forms/button.jsx');
const Carousel = require('../../components/carousel/carousel.jsx');
const Form = require('../../components/forms/form.jsx');
const Input = require('../../components/forms/input.jsx');
const Spinner = require('../../components/spinner/spinner.jsx');
@ -16,11 +17,13 @@ const Components = () => (
<h1>Button</h1>
<Button>I love button</Button>
<h1>Form</h1>
<Input
maxLength="30"
name="test"
type="text"
/>
<Form>
<Input
maxLength="30"
name="test"
type="text"
/>
</Form>
<h1>Box Component</h1>
<Box
more="Cat Gifs"

View file

@ -51,13 +51,6 @@ const ConferenceSplash = () => (
value={new Date(2021, 6, 22)}
year="numeric"
/>
{' - '}
<FormattedDate
day="2-digit"
month="long"
value={new Date(2021, 6, 24)}
year="numeric"
/>
</td>
</tr>
<tr className="conf2020-panel-row">
@ -75,7 +68,7 @@ const ConferenceSplash = () => (
</table>
<a
className="button mod-2020-panel"
href="http://scratch2020.eventbrite.com/"
href="http://scratch2021.eventbrite.com/"
>
<FormattedMessage id="conference-2020.register" />
</a>

View file

@ -1,7 +1,7 @@
{
"conference-2020.title": "Scratch Around the World:",
"conference-2020.subtitle": "An Online Conference",
"conference-2020.dateDesc": "July 22",
"conference-2020.dateDesc": "July 22, 2021",
"conference-2020.locationDetails": "Online",
"conference-2020.date": "When:",

View file

@ -28,8 +28,9 @@ const JUST_MUTED_ERROR = 'isBad';
const ComposeStatus = keyMirror({
EDITING: null,
SUBMITTING: null,
REJECTED: null,
REJECTED_MUTE: null
REJECTED: null, // comment rejected for a reason other than muting (such as commenting too quickly)
REJECTED_MUTE: null, // comment made in this ComposeComment was rejected and muted the user
COMPOSE_DISALLOWED: null // user is already muted due to past behavior
});
class ComposeComment extends React.Component {
@ -48,7 +49,7 @@ class ComposeComment extends React.Component {
this.props.muteStatus.muteExpiresAt * 1000 : 0; // convert to ms
this.state = {
message: '',
status: ComposeStatus.EDITING,
status: muteExpiresAtMs > Date.now() ? ComposeStatus.COMPOSE_DISALLOWED : ComposeStatus.EDITING,
error: null,
appealId: null,
muteOpen: muteExpiresAtMs > Date.now() && this.props.isReply,
@ -96,14 +97,23 @@ class ComposeComment extends React.Component {
let muteOpen = false;
let muteExpiresAtMs = 0;
let rejectedStatus = ComposeStatus.REJECTED;
let justMuted = true;
let showWarning = false;
let muteType = null;
if (body.status && body.status.mute_status) {
muteExpiresAtMs = body.status.mute_status.muteExpiresAt * 1000; // convert to ms
rejectedStatus = ComposeStatus.REJECTED_MUTE;
if (this.shouldShowMuteModal(body.status.mute_status)) {
if (body.rejected === JUST_MUTED_ERROR) {
rejectedStatus = ComposeStatus.REJECTED_MUTE;
} else {
rejectedStatus = ComposeStatus.COMPOSE_DISALLOWED;
justMuted = false;
}
if (this.shouldShowMuteModal(body.status.mute_status, justMuted)) {
muteOpen = true;
}
showWarning = body.status.mute_status.showWarning;
muteType = body.status.mute_status.currentMessageType;
this.setupMuteExpirationTimeout(muteExpiresAtMs);
@ -152,7 +162,7 @@ class ComposeComment extends React.Component {
// Cancel (i.e. complete) the reply action if the user clicked on the reply button while
// alreay muted. This "closes" the reply. If they just got muted, we want to leave it open
// so the blue CommentingStatus box shows.
if (this.props.isReply && this.state.status !== ComposeStatus.REJECTED_MUTE) {
if (this.props.isReply && this.state.status === ComposeStatus.COMPOSE_DISALLOWED) {
this.handleCancel();
}
}
@ -162,7 +172,7 @@ class ComposeComment extends React.Component {
muteOpen: true
});
}
shouldShowMuteModal (muteStatus) {
shouldShowMuteModal (muteStatus, justMuted) {
// We should show the mute modal if the user is in danger of being blocked or
// when the user is newly muted or hasn't seen it for a while.
// We don't want to show it more than about once a week.
@ -176,6 +186,17 @@ class ComposeComment extends React.Component {
return false;
}
// If the user is already muted (for example, in a different tab),
// do not show modal unless the comment is a reply. We always want to show
// the modal on replies when the user is already muted because the blue box
// may be out-of-sight for them.
if (!justMuted) {
if (this.props.isReply) {
return true;
}
return false;
}
// If the backend tells us to show a warning about getting blocked, we should show the modal
// regardless of what the offenses list looks like.
if (muteStatus.showWarning) {
@ -199,41 +220,47 @@ class ComposeComment extends React.Component {
// Decides which step of the mute modal to start on. If this was a reply button click,
// we show them the step that tells them how much time is left on their mute, otherwise
// they start at the beginning of the progression.
return this.props.isReply && this.state.status !== ComposeStatus.REJECTED_MUTE ?
return this.props.isReply && this.state.status === ComposeStatus.COMPOSE_DISALLOWED ?
MuteModal.steps.MUTE_INFO : MuteModal.steps.COMMENT_ISSUE;
}
getMuteMessageInfo () {
getMuteMessageInfo (justMuted) {
// return the ids for the messages that are shown for this mute type
// If mute modals have more than one unique "step" we could pass an array of steps
const messageInfo = {
pii: {
name: 'pii',
commentType: 'comment.type.pii',
commentType: justMuted ? 'comment.type.pii' : 'comment.type.pii.past',
muteStepHeader: 'comment.pii.header',
muteStepContent: ['comment.pii.content1', 'comment.pii.content2', 'comment.pii.content3']
},
unconstructive: {
name: 'unconstructive',
commentType: 'comment.type.unconstructive',
commentType: justMuted ? 'comment.type.unconstructive' : 'comment.type.unconstructive.past',
muteStepHeader: 'comment.unconstructive.header',
muteStepContent: ['comment.unconstructive.content1', 'comment.unconstructive.content2']
muteStepContent: [
justMuted ? 'comment.unconstructive.content1' : 'comment.type.unconstructive.past',
'comment.unconstructive.content2'
]
},
vulgarity: {
name: 'vulgarity',
commentType: 'comment.type.vulgarity',
commentType: justMuted ? 'comment.type.vulgarity' : 'comment.type.vulgarity.past',
muteStepHeader: 'comment.vulgarity.header',
muteStepContent: ['comment.vulgarity.content1', 'comment.vulgarity.content2']
muteStepContent: [
justMuted ? 'comment.vulgarity.content1' : 'comment.type.vulgarity.past',
'comment.vulgarity.content2'
]
},
spam: {
name: 'spam',
commentType: 'comment.type.spam',
commentType: justMuted ? 'comment.type.spam' : 'comment.type.spam.past',
muteStepHeader: 'comment.spam.header',
muteStepContent: ['comment.spam.content1', 'comment.spam.content2']
},
general: {
name: 'general',
commentType: 'comment.type.general',
commentType: justMuted ? 'comment.type.general' : 'comment.type.general.past',
muteStepHeader: 'comment.general.header',
muteStepContent: ['comment.general.content1']
}
@ -258,10 +285,20 @@ class ComposeComment extends React.Component {
render () {
return (
<React.Fragment>
{(this.isMuted() && !(this.props.isReply && this.state.status !== ComposeStatus.REJECTED_MUTE)) ? (
{/* If a user is muted, show the blue mute box, unless
the comment is a reply and the user was already muted before attempting to make it. */}
{(this.isMuted() && !(this.props.isReply && this.state.status === ComposeStatus.COMPOSE_DISALLOWED)) ? (
<FlexRow className="comment">
<CommentingStatus>
<p><FormattedMessage id={this.getMuteMessageInfo().commentType} /></p>
<p>
<FormattedMessage
id={
this.getMuteMessageInfo(
this.state.status === ComposeStatus.REJECTED_MUTE
).commentType
}
/>
</p>
<p>
<FormattedMessage
id="comments.muted.duration"
@ -287,7 +324,7 @@ class ComposeComment extends React.Component {
</CommentingStatus>
</FlexRow>
) : null }
{!this.isMuted() || (this.isMuted() && this.state.status === ComposeStatus.REJECTED_MUTE) ? (
{this.state.status === ComposeStatus.COMPOSE_DISALLOWED ? null : (
<div
className={classNames('flex-row',
'comment',
@ -298,7 +335,7 @@ class ComposeComment extends React.Component {
<Avatar src={this.props.user.thumbnailUrl} />
</a>
<FlexRow className="compose-comment column">
{this.state.error && this.state.status !== ComposeStatus.REJECTED_MUTE ? (
{this.state.status === ComposeStatus.REJECTED ? (
<FlexRow className="compose-error-row">
<div className="compose-error-tip">
<FormattedMessage
@ -360,7 +397,7 @@ class ComposeComment extends React.Component {
</Formsy>
</FlexRow>
</div>
) : null }
)}
{this.state.muteOpen ? (
<MuteModal
isOpen
@ -368,10 +405,10 @@ class ComposeComment extends React.Component {
useStandardSizes
className="mod-mute"
commentContent={this.state.message}
muteModalMessages={this.getMuteMessageInfo()}
muteModalMessages={this.getMuteMessageInfo(this.state.status === ComposeStatus.REJECTED_MUTE)}
shouldCloseOnOverlayClick={false}
showFeedback={
this.state.status === ComposeStatus.REJECTED_MUTE && this.state.error === JUST_MUTED_ERROR
this.state.status === ComposeStatus.REJECTED_MUTE
}
showWarning={this.state.showWarning}
startStep={this.getMuteModalStartStep()}

View file

@ -47,22 +47,27 @@
"project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project.",
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful.",
"comment.type.general": "It appears that your most recent comment didn't follow the Scratch Community Guidelines.",
"comment.type.general.past": "It appears that one of your recent comments didnt follow the Scratch Community Guidelines.",
"comment.general.header": "We encourage you to post comments that follow the Scratch Community Guidelines.",
"comment.general.content1": "On Scratch, it's important for comments to be kind, to be appropriate for all ages, and to not contain spam.",
"comment.type.pii": "Your most recent comment appeared to be sharing or asking for private information.",
"comment.type.pii.past": "It appears that one of your recent comments was sharing or asking for private information.",
"comment.pii.header": "Please be sure not to share private information on Scratch.",
"comment.pii.content1": "It appears that you were sharing or asking for private information.",
"comment.pii.content2": "Things you share on Scratch can be seen by everyone, and can appear in search engines. Private information can be used by other people in harmful ways, so its important to keep it private.",
"comment.pii.content3": "This is a serious safety issue.",
"comment.type.unconstructive": "It appears that your most recent comment was saying something that might have been hurtful.",
"comment.type.unconstructive.past": "It appears that one of your recent comments was saying something that might have been hurtful.",
"comment.unconstructive.header": "We encourage you to be supportive when commenting on other peoples projects",
"comment.unconstructive.content1": "It appears that your comment was saying something that might have been hurtful.",
"comment.unconstructive.content2": "If you think something could be better, you can say something you like about the project, and make a suggestion about how to improve it.",
"comment.type.vulgarity": "Your most recent comment appeared to include a bad word.",
"comment.type.vulgarity.past": "It appears that one of your recent comments contained a bad word.",
"comment.vulgarity.header": "We encourage you to use language thats appropriate for all ages.",
"comment.vulgarity.content1": "It appears that your comment contains a bad word.",
"comment.vulgarity.content2": "Scratch has users of all ages, so its important to use language that is appropriate for all Scratchers.",
"comment.type.spam": "Your most recent comment appeared to contain advertising, text art, or a chain message.",
"comment.type.spam.past": "It appears that one of your recent comments contained advertising, text art, or a chain message.",
"comment.spam.header": "We encourage you not to advertise, copy and paste text art, or ask others to copy comments.",
"comment.spam.content1": "Even though advertisements, text art, and chain mail can be fun, they start to fill up the website, and we want to make sure there is room for other comments.",
"comment.spam.content2": "Thank you for helping us keep Scratch a friendly, creative community!"

View file

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
const Debug = ({label, data}) => (<div style={{padding: '2rem', border: '1px solid red', margin: '2rem'}}>
<small>{label}</small>
<code>
<pre style={{fontSize: '0.75rem'}}>
{JSON.stringify(data, null, ' ')}
</pre>
</code>
</div>);
Debug.propTypes = {
label: PropTypes.string,
data: PropTypes.any // eslint-disable-line react/forbid-prop-types
};
export default Debug;

View file

@ -0,0 +1,28 @@
const ITEM_LIMIT = 4;
const projectFetcher = (studioId, offset) =>
fetch(`${process.env.API_HOST}/studios/${studioId}/projects?limit=${ITEM_LIMIT}&offset=${offset}`)
.then(response => response.json())
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
const curatorFetcher = (studioId, offset) =>
fetch(`${process.env.API_HOST}/studios/${studioId}/curators?limit=${ITEM_LIMIT}&offset=${offset}`)
.then(response => response.json())
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
const managerFetcher = (studioId, offset) =>
fetch(`${process.env.API_HOST}/studios/${studioId}/managers?limit=${ITEM_LIMIT}&offset=${offset}`)
.then(response => response.json())
.then(data => ({items: data, moreToLoad: data.length === ITEM_LIMIT}));
const activityFetcher = studioId =>
fetch(`${process.env.API_HOST}/studios/${studioId}/activity`)
.then(response => response.json())
.then(data => ({items: data, moreToLoad: false})); // No pagination on the activity feed
export {
activityFetcher,
projectFetcher,
curatorFetcher,
managerFetcher
};

View file

@ -0,0 +1,10 @@
import InfiniteList from '../../../redux/infinite-list';
const projects = InfiniteList('projects');
const curators = InfiniteList('curators');
const managers = InfiniteList('managers');
const activity = InfiniteList('activity');
export {
projects, curators, managers, activity
};

View file

@ -1,15 +1,54 @@
import React from 'react';
import {useParams} from 'react-router-dom';
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
const StudioActivity = () => {
import {connect} from 'react-redux';
import {useParams} from 'react-router';
import {activity} from './lib/redux-modules';
import {activityFetcher} from './lib/fetchers';
import Debug from './debug.jsx';
const StudioActivity = ({items, loading, error, onInitialLoad}) => {
const {studioId} = useParams();
// Fetch the data if none has been loaded yet. This would run only once,
// since studioId doesnt change, but the component is potentially mounted
// multiple times because of tab routing, so need to check for empty items.
useEffect(() => {
if (studioId && items.length === 0) onInitialLoad(studioId);
}, [studioId]); // items.length intentionally left out
return (
<div>
<h2>Activity</h2>
<p>Studio {studioId}</p>
{loading && <div>Loading...</div>}
{error && <Debug
label="Error"
data={error}
/>}
<div>
{items.map((item, index) =>
(<Debug
label="Activity Item"
data={item}
key={index}
/>)
)}
</div>
</div>
);
};
export default StudioActivity;
StudioActivity.propTypes = {
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
onInitialLoad: PropTypes.func
};
export default connect(
state => activity.selector(state),
dispatch => ({
onInitialLoad: studioId => dispatch(
activity.actions.loadMore(activityFetcher.bind(null, studioId, 0)))
})
)(StudioActivity);

View file

@ -1,16 +1,77 @@
import React from 'react';
import React, {useEffect, useCallback} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import {curators, managers} from './lib/redux-modules';
import {curatorFetcher, managerFetcher} from './lib/fetchers';
import Debug from './debug.jsx';
const StudioCurators = () => {
const {studioId} = useParams();
return (
<div>
<h2>Curators</h2>
<p>Studio {studioId}</p>
<h3>Managers</h3>
<ManagerList studioId={studioId} />
<hr />
<h3>Curators</h3>
<CuratorList studioId={studioId} />
</div>
);
};
const MemberList = ({studioId, items, error, loading, moreToLoad, onLoadMore}) => {
useEffect(() => {
if (studioId && items.length === 0) onLoadMore(studioId, 0);
}, [studioId]);
const handleLoadMore = useCallback(() => onLoadMore(studioId, items.length), [studioId, items.length]);
return (<React.Fragment>
{error && <Debug
label="Error"
data={error}
/>}
{items.map((item, index) =>
(<Debug
label="Member"
data={item}
key={index}
/>)
)}
{loading ? <small>Loading...</small> : (
moreToLoad ?
<button onClick={handleLoadMore}>
Load more
</button> :
<small>No more to load</small>
)}
</React.Fragment>);
};
MemberList.propTypes = {
studioId: PropTypes.string,
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
moreToLoad: PropTypes.bool,
onLoadMore: PropTypes.func
};
const ManagerList = connect(
state => managers.selector(state),
dispatch => ({
onLoadMore: (studioId, offset) => dispatch(
managers.actions.loadMore(managerFetcher.bind(null, studioId, offset)))
})
)(MemberList);
const CuratorList = connect(
state => curators.selector(state),
dispatch => ({
onLoadMore: (studioId, offset) => dispatch(
curators.actions.loadMore(curatorFetcher.bind(null, studioId, offset)))
})
)(MemberList);
export default StudioCurators;

View file

@ -1,15 +1,54 @@
import React from 'react';
import React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
import Debug from './debug.jsx';
import {getInfo, getRoles} from '../../redux/studio';
const StudioInfo = () => {
const StudioInfo = ({username, studio, token, onLoadInfo, onLoadRoles}) => {
const {studioId} = useParams();
useEffect(() => { // Load studio info after first render
if (studioId) onLoadInfo(studioId);
}, [studioId]);
useEffect(() => { // Load roles info once the username is available
if (studioId && username && token) onLoadRoles(studioId, username, token);
}, [studioId, username, token]);
return (
<div>
<h2>Studio Info</h2>
<p>Studio {studioId}</p>
<Debug
label="Studio Info"
data={studio}
/>
</div>
);
};
export default StudioInfo;
StudioInfo.propTypes = {
username: PropTypes.string,
token: PropTypes.string,
studio: PropTypes.shape({
// Fill this in as the data is used, just for demo now
}),
onLoadInfo: PropTypes.func,
onLoadRoles: PropTypes.func
};
export default connect(
state => {
const user = state.session.session.user;
return {
studio: state.studio,
username: user && user.username,
token: user && user.token
};
},
dispatch => ({
onLoadInfo: studioId => dispatch(getInfo(studioId)),
onLoadRoles: (studioId, username, token) => dispatch(
getRoles(studioId, username, token))
})
)(StudioInfo);

View file

@ -1,15 +1,65 @@
import React from 'react';
import React, {useEffect, useCallback} from 'react';
import PropTypes from 'prop-types';
import {useParams} from 'react-router-dom';
import {connect} from 'react-redux';
const StudioProjects = () => {
import {projectFetcher} from './lib/fetchers';
import {projects} from './lib/redux-modules';
import Debug from './debug.jsx';
const {actions, selector} = projects;
const StudioProjects = ({
items, error, loading, moreToLoad, onLoadMore
}) => {
const {studioId} = useParams();
useEffect(() => {
if (studioId && items.length === 0) onLoadMore(studioId, 0);
}, [studioId]);
const handleLoadMore = useCallback(() => onLoadMore(studioId, items.length), [studioId, items.length]);
return (
<div>
<h2>Projects</h2>
<p>Studio {studioId}</p>
{error && <Debug
label="Error"
data={error}
/>}
<div>
{items.map((item, index) =>
(<Debug
label="Project"
data={item}
key={index}
/>)
)}
{loading ? <small>Loading...</small> : (
moreToLoad ?
<button onClick={handleLoadMore}>
Load more
</button> :
<small>No more to load</small>
)}
</div>
</div>
);
};
export default StudioProjects;
StudioProjects.propTypes = {
items: PropTypes.array, // eslint-disable-line react/forbid-prop-types
loading: PropTypes.bool,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
moreToLoad: PropTypes.bool,
onLoadMore: PropTypes.func
};
const mapStateToProps = state => selector(state);
const mapDispatchToProps = dispatch => ({
onLoadMore: (studioId, offset) => dispatch(
actions.loadMore(projectFetcher.bind(null, studioId, offset))
)
});
export default connect(mapStateToProps, mapDispatchToProps)(StudioProjects);

View file

@ -9,6 +9,7 @@ import {
import Page from '../../components/page/www/page.jsx';
import render from '../../lib/render.jsx';
import StudioTabNav from './studio-tab-nav.jsx';
import StudioProjects from './studio-projects.jsx';
import StudioInfo from './studio-info.jsx';
@ -16,6 +17,15 @@ import StudioCurators from './studio-curators.jsx';
import StudioComments from './studio-comments.jsx';
import StudioActivity from './studio-activity.jsx';
import {
projects,
curators,
managers,
activity
} from './lib/redux-modules';
const {studioReducer} = require('../../redux/studio');
const StudioShell = () => {
const match = useRouteMatch();
@ -59,5 +69,12 @@ render(
</Switch>
</Router>
</Page>,
document.getElementById('app')
document.getElementById('app'),
{
[projects.key]: projects.reducer,
[curators.key]: curators.reducer,
[managers.key]: managers.reducer,
[activity.key]: activity.reducer,
studio: studioReducer
}
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View file

@ -51,6 +51,36 @@ describe('Compose Comment test', () => {
return wrapper.dive(); // unwrap redux connect(injectIntl(ComposeComment))
};
test('status is EDITING when props do not contain a muteStatus ', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.state.status).toBe('EDITING');
});
test('status is COMPOSE_DISALLOWED when props contain a future mute', () => {
jest.useFakeTimers();
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const mutedStore = mockStore({
session: {
session: {
user: {},
permissions: {
mute_status: {
muteExpiresAt: 5,
offenses: [],
showWarning: true
}
}
}
}
});
const component = getComposeCommentWrapper({}, mutedStore);
const commentInstance = component.instance();
expect(commentInstance.state.status).toBe('COMPOSE_DISALLOWED');
global.Date.now = realDateNow;
});
test('Modal & Comment status do not show ', () => {
const component = getComposeCommentWrapper({});
// Comment compsoe box is there
@ -68,7 +98,10 @@ describe('Compose Comment test', () => {
test('Error messages shows when comment rejected ', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({error: 'isFlood'});
commentInstance.setState({
error: 'isFlood',
status: 'REJECTED'
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true);
// Buttons stay enabled when comment rejected for non-mute reasons
@ -76,24 +109,25 @@ describe('Compose Comment test', () => {
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
});
test('No error message shows when comment rejected because user muted ', () => {
test('No error message shows when comment rejected because user is already muted ', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
error: 'isMuted',
status: 'REJECTED_MUTE'
status: 'COMPOSE_DISALLOWED'
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
});
test('Comment Status shows but compose box does not when mute expiration in the future ', () => {
test('Comment Status shows but compose box does not when you load the page and you are already muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({muteExpiresAtMs: 100});
commentInstance.setState({muteExpiresAtMs: 100, status: 'COMPOSE_DISALLOWED'});
component.update();
// Compose box should be hidden if muted unless they got muted due to a comment they just posted.
expect(component.find('FlexRow.compose-comment').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toEqual(false);
@ -172,7 +206,7 @@ describe('Compose Comment test', () => {
expect(component.find('CommentingStatus').exists()).toEqual(true);
global.Date.now = realDateNow;
});
test('Comment Status shows when user just submitted a reply comment that got them muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
@ -233,7 +267,7 @@ describe('Compose Comment test', () => {
const commentInstance = component.instance();
commentInstance.setState({
error: 'some error',
status: 'FLOOD'
status: 'REJECTED'
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true);
@ -335,7 +369,7 @@ describe('Compose Comment test', () => {
expect(component.find('MuteModal').props().showFeedback).toBe(true);
commentInstance.setState({
status: 'REJECTED_MUTE',
status: 'COMPOSE_DISALLOWED',
error: 'isMute',
showWarning: true,
muteOpen: true
@ -356,7 +390,6 @@ describe('Compose Comment test', () => {
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showFeedback).toBe(false);
});
test('shouldShowMuteModal is false when muteStatus is undefined ', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal()).toBe(false);
@ -389,7 +422,7 @@ describe('Compose Comment test', () => {
offenses: [offense]
};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(true);
expect(commentInstance.shouldShowMuteModal(muteStatus, true)).toBe(true);
global.Date.now = realDateNow;
});
@ -410,7 +443,7 @@ describe('Compose Comment test', () => {
offenses: offenses
};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(false);
expect(commentInstance.shouldShowMuteModal(muteStatus, true)).toBe(false);
global.Date.now = realDateNow;
});
@ -432,11 +465,47 @@ describe('Compose Comment test', () => {
showWarning: true
};
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus)).toBe(true);
expect(commentInstance.shouldShowMuteModal(muteStatus, true)).toBe(true);
global.Date.now = realDateNow;
});
test('getMuteModalStartStep: not a reply ', () => {
test('shouldShowMuteModal is false when the user is already muted, even when only 1 recent offesnse ', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
// it look like it was created < 2 minutes ago.
const offense = {
expiresAt: '1000',
createdAt: '-60' // ~1 ago min given shouldShowMuteModal's conversions,
};
const muteStatus = {
offenses: [offense]
};
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus, justMuted)).toBe(false);
global.Date.now = realDateNow;
});
test('shouldShowMuteModal is true when the user is already muted if the comment is a reply', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
// Since Date.now mocked to 0 above, we just need a small number to make
// it look like it was created < 2 minutes ago.
const offense = {
expiresAt: '1000',
createdAt: '-60' // ~1 ago min given shouldShowMuteModal's conversions,
};
const muteStatus = {
offenses: [offense]
};
const justMuted = false;
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
expect(commentInstance.shouldShowMuteModal(muteStatus, justMuted)).toBe(true);
global.Date.now = realDateNow;
});
test('getMuteModalStartStep: not a reply', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.getMuteModalStartStep()).toBe(0);
});
@ -452,7 +521,7 @@ describe('Compose Comment test', () => {
test('getMuteModalStartStep: A reply click when already muted ', () => {
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
commentInstance.setState({
status: 'EDITING'
status: 'COMPOSE_DISALLOWED'
});
expect(commentInstance.getMuteModalStartStep()).toBe(1);
});
@ -486,20 +555,53 @@ describe('Compose Comment test', () => {
global.Date.now = realDateNow;
});
test('getMuteMessageInfo: muteType set', () => {
test('getMuteMessageInfo: muteType set and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'unconstructive'});
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.unconstructive');
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.unconstructive');
expect(commentInstance.getMuteMessageInfo(justMuted)
.muteStepContent[0]).toBe('comment.unconstructive.content1');
});
test('getMuteMessageInfo: muteType not set', () => {
test('getMuteMessageInfo: muteType set and already muted', () => {
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.general');
commentInstance.setState({muteType: 'pii'});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.pii.past');
// PII has the same content1 regardless of whether you were just muted
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.pii.content1');
commentInstance.setState({muteType: 'vulgarity'});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.vulgarity.past');
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.type.vulgarity.past');
});
test('getMuteMessageInfo: muteType set to something we don\'t have messages for', () => {
test('getMuteMessageInfo: muteType not set and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general');
// general has the same content1 regardless of whether you were just muted
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.general.content1');
});
test('getMuteMessageInfo: muteType not set and already muted', () => {
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general.past');
});
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'spaghetti'});
expect(commentInstance.getMuteMessageInfo().commentType).toBe('comment.type.general');
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general');
});
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and already muted', () => {
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'spaghetti'});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general.past');
});
});

View file

@ -0,0 +1,175 @@
/* global Promise */
import InfiniteList from '../../../src/redux/infinite-list';
const module = InfiniteList('test-key');
let initialState;
describe('Infinite List redux module', () => {
beforeEach(() => {
initialState = module.reducer(undefined, {}); // eslint-disable-line no-undefined
});
describe('reducer', () => {
test('module contains a reducer', () => {
expect(typeof module.reducer).toBe('function');
});
test('initial state', () => {
expect(initialState).toMatchObject({
loading: true,
error: null,
items: [],
moreToLoad: false
});
});
describe('LOADING', () => {
let action;
beforeEach(() => {
action = module.actions.loading();
initialState.loading = false;
initialState.items = [1, 2, 3];
initialState.error = new Error();
});
test('sets the loading state', () => {
const newState = module.reducer(initialState, action);
expect(newState.loading).toBe(true);
});
test('maintains any existing data', () => {
const newState = module.reducer(initialState, action);
expect(newState.items).toBe(initialState.items);
});
test('clears any existing error', () => {
const newState = module.reducer(initialState, action);
expect(newState.error).toBe(null);
});
});
describe('APPEND', () => {
let action;
beforeEach(() => {
action = module.actions.append([4, 5, 6], true);
});
test('appends the new items', () => {
initialState.items = [1, 2, 3];
const newState = module.reducer(initialState, action);
expect(newState.items).toEqual([1, 2, 3, 4, 5, 6]);
});
test('sets the moreToLoad state', () => {
initialState.moreToLoad = false;
const newState = module.reducer(initialState, action);
expect(newState.moreToLoad).toEqual(true);
});
test('clears any existing error and loading state', () => {
initialState.error = new Error();
initialState.loading = true;
const newState = module.reducer(initialState, action);
expect(newState.error).toBe(null);
expect(newState.error).toBe(null);
});
});
describe('REPLACE', () => {
let action;
beforeEach(() => {
action = module.actions.replace(2, 55);
});
test('replaces the given index with the new item', () => {
initialState.items = [8, 9, 10, 11];
const newState = module.reducer(initialState, action);
expect(newState.items).toEqual([8, 9, 55, 11]);
});
});
describe('REMOVE', () => {
let action;
beforeEach(() => {
action = module.actions.remove(2);
});
test('removes the given index', () => {
initialState.items = [8, 9, 10, 11];
const newState = module.reducer(initialState, action);
expect(newState.items).toEqual([8, 9, 11]);
});
});
describe('CREATE', () => {
let action;
beforeEach(() => {
action = module.actions.create(7);
});
test('prepends the given item', () => {
initialState.items = [8, 9, 10, 11];
const newState = module.reducer(initialState, action);
expect(newState.items).toEqual([7, 8, 9, 10, 11]);
});
});
describe('ERROR', () => {
let action;
let error = new Error();
beforeEach(() => {
action = module.actions.error(error);
});
test('sets the error state', () => {
const newState = module.reducer(initialState, action);
expect(newState.error).toBe(error);
});
test('resets loading to false', () => {
initialState.loading = true;
const newState = module.reducer(initialState, action);
expect(newState.loading).toBe(false);
});
test('maintains any existing data', () => {
initialState.items = [1, 2, 3];
const newState = module.reducer(initialState, action);
expect(newState.items).toEqual([1, 2, 3]);
});
});
});
describe('action creators', () => {
test('module contains actions creators', () => {
// The actual action creators are tested above in the reducer tests
for (let key in module.actions) {
expect(typeof module.actions[key]).toBe('function');
}
});
describe('loadMore', () => {
test('returns a thunk function, rather than a standard action object', () => {
expect(typeof module.actions.loadMore()).toBe('function');
});
test('calls loading and the fetcher', () => {
let dispatch = jest.fn();
let fetcher = jest.fn(() => new Promise(() => { })); // that never resolves
module.actions.loadMore(fetcher)(dispatch);
expect(dispatch).toHaveBeenCalledWith(module.actions.loading());
expect(fetcher).toHaveBeenCalled();
});
test('calls append with resolved result from fetcher', async () => {
let dispatch = jest.fn();
let fetcher = jest.fn(() => Promise.resolve({items: ['a', 'b'], moreToLoad: false}));
await module.actions.loadMore(fetcher)(dispatch);
expect(dispatch.mock.calls[1][0]) // the second call to dispatch, after LOADING
.toEqual(module.actions.append(['a', 'b'], false));
});
test('calls error with rejecting promise from fetcher', async () => {
let error = new Error();
let dispatch = jest.fn();
let fetcher = jest.fn(() => Promise.reject(error));
await module.actions.loadMore(fetcher)(dispatch);
expect(dispatch.mock.calls[1][0]) // the second call to dispatch, after LOADING
.toEqual(module.actions.error(error));
});
});
});
describe('selector', () => {
test('will return the slice of state defined by the key', () => {
const state = {
[module.key]: module.reducer(undefined, {}) // eslint-disable-line no-undefined
};
expect(module.selector(state)).toBe(initialState);
});
});
});