Merge pull request #6428 from LLK/release/2022-01-05

[Master] release/2022-01-05
This commit is contained in:
chrisgarrity 2022-01-06 14:00:11 -05:00 committed by GitHub
commit 17a96b4065
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1006 additions and 276 deletions

View file

@ -38,6 +38,33 @@ aliases:
keys: keys:
- v1-build-{{ .Revision }} - v1-build-{{ .Revision }}
- v1-build- - v1-build-
- &build_no_cache
<<: *defaults
resource_class: large
steps:
- checkout
- run:
name: "setup"
command: |
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
- store_test_results:
path: test/results
- &build - &build
<<: *defaults <<: *defaults
resource_class: large resource_class: large
@ -129,6 +156,8 @@ jobs:
<<: *integration_jest <<: *integration_jest
update-translations: update-translations:
<<: *update-translations <<: *update-translations
build-no-cache:
<<: *build_no_cache
workflows: workflows:
build-test-deploy: build-test-deploy:
@ -139,8 +168,10 @@ workflows:
- scratch-www-staging - scratch-www-staging
filters: filters:
branches: branches:
ignore: only:
- master - develop
- /^hotfix\/.*/
- /^release\/.*/
- build-production: - build-production:
context: context:
- scratch-www-all - scratch-www-all
@ -209,3 +240,13 @@ workflows:
branches: branches:
only: only:
- develop - develop
build-test-no-deploy:
jobs:
- build-no-cache:
filters:
branches:
ignore:
- develop
- master
- /^hotfix\/.*/
- /^release\/.*/

158
package-lock.json generated
View file

@ -4509,9 +4509,9 @@
"dev": true "dev": true
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001282", "version": "1.0.30001296",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz",
"integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==",
"dev": true "dev": true
}, },
"canvas-fit": { "canvas-fit": {
@ -4655,9 +4655,9 @@
} }
}, },
"chromedriver": { "chromedriver": {
"version": "95.0.0", "version": "96.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-95.0.0.tgz", "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-96.0.0.tgz",
"integrity": "sha512-HwSg7S0ZZYsHTjULwxFHrrUqEpz1+ljDudJM3eOquvqD5QKnR5pSe/GlBTY9UU2tVFRYz8bEHYC4Y8qxciQiLQ==", "integrity": "sha512-4g6Hn5RHGsbaBmOrJbDlz/hdVPOc22eRsbvoAAMqkZxR2NJCcddHzCw2FAQeW8lX/C7xWVz3nyDsKX3fE9lIIw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@testim/chrome-version": "^1.0.7", "@testim/chrome-version": "^1.0.7",
@ -20578,9 +20578,9 @@
} }
}, },
"scratch-gui": { "scratch-gui": {
"version": "0.1.0-prerelease.20211117061326", "version": "0.1.0-prerelease.20220105091637",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20211117061326.tgz", "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20220105091637.tgz",
"integrity": "sha512-+JF0VztQWTtbVFnsRWpINXCU0HpbmV79or80Ho3akOvYkrJtGdMZt1lyEqYUflBAGRrLthZsefl/wiTLay6wfw==", "integrity": "sha512-a6QHqOKotK4T+F7V1O3Bec8KRTVqCD8N0syPyOX04ja3MlGjA2D7axT/xCfKeTldnD73Tko0vIqqRgZQsy2Dmw==",
"dev": true, "dev": true,
"requires": { "requires": {
"arraybuffer-loader": "^1.0.6", "arraybuffer-loader": "^1.0.6",
@ -20632,13 +20632,13 @@
"redux-throttle": "0.1.1", "redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344", "scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20211110095305", "scratch-blocks": "0.1.0-prerelease.20211110095305",
"scratch-l10n": "3.14.20211117031600", "scratch-l10n": "3.14.20220105031522",
"scratch-paint": "0.2.0-prerelease.20211027080909", "scratch-paint": "0.2.0-prerelease.20211027080909",
"scratch-render": "0.1.0-prerelease.20211028200436", "scratch-render": "0.1.0-prerelease.20211028200436",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003", "scratch-render-fonts": "1.0.0-prerelease.20210401210003",
"scratch-storage": "1.3.5", "scratch-storage": "1.3.5",
"scratch-svg-renderer": "0.2.0-prerelease.20210727023023", "scratch-svg-renderer": "0.2.0-prerelease.20210727023023",
"scratch-vm": "0.2.0-prerelease.20211110140254", "scratch-vm": "0.2.0-prerelease.20220102085704",
"startaudiocontext": "1.2.1", "startaudiocontext": "1.2.1",
"style-loader": "^0.23.0", "style-loader": "^0.23.0",
"text-encoding": "0.7.0", "text-encoding": "0.7.0",
@ -20678,13 +20678,13 @@
"dev": true "dev": true
}, },
"browserslist": { "browserslist": {
"version": "4.18.1", "version": "4.19.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz",
"integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001280", "caniuse-lite": "^1.0.30001286",
"electron-to-chromium": "^1.3.896", "electron-to-chromium": "^1.4.17",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"node-releases": "^2.0.1", "node-releases": "^2.0.1",
"picocolors": "^1.0.0" "picocolors": "^1.0.0"
@ -20789,9 +20789,9 @@
"dev": true "dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.900", "version": "1.4.35",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.35.tgz",
"integrity": "sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ==", "integrity": "sha512-wzTOMh6HGFWeALMI3bif0mzgRrVGyP1BdFRx7IvWukFrSC5QVQELENuy+Fm2dCrAdQH9T3nuqr07n94nPDFBWA==",
"dev": true "dev": true
}, },
"has-flag": { "has-flag": {
@ -20905,9 +20905,9 @@
} }
}, },
"postcss-value-parser": { "postcss-value-parser": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "dev": true
}, },
"react": { "react": {
@ -20934,6 +20934,12 @@
"prop-types": "^15.6.0" "prop-types": "^15.6.0"
} }
}, },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"react-modal": { "react-modal": {
"version": "3.9.1", "version": "3.9.1",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.9.1.tgz", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.9.1.tgz",
@ -20958,14 +20964,14 @@
}, },
"dependencies": { "dependencies": {
"prop-types": { "prop-types": {
"version": "15.7.2", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true, "dev": true,
"requires": { "requires": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"react-is": "^16.8.1" "react-is": "^16.13.1"
} }
} }
} }
@ -21054,9 +21060,9 @@
} }
}, },
"scratch-l10n": { "scratch-l10n": {
"version": "3.14.20211117031600", "version": "3.14.20220105031522",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.14.20211117031600.tgz", "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.14.20220105031522.tgz",
"integrity": "sha512-ekVIL9BLg6ZISYYbd3ID35rg0NcCyYvrrmgtIwUl0y7bgc+DSFBaZZoQN0rBahIM6tsEDpbc278nMDHKMhs0VA==", "integrity": "sha512-Rtji2weEQvFMzuxvM61B3zMqIRiKaTKMU59jWh5xYykKvRBre/DejtjG79XeBT64O4aCt565h4t5HRX/d1YUmA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/cli": "^7.1.2", "@babel/cli": "^7.1.2",
@ -21285,9 +21291,9 @@
"dev": true "dev": true
}, },
"scratch-vm": { "scratch-vm": {
"version": "0.2.0-prerelease.20211110140254", "version": "0.2.0-prerelease.20220102085704",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20211110140254.tgz", "resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20220102085704.tgz",
"integrity": "sha512-6gNNDXJg3WagT/tA7bjLJcQeDqhvu/FZVdIAlS3A96C/6sT3jiWljDeRpzl/EbJWAxwzbQ6CBiZ3ZDf9eLgQ2Q==", "integrity": "sha512-nf0uhCde7XjdEj8SsSfJlXygbwOQXQK5NnOCCJBCbav/bML0NnuE+O6WtYPi4/HiFftT/QqSacEpGkgs5AhkuQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@vernier/godirect": "1.5.0", "@vernier/godirect": "1.5.0",
@ -21431,21 +21437,20 @@
"dev": true "dev": true
}, },
"selenium-webdriver": { "selenium-webdriver": {
"version": "3.6.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.0.tgz",
"integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", "integrity": "sha512-kUDH4N8WruYprTzvug4Pl73Th+WKb5YiLz8z/anOpHyUNUdM3UzrdTOxmSNaf9AczzBeY+qXihzku8D1lMaKOg==",
"dev": true, "dev": true,
"requires": { "requires": {
"jszip": "^3.1.3", "jszip": "^3.6.0",
"rimraf": "^2.5.4", "tmp": "^0.2.1",
"tmp": "0.0.30", "ws": ">=7.4.6"
"xml2js": "^0.4.17"
}, },
"dependencies": { "dependencies": {
"glob": { "glob": {
"version": "7.1.4", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@ -21456,23 +21461,56 @@
"path-is-absolute": "^1.0.0" "path-is-absolute": "^1.0.0"
} }
}, },
"jszip": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
"dev": true,
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
},
"rimraf": { "rimraf": {
"version": "2.6.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true, "dev": true,
"requires": { "requires": {
"glob": "^7.1.3" "glob": "^7.1.3"
} }
}, },
"tmp": { "tmp": {
"version": "0.0.30", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
"integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"os-tmpdir": "~1.0.1" "rimraf": "^3.0.0"
} }
},
"ws": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz",
"integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==",
"dev": true
} }
} }
}, },
@ -21531,6 +21569,12 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true "dev": true
}, },
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
},
"set-value": { "set-value": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@ -27721,22 +27765,6 @@
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
"dev": true "dev": true
}, },
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"dev": true,
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
"dev": true
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View file

@ -7,8 +7,8 @@
"test": "npm run test:lint && npm run build && npm run test:unit", "test": "npm run test:lint && npm run build && npm run test:unit",
"test:lint": "eslint . --ext .js,.jsx,.json", "test:lint": "eslint . --ext .js,.jsx,.json",
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml", "test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml",
"test:integration": "jest ./test/integration/*.test.js --reporters=default --runInBand", "test:integration": "jest ./test/integration/*.test.js --reporters=default --maxWorkers=5",
"test:integration:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default --runInBand", "test:integration:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js --reporters=default --maxWorkers=5",
"test:unit": "npm run test:unit:jest && npm run test:unit:tap", "test:unit": "npm run test:unit:jest && npm run test:unit:tap",
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization", "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:unit": "jest ./test/unit/ --reporters=default",
@ -72,7 +72,7 @@
"babel-preset-es2015": "6.22.0", "babel-preset-es2015": "6.22.0",
"babel-preset-react": "6.22.0", "babel-preset-react": "6.22.0",
"bowser": "1.9.4", "bowser": "1.9.4",
"chromedriver": "95.0.0", "chromedriver": "96.0.0",
"classnames": "2.2.5", "classnames": "2.2.5",
"cookie": "0.4.1", "cookie": "0.4.1",
"copy-webpack-plugin": "4.6.0", "copy-webpack-plugin": "4.6.0",
@ -127,9 +127,9 @@
"redux-thunk": "2.0.1", "redux-thunk": "2.0.1",
"regenerator-runtime": "0.13.9", "regenerator-runtime": "0.13.9",
"sass-loader": "6.0.6", "sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20211117061326", "scratch-gui": "0.1.0-prerelease.20220105091637",
"scratch-l10n": "3.14.20211117031600", "scratch-l10n": "3.14.20220105031522",
"selenium-webdriver": "3.6.0", "selenium-webdriver": "4.1.0",
"slick-carousel": "1.6.0", "slick-carousel": "1.6.0",
"style-loader": "0.12.3", "style-loader": "0.12.3",
"tap": "14.11.0", "tap": "14.11.0",

