Compare commits
321
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf936d1371 | ||
|
|
fed67b9cf7 | ||
|
|
3f10da2918 | ||
|
|
b3ffd587a6 | ||
|
|
58ab777269 | ||
|
|
7719a3131c | ||
|
|
fbd95135a5 | ||
|
|
f5cd6ef4d7 | ||
|
|
3f4424ee5f | ||
|
|
42193a64f0 | ||
|
|
d800214667 | ||
|
|
8f51dae51c | ||
|
|
8a82741626 | ||
|
|
18f59c0015 | ||
|
|
a49b9ad996 | ||
|
|
41d708f123 | ||
|
|
12d72f1a61 | ||
|
|
5a714e7c32 | ||
|
|
93ece03830 | ||
|
|
a555a8c10f | ||
|
|
4633f512e6 | ||
|
|
c2b9d41343 | ||
|
|
a3cfbcae80 | ||
|
|
e94ed032e9 | ||
|
|
b8d6d52515 | ||
|
|
21ac3dd330 | ||
|
|
e32fdb8ab2 | ||
|
|
cb9492570b | ||
|
|
051a04c613 | ||
|
|
c5aa2468ae | ||
|
|
202238a3d1 | ||
|
|
f6c44b85a3 | ||
|
|
560a057e15 | ||
|
|
9b0edd6590 | ||
|
|
3ab338167f | ||
|
|
1d701912c3 | ||
|
|
1576e11347 | ||
|
|
9c07bee5a0 | ||
|
|
de9ef4b0c3 | ||
|
|
bc801cb009 | ||
|
|
61dfcc7fcb | ||
|
|
80eecb6cb4 | ||
|
|
e130705823 | ||
|
|
090eadd1a9 | ||
|
|
3bb819b0ed | ||
|
|
82571e03f6 | ||
|
|
6410108da7 | ||
|
|
81efb99fbe | ||
|
|
999d143d0b | ||
|
|
dae95150ab | ||
|
|
79d5fff426 | ||
|
|
aebeb7ba75 | ||
|
|
874841157a | ||
|
|
1767dbd7d3 | ||
|
|
fa0b6e9f71 | ||
|
|
8fb88f25af | ||
|
|
073f0e5315 | ||
|
|
e2ce299604 | ||
|
|
dcbf575d93 | ||
|
|
d1bedf6e37 | ||
|
|
64d375ca3f | ||
|
|
5ef39df14a | ||
|
|
16f467041f | ||
|
|
9ed27cffaa | ||
|
|
4d8ecc3e41 | ||
|
|
cdf0a9d545 | ||
|
|
41e1fc4825 | ||
|
|
d87d0c0770 | ||
|
|
0474b49f25 | ||
|
|
5c0e7737d6 | ||
|
|
91d5035592 | ||
|
|
e7764c5676 | ||
|
|
ccb60e399f | ||
|
|
130428e4a8 | ||
|
|
0fba935ec9 | ||
|
|
23527096c0 | ||
|
|
e34a453f8f | ||
|
|
17e6e59d89 | ||
|
|
568d642e48 | ||
|
|
9532c3dab2 | ||
|
|
7d88656ec5 | ||
|
|
a0abf3ca19 | ||
|
|
d86db2c66a | ||
|
|
c342463415 | ||
|
|
bcc8f787b1 | ||
|
|
6129c6dd46 | ||
|
|
37beb338da | ||
|
|
7ba07c805e | ||
|
|
6c03b8e034 | ||
|
|
6e6288a2ab | ||
|
|
a9a4b1b79b | ||
|
|
54d7ce2b9b | ||
|
|
c17edd0e05 | ||
|
|
4d22bf270b | ||
|
|
7206495177 | ||
|
|
24323842bf | ||
|
|
3703fa618e | ||
|
|
ff2921c605 | ||
|
|
35399063f3 | ||
|
|
d4a77c6f38 | ||
|
|
d4d794e730 | ||
|
|
85fa229d94 | ||
|
|
6164327b45 | ||
|
|
1a0ee34f0b | ||
|
|
86a1ffde90 | ||
|
|
17088f64b4 | ||
|
|
e7c2abed13 | ||
|
|
719ef3b388 | ||
|
|
e4e9ab5e1b | ||
|
|
4d92e92709 | ||
|
|
4b2222fa3e | ||
|
|
7dfaebd263 | ||
|
|
884843fb82 | ||
|
|
432c448e5c | ||
|
|
a1fa132280 | ||
|
|
ad1af074fc | ||
|
|
952a584d74 | ||
|
|
501caf7793 | ||
|
|
c3b32570be | ||
|
|
f9183b954b | ||
|
|
39ceac4abf | ||
|
|
cc22bfc636 | ||
|
|
ed945d392d | ||
|
|
dee7462c57 | ||
|
|
87b076c6a1 | ||
|
|
c966acd987 | ||
|
|
84d000a277 | ||
|
|
fde5ecf24d | ||
|
|
932542e30a | ||
|
|
3e1f3b743b | ||
|
|
886e2ebb8f | ||
|
|
d2f120da7e | ||
|
|
45752c2660 | ||
|
|
2f7e9967ab | ||
|
|
fe7a943780 | ||
|
|
637a09d4e9 | ||
|
|
cf52d1ee6c | ||
|
|
2eabf2aefd | ||
|
|
8412cfc46e | ||
|
|
832cf69e5d | ||
|
|
c039845b26 | ||
|
|
f6c586cec6 | ||
|
|
b36b300fc2 | ||
|
|
5c0a0c58e5 | ||
|
|
a64f623c20 | ||
|
|
c688210751 | ||
|
|
c740407aea | ||
|
|
05afad1f73 | ||
|
|
c4676cc5a6 | ||
|
|
f4c09b35e9 | ||
|
|
25136598e3 | ||
|
|
a7d141a83a | ||
|
|
4366b08f94 | ||
|
|
dd43c53db8 | ||
|
|
b77b3464db | ||
|
|
065e5b6d77 | ||
|
|
636286b427 | ||
|
|
bcbf4259a0 | ||
|
|
b6f4de8796 | ||
|
|
ce7e4d085f | ||
|
|
5910c170f1 | ||
|
|
cfcba71f94 | ||
|
|
3131ece71e | ||
|
|
93525be462 | ||
|
|
9ce52307d2 | ||
|
|
41f27bd860 | ||
|
|
6260f9c093 | ||
|
|
fbff6843f4 | ||
|
|
80662eb585 | ||
|
|
dd90ec866c | ||
|
|
893ab2c406 | ||
|
|
d25afb7c0e | ||
|
|
86a05f55f3 | ||
|
|
27c352196d | ||
|
|
8e9a17b4e7 | ||
|
|
3e2686cd67 | ||
|
|
a8595edf8f | ||
|
|
1c2530ae09 | ||
|
|
527cc0b0fb | ||
|
|
78143a1b35 | ||
|
|
ae71ac1941 | ||
|
|
180192ae15 | ||
|
|
e74bba9257 | ||
|
|
55b3584799 | ||
|
|
773a2e8672 | ||
|
|
083bb169d4 | ||
|
|
eb180f6f03 | ||
|
|
98fff1e044 | ||
|
|
45de3567db | ||
|
|
6320ae3a99 | ||
|
|
24957f3de4 | ||
|
|
22bd7219d7 | ||
|
|
aeb8238ecb | ||
|
|
bab0e4fd5c | ||
|
|
266350153e | ||
|
|
f425b27cf6 | ||
|
|
95ef21d3ad | ||
|
|
9aba9ec3fb | ||
|
|
3e4cf00eca | ||
|
|
a8c5083aa3 | ||
|
|
a1e72203c8 | ||
|
|
cc8f39e590 | ||
|
|
6eb0f57736 | ||
|
|
c1d4b8745a | ||
|
|
9219af1db1 | ||
|
|
6f7962abd4 | ||
|
|
dcf92b2455 | ||
|
|
7a1db103ba | ||
|
|
394f2c8c16 | ||
|
|
bbd95f4672 | ||
|
|
5378c57537 | ||
|
|
a93e9df129 | ||
|
|
221978b24e | ||
|
|
333e95c898 | ||
|
|
953702e238 | ||
|
|
fb3b1ced7c | ||
|
|
17ae4ea197 | ||
|
|
16d15bd4f1 | ||
|
|
a6b5c237cc | ||
|
|
10d014a7ff | ||
|
|
e0019c93fc | ||
|
|
00e8b3cdb2 | ||
|
|
f7853a9354 | ||
|
|
d972629c29 | ||
|
|
9d27a1af23 | ||
|
|
98ebd453d7 | ||
|
|
63b5a9a059 | ||
|
|
8ea62f5ea5 | ||
|
|
bc2152641b | ||
|
|
9883e0efb6 | ||
|
|
cf8a2e41df | ||
|
|
25986f0f31 | ||
|
|
230503cb27 | ||
|
|
5a614a1041 | ||
|
|
988ce22121 | ||
|
|
ebf369e293 | ||
|
|
b6a63a55ad | ||
|
|
78fe4c921c | ||
|
|
953a42d35e | ||
|
|
584bfeee5e | ||
|
|
f7d5c1898e | ||
|
|
77c5bc7254 | ||
|
|
a150efc743 | ||
|
|
8b53d1ed57 | ||
|
|
8e4c76f74f | ||
|
|
9053277889 | ||
|
|
6baf5c0e48 | ||
|
|
98c4e4df65 | ||
|
|
08160e588a | ||
|
|
fc998b9779 | ||
|
|
d0eb28f11c | ||
|
|
4f7fd39f5e | ||
|
|
3aed7007e8 | ||
|
|
731f3bbf5d | ||
|
|
715e46fe1c | ||
|
|
cc68eef196 | ||
|
|
550e236f48 | ||
|
|
40415e2359 | ||
|
|
d20b68c939 | ||
|
|
6af3fda54a | ||
|
|
43191504ff | ||
|
|
3008feb1de | ||
|
|
78c2aea407 | ||
|
|
b41b1c11bb | ||
|
|
8e82c2d1e3 | ||
|
|
a75e2b0ecf | ||
|
|
a731370478 | ||
|
|
f3ced9c9ea | ||
|
|
dee0320164 | ||
|
|
942b8d6e6a | ||
|
|
c296732753 | ||
|
|
212ffb42d3 | ||
|
|
af0088df88 | ||
|
|
63bdc80388 | ||
|
|
a27ddaf4ad | ||
|
|
5ee641e70f | ||
|
|
e9d16dcd2a | ||
|
|
d156a3d995 | ||
|
|
1019726bcf | ||
|
|
3fbb7fe8d4 | ||
|
|
124a6496f5 | ||
|
|
2e11787ac5 | ||
|
|
53bdab3425 | ||
|
|
058fd000fa | ||
|
|
dc727f1b57 | ||
|
|
bab503addb | ||
|
|
662726fa52 | ||
|
|
a0b4aea8a1 | ||
|
|
b180dd4c0d | ||
|
|
02cdbf68cd | ||
|
|
f1f1f54ec6 | ||
|
|
256bb87858 | ||
|
|
bdbf99f606 | ||
|
|
8e3603aa8b | ||
|
|
a5baf0cf5f | ||
|
|
7912e4306c | ||
|
|
5fa8beb230 | ||
|
|
73fad0b7f5 | ||
|
|
147a4ed586 | ||
|
|
b1a3c4b446 | ||
|
|
89a748dd2b | ||
|
|
8c6b52f43c | ||
|
|
2ebcd850d6 | ||
|
|
9d307f0e08 | ||
|
|
fde7349b2b | ||
|
|
8690b696a5 | ||
|
|
a7021e32a4 | ||
|
|
863276e75d | ||
|
|
ca38789e3f | ||
|
|
3a8fe3f1b2 | ||
|
|
5c259b70db | ||
|
|
3dd86c28f9 | ||
|
|
85cd56307d | ||
|
|
a4e27112ab | ||
|
|
50265fe843 | ||
|
|
461877b733 | ||
|
|
9dcaa0985f | ||
|
|
2429f63e02 | ||
|
|
28cda95346 | ||
|
|
42375da2e1 | ||
|
|
79fa9bef65 |
No files matched your search
@@ -12,6 +12,7 @@
|
||||
*.svg binary
|
||||
|
||||
# Prefer LF for most file types
|
||||
*.css text eol=lf
|
||||
*.frag text eol=lf
|
||||
*.htm text eol=lf
|
||||
*.html text eol=lf
|
||||
|
||||
+56
-3
@@ -1,11 +1,64 @@
|
||||
## Contributing
|
||||
The development of Scratch is an ongoing process, and we love to have people in the Scratch and open source communities help us along the way.
|
||||
|
||||
If you're interested in contributing, please take a look at the [issues](https://github.com/LLK/scratch-gui/issues) on this repository.
|
||||
Two great ways of helping are by identifying bugs and documenting them as issues, or fixing issues and creating pull requests. When looking for bugs to fix, please look for the ["Help Wanted" label](https://github.com/LLK/scratch-gui/issues?q=label%3A%22help+wanted%22). Bugs with this label have been specifically set aside for Open Source contributors. Issues without the label can also be worked on but we ask that you comment on the issue prior to starting work. When submitting pull requests please be patient -- it can take a while to find time to review them. The organization and class structures can't be radically changed without significant coordination and collaboration from the Scratch Team, so these types of changes should be avoided.
|
||||
### Ways to Help
|
||||
|
||||
It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch, but some think that estimate is a little low. While we welcome suggestions for new features in our [suggestions forum](https://scratch.mit.edu/discuss/1/) (especially ones that come with mockups), we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group. Why? Because we have a strong belief in the value of keeping things simple for new users. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
|
||||
* **Documenting bugs**
|
||||
* If you've identified a bug in Scratch you should first check to see if it's been filed as an issue, if not you can file one. Make sure you follow the issue template.
|
||||
* It's important that we can consistently reproduce issues. When writing an issue, be sure to follow our [reproduction step guidelines](https://github.com/LLK/scratch-gui/wiki/Writing-good-repro-steps).
|
||||
* Some issues are marked "Needs Repro". Adding a comment with good reproduction steps to those issues is a great way to help.
|
||||
* If you don't have an issue in mind already, you can look through the [Bugs & Glitches forum.](https://scratch.mit.edu/discuss/3/) Look for users reporting problems, reproduce the problem yourself, and file new issues following our guidelines.
|
||||
|
||||
* **Fixing bugs**
|
||||
* You can request to fix a bug in a comment on the issue if you at mention the repo coordinator, who for this repo is @chrisgarrity.
|
||||
* If the issue is marked "Help Wanted" you can go ahead and start working on it!
|
||||
* **We will only accept Pull Requests for bugs that have an issue filed that has a priority label**
|
||||
* If you're interested in fixing a bug with no issue, file the issue first and wait for it to have a priority added to it.
|
||||
|
||||
* We are not looking for Pull Requests ("PR") for every issue and may deny a PR if it doesn't fit our criteria.
|
||||
* We are far more likely to accept a PR if it is for an issue marked with Help Wanted.
|
||||
* We will not accept PRs for issues marked with "Needs Discussion" or "Needs Design."
|
||||
* Wait until the Repo Coordinator assigns the issue to you before you begin work or submit a PR.
|
||||
|
||||
### Learning Git and Github
|
||||
|
||||
If you want to work on fixing issues, you should be familiar with Git and Github.
|
||||
|
||||
* [Learn Git branching](https://learngitbranching.js.org/) includes an introduction to basic git commands and useful branching features.
|
||||
* Here's a general introduction to [contributing to an open source project](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
|
||||
|
||||
**Important:** we follow the [Github Flow process](https://guides.github.com/introduction/flow/) as our development process.
|
||||
|
||||
### How to Fix Bugs
|
||||
1. Identify which Github issue you are working on. Leave a comment on the issue to let us (and other contributors) know you're working on it.
|
||||
2. Make sure you have a fork of this repo (see [Github's forking a repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) for details)
|
||||
3. Switch to the `develop` branch, and pull down the latest changes from upstream
|
||||
4. Run the code, and reproduce the problem
|
||||
5. Create your branch from the `develop` branch
|
||||
6. Make code changes to fix the problem
|
||||
7. Run `npm test` to make sure that your changes pass our tests
|
||||
8. Commit your changes
|
||||
9. Push your branch to your fork
|
||||
10. Create your pull request
|
||||
1. Make sure to follow the template in the PR description
|
||||
1. Remember to check the “[Allow edits from maintainers](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)” box
|
||||
|
||||
When submitting pull requests keep in mind:
|
||||
* please be patient -- it can take a while to find time to review them
|
||||
* try to change the least amount of code necessary to fix the bug
|
||||
* the code can't be radically changed without significant coordination with the Scratch Team, so these types of changes should be avoided
|
||||
* if you find yourself changing a substantial amount of code or considering radical changes, please ask for clarification -- we may have envisioned a different approach, or underestimated the amount of effort
|
||||
|
||||
### Suggestions
|
||||

|
||||
|
||||
Please note: **_we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group_**.
|
||||
|
||||
Why? Because we have a strong belief in the value of keeping things simple for new users. It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
|
||||
|
||||
We welcome suggestions! If you want to suggest a feature, please post in our [suggestions forum](https://scratch.mit.edu/discuss/1/). Your suggestion will be helped if you include a mockup design; this can be simple, even hand-drawn.
|
||||
|
||||
### Other resources
|
||||
Beyond this repo, there are also some other resources that you might want to take a look at:
|
||||
* [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch)
|
||||
* [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch
|
||||
|
||||
+5
-2
@@ -5,8 +5,11 @@
|
||||
/node_modules
|
||||
npm-*
|
||||
|
||||
# Double copies of all the static assets and tutorial gifs
|
||||
/src
|
||||
# TO REVERT: the scratch-desktop branch wants to build scratch-gui from source.
|
||||
# To support that we need to un-ignore the /src directory.
|
||||
# Once we merge the scratch-desktop branch into develop we should re-ignore /src here.
|
||||
# We can also do that by omitting or reverting this commit during the merge.
|
||||
# /src
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ dist: xenial
|
||||
addons:
|
||||
chrome: stable
|
||||
node_js:
|
||||
- 8
|
||||
- 10
|
||||
env:
|
||||
global:
|
||||
- CHROMEDRIVER_VERSION=LATEST
|
||||
@@ -54,7 +54,7 @@ deploy:
|
||||
- provider: script
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^greenkeeper/
|
||||
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^dependabot/
|
||||
tags: false # Don't push tags to gh-pages
|
||||
skip_cleanup: true
|
||||
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
|
||||
|
||||
Generated
+4529
-1211
File diff suppressed because it is too large.
Load diff
+12
-11
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "npm run clean && webpack --progress --colors --bail",
|
||||
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
|
||||
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1) [skip ci]\"",
|
||||
"prune": "./prune-gh-pages.sh",
|
||||
"i18n:push": "tx-push-src scratch-editor interface translations/en.json",
|
||||
"i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push",
|
||||
@@ -44,7 +44,7 @@
|
||||
"babel-loader": "^8.0.4",
|
||||
"base64-loader": "1.0.0",
|
||||
"bowser": "1.9.4",
|
||||
"chromedriver": "80.0.0",
|
||||
"chromedriver": "83.0.0",
|
||||
"classnames": "2.2.6",
|
||||
"computed-style-to-inline-style": "3.0.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
@@ -108,14 +108,14 @@
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-throttle": "0.1.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20190925183642",
|
||||
"scratch-l10n": "3.7.20200212141356",
|
||||
"scratch-blocks": "0.1.0-prerelease.1581428931",
|
||||
"scratch-paint": "0.2.0-prerelease.20200213174123",
|
||||
"scratch-render": "0.1.0-prerelease.20200205011809",
|
||||
"scratch-storage": "1.3.2",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20200205003400",
|
||||
"scratch-vm": "0.2.0-prerelease.20191227164934",
|
||||
"scratch-audio": "0.1.0-prerelease.20200528195344",
|
||||
"scratch-blocks": "0.1.0-prerelease.20200617163252",
|
||||
"scratch-l10n": "3.10.20200617210158",
|
||||
"scratch-paint": "0.2.0-prerelease.20200615150428",
|
||||
"scratch-render": "0.1.0-prerelease.20200617185826",
|
||||
"scratch-storage": "1.3.3",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20200610220938",
|
||||
"scratch-vm": "0.2.0-prerelease.20200618162938",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.23.0",
|
||||
@@ -140,7 +140,8 @@
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
||||
"editor-msgs(\\.js)?$": "<rootDir>/test/__mocks__/editor-msgs-mock.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
padding-right: 1rem;
|
||||
margin-bottom: 7px;
|
||||
min-height: 1.5rem;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.alert.warn {
|
||||
|
||||
@@ -309,6 +309,7 @@ $fade-out-distance: 15px;
|
||||
z-index: $z-index-alerts;
|
||||
position: absolute;
|
||||
margin-top: 53px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -89,6 +89,7 @@ const GUIComponent = props => {
|
||||
loading,
|
||||
logo,
|
||||
renderLogin,
|
||||
onClickAbout,
|
||||
onClickAccountNav,
|
||||
onCloseAccountNav,
|
||||
onLogOut,
|
||||
@@ -105,6 +106,7 @@ const GUIComponent = props => {
|
||||
onRequestCloseTelemetryModal,
|
||||
onSeeCommunity,
|
||||
onShare,
|
||||
onShowPrivacyPolicy,
|
||||
onTelemetryModalCancel,
|
||||
onTelemetryModalOptIn,
|
||||
onTelemetryModalOptOut,
|
||||
@@ -162,6 +164,7 @@ const GUIComponent = props => {
|
||||
onOptIn={onTelemetryModalOptIn}
|
||||
onOptOut={onTelemetryModalOptOut}
|
||||
onRequestClose={onRequestCloseTelemetryModal}
|
||||
onShowPrivacyPolicy={onShowPrivacyPolicy}
|
||||
/>
|
||||
) : null}
|
||||
{loading ? (
|
||||
@@ -218,6 +221,7 @@ const GUIComponent = props => {
|
||||
logo={logo}
|
||||
renderLogin={renderLogin}
|
||||
showComingSoon={showComingSoon}
|
||||
onClickAbout={onClickAbout}
|
||||
onClickAccountNav={onClickAccountNav}
|
||||
onClickLogo={onClickLogo}
|
||||
onCloseAccountNav={onCloseAccountNav}
|
||||
@@ -388,6 +392,7 @@ GUIComponent.propTypes = {
|
||||
onActivateCostumesTab: PropTypes.func,
|
||||
onActivateSoundsTab: PropTypes.func,
|
||||
onActivateTab: PropTypes.func,
|
||||
onClickAbout: PropTypes.func,
|
||||
onClickAccountNav: PropTypes.func,
|
||||
onClickLogo: PropTypes.func,
|
||||
onCloseAccountNav: PropTypes.func,
|
||||
@@ -399,6 +404,7 @@ GUIComponent.propTypes = {
|
||||
onRequestCloseTelemetryModal: PropTypes.func,
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onShowPrivacyPolicy: PropTypes.func,
|
||||
onTabSelect: PropTypes.func,
|
||||
onTelemetryModalCancel: PropTypes.func,
|
||||
onTelemetryModalOptIn: PropTypes.func,
|
||||
|
||||
@@ -49,19 +49,33 @@ const getAssetTypeForFileExtension = function (fileExtension) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
|
||||
* Figure out one or more icon(s) for a library item.
|
||||
* If it's an animated thumbnail, this will return an array of `imageSource`.
|
||||
* Otherwise it'll return just one `imageSource`.
|
||||
* @param {object} item - either a library item or one of a library item's costumes.
|
||||
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
|
||||
* The latter is used internally as part of processing an animated thumbnail.
|
||||
* @returns {LibraryItem.PropTypes.icons} - an `imageSource` or array of them, ready for `LibraryItem` & `ScratchImage`
|
||||
*/
|
||||
const getItemImageSource = function (item) {
|
||||
const getItemIcons = function (item) {
|
||||
const costumes = (item.json && item.json.costumes) || item.costumes;
|
||||
if (costumes) {
|
||||
return costumes.map(getItemIcons);
|
||||
}
|
||||
|
||||
if (item.rawURL) {
|
||||
return {
|
||||
uri: item.rawURL
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
|
||||
const md5ext = item.md5 || item.baseLayerMD5;
|
||||
if (item.assetId && item.dataFormat) {
|
||||
return {
|
||||
assetId: item.assetId,
|
||||
assetType: getAssetTypeForFileExtension(item.dataFormat)
|
||||
};
|
||||
}
|
||||
|
||||
const md5ext = item.md5ext || item.md5 || item.baseLayerMD5;
|
||||
if (md5ext) {
|
||||
const [assetId, fileExtension] = md5ext.split('.');
|
||||
return {
|
||||
@@ -251,8 +265,7 @@ class LibraryComponent extends React.Component {
|
||||
ref={this.setFilteredDataRef}
|
||||
>
|
||||
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => {
|
||||
const iconSource = getItemImageSource(dataItem);
|
||||
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
|
||||
const icons = getItemIcons(dataItem);
|
||||
return (<LibraryItem
|
||||
bluetoothRequired={dataItem.bluetoothRequired}
|
||||
collaborator={dataItem.collaborator}
|
||||
@@ -261,7 +274,6 @@ class LibraryComponent extends React.Component {
|
||||
extensionId={dataItem.extensionId}
|
||||
featured={dataItem.featured}
|
||||
hidden={dataItem.hidden}
|
||||
iconSource={iconSource}
|
||||
icons={icons}
|
||||
id={index}
|
||||
insetIconURL={dataItem.insetIconURL}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M10 2c4.418 0 8 3.582 8 8s-3.582 8-8 8-8-3.582-8-8 3.582-8 8-8zm-.072 10.42c-.256 0-.472.076-.648.227-.176.151-.264.338-.264.56 0 .25.091.445.273.584.182.139.395.209.639.209.236 0 .444-.07.624-.211s.27-.335.27-.581c0-.222-.086-.409-.258-.56-.172-.151-.384-.227-.636-.227zm.144-6.42c-.624 0-1.17.113-1.638.338-.468.225-.824.51-1.068.853-.244.343-.366.683-.366 1.017 0 .162.077.313.231.452.154.139.343.209.567.209.38 0 .638-.199.774-.597.144-.38.32-.668.528-.864.208-.196.532-.293.972-.293.376 0 .683.097.921.291.238.194.357.432.357.713 0 .144-.039.278-.117.402-.078.123-.174.235-.288.336-.114.1-.299.249-.555.446-.292.225-.524.42-.696.584-.172.164-.31.354-.414.571-.104.217-.156.473-.156.769 0 .236.071.414.213.534.142.12.317.18.525.18.4 0 .638-.183.714-.55l.099-.362.093-.206.183-.227.327-.288c.492-.387.833-.663 1.023-.827.19-.164.354-.358.492-.584.138-.225.207-.488.207-.787 0-.38-.121-.733-.363-1.057-.242-.324-.585-.58-1.029-.769-.444-.188-.956-.283-1.536-.283z" id="a"/></defs><g transform="translate(-2 -2)" fill="none"><mask><use xlink:href="#a"/></mask><use fill="#fff" xlink:href="#a"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -218,3 +218,9 @@
|
||||
.mystuff > a {
|
||||
background-image: url("/images/mystuff.png");
|
||||
}
|
||||
|
||||
.about-icon {
|
||||
height: 1.25rem;
|
||||
margin: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import profileIcon from './icon--profile.png';
|
||||
import remixIcon from './icon--remix.svg';
|
||||
import dropdownCaret from './dropdown-caret.svg';
|
||||
import languageIcon from '../language-selector/language-icon.svg';
|
||||
import aboutIcon from './icon--about.svg';
|
||||
|
||||
import scratchLogo from './scratch-logo.svg';
|
||||
|
||||
@@ -141,6 +142,19 @@ MenuItemTooltip.propTypes = {
|
||||
isRtl: PropTypes.bool
|
||||
};
|
||||
|
||||
const AboutButton = props => (
|
||||
<Button
|
||||
className={classNames(styles.menuBarItem, styles.hoverable)}
|
||||
iconClassName={styles.aboutIcon}
|
||||
iconSrc={aboutIcon}
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
AboutButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class MenuBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -311,6 +325,8 @@ class MenuBar extends React.Component {
|
||||
{remixMessage}
|
||||
</Button>
|
||||
);
|
||||
// Show the About button only if we have a handler for it (like in the desktop app)
|
||||
const aboutButton = this.props.onClickAbout ? <AboutButton onClick={this.props.onClickAbout} /> : null;
|
||||
return (
|
||||
<Box
|
||||
className={classNames(
|
||||
@@ -689,6 +705,8 @@ class MenuBar extends React.Component {
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{aboutButton}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -722,6 +740,7 @@ MenuBar.propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
loginMenuOpen: PropTypes.bool,
|
||||
logo: PropTypes.string,
|
||||
onClickAbout: PropTypes.func,
|
||||
onClickAccount: PropTypes.func,
|
||||
onClickEdit: PropTypes.func,
|
||||
onClickFile: PropTypes.func,
|
||||
|
||||
@@ -94,7 +94,6 @@ class ScratchImage extends React.PureComponent {
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
src: _src,
|
||||
imageSource: _imageSource,
|
||||
...imgProps
|
||||
} = this.props;
|
||||
@@ -109,8 +108,8 @@ class ScratchImage extends React.PureComponent {
|
||||
ScratchImage.loadPendingImages();
|
||||
return (
|
||||
<img
|
||||
src={this.state.imageURI}
|
||||
{...imgProps}
|
||||
{...imgProps} // do this first in case it contains `src`
|
||||
src={this.state.imageURI} // overrides imgProps.src if present
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -133,7 +132,6 @@ to adjust for the border using a different method */
|
||||
}
|
||||
|
||||
.frame {
|
||||
box-sizing: content-box !important;
|
||||
background: $motion-transparent;
|
||||
border: 2px solid $motion-primary;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
@@ -101,6 +101,9 @@ class TelemetryModal extends React.PureComponent {
|
||||
privacyPolicyLink: (<a
|
||||
className={styles.privacyPolicyLink}
|
||||
href="https://scratch.mit.edu/privacy_policy/"
|
||||
onClick={this.props.onShowPrivacyPolicy}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FormattedMessage {...messages.privacyPolicyLink} />
|
||||
</a>)
|
||||
@@ -134,7 +137,8 @@ TelemetryModal.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onOptIn: PropTypes.func.isRequired,
|
||||
onOptOut: PropTypes.func.isRequired,
|
||||
onRequestClose: PropTypes.func
|
||||
onRequestClose: PropTypes.func,
|
||||
onShowPrivacyPolicy: PropTypes.func
|
||||
};
|
||||
|
||||
export default injectIntl(TelemetryModal);
|
||||
@@ -338,7 +338,7 @@ class Blocks extends React.Component {
|
||||
const stageCostumes = stage.getCostumes();
|
||||
const targetCostumes = target.getCostumes();
|
||||
const targetSounds = target.getSounds();
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(target);
|
||||
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
|
||||
targetCostumes[targetCostumes.length - 1].name,
|
||||
stageCostumes[stageCostumes.length - 1].name,
|
||||
|
||||
@@ -21,6 +21,7 @@ class LibraryItem extends React.PureComponent {
|
||||
'startRotatingIcons',
|
||||
'stopRotatingIcons'
|
||||
]);
|
||||
this.hasIconsArray = Array.isArray(props.icons);
|
||||
this.state = {
|
||||
iconIndex: 0,
|
||||
isRotatingIcon: false
|
||||
@@ -53,7 +54,7 @@ class LibraryItem extends React.PureComponent {
|
||||
// only show hover effects on the item if not showing a play button
|
||||
if (!this.props.showPlayButton) {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
if (this.hasIconsArray) {
|
||||
this.stopRotatingIcons();
|
||||
this.setState({
|
||||
isRotatingIcon: true
|
||||
@@ -65,7 +66,7 @@ class LibraryItem extends React.PureComponent {
|
||||
// only show hover effects on the item if not showing a play button
|
||||
if (!this.props.showPlayButton) {
|
||||
this.props.onMouseLeave(this.props.id);
|
||||
if (this.props.icons && this.props.icons.length) {
|
||||
if (this.hasIconsArray) {
|
||||
this.setState({
|
||||
isRotatingIcon: false
|
||||
}, this.stopRotatingIcons);
|
||||
@@ -92,13 +93,18 @@ class LibraryItem extends React.PureComponent {
|
||||
this.setState({iconIndex: nextIconIndex});
|
||||
}
|
||||
curIconSource () {
|
||||
if (this.props.icons &&
|
||||
this.state.isRotatingIcon &&
|
||||
this.state.iconIndex < this.props.icons.length &&
|
||||
this.props.icons[this.state.iconIndex]) {
|
||||
return this.props.icons[this.state.iconIndex];
|
||||
if (this.hasIconsArray) {
|
||||
if (this.state.isRotatingIcon &&
|
||||
this.state.iconIndex < this.props.icons.length &&
|
||||
this.props.icons[this.state.iconIndex]) {
|
||||
// multiple icons, currently animating: show current frame
|
||||
return this.props.icons[this.state.iconIndex];
|
||||
}
|
||||
// multiple icons, not currently animating: show first frame
|
||||
return this.props.icons[0];
|
||||
}
|
||||
return this.props.iconSource;
|
||||
// single icon
|
||||
return this.props.icons;
|
||||
}
|
||||
render () {
|
||||
const iconSource = this.curIconSource();
|
||||
@@ -142,8 +148,10 @@ LibraryItem.propTypes = {
|
||||
extensionId: PropTypes.string,
|
||||
featured: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
|
||||
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
|
||||
icons: PropTypes.oneOfType([
|
||||
LibraryItemComponent.propTypes.iconSource, // single icon
|
||||
PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource) // rotating icons
|
||||
]),
|
||||
id: PropTypes.number.isRequired,
|
||||
insetIconURL: PropTypes.string,
|
||||
internetConnectionRequired: PropTypes.bool,
|
||||
|
||||
@@ -2,10 +2,22 @@ import StartAudioContext from 'startaudiocontext';
|
||||
import bowser from 'bowser';
|
||||
|
||||
let AUDIO_CONTEXT;
|
||||
if (!bowser.msie) {
|
||||
AUDIO_CONTEXT = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
StartAudioContext(AUDIO_CONTEXT);
|
||||
if (!bowser.msie) {
|
||||
/**
|
||||
* AudioContext can be initialized only when user interaction event happens
|
||||
*/
|
||||
const event =
|
||||
typeof document.ontouchstart === 'undefined' ?
|
||||
'mousedown' :
|
||||
'touchstart';
|
||||
const initAudioContext = () => {
|
||||
document.removeEventListener(event, initAudioContext);
|
||||
AUDIO_CONTEXT = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
StartAudioContext(AUDIO_CONTEXT);
|
||||
};
|
||||
document.addEventListener(event, initAudioContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,11 +8,23 @@ export default (filename, blob) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = filename;
|
||||
downloadLink.type = blob.type;
|
||||
downloadLink.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(downloadLink);
|
||||
if ('download' in HTMLAnchorElement.prototype) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = filename;
|
||||
downloadLink.type = blob.type;
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// iOS Safari, open a new page and set href to data-uri
|
||||
let popup = window.open('', '_blank');
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
popup.location.href = reader.result;
|
||||
popup = null;
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -78,7 +78,7 @@ const DropAreaHOC = function (dragTypes) {
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const componentProps = omit(this.props, ['onDrop', 'dragInfo']);
|
||||
const componentProps = omit(this.props, ['onDrop', 'dragInfo', 'componentRef']);
|
||||
return (
|
||||
<WrappedComponent
|
||||
containerRef={this.setRef}
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Blue Sky 2 ",
|
||||
"md5": "5a906c2f272b77f59f0ef207857a8b3a.svg",
|
||||
"md5": "8eb8790be5507fdccf73e7c1570bbbab.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"outdoors",
|
||||
@@ -286,7 +286,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Circles",
|
||||
"md5": "4e29033ec2b891a8f1ca21242811d403.svg",
|
||||
"md5": "c9847be305920807c5597d81576dd0c4.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"patterns"
|
||||
@@ -491,7 +491,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Hearts",
|
||||
"md5": "26d3418b2fbc1af2c5fea82e1c3df1db.svg",
|
||||
"md5": "f98526ccb0eec3ac7d6c8f8ab502825e.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"patterns"
|
||||
@@ -1050,7 +1050,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Underwater 1",
|
||||
"md5": "fb907f72b310acc8b95cbf2d2cccabc9.svg",
|
||||
"md5": "d3344650f594bcecdf46aa4a9441badd.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"ocean",
|
||||
@@ -1175,7 +1175,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Witch House",
|
||||
"md5": "597b9a9813fe5d8d387283138a0b8f2b.svg",
|
||||
"md5": "30085b2d27beb5acdbe895d8b3e64b04.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"fantasy",
|
||||
@@ -1190,7 +1190,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Woods",
|
||||
"md5": "d26cfd278999c9e37d6af75c296a58df.svg",
|
||||
"md5": "f3eb165d6f3fd23370f97079f2e631bf.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"fantasy",
|
||||
|
||||
+1740
-801
File diff suppressed because it is too large.
Load diff
+545
-553
File diff suppressed because it is too large.
Load diff
+14873
-10065
File diff suppressed because it is too large.
Load diff
@@ -251,6 +251,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
|
||||
if (id && this.props.onUpdateProjectThumbnail) {
|
||||
this.storeProjectThumbnail(id);
|
||||
}
|
||||
this.reportTelemetryEvent('projectDidSave');
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -291,9 +292,15 @@ const ProjectSaverHOC = function (WrappedComponent) {
|
||||
*/
|
||||
// TODO make a telemetry HOC and move this stuff there
|
||||
reportTelemetryEvent (event) {
|
||||
if (this.props.onProjectTelemetryEvent) {
|
||||
const metadata = collectMetadata(this.props.vm, this.props.reduxProjectTitle, this.props.locale);
|
||||
this.props.onProjectTelemetryEvent(event, metadata);
|
||||
try {
|
||||
if (this.props.onProjectTelemetryEvent) {
|
||||
const metadata = collectMetadata(this.props.vm, this.props.reduxProjectTitle, this.props.locale);
|
||||
this.props.onProjectTelemetryEvent(event, metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Telemetry error', event, e);
|
||||
// This is intentionally fire/forget because a failure
|
||||
// to report telemetry should not block saving
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ const randomizeSpritePosition = spriteObject => {
|
||||
const randomY = Math.floor((100 * Math.random()) - 50);
|
||||
if (spriteObject.hasOwnProperty('json')) {
|
||||
// Library sprite object
|
||||
spriteObject.json.scratchX = randomX;
|
||||
spriteObject.json.scratchY = randomY;
|
||||
spriteObject.json.x = randomX;
|
||||
spriteObject.json.y = randomY;
|
||||
} else if (spriteObject.hasOwnProperty('x') && spriteObject.hasOwnProperty('y')) {
|
||||
// Scratch 3 sprite object
|
||||
spriteObject.x = randomX;
|
||||
|
||||
@@ -17,7 +17,7 @@ const START_AUTO_UPDATING = 'scratch-gui/project-state/START_AUTO_UPDATING';
|
||||
const START_CREATING_NEW = 'scratch-gui/project-state/START_CREATING_NEW';
|
||||
const START_ERROR = 'scratch-gui/project-state/START_ERROR';
|
||||
const START_FETCHING_NEW = 'scratch-gui/project-state/START_FETCHING_NEW';
|
||||
const START_LOADING_VM_FILE_UPLOAD = 'scratch-gui/project-state/START_LOADING_FILE_UPLOAD';
|
||||
const START_LOADING_VM_FILE_UPLOAD = 'scratch-gui/project-state/START_LOADING_VM_FILE_UPLOAD';
|
||||
const START_MANUAL_UPDATING = 'scratch-gui/project-state/START_MANUAL_UPDATING';
|
||||
const START_REMIXING = 'scratch-gui/project-state/START_REMIXING';
|
||||
const START_UPDATING_BEFORE_CREATING_COPY = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_COPY';
|
||||
@@ -413,32 +413,31 @@ const onFetchedProjectData = (projectData, loadingState) => {
|
||||
};
|
||||
|
||||
const onLoadedProject = (loadingState, canSave, success) => {
|
||||
if (success) {
|
||||
switch (loadingState) {
|
||||
case LoadingState.LOADING_VM_WITH_ID:
|
||||
return {
|
||||
type: DONE_LOADING_VM_WITH_ID
|
||||
};
|
||||
case LoadingState.LOADING_VM_FILE_UPLOAD:
|
||||
if (canSave) {
|
||||
return {
|
||||
type: DONE_LOADING_VM_TO_SAVE
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: DONE_LOADING_VM_WITHOUT_ID
|
||||
};
|
||||
case LoadingState.LOADING_VM_NEW_DEFAULT:
|
||||
return {
|
||||
type: DONE_LOADING_VM_WITHOUT_ID
|
||||
};
|
||||
default:
|
||||
return;
|
||||
switch (loadingState) {
|
||||
case LoadingState.LOADING_VM_WITH_ID:
|
||||
if (success) {
|
||||
return {type: DONE_LOADING_VM_WITH_ID};
|
||||
}
|
||||
// failed to load project; just keep showing current project
|
||||
return {type: RETURN_TO_SHOWING};
|
||||
case LoadingState.LOADING_VM_FILE_UPLOAD:
|
||||
if (success) {
|
||||
if (canSave) {
|
||||
return {type: DONE_LOADING_VM_TO_SAVE};
|
||||
}
|
||||
return {type: DONE_LOADING_VM_WITHOUT_ID};
|
||||
}
|
||||
// failed to load project; just keep showing current project
|
||||
return {type: RETURN_TO_SHOWING};
|
||||
case LoadingState.LOADING_VM_NEW_DEFAULT:
|
||||
if (success) {
|
||||
return {type: DONE_LOADING_VM_WITHOUT_ID};
|
||||
}
|
||||
// failed to load default project; show error
|
||||
return {type: START_ERROR};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
return {
|
||||
type: RETURN_TO_SHOWING
|
||||
};
|
||||
};
|
||||
|
||||
const doneUpdatingProject = loadingState => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
en: {}
|
||||
};
|
||||
@@ -19,6 +19,7 @@ class SeleniumHelper {
|
||||
'clickText',
|
||||
'clickButton',
|
||||
'clickXpath',
|
||||
'clickBlocksCategory',
|
||||
'elementIsVisible',
|
||||
'findByText',
|
||||
'findByXpath',
|
||||
@@ -124,6 +125,17 @@ class SeleniumHelper {
|
||||
return this.findByText(text, scope).then(el => el.click());
|
||||
}
|
||||
|
||||
async clickBlocksCategory (categoryText) {
|
||||
// The toolbox is destroyed and recreated several times, so avoid clicking on a nonexistent element and erroring
|
||||
// out. First we wait for the block pane itself to appear, then wait 100ms for the toolbox to finish refreshing,
|
||||
// then finally click the toolbox text.
|
||||
|
||||
await this.findByXpath('//div[contains(@class, "blocks_blocks")]');
|
||||
await this.driver.sleep(100);
|
||||
await this.clickText(categoryText, 'div[contains(@class, "blocks_blocks")]');
|
||||
await this.driver.sleep(500); // Wait for scroll to finish
|
||||
}
|
||||
|
||||
rightClickText (text, scope) {
|
||||
return this.findByText(text, scope).then(el => this.driver.actions()
|
||||
.click(el, Button.RIGHT)
|
||||
|
||||
@@ -3,12 +3,14 @@ import SeleniumHelper from '../helpers/selenium-helper';
|
||||
|
||||
const {
|
||||
clickText,
|
||||
clickBlocksCategory,
|
||||
clickButton,
|
||||
clickXpath,
|
||||
findByText,
|
||||
findByXpath,
|
||||
getDriver,
|
||||
getLogs,
|
||||
Key,
|
||||
loadUri,
|
||||
rightClickText,
|
||||
scope
|
||||
@@ -30,8 +32,7 @@ describe('Working with the blocks', () => {
|
||||
test('Blocks report when clicked in the toolbox', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Code');
|
||||
await clickText('Operators', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Operators');
|
||||
await clickText('join', scope.blocksTab); // Click "join <hello> <world>" block
|
||||
await findByText('apple banana', scope.reportedValue); // Tooltip with result
|
||||
const logs = await getLogs();
|
||||
@@ -40,14 +41,13 @@ describe('Working with the blocks', () => {
|
||||
|
||||
test('Switching sprites updates the block menus', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Sound', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Sound');
|
||||
// "Meow" sound block should be visible
|
||||
await findByText('Meow', scope.blocksTab);
|
||||
await clickText('Backdrops'); // Switch to the backdrop
|
||||
// Now "pop" sound block should be visible and motion blocks hidden
|
||||
await findByText('pop', scope.blocksTab);
|
||||
await clickText('Motion', scope.blocksTab);
|
||||
await clickBlocksCategory('Motion');
|
||||
await findByText('Stage selected: no motion blocks');
|
||||
|
||||
const logs = await getLogs();
|
||||
@@ -57,8 +57,7 @@ describe('Working with the blocks', () => {
|
||||
test('Creating variables', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Code');
|
||||
await clickText('Variables', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Variables');
|
||||
|
||||
// Expect a default variable "my variable" to be visible
|
||||
await clickText('my\u00A0variable', scope.blocksTab);
|
||||
@@ -74,8 +73,7 @@ describe('Working with the blocks', () => {
|
||||
await clickButton('OK');
|
||||
|
||||
// Make sure reporting works on a new variable
|
||||
await clickText('Variables', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Variables');
|
||||
await clickText('score', scope.blocksTab);
|
||||
await findByText('0', scope.reportedValue); // Tooltip with result
|
||||
|
||||
@@ -99,8 +97,7 @@ describe('Working with the blocks', () => {
|
||||
test('Creating a list', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Code');
|
||||
await clickText('Variables', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Variables');
|
||||
|
||||
await clickText('Make a List');
|
||||
let el = await findByXpath("//input[@name='New list name:']");
|
||||
@@ -132,8 +129,7 @@ describe('Working with the blocks', () => {
|
||||
|
||||
test('Custom procedures', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('My Blocks');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('My Blocks');
|
||||
await clickText('Make a Block');
|
||||
// Click on the "add an input" buttons
|
||||
await clickText('number or text', scope.modal);
|
||||
@@ -164,8 +160,7 @@ describe('Working with the blocks', () => {
|
||||
test('Record option from sound block menu opens sound recorder', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Code');
|
||||
await clickText('Sound', scope.blocksTab);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickBlocksCategory('Sound');
|
||||
await clickText('Meow', scope.blocksTab); // Click "play sound <Meow> until done" block
|
||||
await clickText('record'); // Click "record..." option in the block's sound menu
|
||||
// Access has been force denied, so close the alert that comes up
|
||||
@@ -185,15 +180,18 @@ describe('Working with the blocks', () => {
|
||||
await clickText('costume2', scope.costumesTab);
|
||||
const el = await findByXpath("//input[@value='costume2']");
|
||||
await el.sendKeys('newname');
|
||||
await el.sendKeys(Key.ENTER);
|
||||
// wait until the updated costume appears in costume item list panel
|
||||
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
|
||||
"//div[contains(text(), 'newname')]");
|
||||
|
||||
// Make sure it is updated in the block menu
|
||||
await clickText('Code');
|
||||
await clickText('Looks', scope.blocksTab);
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Looks');
|
||||
await clickText('newname', scope.blocksTab);
|
||||
});
|
||||
|
||||
test.skip('Renaming costume with a special character should not break toolbox', async () => {
|
||||
test('Renaming costume with a special character should not break toolbox', async () => {
|
||||
await loadUri(uri);
|
||||
|
||||
// Rename the costume
|
||||
@@ -201,22 +199,24 @@ describe('Working with the blocks', () => {
|
||||
await clickText('costume2', scope.costumesTab);
|
||||
const el = await findByXpath("//input[@value='costume2']");
|
||||
await el.sendKeys('<NewCostume>');
|
||||
await el.sendKeys(Key.ENTER);
|
||||
// wait until the updated costume appears in costume item list panel
|
||||
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
|
||||
"//div[contains(text(), '<NewCostume>')]");
|
||||
|
||||
// Make sure it is updated in the block menu
|
||||
await clickText('Code');
|
||||
await clickText('Looks', scope.blocksTab);
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Looks');
|
||||
await clickText('<NewCostume>', scope.blocksTab);
|
||||
|
||||
await clickText('Sound', scope.blocksTab);
|
||||
await clickBlocksCategory('Sound');
|
||||
});
|
||||
|
||||
test('Adding costumes DOES update the default costume name in the toolbox', async () => {
|
||||
await loadUri(uri);
|
||||
|
||||
// By default, costume2 is in the costume tab
|
||||
await clickText('Looks', scope.blocksTab);
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Looks');
|
||||
await clickText('costume2', scope.blocksTab);
|
||||
|
||||
// Also check that adding a new costume does update the list
|
||||
@@ -226,6 +226,9 @@ describe('Working with the blocks', () => {
|
||||
.perform();
|
||||
await driver.sleep(500); // Wait for thermometer menu to come up
|
||||
await clickXpath('//button[@aria-label="Paint"]');
|
||||
// wait until the new costume appears in costume item list panel
|
||||
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
|
||||
"//div[contains(text(), 'costume3')]");
|
||||
await clickText('costume3', scope.costumesTab);
|
||||
// Check that the menu has been updated
|
||||
await clickText('Code');
|
||||
@@ -233,14 +236,16 @@ describe('Working with the blocks', () => {
|
||||
});
|
||||
|
||||
// Skipped because it was flakey on travis, but seems to run locally ok
|
||||
test.skip('Adding a sound DOES update the default sound name in the toolbox', async () => {
|
||||
test('Adding a sound DOES update the default sound name in the toolbox', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Sounds');
|
||||
await clickXpath('//button[@aria-label="Choose a Sound"]');
|
||||
await clickText('A Bass', scope.modal); // Should close the modal
|
||||
// wait until the selected sound appears in sounds item list panel
|
||||
await findByXpath("//div[contains(@class,'sprite-selector-item_is-selected_')]" +
|
||||
"//div[contains(text(), 'A Bass')]");
|
||||
await clickText('Code');
|
||||
await clickText('Sound', scope.blocksTab);
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Sound');
|
||||
await clickText('A\u00A0Bass', scope.blocksTab); // Need for block text
|
||||
});
|
||||
|
||||
@@ -249,15 +254,13 @@ describe('Working with the blocks', () => {
|
||||
const playerUri = path.resolve(__dirname, '../../build/player.html');
|
||||
await loadUri(playerUri);
|
||||
await clickText('See inside');
|
||||
await clickText('Variables');
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Variables');
|
||||
await clickText('my\u00A0variable');
|
||||
|
||||
await clickText('See Project Page');
|
||||
await clickText('See inside');
|
||||
|
||||
await clickText('Variables');
|
||||
await driver.sleep(500); // Wait for scroll to finish
|
||||
await clickBlocksCategory('Variables');
|
||||
await clickText('my\u00A0variable');
|
||||
});
|
||||
|
||||
@@ -266,8 +269,7 @@ describe('Working with the blocks', () => {
|
||||
await loadUri(uri);
|
||||
await clickText('Costumes');
|
||||
await clickText('Code');
|
||||
await clickText('Variables', scope.blocksTab);
|
||||
await driver.sleep(500); // Wait for scroll
|
||||
await clickBlocksCategory('Variables');
|
||||
await clickText('Make a List');
|
||||
const el = await findByXpath("//input[@name='New list name:']");
|
||||
await el.sendKeys('list1');
|
||||
|
||||
@@ -25,16 +25,12 @@ describe('Localization', () => {
|
||||
});
|
||||
|
||||
test('Switching languages', async () => {
|
||||
await driver.quit();
|
||||
driver = getDriver();
|
||||
await loadUri(uri);
|
||||
|
||||
// Add a sprite to make sure it stays when switching languages
|
||||
await clickText('Costumes');
|
||||
await clickXpath('//button[@aria-label="Choose a Sprite"]');
|
||||
await clickText('Apple', scope.modal); // Closes modal
|
||||
|
||||
await clickText('Code');
|
||||
await clickXpath('//*[@aria-label="language selector"]');
|
||||
await clickText('Deutsch');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers';
|
||||
import MenuBar from '../../../src/components/menu-bar/menu-bar';
|
||||
import {menuInitialState} from '../../../src/reducers/menus';
|
||||
import {LoadingState} from '../../../src/reducers/project-state';
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {Provider} from 'react-redux';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
|
||||
describe('MenuBar Component', () => {
|
||||
const store = configureStore()({
|
||||
locales: {
|
||||
isRtl: false,
|
||||
locale: 'en-US'
|
||||
},
|
||||
scratchGui: {
|
||||
menus: menuInitialState,
|
||||
projectState: {
|
||||
loadingState: LoadingState.NOT_LOADED
|
||||
},
|
||||
vm: new VM()
|
||||
}
|
||||
});
|
||||
|
||||
const getComponent = function (props = {}) {
|
||||
return <Provider store={store}><MenuBar {...props} /></Provider>;
|
||||
};
|
||||
|
||||
test('menu bar with no About handler has no About button', () => {
|
||||
const menuBar = mountWithIntl(getComponent());
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(button.exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('menu bar with an About handler has an About button', () => {
|
||||
const onClickAbout = jest.fn();
|
||||
const menuBar = mountWithIntl(getComponent({onClickAbout}));
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(button.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('clicking on About button calls the handler', () => {
|
||||
const onClickAbout = jest.fn();
|
||||
const menuBar = mountWithIntl(getComponent({onClickAbout}));
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(onClickAbout).toHaveBeenCalledTimes(0);
|
||||
button.simulate('click');
|
||||
expect(onClickAbout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -92,99 +92,181 @@ test('onFetchedProjectData new loads project data into vm', () => {
|
||||
expect(resultState.projectData).toBe('1010101');
|
||||
});
|
||||
|
||||
test('onLoadedProject upload, with canSave false, shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
// onLoadedProject: LOADING_VM_WITH_ID
|
||||
|
||||
test('onLoadedProject upload, with canSave true, prepares to save', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
|
||||
});
|
||||
|
||||
test('onLoadedProject with id shows with id', () => {
|
||||
test('onLoadedProject(LOADING_VM_WITH_ID, true, true) results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('onLoadedProject new shows without id', () => {
|
||||
test('onLoadedProject(LOADING_VM_WITH_ID, false, true) results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
// case where we started out viewing a project with a projectId, then
|
||||
// started to load another project; but loading fails. We go back to
|
||||
// showing the original project.
|
||||
test('onLoadedProject(LOADING_VM_WITH_ID, false, false), with project id, ' +
|
||||
'results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
// case where we started out viewing a project with default projectId, then
|
||||
// started to load one with an id, such as in standalone mode when user adds
|
||||
// '#PROJECT_ID_NUMBER' to the URI; but loading fails. We go back to
|
||||
// showing the original project.
|
||||
test('onLoadedProject(LOADING_VM_WITH_ID, false, false), with no project id, ' +
|
||||
'results in state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
// onLoadedProject: LOADING_VM_FILE_UPLOAD
|
||||
|
||||
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, true, true) prepares to save', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, true) results in state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('onLoadedProject new, to save shows without id', () => {
|
||||
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, false), when we know project id, ' +
|
||||
'results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, false, false), when we ' +
|
||||
'don\'t know project id, results in state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
// onLoadedProject: LOADING_VM_NEW_DEFAULT
|
||||
|
||||
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, true, true) results in state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('onLoadedProject with success false, no project id, shows without id', () => {
|
||||
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, false, true) results in state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID,
|
||||
projectId: null
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('onLoadedProject with success false, valid project id, shows with id', () => {
|
||||
test('onLoadedProject(LOADING_VM_NEW_DEFAULT, false, false) results in ERROR state', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID,
|
||||
projectId: '12345'
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.loadingState).toBe(LoadingState.ERROR);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('doneUpdatingProject with id shows with id', () => {
|
||||
// doneUpdatingProject
|
||||
|
||||
test('doneUpdatingProject with id results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.MANUAL_UPDATING
|
||||
};
|
||||
const action = doneUpdatingProject(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('doneUpdatingProject with id, before copy, creates copy', () => {
|
||||
test('doneUpdatingProject with id, before copy occurs, results in state CREATING_COPY', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.UPDATING_BEFORE_COPY
|
||||
};
|
||||
const action = doneUpdatingProject(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.CREATING_COPY);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('doneUpdatingProject with id, before new, fetches default project', () => {
|
||||
test('doneUpdatingProject with id, before new, results in state FETCHING_NEW_DEFAULT', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.UPDATING_BEFORE_NEW
|
||||
};
|
||||
const action = doneUpdatingProject(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.FETCHING_NEW_DEFAULT);
|
||||
expect(resultState.projectId).toBe('0'); // resets id
|
||||
});
|
||||
|
||||
test('setProjectId, with same id as before, should show with id, not fetch', () => {
|
||||
test('calling setProjectId, using with same id as already showing, ' +
|
||||
'results in state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
@@ -195,7 +277,8 @@ test('setProjectId, with same id as before, should show with id, not fetch', ()
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('setProjectId, with different id as before, should fetch with id, not show with id', () => {
|
||||
test('calling setProjectId, using different id from project already showing, ' +
|
||||
'results in state FETCHING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
projectId: 99,
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
@@ -206,7 +289,8 @@ test('setProjectId, with different id as before, should fetch with id, not show
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('setProjectId, with same id as before, but not same type, should fetch because not ===', () => {
|
||||
test('setProjectId, with same id as before, but not same type, ' +
|
||||
'results in FETCHING_WITH_ID because the two projectIds are not strictly equal', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
@@ -217,85 +301,104 @@ test('setProjectId, with same id as before, but not same type, should fetch beca
|
||||
expect(resultState.projectId).toBe(100);
|
||||
});
|
||||
|
||||
test('requestNewProject, when can\'t create new, should fetch default project without id', () => {
|
||||
test('requestNewProject, when can\'t create/save, results in FETCHING_NEW_DEFAULT', () => {
|
||||
const initialState = {
|
||||
projectId: '0',
|
||||
loadingState: LoadingState.SHOWING_WITHOUT_ID
|
||||
};
|
||||
const action = requestNewProject(false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.FETCHING_NEW_DEFAULT);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('requestNewProject, when can create new, should save and prepare to fetch default project', () => {
|
||||
test('requestNewProject, when can create/save, results in UPDATING_BEFORE_NEW ' +
|
||||
'(in order to save before fetching default project)', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = requestNewProject(true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_NEW);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('requestProjectUpload when project not loaded should load', () => {
|
||||
test('requestProjectUpload when project not loaded results in state LOADING_VM_FILE_UPLOAD', () => {
|
||||
const initialState = {
|
||||
projectId: null,
|
||||
loadingState: LoadingState.NOT_LOADED
|
||||
};
|
||||
const action = requestProjectUpload(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
|
||||
expect(resultState.projectId).toBe(null);
|
||||
});
|
||||
|
||||
test('requestProjectUpload when showing project with id should load', () => {
|
||||
test('requestProjectUpload when showing project with id results in state LOADING_VM_FILE_UPLOAD', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = requestProjectUpload(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('requestProjectUpload when showing project without id should load', () => {
|
||||
test('requestProjectUpload when showing project without id results in state LOADING_VM_FILE_UPLOAD', () => {
|
||||
const initialState = {
|
||||
projectId: null,
|
||||
loadingState: LoadingState.SHOWING_WITHOUT_ID
|
||||
};
|
||||
const action = requestProjectUpload(initialState.loadingState);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_FILE_UPLOAD);
|
||||
expect(resultState.projectId).toBe(null);
|
||||
});
|
||||
|
||||
test('manualUpdateProject should prepare to update', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = manualUpdateProject();
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.MANUAL_UPDATING);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('autoUpdateProject should prepare to update', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = autoUpdateProject();
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('saveProjectAsCopy should save, before preparing to save as a copy', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = saveProjectAsCopy();
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.UPDATING_BEFORE_COPY);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('remixProject should prepare to remix', () => {
|
||||
const initialState = {
|
||||
projectId: '100',
|
||||
loadingState: LoadingState.SHOWING_WITH_ID
|
||||
};
|
||||
const action = remixProject();
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.REMIXING);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
});
|
||||
|
||||
test('projectError from various states should show error', () => {
|
||||
@@ -314,11 +417,13 @@ test('projectError from various states should show error', () => {
|
||||
for (const startState of startStates) {
|
||||
const initialState = {
|
||||
error: null,
|
||||
projectId: '100',
|
||||
loadingState: startState
|
||||
};
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.error).toEqual('Error string');
|
||||
expect(resultState.projectId).toBe('100');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -332,11 +437,13 @@ test('fatal projectError should show error state', () => {
|
||||
for (const startState of startStates) {
|
||||
const initialState = {
|
||||
error: null,
|
||||
projectId: '100',
|
||||
loadingState: startState
|
||||
};
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.ERROR);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -351,15 +458,18 @@ test('non-fatal projectError should show normal state', () => {
|
||||
for (const startState of startStates) {
|
||||
const initialState = {
|
||||
error: null,
|
||||
projectId: '100',
|
||||
loadingState: startState
|
||||
};
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('100');
|
||||
}
|
||||
});
|
||||
|
||||
test('projectError when creating new while viewing project with id should show project with id', () => {
|
||||
test('projectError when creating new while viewing project with id should ' +
|
||||
'go back to state SHOWING_WITH_ID', () => {
|
||||
const initialState = {
|
||||
error: null,
|
||||
loadingState: LoadingState.CREATING_NEW,
|
||||
@@ -368,9 +478,11 @@ test('projectError when creating new while viewing project with id should show p
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
expect(resultState.projectId).toBe('12345');
|
||||
});
|
||||
|
||||
test('projectError when creating new while logged out, looking at default project should show default project', () => {
|
||||
test('projectError when creating new while logged out, looking at default project ' +
|
||||
'should go back to state SHOWING_WITHOUT_ID', () => {
|
||||
const initialState = {
|
||||
error: null,
|
||||
loadingState: LoadingState.CREATING_NEW,
|
||||
@@ -379,15 +491,19 @@ test('projectError when creating new while logged out, looking at default projec
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.projectId).toBe('0');
|
||||
});
|
||||
|
||||
test('projectError from showing project should show error', () => {
|
||||
test('projectError encountered while in state FETCHING_WITH_ID results in ' +
|
||||
'ERROR state', () => {
|
||||
const initialState = {
|
||||
error: null,
|
||||
projectId: null,
|
||||
loadingState: LoadingState.FETCHING_WITH_ID
|
||||
};
|
||||
const action = projectError('Error string');
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.ERROR);
|
||||
expect(resultState.projectId).toBe(null);
|
||||
expect(resultState.error).toEqual('Error string');
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'web-audio-test-api';
|
||||
import SharedAudioContext from '../../../src/lib/audio/shared-audio-context';
|
||||
|
||||
describe('Shared Audio Context', () => {
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
test('returns empty object without user gesture', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
expect(sharedAudioContext).toMatchObject({});
|
||||
});
|
||||
|
||||
test('returns AudioContext when mousedown is triggered', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
const event = new Event('mousedown');
|
||||
document.dispatchEvent(event);
|
||||
expect(sharedAudioContext).toMatchObject(audioContext);
|
||||
});
|
||||
|
||||
test('returns AudioContext when touchstart is triggered', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
const event = new Event('touchstart');
|
||||
document.dispatchEvent(event);
|
||||
expect(sharedAudioContext).toMatchObject(audioContext);
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ describe('projectSaverHOC', () => {
|
||||
expect(mockedUpdateProject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('if canSave is alreatdy true and we show a project with an id, project will NOT be saved', () => {
|
||||
test('if canSave is already true and we show a project with an id, project will NOT be saved', () => {
|
||||
const mockedSaveProject = jest.fn();
|
||||
const Component = () => <div />;
|
||||
const WrappedComponent = projectSaverHOC(Component);
|
||||
|
||||
Reference in new issue
Block a user