View file

@ -3,7 +3,7 @@
$navigation-height: 50px; $navigation-height: 50px;
.banner { .banner {
position: fixed; position: sticky;
top: $navigation-height; top: $navigation-height;
z-index: 9; z-index: 9;
box-shadow: 0 1px 1px $ui-dark-gray; box-shadow: 0 1px 1px $ui-dark-gray;

View file

@ -0,0 +1,54 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import DropdownBanner from '../banner.jsx';
const FormattedMessage = require('react-intl').FormattedMessage;
const EmailConfirmationModal = require('../../../components/modal/email-confirmation/modal.jsx');
const EmailConfirmationBanner = ({onRequestDismiss}) => {
const [showEmailConfirmationModal, setShowEmailConfirmationModal] = useState(false);
return (
<React.Fragment>
{(showEmailConfirmationModal && <EmailConfirmationModal
isOpen
onRequestClose={() => {
setShowEmailConfirmationModal(false);
}}
/>)}
<DropdownBanner
className="warning"
key="confirmedEmail"
onRequestDismiss={onRequestDismiss}
>
<FormattedMessage
id="emailConfirmationBanner.confirm"
values={{
confirmLink: (
<a
className="showEmailConfirmationModalLink"
href="#"
onClick={() => {
setShowEmailConfirmationModal(true);
}}
>
<FormattedMessage id="emailConfirmationBanner.confirmLinkText" />
</a>
),
faqLink: (
<a href="/faq/#accounts">
<FormattedMessage id="emailConfirmationBanner.faqLinkText" />
</a>
)
}}
/>
</DropdownBanner>
</React.Fragment>);
};
EmailConfirmationBanner.propTypes = {
onRequestDismiss: PropTypes.func
};
module.exports = EmailConfirmationBanner;

View file

@ -0,0 +1,101 @@
import React, {useState} from 'react';
const connect = require('react-redux').connect;
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import Modal from '../base/modal.jsx';
require('./modal.scss');
const EmailConfirmationModal = ({
email, onRequestClose, isOpen
}) => {
const [showEmailTips, setShowEmailTips] = useState(false);
return (
<Modal
isOpen={isOpen}
showCloseButton
useStandardSizes
onRequestClose={onRequestClose}
>
<div className="top-close-bar" />
<div className="modal-main-content">
<img
className="modal-image"
alt="email-confirmation-illustration"
src="/svgs/modal/confirm-email-illustration.svg"
/>
<div className="modal-text-content">
{showEmailTips ?
(<React.Fragment>
<h1><FormattedMessage id="emailConfirmationModal.confirmingTips" /></h1>
<ul>
<li><FormattedMessage id="emailConfirmationModal.tipWaitTenMinutes" /></li>
<li><FormattedMessage id="emailConfirmationModal.tipCheckSpam" /></li>
<li><FormattedMessage
id="emailConfirmationModal.correctEmail"
values={
{accountSettings:
(<a href="/accounts/email_change/">
<FormattedMessage id="emailConfirmationModal.accountSettings" />
</a>)
}
}
/></li>
</ul>
</React.Fragment>) :
(<React.Fragment>
<h1><FormattedMessage id="emailConfirmationModal.confirm" /></h1>
<p><FormattedMessage id="emailConfirmationModal.wantToShare" /></p>
<p><FormattedMessage id="emailConfirmationModal.clickEmailLink" /></p>
<p><b>{email}</b></p>
<a href="/accounts/email_change/">
<FormattedMessage id="emailConfirmationModal.resendEmail" />
</a>
</React.Fragment>)
}
</div>
</div>
<div className="guide-footer">
{showEmailTips ?
(<React.Fragment>
<FormattedMessage
id="emailConfirmationModal.wantMoreInfo"
values={
{FAQLink:
(<a href="/faq#accounts">
<FormattedMessage id="emailConfirmationModal.checkOutFAQ" />
</a>)
}
}
/>
</React.Fragment>) :
(<React.Fragment>
<FormattedMessage
id="emailConfirmationModal.havingTrouble"
values={{tipsLink: (
<a
onClick={e => { // eslint-disable-line react/jsx-no-bind
e.preventDefault();
setShowEmailTips(true);
}}
>
<FormattedMessage id="emailConfirmationModal.checkOutTips" />
</a>)}}
/>
</React.Fragment>)}
</div>
</Modal>);
};
EmailConfirmationModal.propTypes = {
email: PropTypes.string,
isOpen: PropTypes.bool,
onRequestClose: PropTypes.func
};
const mapStateToProps = state => ({
email: state.session.session.user.email
});
module.exports = connect(mapStateToProps)(EmailConfirmationModal);

View file

@ -0,0 +1,45 @@
@import "../../../colors";
@import "../../../frameless";
.modal-content{
border-radius: 4px;
overflow: hidden;
max-width: 500px;
h1{
font-size: 2rem;
line-height: 2.5rem;
}
.modal-content-close{
top: 5px;
right: 5px;
}
.modal-main-content{
display: flex;
}
.modal-text-content{
margin-top: 30px;
margin-bottom: 30px;
margin-right: 20px;
margin-left: 20px;
}
.modal-image{
margin-left: -70px;
}
.top-close-bar{
height: 44px;
background-color: #0EBD8C;
}
.guide-footer{
text-align: center;
border-top: 1px solid rgba(77, 151, 255, 0.15);
padding: 20px;
}
}

View file

@ -245,6 +245,24 @@
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!", "registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
"registration.welcomeStepTitleNonEducator": "Welcome to Scratch, {username}!", "registration.welcomeStepTitleNonEducator": "Welcome to Scratch, {username}!",
"emailConfirmationBanner.confirm": "{confirmLink} to enable sharing. {faqLink}",
"emailConfirmationBanner.confirmLinkText": "Confirm your email",
"emailConfirmationBanner.faqLinkText": "Having trouble?",
"emailConfirmationModal.confirm": "Confirm your email",
"emailConfirmationModal.wantToShare": "Want to share on Scratch?",
"emailConfirmationModal.clickEmailLink": "Confirm your email address by clicking the link in the email we sent to:",
"emailConfirmationModal.resendEmail": "Resend confirmation email",
"emailConfirmationModal.confirmingTips": "Tips for confirming your email address",
"emailConfirmationModal.tipWaitTenMinutes": "Wait for ten minutes. The email may take a while to arrive.",
"emailConfirmationModal.tipCheckSpam": "Check your spam folder.",
"emailConfirmationModal.correctEmail": "Make sure your email address is correct, see {accountSettings}.",
"emailConfirmationModal.accountSettings": "Account Settings",
"emailConfirmationModal.wantMoreInfo": "Want more information? {FAQLink}",
"emailConfirmationModal.checkOutFAQ": "Check out the FAQ",
"emailConfirmationModal.havingTrouble": "Having Trouble? {tipsLink}",
"emailConfirmationModal.checkOutTips": "Check out these tips",
"thumbnail.by": "by", "thumbnail.by": "by",
"report.error": "Something went wrong when trying to send your message. Please try again.", "report.error": "Something went wrong when trying to send your message. Please try again.",
"report.project": "Report Project", "report.project": "Report Project",

View file

@ -4,6 +4,7 @@ const api = require('../lib/api');
const log = require('../lib/log'); const log = require('../lib/log');
const COMMENT_LIMIT = 20; const COMMENT_LIMIT = 20;
const REPLY_LIMIT = 25;
const { const {
addNewComment, addNewComment,
@ -44,7 +45,7 @@ const getReplies = (commentIds, offset) => ((dispatch, getState) => {
api({ api({
uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${parentId}/replies`, uri: `${isAdmin ? '/admin' : ''}/studios/${studioId}/comments/${parentId}/replies`,
authentication: token ? token : null, authentication: token ? token : null,
params: {offset: offset || 0, limit: COMMENT_LIMIT} params: {offset: offset || 0, limit: REPLY_LIMIT}
}, (err, body, res) => { }, (err, body, res) => {
if (err) { if (err) {
return callback(`Error fetching comment replies: ${err}`); return callback(`Error fetching comment replies: ${err}`);

View file

@ -248,6 +248,13 @@
"view": "privacypolicy/privacypolicy", "view": "privacypolicy/privacypolicy",
"title": "Privacy Policy" "title": "Privacy Policy"
}, },
{
"name": "privacypolicy-apps",
"pattern": "^/privacy_policy/apps?$",
"routeAlias": "/privacy_policy/apps?$",
"view": "privacypolicy-apps/privacypolicy-apps",
"title": "Privacy Policy"
},
{ {
"name": "research", "name": "research",
"pattern": "^/research/?$", "pattern": "^/research/?$",

View file

@ -1,6 +1,7 @@
{ {
"parents.title": "For Parents", "parents.title": "For Parents",
"parents.intro": "Scratch is a programming language and an online community where children\n can program and share interactive media such as stories, games, and \nanimation with people from all over the world. As children create with \nScratch, they learn to think creatively, work collaboratively, and \nreason systematically. Scratch is designed and maintained by the \nLifelong Kindergarten group at the MIT Media Lab. ", "parents.intro": "Scratch is a programming language and an online community where children\n can program and share interactive media such as stories, games, and \nanimation with people from all over the world. As children create with \nScratch, they learn to think creatively, work collaboratively, and \nreason systematically. Scratch is designed, developed, and moderated by the {scratchFoundation}, a nonprofit organization. ",
"parents.scratchFoundationLinkText": "Scratch Foundation",
"parents.overview": "How it works", "parents.overview": "How it works",
"parents.faq": "FAQ", "parents.faq": "FAQ",
"parents.overviewTitle": "How does Scratch work for children?", "parents.overviewTitle": "How does Scratch work for children?",
@ -27,8 +28,8 @@
"parents.faqCommunityTitle": "What is the Scratch online community?", "parents.faqCommunityTitle": "What is the Scratch online community?",
"parents.faqCommunityBody": "When participating in the Scratch online community, members can explore and experiment in an open learning community with other Scratch members from all backgrounds, ages, and interests. Members can share their work, get feedback, and learn from each other.", "parents.faqCommunityBody": "When participating in the Scratch online community, members can explore and experiment in an open learning community with other Scratch members from all backgrounds, ages, and interests. Members can share their work, get feedback, and learn from each other.",
"parents.faqGuidelinesTitle": "What are the guidelines for the Scratch online community?", "parents.faqGuidelinesTitle": "What are the guidelines for the Scratch online community?",
"parents.faqGuidelinesBody": "The MIT Scratch Team works with the community to maintain a friendly and respectful environment for people of all ages, races, ethnicities, religions, sexual orientations, and gender identities. You can help your child learn how to participate by reviewing the {communityGuidelines} together. Members are asked to comment constructively and to help keep the website friendly by reporting any content that does not follow the community guidelines. The Scratch Team works each day to manage activity on the site and respond to reports, with the help of tools such as the {CleanSpeak} profanity filter.", "parents.faqGuidelinesBody": "The Scratch Team works with the community to maintain a friendly and respectful environment for people of all ages, races, ethnicities, religions, sexual orientations, and gender identities. You can help your child learn how to participate by reviewing the {communityGuidelines} together. Members are asked to comment constructively and to help keep the website friendly by reporting any content that does not follow the Community Guidelines. The Scratch Team works each day to manage activity on the site and respond to reports, with the help of tools such as the {CleanSpeak} profanity filter.",
"parents.faqCommunityGuidelinesLinkText": "community guidelines", "parents.faqCommunityGuidelinesLinkText": "Community Guidelines",
"parents.faqPrivacyPolicyTitle": "What is your privacy policy?", "parents.faqPrivacyPolicyTitle": "What is your privacy policy?",
"parents.faqPrivacyPolicyBody": "To protect children's online privacy, we limit what we collect during the signup process, and what we make public on the website. We don't sell or rent account information to anyone. You can find out more about our {privacyPolicy} page.", "parents.faqPrivacyPolicyBody": "To protect children's online privacy, we limit what we collect during the signup process, and what we make public on the website. We don't sell or rent account information to anyone. You can find out more about our {privacyPolicy} page.",
"parents.faqFAQLinkText": "frequently asked questions page", "parents.faqFAQLinkText": "frequently asked questions page",

View file

@ -19,7 +19,18 @@ const Landing = () => (
</h1> </h1>
<FlexRow className="masthead-info"> <FlexRow className="masthead-info">
<p className="title-banner-p intro"> <p className="title-banner-p intro">
<FormattedMessage id="parents.intro" /> <FormattedMessage
id="parents.intro"
values={{
scratchFoundation: (
<a href="http://scratchfoundation.org">
<FormattedMessage
id="parents.scratchFoundationLinkText"
/>
</a>
)
}}
/>
</p> </p>
<div className="ted-talk"> <div className="ted-talk">
<iframe <iframe

View file

@ -32,6 +32,8 @@ const ComposeComment = require('./comment/compose-comment.jsx');
const ExtensionChip = require('./extension-chip.jsx'); const ExtensionChip = require('./extension-chip.jsx');
const thumbnailUrl = require('../../lib/user-thumbnail'); const thumbnailUrl = require('../../lib/user-thumbnail');
const FormsyProjectUpdater = require('./formsy-project-updater.jsx'); const FormsyProjectUpdater = require('./formsy-project-updater.jsx');
const EmailConfirmationModal = require('../../components/modal/email-confirmation/modal.jsx');
const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
@ -62,6 +64,7 @@ const PreviewPresentation = ({
canRestoreComments, canRestoreComments,
canSave, canSave,
canShare, canShare,
canSeeShare,
canToggleComments, canToggleComments,
canUseBackpack, canUseBackpack,
cloudHost, cloudHost,
@ -89,7 +92,9 @@ const PreviewPresentation = ({
onAddComment, onAddComment,
onAddToStudioClicked, onAddToStudioClicked,
onAddToStudioClosed, onAddToStudioClosed,
onBannerDismiss,
onCloseAdminPanel, onCloseAdminPanel,
onCloseEmailConfirmationModal,
onDeleteComment, onDeleteComment,
onFavoriteClicked, onFavoriteClicked,
onGreenFlag, onGreenFlag,
@ -109,6 +114,7 @@ const PreviewPresentation = ({
onSeeInside, onSeeInside,
onSetProjectThumbnailer, onSetProjectThumbnailer,
onShare, onShare,
onShareAttempt,
onSocialClicked, onSocialClicked,
onSocialClosed, onSocialClosed,
onToggleComments, onToggleComments,
@ -129,6 +135,8 @@ const PreviewPresentation = ({
reportOpen, reportOpen,
showAdminPanel, showAdminPanel,
showModInfo, showModInfo,
showEmailConfirmationModal,
showEmailConfirmationBanner,
singleCommentId, singleCommentId,
socialOpen, socialOpen,
userOwnsProject, userOwnsProject,
@ -170,7 +178,7 @@ const PreviewPresentation = ({
} }
/> />
); );
} else if (canShare) { } else if (canSeeShare) {
if (isShared && justShared) { // if was shared a while ago, don't show any share banner if (isShared && justShared) { // if was shared a while ago, don't show any share banner
if (isNewScratcher) { if (isNewScratcher) {
banner = (<Banner banner = (<Banner
@ -187,7 +195,7 @@ const PreviewPresentation = ({
banner = (<Banner banner = (<Banner
actionMessage={<FormattedMessage id="project.share.shareButton" />} actionMessage={<FormattedMessage id="project.share.shareButton" />}
message={<FormattedMessage id="project.share.notShared" />} message={<FormattedMessage id="project.share.notShared" />}
onAction={onShare} onAction={canShare ? onShare : onShareAttempt}
/>); />);
} }
} }
@ -208,6 +216,10 @@ const PreviewPresentation = ({
); );
return ( return (
<div className="preview"> <div className="preview">
{showEmailConfirmationModal && <EmailConfirmationModal
isOpen
onRequestClose={onCloseEmailConfirmationModal}
/>}
{showAdminPanel && ( {showAdminPanel && (
<AdminPanel <AdminPanel
className={classNames('project-admin-panel', { className={classNames('project-admin-panel', {
@ -228,6 +240,11 @@ const PreviewPresentation = ({
)} )}
{ projectInfo && projectInfo.author && projectInfo.author.id && ( { projectInfo && projectInfo.author && projectInfo.author.id && (
<React.Fragment> <React.Fragment>
{showEmailConfirmationBanner && <EmailConfirmationBanner
/* eslint-disable react/jsx-no-bind */
onRequestDismiss={() => onBannerDismiss('confirmed_email')}
/* eslint-enable react/jsx-no-bind */
/>}
{banner} {banner}
<div className="inner"> <div className="inner">
<FlexRow className="preview-row force-row"> <FlexRow className="preview-row force-row">
@ -689,6 +706,7 @@ PreviewPresentation.propTypes = {
backpackHost: PropTypes.string, backpackHost: PropTypes.string,
canAddToStudio: PropTypes.bool, canAddToStudio: PropTypes.bool,
canDeleteComments: PropTypes.bool, canDeleteComments: PropTypes.bool,
canSeeShare: PropTypes.bool,
canRemix: PropTypes.bool, canRemix: PropTypes.bool,
canReport: PropTypes.bool, canReport: PropTypes.bool,
canRestoreComments: PropTypes.bool, canRestoreComments: PropTypes.bool,
@ -724,7 +742,9 @@ PreviewPresentation.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onAddToStudioClicked: PropTypes.func, onAddToStudioClicked: PropTypes.func,
onAddToStudioClosed: PropTypes.func, onAddToStudioClosed: PropTypes.func,
onBannerDismiss: PropTypes.func,
onCloseAdminPanel: PropTypes.func, onCloseAdminPanel: PropTypes.func,
onCloseEmailConfirmationModal: PropTypes.func,
onDeleteComment: PropTypes.func, onDeleteComment: PropTypes.func,
onFavoriteClicked: PropTypes.func, onFavoriteClicked: PropTypes.func,
onGreenFlag: PropTypes.func, onGreenFlag: PropTypes.func,
@ -743,6 +763,7 @@ PreviewPresentation.propTypes = {
onSeeAllComments: PropTypes.func, onSeeAllComments: PropTypes.func,
onSeeInside: PropTypes.func, onSeeInside: PropTypes.func,
onSetProjectThumbnailer: PropTypes.func, onSetProjectThumbnailer: PropTypes.func,
onShareAttempt: PropTypes.func,
onShare: PropTypes.func, onShare: PropTypes.func,
onSocialClicked: PropTypes.func, onSocialClicked: PropTypes.func,
onSocialClosed: PropTypes.func, onSocialClosed: PropTypes.func,
@ -762,6 +783,8 @@ PreviewPresentation.propTypes = {
reportOpen: PropTypes.bool, reportOpen: PropTypes.bool,
showAdminPanel: PropTypes.bool, showAdminPanel: PropTypes.bool,
showCloudDataAlert: PropTypes.bool, showCloudDataAlert: PropTypes.bool,
showEmailConfirmationModal: PropTypes.bool,
showEmailConfirmationBanner: PropTypes.bool,
showModInfo: PropTypes.bool, showModInfo: PropTypes.bool,
showUsernameBlockAlert: PropTypes.bool, showUsernameBlockAlert: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),

View file

@ -62,6 +62,8 @@ class Preview extends React.Component {
'handleMessage', 'handleMessage',
'handlePopState', 'handlePopState',
'handleCloseAdminPanel', 'handleCloseAdminPanel',
'handleCloseEmailConfirmationModal',
'handleBannerDismiss',
'handleIsRemixing', 'handleIsRemixing',
'handleOpenAdminPanel', 'handleOpenAdminPanel',
'handleReportClick', 'handleReportClick',
@ -78,6 +80,7 @@ class Preview extends React.Component {
'handleSeeInside', 'handleSeeInside',
'handleSetProjectThumbnailer', 'handleSetProjectThumbnailer',
'handleShare', 'handleShare',
'handleShareAttempt',
'handleUpdateProjectData', 'handleUpdateProjectData',
'handleUpdateProjectId', 'handleUpdateProjectId',
'handleUpdateProjectTitle', 'handleUpdateProjectTitle',
@ -121,6 +124,7 @@ class Preview extends React.Component {
}, },
showCloudDataAlert: false, showCloudDataAlert: false,
showUsernameBlockAlert: false, showUsernameBlockAlert: false,
showEmailConfirmationModal: false,
projectId: parts[1] === 'editor' ? '0' : parts[1], projectId: parts[1] === 'editor' ? '0' : parts[1],
reportOpen: false, reportOpen: false,
singleCommentId: singleCommentId, singleCommentId: singleCommentId,
@ -619,6 +623,25 @@ class Preview extends React.Component {
justShared: true justShared: true
}); });
} }
handleShareAttempt () {
this.setState({
showEmailConfirmationModal: true
});
}
handleCloseEmailConfirmationModal () {
this.setState({showEmailConfirmationModal: false});
}
handleBannerDismiss (cue) {
api({
host: '',
uri: '/site-api/users/set-template-cue/',
method: 'post',
useCsrf: true,
json: {cue: cue, value: false}
}, err => {
if (!err) this.props.refreshSession();
});
}
handleUpdateProjectTitle (title) { handleUpdateProjectTitle (title) {
this.props.updateProject( this.props.updateProject(
this.props.projectInfo.id, this.props.projectInfo.id,
@ -727,6 +750,7 @@ class Preview extends React.Component {
canRestoreComments={this.props.isAdmin} canRestoreComments={this.props.isAdmin}
canSave={this.props.canSave} canSave={this.props.canSave}
canShare={this.props.canShare || this.props.isAdmin} canShare={this.props.canShare || this.props.isAdmin}
canSeeShare={this.props.userOwnsProject || this.props.isAdmin}
canToggleComments={this.props.canToggleComments} canToggleComments={this.props.canToggleComments}
canUseBackpack={this.props.canUseBackpack} canUseBackpack={this.props.canUseBackpack}
cloudHost={this.props.cloudHost} cloudHost={this.props.cloudHost}
@ -762,6 +786,8 @@ class Preview extends React.Component {
showAdminPanel={this.props.isAdmin} showAdminPanel={this.props.isAdmin}
showCloudDataAlert={this.state.showCloudDataAlert} showCloudDataAlert={this.state.showCloudDataAlert}
showModInfo={this.props.isAdmin} showModInfo={this.props.isAdmin}
showEmailConfirmationModal={this.state.showEmailConfirmationModal}
showEmailConfirmationBanner={this.props.showEmailConfirmationBanner}
showUsernameBlockAlert={this.state.showUsernameBlockAlert} showUsernameBlockAlert={this.state.showUsernameBlockAlert}
singleCommentId={this.state.singleCommentId} singleCommentId={this.state.singleCommentId}
socialOpen={this.state.socialOpen} socialOpen={this.state.socialOpen}
@ -770,7 +796,9 @@ class Preview extends React.Component {
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
onAddToStudioClicked={this.handleAddToStudioClick} onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose} onAddToStudioClosed={this.handleAddToStudioClose}
onBannerDismiss={this.handleBannerDismiss}
onCloseAdminPanel={this.handleCloseAdminPanel} onCloseAdminPanel={this.handleCloseAdminPanel}
onCloseEmailConfirmationModal={this.handleCloseEmailConfirmationModal}
onDeleteComment={this.handleDeleteComment} onDeleteComment={this.handleDeleteComment}
onFavoriteClicked={this.handleFavoriteToggle} onFavoriteClicked={this.handleFavoriteToggle}
onGreenFlag={this.handleGreenFlag} onGreenFlag={this.handleGreenFlag}
@ -790,6 +818,7 @@ class Preview extends React.Component {
onSeeInside={this.handleSeeInside} onSeeInside={this.handleSeeInside}
onSetProjectThumbnailer={this.handleSetProjectThumbnailer} onSetProjectThumbnailer={this.handleSetProjectThumbnailer}
onShare={this.handleShare} onShare={this.handleShare}
onShareAttempt={this.handleShareAttempt}
onSocialClicked={this.handleSocialClick} onSocialClicked={this.handleSocialClick}
onSocialClosed={this.handleSocialClose} onSocialClosed={this.handleSocialClose}
onToggleComments={this.handleToggleComments} onToggleComments={this.handleToggleComments}
@ -917,6 +946,7 @@ Preview.propTypes = {
projectInfo: projectShape, projectInfo: projectShape,
projectNotAvailable: PropTypes.bool, projectNotAvailable: PropTypes.bool,
projectStudios: PropTypes.arrayOf(PropTypes.object), projectStudios: PropTypes.arrayOf(PropTypes.object),
refreshSession: PropTypes.func,
registrationOpen: PropTypes.bool, registrationOpen: PropTypes.bool,
remixProject: PropTypes.func, remixProject: PropTypes.func,
remixes: PropTypes.arrayOf(PropTypes.object), remixes: PropTypes.arrayOf(PropTypes.object),
@ -929,6 +959,7 @@ Preview.propTypes = {
setLovedStatus: PropTypes.func.isRequired, setLovedStatus: PropTypes.func.isRequired,
setPlayer: PropTypes.func.isRequired, setPlayer: PropTypes.func.isRequired,
shareProject: PropTypes.func.isRequired, shareProject: PropTypes.func.isRequired,
showEmailConfirmationBanner: PropTypes.bool,
toggleStudio: PropTypes.func.isRequired, toggleStudio: PropTypes.func.isRequired,
updateProject: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired,
useScratch3Registration: PropTypes.bool, useScratch3Registration: PropTypes.bool,
@ -985,7 +1016,9 @@ const mapStateToProps = state => {
(authorUsername === state.session.session.user.username || (authorUsername === state.session.session.user.username ||
state.permissions.admin === true); state.permissions.admin === true);
const areCommentsOn = state.session.session.flags && selectProjectCommentsGloballyEnabled(state); const areCommentsOn = state.session.session.flags && selectProjectCommentsGloballyEnabled(state);
const showEmailConfirmationBanner = state.session.session.flags &&
state.session.session.flags.has_outstanding_email_confirmation &&
state.session.session.flags.confirm_email_banner;
// if we don't have projectInfo, assume it's shared until we know otherwise // if we don't have projectInfo, assume it's shared until we know otherwise
const isShared = !projectInfoPresent || state.preview.projectInfo.is_published; const isShared = !projectInfoPresent || state.preview.projectInfo.is_published;
@ -1032,6 +1065,7 @@ const mapStateToProps = state => {
remixes: state.preview.remixes, remixes: state.preview.remixes,
replies: state.comments.replies, replies: state.comments.replies,
sessionStatus: state.session.status, // check if used sessionStatus: state.session.status, // check if used
showEmailConfirmationBanner,
useScratch3Registration: state.navigation.useScratch3Registration, useScratch3Registration: state.navigation.useScratch3Registration,
user: state.session.session.user, user: state.session.session.user,
userOwnsProject: userOwnsProject, userOwnsProject: userOwnsProject,
@ -1146,6 +1180,9 @@ const mapDispatchToProps = dispatch => ({
dispatch(GUI.remixProject()); dispatch(GUI.remixProject());
dispatch(projectCommentActions.resetComments()); dispatch(projectCommentActions.resetComments());
}, },
refreshSession: () => {
dispatch(sessionActions.refreshSession());
},
setPlayer: player => { setPlayer: player => {
dispatch(GUI.setPlayer(player)); dispatch(GUI.setPlayer(player));
}, },

View file

@ -0,0 +1,68 @@
{
"privacyApps.title":"Privacy Policy",
"privacyApps.updated":"The Scratch Privacy Policy was last updated: January 6, 2022",
"privacyApps.intro":"The Scratch Foundation (“Scratch”, “we” or “us”) understands how important privacy is to our community. We wrote this Privacy Policy to explain what Personal Information (“Information”) we collect through our offline editor (the “Scratch App”), how we use, process, and share it, and what were doing to keep it safe. It also tells you about your rights and choices with respect to your Personal Information, and how you can contact us if you have any questions or concerns.",
"privacyApps.collectionHeader": "What Information Does Scratch Collect About Me?",
"privacyApps.collectionParagraph": "For the purpose of this Privacy Policy, “Information” means any information relating to an identified or identifiable individual. The Scratch App automatically collects and stores locally the following Information through its telemetry system: the title of your project in text form, language setting, time zone and events related to your use of the Scratch App (namely when the Scratch App was opened and closed, if a project file has been loaded or saved, or if a new project is created). If you choose to turn on the telemetry sharing feature, the Scratch App will transmit this information to Scratch. Projects created in the Scratch App are not transmitted to or accessible by Scratch unless you choose to upload your project to the Scratch Online Community, at which point the information you share will be subject to the terms of the Scratch Online Community {privacyPolicyLink}. Please see the section “What Happens if I Upload My Project to the Scratch Online Community?” below for more information.",
"privacyApps.privacyPolicyLinkText": "Privacy Policy",
"privacyApps.usageHeader": "How Does Scratch Use My Information?",
"privacyApps.usageIntro": "We use this Information for the following purposes:",
"privacyApps.analyticsTitle": "Analytics and Improving the Scratch App",
"privacyApps.analyticsDescription": "We use the Information to analyze use of the Scratch App and to enhance your learning experience on the Scratch App.",
"privacyApps.researchTitle": "Academic and Scientific Research",
"privacyApps.researchDescription": "We de-identify and aggregate Information for statistical analysis in the context of scientific and academic research. For example, to help us understand how people learn through the Scratch App and how we can enhance learning tools for young people. The results of such research are shared with educators and researchers through conferences, journals, and other academic or scientific publications. You can find out more on our {researchPageLink} page.",
"privacyApps.researchPageLinkText": "Research",
"privacyApps.legalTitle": "Legal",
"privacyApps.legalDescription": "We may use your Information to enforce our {termsOfUseLink}, to defend our legal rights, and to comply with our legal obligations and internal policies. We may do this by analyzing your use of the Scratch App.",
"privacyApps.termsOfUseLinkText": "Terms of Use",
"privacyApps.processingHeader": "What Are The Legal Grounds For Processing Your Information?",
"privacyApps.processingParagraph": "If you are located in the European Economic Area, the United Kingdom or Switzerland, we only process your Information based on a valid legal ground. A “legal ground” is a reason that justifies our use of your Information. In this case, we or a third party have a legitimate interest in using your Information (if you choose to allow the Scratch App to send the Scratch team your Information) to create, analyze and share your aggregated or de-identified Information for research purposes, to analyze and enhance your learning experience on the Scratch App and otherwise ensure and improve the safety, security, and performance of the Scratch App. We only rely on our or a third partys legitimate interests to process your Information when these interests are not overridden by your rights and interests.",
"privacyApps.sharingHeader": "How Does Scratch Share My Information?",
"privacyApps.sharingIntro": "We disclose information that we collect through the Scratch App to third parties in the following circumstances:",
"privacyApps.serviceProvidersTitle": "Service Providers",
"privacyApps.serviceProvidersDescription": "To third parties who provide services such as website hosting, data analysis, Information technology and related infrastructure provisions, customer service, email delivery, and other services.",
"privacyApps.researchSharingDescription": "To research institutions, such as the Massachusetts Institute of Technology (MIT), to learn about how our users learn through the Scratch App and develop new learning tools. The results of this research or the statistical analysis may be shared through conferences, journals, and other publications.",
"privacyApps.mergerTitle": "Merger",
"privacyApps.mergerDescription": "To a potential or actual acquirer, successor, or assignee as part of any reorganization, merger, sale, joint venture, assignment, transfer, or other disposition of all or any portion of our organization or assets. You will have the opportunity to opt out of any such transfer if the new entitys planned processing of your Information differs materially from that set forth in this Privacy Policy.",
"privacyApps.legalSharingDescription": "If required to do so by law or in the good faith belief that such action is appropriate: (a) under applicable law, including laws outside your country of residence; (b) to comply with legal process; (c) to respond to requests from public and government authorities, such as school, school districts, and law enforcement, including public and government authorities outside your country of residence; (d) to enforce our terms and conditions; (e) to protect our operations or those of any of our affiliates; (f) to protect our rights, privacy, safety, or property, and/or that of our affiliates, you, or others; and (g) to allow us to pursue available remedies or limit the damages that we may sustain.",
"privacyApps.communityHeader": "What Happens If I Upload My Project to the Scratch Online Community?",
"privacyApps.communityParagraph": "While using the Scratch App, you may choose to upload your project to the Scratch online community (“Online Community”). If you choose to upload your project to the Online Community, you are sharing your information outside of the Scratch App and providing it to the Online Community service. The information you share when uploading your project, such as your account and project information, will be governed by the Scratch online community {privacyPolicyLink}.",
"privacyApps.studentsHeader": "Children and Student Privacy",
"privacyApps.coppa": "The Scratch Foundation is a 501(c)(3) nonprofit organization. As such, the Children's Online Privacy Protection Act (COPPA) does not apply to Scratch. Nevertheless, Scratch takes children's privacy seriously. Scratch collects only minimal information from its users, and only uses and discloses information to provide the services and for limited other purposes, such as research, as described in this Privacy Policy.",
"privacyApps.ferpa": "Scratch does not collect information from a student's education record, as defined by the Family Educational Rights and Privacy Act (FERPA). Scratch does not disclose information of students to any third parties except as described in this Privacy Policy.",
"privacyApps.eeaHeader": "Your Data Protection Rights (EEA)",
"privacyApps.eeaIntro": "If you are located in the European Economic Area, the United Kingdom or Switzerland, you have certain rights in relation to your Information:",
"privacyApps.accessTitle": "Access, Correction and Data Portability",
"privacyApps.accessDescription": "You may ask for an overview of the Information we process about you and to receive a copy of your Information. You also have the right to request to correct incomplete, inaccurate or outdated Information. To the extent required by applicable law, you may request us to provide your Information to another company.",
"privacyApps.objectionTitle": "Objection",
"privacyApps.objectionDescription": "You may object to (this means “ask us to stop”) any use of your Information that is not (i) processed to comply with a legal obligation, (ii) necessary to do what is provided in a contract between Scratch and you, or (iii) if we have a compelling reason to do so (such as, to ensure safety and security in our online community). If you do object, we will work with you to find a reasonable solution.",
"privacyApps.deletionTitle": "Deletion",
"privacyApps.deletionDescription": "You may also request the deletion of your Information, as permitted under applicable law. This applies, for instance, where your Information is outdated or the processing is not necessary or is unlawful; where you withdraw your consent to our processing based on such consent; or where you have objected to our processing. In some situations, we may need to retain your Information due to legal obligations or for litigation purposes. If you want to have all of your Information removed from our servers, please contact {helpEmail} for assistance.",
"privacyApps.restrictionTitle": "Restriction of Processing",
"privacyApps.restrictionDescription": "You may request that we restrict processing of your Information while we are processing a request relating to (i) the accuracy of your Information, (ii) the lawfulness of the processing of your Information, or (iii) our legitimate interests to process this Information. You may also request that we restrict processing of your Information if you wish to use the Information for litigation purposes.",
"privacyApps.withdrawalTitle": "Withdrawal Of Consent",
"privacyApps.withdrawalDescription": "Where we rely on consent for the processing of your Information, you have the right to withdraw it at any time and free of charge. When you do so, this will not affect the lawfulness of the processing before your consent withdrawal.",
"privacyApps.eeaComplaint":"In addition to the above-mentioned rights, you also have the right to lodge a complaint with a competent supervisory authority subject to applicable law. However, there are exceptions and limitations to each of these rights. We may, for example, refuse to act on a request if the request is manifestly unfounded or excessive, or if the request is likely to adversely affect the rights and freedoms of others, prejudice the execution or enforcement of the law, interfere with pending or future litigation, or infringe applicable law. To submit a request to exercise your rights, please contact {helpEmail} for assistance.",
"privacyApps.retentionHeader": "Data Retention",
"privacyApps.retentionParagraph": "We take measures to delete your Information or keep it in a form that does not allow you to be identified when this Information is no longer necessary for the purposes for which we process it, unless we are required by law to keep this Information for a longer period. When determining the retention period, we take into account various criteria, such as the type of services requested by or provided to you, the nature and length of our relationship with you, possible re-enrollment with our services, the impact on the services we provide to you if we delete some Information from or about you, mandatory retention periods provided by law and the statute of limitations.",
"privacyApps.protectHeader": "How Does Scratch Protect My Information?",
"privacyApps.protectParagraph": "Scratch has in place administrative, physical, and technical procedures that are intended to protect the Information we collect on the Scratch App against accidental or unlawful destruction, accidental loss, unauthorized alteration, unauthorized disclosure or access, misuse, and any other unlawful form of processing of the Information. However, as effective as these measures are, no security system is impenetrable. We cannot completely guarantee the security of our databases, nor can we guarantee that the Information you supply will not be intercepted while being transmitted to us over the Internet.",
"privacyApps.internationalTransferHeader": "International Data Transfer",
"privacyApps.internationalTransferParagraph": "We may transfer your Information to countries other than the country where you are located, including to the U.S. (where our Scratch servers are located) or any other country in which we or our service providers maintain facilities. If you are located in the European Economic Area, the United Kingdom or Switzerland, or other regions with laws governing data collection and use that may differ from U.S. law, please note that we may transfer your Information to a country and jurisdiction that does not have the same data protection laws as your jurisdiction. We apply appropriate safeguards to the Information processed and transferred on our behalf. Please contact us for more information on the safeguards used.",
"privacyApps.notificationsHeader": "Notifications Of Changes To The Privacy Policy",
"privacyApps.notificationsParagraph": "We review our Privacy Policy on a periodic basis, and we may modify our policies as appropriate. We will notify you of any material changes. We encourage you to review our Privacy Policy on a regular basis. The “Last Updated” date at the top of this page indicates when this Privacy Policy was last revised. Your continued use of the Scratch App following these changes means that you accept the revised Privacy Policy.",
"privacyApps.contactHeader": "Contact Us",
"privacyApps.contactIntro": "The Scratch Foundation is the entity responsible for the processing of your Information. If you have any questions about this Privacy Policy, or if you would like to exercise your rights to your Information, you may contact us at {helpEmail} or via mail at:"
}

View file

@ -0,0 +1,311 @@
const React = require('react');
const Page = require('../../components/page/www/page.jsx');
const render = require('../../lib/render.jsx');
const {FormattedMessage, injectIntl, intlShape} = require('react-intl');
const InformationPage = require('../../components/informationpage/informationpage.jsx');
const helpEmailLink = (
<a href="mailto:help@scratch.mit.edu">
help@scratch.mit.edu
</a>
);
const foundationAddress = (
<div>
Scratch Foundation<br />
ATTN: Privacy Policy<br />
201 South Street<br />
Boston, MA, 02111
</div>
);
const PrivacyPolicyApps = props => (
<InformationPage title={props.intl.formatMessage({id: 'privacyApps.title'})}>
<div className="inner info-inner">
<section>
<p className="lastupdate">
<i>
<FormattedMessage id="privacyApps.updated" />
</i>
</p>
<p className="intro">
<FormattedMessage id="privacyApps.intro" />
</p>
</section>
<section id="collection">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.collectionHeader" />
</h3>
<p>
<FormattedMessage
id="privacyApps.collectionParagraph"
values={{
privacyPolicyLink: (
<a href="/privacy_policy/">
<FormattedMessage id="privacyApps.privacyPolicyLinkText" />
</a>
)
}}
/>
</p>
</dl>
</section>
<section id="usage">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.usageHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.usageIntro" />
</p>
<dt>
<FormattedMessage id="privacyApps.analyticsTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.analyticsDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.researchTitle" />
</dt>
<dd>
<FormattedMessage
id="privacyApps.researchDescription"
values={{
researchPageLink: (
<a href="/research/">
<FormattedMessage id="privacyApps.researchPageLinkText" />
</a>
)
}}
/>
</dd>
<dt>
<FormattedMessage id="privacyApps.legalTitle" />
</dt>
<dd>
<FormattedMessage
id="privacyApps.legalDescription"
values={{
termsOfUseLink: (
<a href="/terms_of_use/">
<FormattedMessage id="privacyApps.termsOfUseLinkText" />
</a>
)
}}
/>
</dd>
</dl>
</section>
<section id="processing">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.processingHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.processingParagraph" />
</p>
</dl>
</section>
<section id="sharing">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.sharingHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.sharingIntro" />
</p>
<dt>
<FormattedMessage id="privacyApps.serviceProvidersTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.serviceProvidersDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.researchTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.researchSharingDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.mergerTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.mergerDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.legalTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.legalSharingDescription" />
</dd>
</dl>
</section>
<section id="community">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.communityHeader" />
</h3>
<p>
<FormattedMessage
id="privacyApps.communityParagraph"
values={{
privacyPolicyLink: (
<a href="/privacy_policy/">
<FormattedMessage id="privacyApps.privacyPolicyLinkText" />
</a>
)
}}
/>
</p>
</dl>
</section>
<section id="students">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.studentsHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.coppa" />
</p>
<p>
<FormattedMessage id="privacyApps.ferpa" />
</p>
</dl>
</section>
<section id="eea">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.eeaHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.eeaIntro" />
</p>
<dt>
<FormattedMessage id="privacyApps.accessTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.accessDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.objectionTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.objectionDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.deletionTitle" />
</dt>
<dd>
<FormattedMessage
id="privacyApps.deletionDescription"
values={{
helpEmail: helpEmailLink
}}
/>
</dd>
<dt>
<FormattedMessage id="privacyApps.restrictionTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.restrictionDescription" />
</dd>
<dt>
<FormattedMessage id="privacyApps.withdrawalTitle" />
</dt>
<dd>
<FormattedMessage id="privacyApps.withdrawalDescription" />
</dd>
<p>
<FormattedMessage
id="privacyApps.eeaComplaint"
values={{
helpEmail: helpEmailLink
}}
/>
</p>
</dl>
</section>
<section id="retention">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.retentionHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.retentionParagraph" />
</p>
</dl>
</section>
<section id="protect">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.protectHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.protectParagraph" />
</p>
</dl>
</section>
<section id="international-transfer">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.internationalTransferHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.internationalTransferParagraph" />
</p>
</dl>
</section>
<section id="notifications">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.notificationsHeader" />
</h3>
<p>
<FormattedMessage id="privacyApps.notificationsParagraph" />
</p>
</dl>
</section>
<section id="contact">
<dl>
<span className="nav-spacer" />
<h3>
<FormattedMessage id="privacyApps.contactHeader" />
</h3>
<p>
<FormattedMessage
id="privacyApps.contactIntro"
values={{
helpEmail: helpEmailLink
}}
/>
</p>
<p>
{foundationAddress}
</p>
</dl>
</section>
</div>
</InformationPage>
);
PrivacyPolicyApps.propTypes = {
intl: intlShape
};
const IntlPrivacyPolicyApps = injectIntl(PrivacyPolicyApps);
render(<Page><IntlPrivacyPolicyApps /></Page>, document.getElementById('app'));

View file

@ -14,8 +14,7 @@ const AdminPanel = require('../../components/adminpanel/adminpanel.jsx');
const Box = require('../../components/box/box.jsx'); const Box = require('../../components/box/box.jsx');
const Button = require('../../components/forms/button.jsx'); const Button = require('../../components/forms/button.jsx');
const Carousel = require('../../components/carousel/carousel.jsx'); const Carousel = require('../../components/carousel/carousel.jsx');
const DropdownBanner = require('../../components/dropdown-banner/banner.jsx'); const EmailConfirmationBanner = require('../../components/dropdown-banner/email-confirmation/banner.jsx');
const IframeModal = require('../../components/modal/iframe/modal.jsx');
const Intro = require('../../components/intro/intro.jsx'); const Intro = require('../../components/intro/intro.jsx');
const LegacyCarousel = require('../../components/carousel/legacy-carousel.jsx'); const LegacyCarousel = require('../../components/carousel/legacy-carousel.jsx');
const News = require('../../components/news/news.jsx'); const News = require('../../components/news/news.jsx');
@ -201,28 +200,9 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleMessage',
'renderHomepageRows' 'renderHomepageRows'
]); ]);
} }
componentDidMount () {
if (this.props.shouldShowEmailConfirmation) window.addEventListener('message', this.handleMessage);
}
componentWillUnmount () {
window.removeEventListener('message', this.handleMessage);
}
handleMessage (e) {
if (e.origin !== window.location.origin) return;
if (e.source !== this.emailConfirmationiFrame.contentWindow) return;
if (e.data === 'resend-done') {
this.props.onHideEmailConfirmationModal();
} else {
const data = JSON.parse(e.data);
if (data.action === 'leave-page') {
window.location.href = data.uri;
}
}
}
renderHomepageRows () { renderHomepageRows () {
const rows = [ const rows = [
<Box <Box
@ -375,35 +355,12 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
return ( return (
<div className="splash"> <div className="splash">
{this.props.shouldShowEmailConfirmation ? [ {(this.props.shouldShowEmailConfirmation &&
<DropdownBanner <EmailConfirmationBanner
className="warning"
key="confirmedEmail"
onRequestDismiss={() => { // eslint-disable-line react/jsx-no-bind onRequestDismiss={() => { // eslint-disable-line react/jsx-no-bind
this.props.onDismiss('confirmed_email'); this.props.onDismiss('confirmed_email');
}} }}
> />)}
<a
href="#"
onClick={this.props.onShowEmailConfirmationModal}
>
Confirm your email
</a>{' '}to enable sharing.{' '}
<a href="/faq/#accounts">
Having trouble?
</a>
</DropdownBanner>,
<IframeModal
className="mod-confirmation"
componentRef={iframe => { // eslint-disable-line react/jsx-no-bind
this.emailConfirmationiFrame = iframe;
}}
isOpen={this.props.emailConfirmationModalOpen}
key="iframe-modal"
src="/accounts/email_resend_standalone/"
onRequestClose={this.props.onHideEmailConfirmationModal}
/>
] : []}
{this.props.isEducator ? [ {this.props.isEducator ? [
<TeacherBanner <TeacherBanner
key="teacherbanner" key="teacherbanner"
@ -560,7 +517,6 @@ class SplashPresentation extends React.Component { // eslint-disable-line react/
SplashPresentation.propTypes = { SplashPresentation.propTypes = {
activity: PropTypes.arrayOf(PropTypes.object), activity: PropTypes.arrayOf(PropTypes.object),
adminPanelOpen: PropTypes.bool, adminPanelOpen: PropTypes.bool,
emailConfirmationModalOpen: PropTypes.bool.isRequired,
featuredGlobal: PropTypes.shape({ featuredGlobal: PropTypes.shape({
community_featured_projects: PropTypes.array, community_featured_projects: PropTypes.array,
community_featured_studios: PropTypes.array, community_featured_studios: PropTypes.array,
@ -578,10 +534,8 @@ SplashPresentation.propTypes = {
onCloseAdminPanel: PropTypes.func.isRequired, onCloseAdminPanel: PropTypes.func.isRequired,
onCloseDonateBanner: PropTypes.func.isRequired, onCloseDonateBanner: PropTypes.func.isRequired,
onDismiss: PropTypes.func.isRequired, onDismiss: PropTypes.func.isRequired,
onHideEmailConfirmationModal: PropTypes.func.isRequired,
onOpenAdminPanel: PropTypes.func.isRequired, onOpenAdminPanel: PropTypes.func.isRequired,
onRefreshHomepageCache: PropTypes.func.isRequired, onRefreshHomepageCache: PropTypes.func.isRequired,
onShowEmailConfirmationModal: PropTypes.func.isRequired,
refreshCacheStatus: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types refreshCacheStatus: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
sessionStatus: PropTypes.string.isRequired, sessionStatus: PropTypes.string.isRequired,
sharedByFollowing: PropTypes.arrayOf(PropTypes.object), sharedByFollowing: PropTypes.arrayOf(PropTypes.object),

View file

@ -23,8 +23,6 @@ class Splash extends React.Component {
'getNews', 'getNews',
'handleRefreshHomepageCache', 'handleRefreshHomepageCache',
'getHomepageRefreshStatus', 'getHomepageRefreshStatus',
'handleShowEmailConfirmationModal',
'handleHideEmailConfirmationModal',
'handleCloseAdminPanel', 'handleCloseAdminPanel',
'handleCloseDonateBanner', 'handleCloseDonateBanner',
'handleOpenAdminPanel', 'handleOpenAdminPanel',
@ -36,7 +34,6 @@ class Splash extends React.Component {
adminPanelOpen: false, adminPanelOpen: false,
dismissedDonateBanner: false, dismissedDonateBanner: false,
news: [], // gets news posts from the scratch Tumblr news: [], // gets news posts from the scratch Tumblr
emailConfirmationModalOpen: false,
refreshCacheStatus: 'notrequested' refreshCacheStatus: 'notrequested'
}; };
} }
@ -123,12 +120,6 @@ class Splash extends React.Component {
handleCloseDonateBanner () { handleCloseDonateBanner () {
this.setState({dismissedDonateBanner: true}); this.setState({dismissedDonateBanner: true});
} }
handleShowEmailConfirmationModal () {
this.setState({emailConfirmationModalOpen: true});
}
handleHideEmailConfirmationModal () {
this.setState({emailConfirmationModalOpen: false});
}
handleDismiss (cue) { handleDismiss (cue) {
api({ api({
host: '', host: '',
@ -192,7 +183,6 @@ class Splash extends React.Component {
<SplashPresentation <SplashPresentation
activity={this.props.activity} activity={this.props.activity}
adminPanelOpen={this.state.adminPanelOpen} adminPanelOpen={this.state.adminPanelOpen}
emailConfirmationModalOpen={this.state.emailConfirmationModalOpen}
featuredGlobal={this.props.featured} featuredGlobal={this.props.featured}
inStudiosFollowing={this.props.studios} inStudiosFollowing={this.props.studios}
isAdmin={this.props.isAdmin} isAdmin={this.props.isAdmin}
@ -212,10 +202,8 @@ class Splash extends React.Component {
onCloseDonateBanner={this.handleCloseDonateBanner} onCloseDonateBanner={this.handleCloseDonateBanner}
onCloseAdminPanel={this.handleCloseAdminPanel} onCloseAdminPanel={this.handleCloseAdminPanel}
onDismiss={this.handleDismiss} onDismiss={this.handleDismiss}
onHideEmailConfirmationModal={this.handleHideEmailConfirmationModal}
onOpenAdminPanel={this.handleOpenAdminPanel} onOpenAdminPanel={this.handleOpenAdminPanel}
onRefreshHomepageCache={this.handleRefreshHomepageCache} onRefreshHomepageCache={this.handleRefreshHomepageCache}
onShowEmailConfirmationModal={this.handleShowEmailConfirmationModal}
/> />
); );
} }

View file

@ -195,7 +195,7 @@ const getComponentForItem = item => {
</a> </a>
), ),
actorProfileLink: ( actorProfileLink: (
<a href={`/users/${item.recipient_username}`}> <a href={`/users/${item.actor_username}`}>
{item.actor_username} {item.actor_username}
</a> </a>
) )

View file

@ -0,0 +1,32 @@
<svg width="196" height="216" viewBox="0 0 196 216" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="93.6311" cy="107.442" rx="93.6311" ry="104.702" fill="#0EBD8C" fill-opacity="0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M170.396 142.567H114.14C111.92 142.567 110.124 140.759 110.14 138.539L110.376 104.549C110.386 103.301 110.978 102.127 111.976 101.377L139.996 80.3654C141.418 79.2994 143.374 79.2994 144.796 80.3674L172.796 101.367C173.804 102.121 174.396 103.307 174.396 104.567V138.567C174.396 140.775 172.604 142.567 170.396 142.567Z" fill="#4D97FF"/>
<path d="M170.396 142.567H114.14C111.92 142.567 110.124 140.759 110.14 138.539L110.376 104.549C110.386 103.301 110.978 102.127 111.976 101.377L139.996 80.3654C141.418 79.2994 143.374 79.2994 144.796 80.3674L172.796 101.367C173.804 102.121 174.396 103.307 174.396 104.567V138.567C174.396 140.775 172.604 142.567 170.396 142.567" stroke="#3D73CC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.39 86.5606H122.39C120.182 86.5606 118.39 88.3526 118.39 90.5606V122.561C118.39 124.769 120.182 126.561 122.39 126.561H162.39C164.598 126.561 166.39 124.769 166.39 122.561V90.5606C166.39 88.3526 164.598 86.5606 162.39 86.5606Z" fill="#F9F8FF"/>
<path d="M162.39 86.5606H122.39C120.182 86.5606 118.39 88.3526 118.39 90.5606V122.561C118.39 124.769 120.182 126.561 122.39 126.561H162.39C164.598 126.561 166.39 124.769 166.39 122.561V90.5606C166.39 88.3526 164.598 86.5606 162.39 86.5606" stroke="#3D73CC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M149.442 95.9321C150.281 95.0234 151.698 94.9673 152.607 95.8068C153.516 96.6464 153.572 98.0635 152.732 98.9722L141.35 111.292C140.463 112.252 138.946 112.252 138.06 111.292L131.522 104.216C130.682 103.307 130.738 101.89 131.647 101.05C132.556 100.211 133.973 100.267 134.812 101.176L139.705 106.471L149.442 95.9321Z" fill="#0EBD8C"/>
<mask id="mask0_0_11736" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="130" y="95" width="24" height="18">
<path d="M149.442 95.9321C150.281 95.0234 151.698 94.9673 152.607 95.8068C153.516 96.6464 153.572 98.0635 152.732 98.9722L141.35 111.292C140.463 112.252 138.946 112.252 138.06 111.292L131.522 104.216C130.682 103.307 130.738 101.89 131.647 101.05C132.556 100.211 133.973 100.267 134.812 101.176L139.705 106.471L149.442 95.9321Z" fill="white"/>
</mask>
<g mask="url(#mask0_0_11736)">
<rect x="128.127" y="89.6121" width="28" height="28" fill="#0EBD8C"/>
</g>
<g opacity="0.1">
<path fill-rule="evenodd" clip-rule="evenodd" d="M174.39 104.561V106.561L154.15 119.221L148.25 122.901L144.51 125.241C143.21 126.041 141.57 126.041 140.27 125.241L136.49 122.881L130.57 119.181L110.39 106.561V104.081C110.53 103.021 111.09 102.021 111.97 101.381L113.31 100.381L136.51 114.861L139.99 112.341C141.41 111.281 143.37 111.281 144.77 112.341L148.25 114.881L171.47 100.381L172.79 101.361C173.79 102.121 174.39 103.301 174.39 104.561Z" fill="black"/>
<path d="M174.39 104.561V106.561L154.15 119.221L148.25 122.901L144.51 125.241C143.21 126.041 141.57 126.041 140.27 125.241L136.49 122.881L130.57 119.181L110.39 106.561V104.081C110.53 103.021 111.09 102.021 111.97 101.381L113.31 100.381L136.51 114.861L139.99 112.341C141.41 111.281 143.37 111.281 144.77 112.341L148.25 114.881L171.47 100.381L172.79 101.361C173.79 102.121 174.39 103.301 174.39 104.561" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M174.39 106.558L144.51 125.236C143.214 126.046 141.566 126.046 140.27 125.236L110.39 106.558V138.56C110.39 140.77 112.182 142.56 114.39 142.56H170.39C172.598 142.56 174.39 140.77 174.39 138.56V106.558Z" fill="#4D97FF" stroke="#3D73CC" stroke-width="2" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M112.113 140.565L139.993 120.343C141.415 119.285 143.363 119.289 144.779 120.353L172.395 140.565" fill="#4D97FF"/>
<path d="M112.113 140.565L139.993 120.343C141.415 119.285 143.363 119.289 144.779 120.353L172.395 140.565" stroke="#3D73CC" stroke-width="2" stroke-linecap="round"/>
<path d="M126.03 78.832L122.7 72.5" stroke="#4D97FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M113.003 92.6338L106.501 87.7298" stroke="#4D97FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M158.738 78.832L162.068 72.5" stroke="#4D97FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M171.764 92.6338L178.268 87.7298" stroke="#4D97FF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M153.854 174.406C146.574 174.406 140.686 170.266 138.11 163.33C137.474 161.61 137.582 159.938 138.41 158.746C139.058 157.818 140.054 157.282 141.15 157.282C142.27 157.282 143.298 157.846 144.03 158.818C142.882 155.57 139.454 141.074 142.202 137.046C142.798 136.178 143.67 135.698 144.662 135.698C148.662 135.718 149.778 141.322 150.506 146.954C150.986 146.454 151.842 145.866 153.222 145.866C155.738 145.866 156.802 147.374 157.258 149.07C157.738 148.062 158.542 147.306 159.938 147.306C160.442 147.306 160.954 147.422 161.514 147.658C164.086 148.77 164.346 150.914 164.086 152.774C164.514 152.378 165.066 152.122 165.794 152.122C166.442 152.122 167.106 152.366 167.762 152.858C170.662 155.034 169.382 158.282 168.03 161.718C167.394 163.33 166.678 165.154 166.15 167.242C165.102 171.394 159.93 174.406 153.854 174.406Z" fill="white"/>
<mask id="mask1_0_11736" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="136" y="134" width="36" height="43">
<path fill-rule="evenodd" clip-rule="evenodd" d="M136 134H171.204V176.104H136V134Z" fill="white"/>
</mask>
<g mask="url(#mask1_0_11736)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.678 134H144.67H144.662C143.114 134 141.706 134.76 140.798 136.092C138.174 139.936 139.986 149.884 141.39 155.588C141.31 155.584 141.23 155.584 141.15 155.584C139.514 155.584 137.97 156.404 137.014 157.776C135.862 159.428 135.682 161.672 136.518 163.92C139.35 171.552 145.834 176.104 153.854 176.104C160.694 176.104 166.562 172.556 167.798 167.656C168.298 165.68 168.998 163.912 169.61 162.344C170.994 158.828 172.718 154.448 168.778 151.5C167.822 150.78 166.818 150.416 165.79 150.416H165.742C165.458 148.752 164.514 147.1 162.182 146.096C161.422 145.768 160.666 145.604 159.938 145.604C159.038 145.604 158.298 145.8 157.69 146.132C156.798 144.984 155.402 144.168 153.222 144.168C152.714 144.168 152.258 144.224 151.842 144.32C150.974 139 149.346 134.024 144.686 134H144.678ZM144.662 137.4H144.67C149.574 137.42 148.574 155.32 150.594 155.32C150.634 155.32 150.678 155.312 150.726 155.296C152.066 154.808 149.266 147.568 153.222 147.568C157.79 147.568 154.526 155.536 156.878 155.776C156.906 155.78 156.938 155.78 156.966 155.78C158.882 155.78 157.37 149.004 159.938 149.004C160.194 149.004 160.494 149.076 160.838 149.224C165.046 151.036 159.458 157.04 162.522 157.916C162.618 157.944 162.71 157.96 162.794 157.96C164.318 157.96 164.242 153.82 165.79 153.82C166.054 153.82 166.366 153.936 166.742 154.22C169.542 156.32 166.154 160.288 164.502 166.824C163.63 170.264 158.994 172.708 153.854 172.708C148.402 172.708 142.386 169.956 139.706 162.74C138.846 160.42 139.926 158.98 141.15 158.98C141.882 158.98 142.666 159.496 143.118 160.64C143.598 161.84 144.302 162.388 144.91 162.388C145.918 162.388 146.67 160.9 145.706 158.44C144.178 154.536 140.758 137.4 144.662 137.4Z" fill="#575E75"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -3,11 +3,11 @@
import SeleniumHelper from './selenium-helpers.js'; import SeleniumHelper from './selenium-helpers.js';
const { const {
findByXpath,
buildDriver, buildDriver,
clickXpath,
clickText, clickText,
clickXpath,
containsClass, containsClass,
findByXpath,
signIn signIn
} = new SeleniumHelper(); } = new SeleniumHelper();
@ -15,8 +15,6 @@ const {
let username1 = process.env.SMOKE_USERNAME + '4'; let username1 = process.env.SMOKE_USERNAME + '4';
let username2 = process.env.SMOKE_USERNAME + '5'; let username2 = process.env.SMOKE_USERNAME + '5';
let password = process.env.SMOKE_PASSWORD; let password = process.env.SMOKE_PASSWORD;
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
// project for comments (owned by username2) // project for comments (owned by username2)
@ -43,11 +41,7 @@ let projectReply = projectComment + ' reply';
let profileReply = profileComment + ' reply'; let profileReply = profileComment + ' reply';
let studioReply = studioComment + ' reply'; let studioReply = studioComment + ' reply';
if (remote) { jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;
@ -61,7 +55,7 @@ describe('comment tests', async () => {
describe('leave comments', async () => { describe('leave comments', async () => {
beforeAll(async () => { beforeAll(async () => {
await signIn(username1, password, driver); await signIn(username1, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
}); });
@ -80,10 +74,6 @@ describe('comment tests', async () => {
await findByXpath(`//textarea[contains(text(), "${projectComment}")]`); await findByXpath(`//textarea[contains(text(), "${projectComment}")]`);
await clickXpath('//button[@class="button compose-post"]'); await clickXpath('//button[@class="button compose-post"]');
// reload the page
await driver.sleep(5000);
await driver.get(projectUrl);
// find the comment // find the comment
let commentXpath = await `//div[@class="comment-bubble"]/span/span[contains(text(),` + let commentXpath = await `//div[@class="comment-bubble"]/span/span[contains(text(),` +
` "${projectComment}")]`; ` "${projectComment}")]`;
@ -101,9 +91,6 @@ describe('comment tests', async () => {
await commentArea.sendKeys(profileComment); await commentArea.sendKeys(profileComment);
await clickXpath('//div[@class="button small"]/a[contains(text(), "Post")]'); await clickXpath('//div[@class="button small"]/a[contains(text(), "Post")]');
// reload page
await driver.get(profileUrl);
// find the comment // find the comment
let newComment = await findByXpath(`//div[@class="comment "]/div/div[contains(text(),` + let newComment = await findByXpath(`//div[@class="comment "]/div/div[contains(text(),` +
` "${profileComment}")]`); ` "${profileComment}")]`);
@ -123,10 +110,6 @@ describe('comment tests', async () => {
await findByXpath(`//textarea[contains(text(), "${studioComment}")]`); await findByXpath(`//textarea[contains(text(), "${studioComment}")]`);
await clickXpath('//button[@class="button compose-post"]'); await clickXpath('//button[@class="button compose-post"]');
// reload the page
await driver.sleep(5000);
await driver.get(studioUrl);
// find the comment // find the comment
let commentXpath = `//div[@class="comment-bubble"]/span/span[contains(text(), "${studioComment}")]`; let commentXpath = `//div[@class="comment-bubble"]/span/span[contains(text(), "${studioComment}")]`;
let postedComment = await findByXpath(commentXpath); let postedComment = await findByXpath(commentXpath);
@ -137,7 +120,7 @@ describe('comment tests', async () => {
describe('second user tests', async () => { describe('second user tests', async () => {
beforeAll(async () => { beforeAll(async () => {
await signIn(username2, password, driver); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
}); });
@ -277,9 +260,6 @@ describe('comment tests', async () => {
let postButton = await findByXpath(replyRow + '//button[@class = "button compose-post"]'); let postButton = await findByXpath(replyRow + '//button[@class = "button compose-post"]');
await postButton.click(); await postButton.click();
// find reply
await driver.sleep(500);
await driver.get(projectUrl);
let postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`); let postedReply = await findByXpath(`//span[contains(text(), "${projectReply}")]`);
let commentVisible = await postedReply.isDisplayed(); let commentVisible = await postedReply.isDisplayed();
await expect(commentVisible).toBe(true); await expect(commentVisible).toBe(true);
@ -298,7 +278,6 @@ describe('comment tests', async () => {
// click post // click post
await clickXpath(commentXpath + '//a[contains(text(), "Post")]'); await clickXpath(commentXpath + '//a[contains(text(), "Post")]');
// reload the page step has been skipped because caching causes failure
// The reply wasn't findable by xpath after several attempts, but it seems // The reply wasn't findable by xpath after several attempts, but it seems
// better to have this much of a test // better to have this much of a test
}); });
@ -321,8 +300,6 @@ describe('comment tests', async () => {
await postButton.click(); await postButton.click();
// find reply // find reply
await driver.sleep(500);
await driver.get(studioUrl);
let postedReply = await findByXpath(`//span[contains(text(), "${studioReply}")]`); let postedReply = await findByXpath(`//span[contains(text(), "${studioReply}")]`);
let commentVisible = await postedReply.isDisplayed(); let commentVisible = await postedReply.isDisplayed();
await expect(commentVisible).toBe(true); await expect(commentVisible).toBe(true);

View file

@ -7,14 +7,9 @@ const {
buildDriver buildDriver
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
if (remote) { jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(10000);
}
let driver; let driver;

View file

@ -8,14 +8,9 @@ const {
buildDriver buildDriver
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
if (remote) { jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;

View file

@ -8,15 +8,10 @@ const {
buildDriver buildDriver
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let takenUsername = process.env.SMOKE_USERNAME; let takenUsername = process.env.SMOKE_USERNAME;
if (remote){ jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(10000);
}
let driver; let driver;
@ -29,7 +24,7 @@ describe('www-integration join flow', () => {
afterAll(async () => await driver.quit()); afterAll(async () => await driver.quit());
beforeEach(async () => { beforeEach(async () => {
driver.get(rootUrl); await driver.get(rootUrl);
await clickXpath('//a[@class="registrationLink"]'); await clickXpath('//a[@class="registrationLink"]');
}); });

View file

@ -3,25 +3,21 @@
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const { const {
buildDriver,
clickText, clickText,
findByXpath,
clickXpath, clickXpath,
buildDriver findByXpath,
signIn
} = new SeleniumHelper(); } = new SeleniumHelper();
let username = process.env.SMOKE_USERNAME + '1'; let username = process.env.SMOKE_USERNAME + '1';
let password = process.env.SMOKE_PASSWORD; let password = process.env.SMOKE_PASSWORD;
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let myStuffURL = rootUrl + '/mystuff'; let myStuffURL = rootUrl + '/mystuff';
let rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl; let rateLimitCheck = process.env.RATE_LIMIT_CHECK || rootUrl;
if (remote){ jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;
@ -30,14 +26,7 @@ describe('www-integration my_stuff', () => {
driver = await buildDriver('www-integration my_stuff'); driver = await buildDriver('www-integration my_stuff');
await driver.get(rootUrl); await driver.get(rootUrl);
await driver.sleep(1000); await driver.sleep(1000);
await clickXpath('//li[@class="link right login-item"]/a'); await signIn(username, password);
let name = await findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username);
let word = await findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password);
await driver.sleep(500);
await clickXpath('//button[contains(@class, "button") and ' +
'contains(@class, "submit-button") and contains(@class, "white")]');
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
}); });
@ -105,7 +94,7 @@ describe('www-integration my_stuff', () => {
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
let tabs = await findByXpath('//div[@class="studio-tabs"]'); let tabs = await findByXpath('//div[@class="studio-tabs"]');
let tabsVisible = await tabs.isDisplayed(); let tabsVisible = await tabs.isDisplayed();
expect(tabsVisible).toBe(true); await expect(tabsVisible).toBe(true);
}); });
test('New studio rate limited to five', async () =>{ test('New studio rate limited to five', async () =>{

View file

@ -8,14 +8,9 @@ const {
buildDriver buildDriver
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
if (remote) { jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(10000);
}
let driver; let driver;

View file

@ -3,21 +3,17 @@
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const { const {
findByXpath, buildDriver,
clickXpath, clickXpath,
buildDriver findByXpath,
waitUntilVisible
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let projectId = process.env.TEST_PROJECT_ID || 1300006196; let projectId = process.env.TEST_PROJECT_ID || 1300006196;
let projectUrl = rootUrl + '/projects/' + projectId; let projectUrl = rootUrl + '/projects/' + projectId;
if (remote){ jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;
@ -31,7 +27,7 @@ describe('www-integration project-page signed out', () => {
beforeEach(async () => { beforeEach(async () => {
await driver.get(projectUrl); await driver.get(projectUrl);
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]'); let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
await gfOverlay.isDisplayed(); await waitUntilVisible(gfOverlay, driver);
}); });
afterAll(async () => await driver.quit()); afterAll(async () => await driver.quit());

View file

@ -9,14 +9,9 @@ const {
getKey getKey
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
if (remote){ jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;

View file

@ -151,15 +151,13 @@ class SeleniumHelper {
} }
// must be used on a www page // must be used on a www page
async signIn (username, password, driver) { async signIn (username, password) {
await this.clickXpath('//li[@class="link right login-item"]/a'); await this.clickXpath('//li[@class="link right login-item"]/a');
let name = await this.findByXpath('//input[@id="frc-username-1088"]'); let name = await this.findByXpath('//input[@id="frc-username-1088"]');
await name.sendKeys(username); await name.sendKeys(username);
let word = await this.findByXpath('//input[@id="frc-password-1088"]'); let word = await this.findByXpath('//input[@id="frc-password-1088"]');
await word.sendKeys(password); await word.sendKeys(password + this.getKey('ENTER'));
await driver.sleep(500); await this.findByXpath('//span[contains(@class, "profile-name")]');
await this.clickXpath('//button[contains(@class, "button") and ' +
'contains(@class, "submit-button") and contains(@class, "white")]');
} }
urlMatches (regex) { urlMatches (regex) {

View file

@ -3,27 +3,23 @@
const SeleniumHelper = require('./selenium-helpers.js'); const SeleniumHelper = require('./selenium-helpers.js');
const { const {
clickText,
findByXpath,
clickXpath,
clickButton,
buildDriver, buildDriver,
clickButton,
clickText,
clickXpath,
findByXpath,
getKey,
signIn, signIn,
waitUntilVisible waitUntilVisible
} = new SeleniumHelper(); } = new SeleniumHelper();
let username = process.env.SMOKE_USERNAME; let username = process.env.SMOKE_USERNAME;
let password = process.env.SMOKE_PASSWORD; let password = process.env.SMOKE_PASSWORD;
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let scratchr2url = rootUrl + '/users/' + username; let scratchr2url = rootUrl + '/users/' + username;
let wwwURL = rootUrl; let wwwURL = rootUrl;
if (remote){ jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;
@ -77,7 +73,7 @@ describe('www-integration sign-in-and-out', () => {
describe('sign out', () => { describe('sign out', () => {
beforeEach(async () => { beforeEach(async () => {
await driver.get(wwwURL); await driver.get(wwwURL);
await signIn(username, password, driver); await signIn(username, password);
await driver.sleep(500); await driver.sleep(500);
}); });
@ -108,11 +104,11 @@ describe('www-integration sign-in-and-out', () => {
await driver.get(scratchr2url); await driver.get(scratchr2url);
await clickXpath('//li[@class="sign-in dropdown"]/span'); await clickXpath('//li[@class="sign-in dropdown"]/span');
let name = await findByXpath('//input[@id="login_dropdown_username"]'); let name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername); await name.sendKeys(nonsenseUsername + getKey('ENTER'));
await clickButton('Sign in');
// find error // find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]'); let error = await findByXpath('//form[@id="login"]//div[@class="error"]');
await waitUntilVisible(error, driver);
let errorText = await error.getText(); let errorText = await error.getText();
await expect(errorText).toEqual('This field is required.'); await expect(errorText).toEqual('This field is required.');
}); });
@ -126,8 +122,7 @@ describe('www-integration sign-in-and-out', () => {
let name = await findByXpath('//input[@id="login_dropdown_username"]'); let name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(nonsenseUsername); await name.sendKeys(nonsenseUsername);
let word = await findByXpath('//input[@name="password"]'); let word = await findByXpath('//input[@name="password"]');
await word.sendKeys(password); await word.sendKeys(password + getKey('ENTER'));
await clickButton('Sign in');
// find error // find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]'); let error = await findByXpath('//form[@id="login"]//div[@class="error"]');
@ -145,8 +140,7 @@ describe('www-integration sign-in-and-out', () => {
let name = await findByXpath('//input[@id="login_dropdown_username"]'); let name = await findByXpath('//input[@id="login_dropdown_username"]');
await name.sendKeys(username); await name.sendKeys(username);
let word = await findByXpath('//input[@name="password"]'); let word = await findByXpath('//input[@name="password"]');
await word.sendKeys(nonsensePassword); await word.sendKeys(nonsensePassword + getKey('ENTER'));
await clickButton('Sign in');
// find error // find error
let error = await findByXpath('//form[@id="login"]//div[@class="error"]'); let error = await findByXpath('//form[@id="login"]//div[@class="error"]');

View file

@ -9,15 +9,10 @@ const {
findByXpath findByXpath
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let statisticsPage = rootUrl + '/statistics'; let statisticsPage = rootUrl + '/statistics';
if (remote) { jest.setTimeout(60000);
jest.setTimeout(60000);
} else {
jest.setTimeout(10000);
}
let driver; let driver;

View file

@ -10,7 +10,6 @@ const {
signIn signIn
} = new SeleniumHelper(); } = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let studioId = process.env.TEST_STUDIO_ID || 10004360; let studioId = process.env.TEST_STUDIO_ID || 10004360;
let studioUrl = rootUrl + '/studios/' + studioId; let studioUrl = rootUrl + '/studios/' + studioId;
@ -26,11 +25,7 @@ let password = process.env.SMOKE_PASSWORD;
let promoteStudioURL; let promoteStudioURL;
let curatorTab; let curatorTab;
if (remote){ jest.setTimeout(70000);
jest.setTimeout(70000);
} else {
jest.setTimeout(20000);
}
let driver; let driver;
@ -78,14 +73,14 @@ describe('studio management', () => {
await driver.get(rootUrl); await driver.get(rootUrl);
// create a studio for tests // create a studio for tests
await signIn(username2, password, driver); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
await driver.get(rateLimitCheck); await driver.get(rateLimitCheck);
await driver.get(myStuffURL); await driver.get(myStuffURL);
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await findByXpath('//div[@class="studio-tabs"]'); await findByXpath('//div[@class="studio-tabs"]');
promoteStudioURL = await driver.getCurrentUrl(); promoteStudioURL = await driver.getCurrentUrl();
curatorTab = promoteStudioURL + 'curators'; curatorTab = await promoteStudioURL + 'curators';
}); });
beforeEach(async () => { beforeEach(async () => {
@ -99,7 +94,7 @@ describe('studio management', () => {
test('invite a curator', async () => { test('invite a curator', async () => {
// sign in as user2 // sign in as user2
await signIn(username2, password, driver); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// invite user3 to curate // invite user3 to curate
@ -108,13 +103,13 @@ describe('studio management', () => {
await clickXpath('//div[@class="studio-adder-row"]/button'); await clickXpath('//div[@class="studio-adder-row"]/button');
let inviteAlert = await findByXpath('//div[@class="alert-msg"]'); // the confirm alert let inviteAlert = await findByXpath('//div[@class="alert-msg"]'); // the confirm alert
let alertText = await inviteAlert.getText(); let alertText = await inviteAlert.getText();
let successText = `Curator invite sent to "${username3}"`; let successText = await `Curator invite sent to "${username3}"`;
await expect(alertText).toMatch(successText); await expect(alertText).toMatch(successText);
}); });
test('accept curator invite', async () => { test('accept curator invite', async () => {
// Sign in user3 // Sign in user3
await signIn(username3, password, driver); await signIn(username3, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// accept the curator invite // accept the curator invite
@ -126,15 +121,16 @@ describe('studio management', () => {
test('promote to manager', async () => { test('promote to manager', async () => {
// sign in as user2 // sign in as user2
await signIn(username2, password, driver); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without reloading the page // for some reason the user isn't showing up without waiting and reloading the page
await driver.sleep(2000);
await driver.get(curatorTab); await driver.get(curatorTab);
// promote user3 // promote user3
let user3href = '/users/' + username3; let user3href = await '/users/' + username3;
// click kebab menu on the user tile // click kebab menu on the user tile
let kebabMenuXpath = `//a[@href = "${user3href}"]/` + let kebabMenuXpath = await `//a[@href = "${user3href}"]/` +
'following-sibling::div[@class="overflow-menu-container"]'; 'following-sibling::div[@class="overflow-menu-container"]';
await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]'); await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]');
// click promote // click promote
@ -150,15 +146,15 @@ describe('studio management', () => {
test('transfer studio host', async () => { test('transfer studio host', async () => {
// sign in as user2 // sign in as user2
await signIn(username2, password, driver); await signIn(username2, password);
await findByXpath('//span[contains(@class, "profile-name")]'); await findByXpath('//span[contains(@class, "profile-name")]');
// for some reason the user isn't showing up without reloading the page // for some reason the user isn't showing up without reloading the page
await driver.get(curatorTab); await driver.get(curatorTab);
// open kebab menu // open kebab menu
let user2href = '/users/' + username2; let user2href = await '/users/' + username2;
// click kebab menu on the user tile // click kebab menu on the user tile
let kebabMenuXpath = `//a[@href = "${user2href}"]/` + let kebabMenuXpath = await `//a[@href = "${user2href}"]/` +
'following-sibling::div[@class="overflow-menu-container"]'; 'following-sibling::div[@class="overflow-menu-container"]';
await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]'); await clickXpath(kebabMenuXpath + '/button[@class="overflow-menu-trigger"]');

View file

@ -0,0 +1,35 @@
const React = require('react');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const EmailConfirmationBanner = require('../../../src/components/dropdown-banner/email-confirmation/banner.jsx');
jest.mock('../../../src/components/modal/email-confirmation/modal.jsx', () => () => 'MockEmailConfirmationModal');
describe('EmailConfirmationBanner', () => {
test('Clicking "Confirm your email" opens the email confirmation modal', () => {
const component = mountWithIntl(
<EmailConfirmationBanner />
);
expect(component.text()).not.toContain('MockEmailConfirmationModal');
const confirmWrapper = component.find({id: 'emailConfirmationBanner.confirm'});
const confirmLink = mountWithIntl(confirmWrapper.instance().props.values.confirmLink);
confirmLink.simulate('click');
component.update();
expect(component.text()).toContain('MockEmailConfirmationModal');
});
test('Clicking X calls onRequestDismiss', () => {
const requestDismissMock = jest.fn();
const component = mountWithIntl(
<EmailConfirmationBanner onRequestDismiss={requestDismissMock} />
);
component.find('a.close').simulate('click', {preventDefault () {}});
expect(requestDismissMock).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,55 @@
const React = require('react');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const EmailConfirmationModal = require('../../../src/components/modal/email-confirmation/modal.jsx');
import configureStore from 'redux-mock-store';
describe('Modal', () => {
const mockStore = configureStore();
let defaultStore;
const testEmail = 'test123@email.com';
beforeEach(() => {
defaultStore = mockStore({
session: {
session: {
user: {
email: testEmail
},
permissions: {}
}
}
});
});
test('Display email prop correctly', () => {
const component = mountWithIntl(
<EmailConfirmationModal
isOpen
/>, {context: {store: defaultStore}}
);
expect(component.find('div.modal-text-content').text()).toContain(testEmail);
});
test('Clicking on Text changes to tips page', () => {
const component = mountWithIntl(
<EmailConfirmationModal
isOpen
/>, {email: testEmail, context: {store: defaultStore}}
);
const tipsLinkWrapper = component.find({id: 'emailConfirmationModal.havingTrouble'});
const tipsLink = mountWithIntl(tipsLinkWrapper.instance().props.values.tipsLink);
tipsLink.simulate('click');
expect(component.text()).toContain('emailConfirmationModal.confirmingTips');
});
test('Close button shows correctly', () => {
const component = mountWithIntl(
<EmailConfirmationModal isOpen />, {context: {store: defaultStore}}
);
expect(component.find('div.modal-content-close').exists()).toBe(true);
expect(component.find('img.modal-content-close-img').exists()).toBe(true);
});
});