Compare commits

...
Author SHA1 Message Date
Benjamin Wheeler e34d5ed67e Merge pull request #5437 from LLK/revert-4809-refactor-file-uploader
Revert "refactor SB* file uploader into top-level HOC"
2020-02-06 11:38:58 -05:00
Benjamin Wheeler af4ad81811 Revert "refactor SB* file uploader into top-level HOC" 2020-02-06 11:33:03 -05:00
Paul Kaplan c368de81fa Merge pull request #5435 from paulkaplan/resync-paint-lock
Resync package-lock for scratch-paint
2020-02-05 19:25:34 -05:00
greenkeeper[bot] 0104f5bec8 chore(package): update lockfile package-lock.json 2020-02-05 19:23:20 -05:00
DD Liu ae8624e7e7 Merge pull request #5433 from fsih/updatePaint
update paint'
2020-02-05 17:42:54 -05:00
DD Liu 7f8b84829e update paint' 2020-02-05 17:42:12 -05:00
DD Liu d4078a795f Merge pull request #5432 from fsih/updateDeps
Update blocks and l10n
2020-02-05 16:41:42 -05:00
DD Liu f381a6a0d8 Update blocks and l10n 2020-02-05 16:40:00 -05:00
Benjamin Wheeler 962ce680c9 Merge pull request #5430 from benjiwheeler/reduce-project-state-file-size
reduce project state machine example image file size
2020-02-05 16:27:40 -05:00
Ben Wheeler 630ba84350 reduce project state image file size 2020-02-05 16:26:21 -05:00
Benjamin Wheeler 51843d6bef Merge pull request #5416 from benjiwheeler/project-state-diagram
Project state diagram
2020-02-05 16:22:04 -05:00
DD Liu 02be2155d9 Merge pull request #5428 from fsih/unUpdateDeps
Revert "Update dependencies"
2020-02-05 15:32:30 -05:00
DD Liu cca695b54e Revert "Update dependencies"
This is to remove the latest svg-renderer change, "add rounded edges to gradient strokes", because we would like to come up with a more comprehensive testing plan before adding it.
2020-02-05 15:13:31 -05:00
Ben Wheeler 4e633cb596 clearer project state example 2020-02-05 13:38:01 -05:00
DD Liu 8dc343c90d Merge pull request #5427 from LLK/greenkeeper/mkdirp-1.0.3
Greenkeeper/mkdirp 1.0.3
2020-02-05 13:03:56 -05:00
DD Liu 369eb8c3cf Merge pull request #5426 from LLK/greenkeeper/chromedriver-80.0.0
Greenkeeper/chromedriver 80.0.0
2020-02-05 11:02:41 -05:00
DD Liu de9dbe08f4 Merge pull request #5424 from fsih/updateDeps
Update dependencies
2020-02-04 21:38:46 -05:00
DD Liu c050829a75 Update dependencies 2020-02-04 21:28:20 -05:00
greenkeeper[bot] 6e57ea1c91 chore(package): update lockfile package-lock.json 2020-02-05 00:10:26 +00:00
greenkeeper[bot] 53d09e2162 chore(package): update chromedriver to version 80.0.0
Closes #5335
2020-02-05 00:10:22 +00:00
Benjamin Wheeler 5d9afb008a Merge pull request #4809 from benjiwheeler/refactor-file-uploader
refactor SB* file uploader into top-level HOC
2020-02-04 17:32:04 -05:00
Ben Wheeler d19cf91205 removed stale comments 2020-02-04 17:30:33 -05:00
DD Liu 4f70b0f924 Merge pull request #5417 from LLK/greenkeeper/scratch-paint-0.2.0-prerelease.20200204155336
Greenkeeper/scratch paint 0.2.0 prerelease.20200204155336
2020-02-04 16:41:51 -05:00
Ben Wheeler f8a0f46959 added project state example 2020-02-04 13:11:48 -05:00
Ben Wheeler faf0dd2f13 added project state diagram 2020-02-04 13:00:52 -05:00
Ben Wheeler cbb908831d Fixed typo 2020-02-04 11:30:57 -05:00
greenkeeper[bot] b44c0c355a chore(package): update lockfile package-lock.json 2020-02-04 16:09:21 +00:00
greenkeeper[bot] ffa364ed64 chore(package): update scratch-paint to version 0.2.0-prerelease.20200204155336
Closes #5405
2020-02-04 16:09:16 +00:00
Ben Wheeler 59f428900c update menu-bar test per refactor 2020-02-03 11:16:42 -05:00
Ben Wheeler 48245be395 reverted project state changes in f2291d96 2020-01-30 14:18:13 -05:00
Ben Wheeler efed4d3721 file input is dynamically added to DOM, not rendered 2020-01-29 19:38:47 -05:00
DD Liu 95532e147d Merge pull request #5402 from LLK/greenkeeper/scratch-paint-0.2.0-prerelease.20200129174741
Greenkeeper/scratch paint 0.2.0 prerelease.20200129174741
2020-01-29 13:43:31 -05:00
DD Liu 3a56e609a5 Merge branch 'develop' into greenkeeper/scratch-paint-0.2.0-prerelease.20200129174741 2020-01-29 13:05:08 -05:00
Paul Kaplan e63842a336 Merge pull request #5400 from LLK/greenkeeper/scratch-blocks-0.1.0-prerelease.1580309823
Update scratch-blocks to the latest version 🚀
2020-01-29 13:00:34 -05:00
greenkeeper[bot] c9173427b9 chore(package): update lockfile package-lock.json 2020-01-29 17:49:28 +00:00
greenkeeper[bot] 3e5b65072c chore(package): update scratch-paint to version 0.2.0-prerelease.20200129174741
Closes #5401
2020-01-29 17:49:24 +00:00
greenkeeper[bot] 74828b3b09 chore(package): update lockfile package-lock.json 2020-01-29 15:03:07 +00:00
greenkeeper[bot] e77162791f chore(package): update scratch-blocks to version 0.1.0-prerelease.1580309823 2020-01-29 15:03:02 +00:00
Paul Kaplan 0c17cd956d Merge pull request #5399 from LLK/greenkeeper/scratch-l10n-3.7.20200128224436
Update scratch-l10n to the latest version 🚀
2020-01-29 08:46:15 -05:00
Paul Kaplan 287b29019a Merge pull request #5398 from LLK/greenkeeper/scratch-render-0.1.0-prerelease.20200128205403
Update scratch-render to the latest version 🚀
2020-01-29 06:02:37 -05:00
greenkeeper[bot] ed3b58bfb5 chore(package): update lockfile package-lock.json 2020-01-28 22:46:51 +00:00
greenkeeper[bot] c2cd6f21da chore(package): update scratch-l10n to version 3.7.20200128224436 2020-01-28 22:46:47 +00:00
greenkeeper[bot] 066e9dd6bd chore(package): update lockfile package-lock.json 2020-01-28 21:10:45 +00:00
greenkeeper[bot] 5a0860228e chore(package): update scratch-render to version 0.1.0-prerelease.20200128205403 2020-01-28 21:10:41 +00:00
Ben Wheeler f2291d963c continuing sb file uploader refactor into hoc
draft of refactor to make custom confirm modal work for upload; need to further edit sb-file-uploader

Add support for custom confirm modal to sb-file-uploader

Show custom confirm modal when confirming project upload

got custom upload refactor of file upload working, and tested failure case

cleaned up debug code, make project file upload work

simplified logic around canceling file upload

removed old sb-file-uploader.jsx; fixed linter errors

reverted two components to use old strings, not shared

(can do that refactor another time)

removed unnecessary confirm container

removed stray comment

moved, updated sb file uploader tests, related project state tests

removed custom modal, to reduce scope of changes

sb file uploader test uses intl
2020-01-28 14:57:09 -05:00
Benjamin Wheeler 97885c7518 Merge pull request #5198 from benjiwheeler/block-cloud-on-embed
simplify props of cloud-manager-hoc
2020-01-28 14:13:40 -05:00
Ben Wheeler a23dd3e349 shouldNotModifyCloudData into prop canModifyCloudData ; add defaultProps 2020-01-27 14:29:56 -05:00
Paul Kaplan 79f3f6f3f8 Merge pull request #5333 from LLK/paulkaplan-patch-2
Do not show unsupported browser modal for googlebot
2020-01-27 11:47:32 -05:00
greenkeeper[bot] 175395cce3 chore(package): update lockfile package-lock.json 2020-01-24 21:10:12 +00:00
greenkeeper[bot] ee67ddf56f chore(package): update mkdirp to version 1.0.3
Closes #5396
2020-01-24 21:10:09 +00:00
Ray Schamp 4b75bc8f9d Merge pull request #5388 from LLK/greenkeeper/scratch-l10n-3.7.20200114224412
Update scratch-l10n to the latest version 🚀
2020-01-22 16:51:33 -05:00
greenkeeper[bot] 8a4adccae5 chore(package): update lockfile package-lock.json 2020-01-16 00:35:15 +00:00
greenkeeper[bot] 9908d44d81 chore(package): update scratch-l10n to version 3.7.20200114224412 2020-01-16 00:35:10 +00:00
Paul Kaplan d2c74c5198 Merge pull request #5385 from paulkaplan/libraries-3-format
Update library-item to handle 3.0 or 2.0 library formats
2020-01-15 15:32:29 -05:00
Paul Kaplan 80e91a3c5e Update library-item to handle 3.0 or 2.0 library formats 2020-01-14 10:54:30 -05:00
DD Liu 797ce1894b Merge pull request #5384 from fsih/updateRender
Update with render hotfix
2020-01-13 15:37:55 -05:00
DD Liu f41e1c829a Update with render hotfix 2020-01-13 14:59:24 -05:00
Paul Kaplan fa5994abbd Merge pull request #5377 from paulkaplan/fix-long-sound
[fixup] Implement sound downsampler to try to allow more sounds to be edited
2020-01-09 12:19:22 -05:00
Paul Kaplan 76f5cfe1cf Correctly return the sampleRate from the sample dropper 2020-01-09 12:12:41 -05:00
Paul Kaplan fcbf1b051b Make submitNewSamples async and use promises 2020-01-09 12:12:41 -05:00
Paul Kaplan 1dcba6ecd5 Merge pull request #5376 from LLK/revert-5338-fix-long-sound
Revert "Implement sound downsampler to try to allow more sounds to be edited"
2020-01-09 11:47:45 -05:00
Paul Kaplan 79277a1227 Revert "Implement sound downsampler to try to allow more sounds to be edited" 2020-01-09 11:47:08 -05:00
DD Liu e476548c64 Trigger rebuild because my build flaked? 2020-01-09 03:18:32 -05:00
DD Liu 8adb097cd1 Merge pull request #5374 from fsih/updateEmptyPaintBug
Fix empty paint bug
2020-01-09 02:52:28 -05:00
DD Liu 403a7a5883 Update package lock 2020-01-09 02:43:24 -05:00
DD Liu dd5562323e Update svgrenderer deps and renderer 2020-01-09 02:42:34 -05:00
Benjamin Wheeler f261f4614a Merge pull request #5370 from LLK/greenkeeper/scratch-l10n-3.7.20200108141814
Greenkeeper/scratch l10n 3.7.20200108141814
2020-01-08 21:59:19 -05:00
greenkeeper[bot] a93248df31 chore(package): update scratch-l10n to version 3.7.20200108141814
Closes #5354
2020-01-08 21:58:42 -05:00
Benjamin Wheeler 1fa8742dec Merge pull request #5360 from LLK/greenkeeper/scratch-blocks-0.1.0-prerelease.1578322100
Update scratch-blocks to the latest version 🚀
2020-01-08 21:53:40 -05:00
Ben Wheeler 8b0f2c16bc updated package-lock 2020-01-08 21:34:23 -05:00
greenkeeper[bot] 57988d63c2 chore(package): update lockfile package-lock.json 2020-01-08 21:34:23 -05:00
greenkeeper[bot] c3d3a7a464 chore(package): update scratch-blocks to version 0.1.0-prerelease.1578322100 2020-01-08 21:34:23 -05:00
Paul Kaplan e1102f91ef Merge pull request #5369 from LLK/greenkeeper/scratch-paint-0.2.0-prerelease.20200107211248
Update scratch-paint to the latest version 🚀
2020-01-08 10:03:50 -05:00
greenkeeper[bot] e10a3f282f chore(package): update lockfile package-lock.json 2020-01-07 21:59:34 +00:00
greenkeeper[bot] 0ef59001f1 chore(package): update scratch-paint to version 0.2.0-prerelease.20200107211248 2020-01-07 21:59:30 +00:00
DD Liu ad96f2eb30 Merge pull request #5368 from fsih/updateSvgRend
Update SVG Renderer
2020-01-07 16:22:38 -05:00
DD Liu 5fac48acc9 updateSvgRend 2020-01-07 16:04:33 -05:00
DD Liu b19139696b Merge pull request #5367 from LLK/greenkeeper/scratch-paint-0.2.0-prerelease.20200107165446
Update scratch-paint to the latest version 🚀
2020-01-07 14:13:09 -05:00
DD Liu a5ccbfb8b9 Merge branch 'develop' into greenkeeper/scratch-paint-0.2.0-prerelease.20200107165446 2020-01-07 14:12:50 -05:00
DD Liu 7960eed191 Merge pull request #5366 from LLK/greenkeeper/scratch-render-0.1.0-prerelease.20200107165342
Update scratch-render to the latest version 🚀
2020-01-07 14:11:58 -05:00
greenkeeper[bot] cb0e24b5be chore(package): update lockfile package-lock.json 2020-01-07 17:01:55 +00:00
greenkeeper[bot] ccc12a02c3 chore(package): update scratch-paint to version 0.2.0-prerelease.20200107165446 2020-01-07 17:01:50 +00:00
greenkeeper[bot] d505af579a chore(package): update lockfile package-lock.json 2020-01-07 17:01:23 +00:00
greenkeeper[bot] 504097b495 chore(package): update scratch-render to version 0.1.0-prerelease.20200107165342 2020-01-07 17:01:19 +00:00
DD Liu 484eb6bc9f Merge pull request #5358 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20200103191258
Update scratch-svg-renderer to the latest version 🚀
2020-01-07 11:48:59 -05:00
Benjamin Wheeler 96392659fd Merge pull request #5291 from benjiwheeler/standalone-new-project-title-fix
reset title on transitioning to isShowingWithoutId
2020-01-06 15:27:38 -05:00
greenkeeper[bot] 2c16a16da5 chore(package): update lockfile package-lock.json 2020-01-03 19:18:55 +00:00
greenkeeper[bot] ed0db53f36 chore(package): update scratch-svg-renderer to version 0.2.0-prerelease.20200103191258 2020-01-03 19:18:50 +00:00
Ben Wheeler f244da7705 added project-state integration test for new title 2020-01-02 14:29:04 -05:00
Benjamin Wheeler 3d1205e453 Merge pull request #5357 from LLK/greenkeeper/papaparse-5.1.1
Greenkeeper/papaparse 5.1.1
2019-12-30 23:18:50 -05:00
Ben Wheeler 3b1889b941 updated package lock 2019-12-30 23:17:43 -05:00
Paul Kaplan a908345491 Merge pull request #5338 from paulkaplan/fix-long-sound
Implement sound downsampler to try to allow more sounds to be edited
2019-12-30 14:22:34 -05:00
Karishma Chadha 50de01086a Merge pull request #5355 from LLK/greenkeeper/scratch-vm-0.2.0-prerelease.20191227164934
Update scratch-vm to the latest version 🚀
2019-12-27 14:34:16 -05:00
greenkeeper[bot] 57863d99a1 chore(package): update lockfile package-lock.json 2019-12-27 16:52:17 +00:00
greenkeeper[bot] 3e93db7c0b chore(package): update scratch-vm to version 0.2.0-prerelease.20191227164934 2019-12-27 16:52:13 +00:00
Benjamin Wheeler 28f4cc737d Merge pull request #5343 from taybenlor/develop
Fix `doneUpdatingProject` call signature
2019-12-24 21:45:26 -05:00
Paul Kaplan d1d98337c8 Update docs and tests 2019-12-19 16:01:21 -05:00
Eric Rosenbaum bb80fa49ac Move backupDownSampler into util and add tests 2019-12-18 15:19:47 -05:00
greenkeeper[bot] 38e4c5bbfa chore(package): update lockfile package-lock.json 2019-12-18 11:54:33 +00:00
greenkeeper[bot] b3e5dd88fb chore(package): update papaparse to version 5.1.1
Closes #5018
2019-12-18 11:54:30 +00:00
Ben Taylor 6027bb9011 Fix doneUpdatingProject call signature
ProjectSaverHOC has been using the wrong call signature for `doneUpdatingProject` and `onUpdatedProject`.
2019-12-18 09:59:07 +11:00
Eric Rosenbaum e799708707 Use for loop instead of filter (20x faster) 2019-12-16 17:05:29 -05:00
Eric Rosenbaum b699e76972 Use backup down sampler when 22kHz not supported 2019-12-16 13:49:39 -05:00
Paul Kaplan 914ba4910b Use vendor prefixed offline audio context for safari 2019-12-12 15:57:25 -05:00
Paul Kaplan 748f8559e4 Remove the 11khz option because it doesnt work on safari 2019-12-12 15:56:59 -05:00
Paul Kaplan 8fae109811 Add downsampler 2019-12-12 12:30:12 -05:00
Paul Kaplan 7980c23ecc Make submitNewSamples async and use promises 2019-12-12 11:44:15 -05:00
Paul Kaplan 9638444a49 Do not show unsupported browser modal for googlebot
This modal may be preventing the crawler from correctly indexing pages, because it takes over the whole screen.
2019-12-10 06:47:41 -05:00
Ben Wheeler 21d6ce111e added comments 2019-11-12 15:25:07 -05:00
Ben Wheeler fcb6de39f3 more precise case for resetting project title to default 2019-11-12 15:22:40 -05:00
Ben Wheeler 3c6e9723e3 reset title on transitioning to isShowingWithoutId 2019-11-12 15:04:55 -05:00
19 changed files with 651 additions and 217 deletions

No files matched your search

+1 -1
View File
@@ -14,6 +14,6 @@ npm-*
/dist
/.opt-in
# generated translation files
# Generated translation files
/translations
/locale
+56 -1
View File
@@ -74,7 +74,7 @@ See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) fo
*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.*
Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level.
Before running any tests, make sure you have run `npm install` from this (scratch-gui) repository's top level.
#### Main testing command
@@ -194,5 +194,60 @@ Further reading: [Stack Overflow](https://stackoverflow.com/questions/46602286/n
You can publish the GUI to github.io so that others on the Internet can view it.
[Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages)
## Understanding the project state machine
Since so much code throughout scratch-gui depends on the state of the project, which goes through many different phases of loading, displaying and saving, we created a "finite state machine" to make it clear which state it is in at any moment. This is contained in the file src/reducers/project-state.js .
It can be hard to understand the code in src/reducers/project-state.js . There are several types of data and functions used, which relate to each other:
### Loading states
These include state constant strings like:
* `NOT_LOADED` (the default state),
* `ERROR`,
* `FETCHING_WITH_ID`,
* `LOADING_VM_WITH_ID`,
* `REMIXING`,
* `SHOWING_WITH_ID`,
* `SHOWING_WITHOUT_ID`,
* etc.
### Transitions
These are names for the action which causes a state change. Some examples are:
* `START_FETCHING_NEW`,
* `DONE_FETCHING_WITH_ID`,
* `DONE_LOADING_VM_WITH_ID`,
* `SET_PROJECT_ID`,
* `START_AUTO_UPDATING`,
### How transitions relate to loading states
As this diagram of the project state machine shows, various transition actions can move us from one loading state to another:
![Project state diagram](docs/project_state_diagram.svg)
_Note: for clarity, the diagram above excludes states and transitions relating to error handling._
#### Example
Here's an example of how states transition.
Suppose a user clicks on a project, and the page starts to load with url https://scratch.mit.edu/projects/123456 .
Here's what will happen in the project state machine:
![Project state example](docs/project_state_example.png)
1. When the app first mounts, the project state is `NOT_LOADED`.
2. The `SET_PROJECT_ID` redux action is dispatched (from src/lib/project-fetcher-hoc.jsx), with `projectId` set to `123456`. This transitions the state from `NOT_LOADED` to `FETCHING_WITH_ID`.
3. The `FETCHING_WITH_ID` state. In src/lib/project-fetcher-hoc.jsx, the `projectId` value `123456` is used to request the data for that project from the server.
4. When the server responds with the data, src/lib/project-fetcher-hoc.jsx dispatches the `DONE_FETCHING_WITH_ID` action, with `projectData` set. This transitions the state from `FETCHING_WITH_ID` to `LOADING_VM_WITH_ID`.
5. The `LOADING_VM_WITH_ID` state. In src/lib/vm-manager-hoc.jsx, we load the `projectData` into Scratch's virtual machine ("the vm").
6. When loading is done, src/lib/vm-manager-hoc.jsx dispatches the `DONE_LOADING_VM_WITH_ID` action. This transitions the state from `LOADING_VM_WITH_ID` to `SHOWING_WITH_ID`
7. The `SHOWING_WITH_ID` state. Now the project appears normally and is playable and editable.
## Donate
We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1381px" height="1012px" viewBox="-0.5 -0.5 1381 1012" content="&lt;mxfile host=&quot;www.draw.io&quot; modified=&quot;2020-02-04T16:31:44.889Z&quot; agent=&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36&quot; etag=&quot;8a2X5Jj65jOGG04xBBh1&quot; version=&quot;12.6.4&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;58cdce13-f638-feb5-8d6f-7d28b1aa9fa0&quot;&gt;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 truncated

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+265 -43
View File
@@ -20,6 +20,17 @@
"output-file-sync": "^2.0.0",
"slash": "^2.0.0",
"source-map": "^0.5.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"@babel/code-frame": {
@@ -937,6 +948,12 @@
"integrity": "sha512-8DvLPFbipP7O8fii6xItaA041fdW4pdr63m34xlSs15sYP5fX9yiZiO3G44qk5eBPbFNQTG7uxMd5Jo+cVS5Ow==",
"dev": true
},
"@testim/chrome-version": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.0.7.tgz",
"integrity": "sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw==",
"dev": true
},
"@types/babel__core": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz",
@@ -1727,7 +1744,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@@ -1826,6 +1843,17 @@
"loader-utils": "^1.0.2",
"mkdirp": "^0.5.1",
"pify": "^4.0.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"babel-messages": {
@@ -1932,7 +1960,7 @@
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
"integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
"dev": true
},
@@ -1994,6 +2022,15 @@
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@@ -2532,6 +2569,17 @@
"ssri": "^5.2.4",
"unique-filename": "^1.1.0",
"y18n": "^4.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"cache-base": {
@@ -2699,16 +2747,28 @@
}
},
"chromedriver": {
"version": "78.0.1",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-78.0.1.tgz",
"integrity": "sha512-eOsyFk4xb9EECs1VMrDbxO713qN+Bu1XUE8K9AuePc3839TPdAegg72kpXSzkeNqRNZiHbnJUItIVCLFkDqceA==",
"version": "80.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-80.0.0.tgz",
"integrity": "sha512-W4tIbaOve7HeGFLnbbZMV4AUlnBaapL+H41fvDFKOXCmUvgPhxVN9y/c3EgmsOcokLQkqxpOC/txEujms1eT0w==",
"dev": true,
"requires": {
"@testim/chrome-version": "^1.0.7",
"del": "^4.1.1",
"extract-zip": "^1.6.7",
"mkdirp": "^0.5.1",
"request": "^2.88.0",
"tcp-port-used": "^1.0.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"ci-info": {
@@ -3035,6 +3095,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.0"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"copy-descriptor": {
@@ -4209,6 +4280,15 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -4866,6 +4946,17 @@
"debug": "2.6.9",
"mkdirp": "0.5.1",
"yauzl": "2.4.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"extsprintf": {
@@ -7405,6 +7496,15 @@
"requires": {
"lodash": "^4.17.14"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
@@ -7456,6 +7556,15 @@
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"supports-color": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
@@ -7489,6 +7598,15 @@
"ms": "^2.1.1"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -7932,6 +8050,15 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"pretty-format": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz",
@@ -8325,6 +8452,17 @@
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
"pretty-format": "^21.2.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"jest-util": {
@@ -8347,6 +8485,15 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
@@ -9132,13 +9279,10 @@
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz",
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
"dev": true
},
"mkpath": {
"version": "1.0.0",
@@ -9211,6 +9355,15 @@
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"supports-color": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
@@ -9240,6 +9393,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"ms": {
@@ -9804,6 +9968,17 @@
"graceful-fs": "^4.1.11",
"is-plain-obj": "^1.1.0",
"mkdirp": "^0.5.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"p-cancelable": {
@@ -9876,9 +10051,9 @@
"dev": true
},
"papaparse": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.6.2.tgz",
"integrity": "sha512-P/4p6S6wZyXAjcFPnJAjFn7lxMIkd+23prFELhcbfWswQfg7dG1XJfV8/lKvk5lu5BGSSJZFHSDMejjFcB1feg==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.1.1.tgz",
"integrity": "sha512-KPkW4GNQxunmYTeJIjHFrvilcNuHBWrfgbyvmagEmfGOA4hnP1WIkPbv4yABhj1Nam3as4w+7MBiI27BntwqVg==",
"dev": true
},
"parallel-transform": {
@@ -10170,6 +10345,15 @@
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
@@ -11051,7 +11235,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@@ -11524,9 +11708,9 @@
}
},
"scratch-blocks": {
"version": "0.1.0-prerelease.1576850350",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1576850350.tgz",
"integrity": "sha512-P17eUhKZ6RJqr/3BJwx5tSrYPDZlM9azt3mD5kInMfEknkcmYuBeyEffj1/Gl6T/KoWPDNdR+CHuy6FVQtMzmA==",
"version": "0.1.0-prerelease.1580911107",
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1580911107.tgz",
"integrity": "sha512-2+bpWEL/2N569nGVPT4nZ5l9lwfezhsImJmw7XQAsJxzvRt9Ou9ZnkFAJN1trTZq/8krYoqjkXOoSe6gmbXW+g==",
"dev": true,
"requires": {
"exports-loader": "0.6.3",
@@ -11534,9 +11718,9 @@
}
},
"scratch-l10n": {
"version": "3.7.20191219145348",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.7.20191219145348.tgz",
"integrity": "sha512-qfduPyCT0u6i7dURBLVGZw+jVQsz7tevRqovY85DsBe3A2IJZR3HlWkncDlLTVUGYQBY65EyO3CHUIS8Nex3MA==",
"version": "3.7.20200205213636",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.7.20200205213636.tgz",
"integrity": "sha512-e06mKQ1b2rVEjQPX84e872+4/1p8lLD9ofXZXI/Tti7KPeGilIiJmrWCfjbVtXPk7dOTYaOb55Ke0c0rdR8K+w==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@@ -11546,9 +11730,9 @@
}
},
"scratch-paint": {
"version": "0.2.0-prerelease.20191217213717",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20191217213717.tgz",
"integrity": "sha512-TPEYgt7nDEbVnmA/DiiasuYgmGUyrGizhMLzgwx2W8NB8RlEV/PVHbQAsZKoRu+dj8F8bPpz/PYpQOXBUJjNuA==",
"version": "0.2.0-prerelease.20200204235639",
"resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-0.2.0-prerelease.20200204235639.tgz",
"integrity": "sha512-RMBvNrdmo/xgDuP3speGx0WfsYEttmeKI4zu9HiAYHPwRDtKOtRghnNxxFCrcpzG/qOEIV9m2eshVyGj8W9d0g==",
"dev": true,
"requires": {
"@scratch/paper": "0.11.20190729152410",
@@ -11559,7 +11743,7 @@
"minilog": "3.1.0",
"parse-color": "1.0.0",
"prop-types": "^15.5.10",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338"
"scratch-svg-renderer": "0.2.0-prerelease.20200109070519"
},
"dependencies": {
"classnames": {
@@ -11661,9 +11845,9 @@
}
},
"scratch-render": {
"version": "0.1.0-prerelease.20191217212645",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20191217212645.tgz",
"integrity": "sha512-j7NF/lgeWE09cnQzVW8uW28kUZ/QstWTM4slM3Rx8Uuip6tGb8N84Yj/8mMKU+DRXU1bT3yIh8JRSuguzd3Tvg==",
"version": "0.1.0-prerelease.20200128205403",
"resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20200128205403.tgz",
"integrity": "sha512-Lj2oLSFLImc8v+w6crVRq2IzmpE0Ag5B+ED4vIqkILa8euiB56av3bZTOuft2wWYCq45CzxNvXjsxhkkknVeAw==",
"dev": true,
"requires": {
"grapheme-breaker": "0.3.2",
@@ -11673,7 +11857,7 @@
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
"scratch-svg-renderer": "0.2.0-prerelease.20200109070519",
"twgl.js": "4.4.0"
}
},
@@ -11721,16 +11905,16 @@
}
},
"scratch-svg-renderer": {
"version": "0.2.0-prerelease.20191217211338",
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20191217211338.tgz",
"integrity": "sha512-38L23abfi8QOMbPc0DOdLYRvEi19iSI7/5xf9geblONSNF6RYrENSTYvmVw9mOxHYj8FSODKwLhkp5OD6gpekg==",
"version": "0.2.0-prerelease.20200109070519",
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20200109070519.tgz",
"integrity": "sha512-THdZW9gA8mFKvDImHDrWWhvyOymUW3JgMBjpPsaFk2URl1kfNIRQIndfwto2B8a8YbtuyKSv6CWW8Nu+VzBZVA==",
"dev": true,
"requires": {
"base64-js": "1.2.1",
"base64-loader": "1.0.0",
"minilog": "3.1.0",
"scratch-render-fonts": "1.0.0-prerelease.20180906193204",
"transformation-matrix": "1.14.1"
"transformation-matrix": "1.15.0"
},
"dependencies": {
"base64-js": {
@@ -11748,9 +11932,9 @@
"dev": true
},
"scratch-vm": {
"version": "0.2.0-prerelease.20191119203901",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20191119203901.tgz",
"integrity": "sha512-og+u9XHgCN9HFK14RVcOWawFMexnfVh7fLVRq36DSidUQkUPhS4Ls4V9qGCqOAV3WIlI0EXoXy6KdLfieVX+SQ==",
"version": "0.2.0-prerelease.20191227164934",
"resolved": "https://registry.npmjs.org/scratch-vm/-/scratch-vm-0.2.0-prerelease.20191227164934.tgz",
"integrity": "sha512-loaXhKW4pcY5f/+ARXFAsC0LYC22R/QsTTFxLdfhiWDnbO/ZRYQ4FmAlSzxCx0d2VPhI3uu+um5WkBtPinv2jQ==",
"dev": true,
"requires": {
"@vernier/godirect": "1.5.0",
@@ -12765,7 +12949,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
@@ -12999,6 +13183,15 @@
"through2": "^2.0.0"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -13244,7 +13437,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
@@ -13397,9 +13590,9 @@
"dev": true
},
"transformation-matrix": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-1.14.1.tgz",
"integrity": "sha512-zbfndGg//cMW4aytXuF4dkBuUu/SUBA94U2zkmDoRwcplS9hCCn/S3VDp4A7CXw5tVxeCm9F6zdfHzWzei6nXg==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-1.15.0.tgz",
"integrity": "sha512-808EqPQbmUD6/IMpWUXLOZcblCHf9xaiB+un0RYNNE9+6VRjoiw6Be8R32tZ0ips1PX/15tlnA2Ev4UUgg827Q==",
"dev": true
},
"transifex": {
@@ -14079,6 +14272,15 @@
"readable-stream": "^2.0.1"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -14344,6 +14546,15 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
@@ -14702,7 +14913,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
@@ -14745,6 +14956,17 @@
"dev": true,
"requires": {
"mkdirp": "^0.5.1"
},
"dependencies": {
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
}
}
},
"write-file-atomic": {
+9 -9
View File
@@ -44,7 +44,7 @@
"babel-loader": "^8.0.4",
"base64-loader": "1.0.0",
"bowser": "1.9.4",
"chromedriver": "78.0.1",
"chromedriver": "80.0.0",
"classnames": "2.2.6",
"computed-style-to-inline-style": "3.0.0",
"copy-webpack-plugin": "^4.5.1",
@@ -78,9 +78,9 @@
"lodash.pick": "4.4.0",
"lodash.throttle": "4.0.1",
"minilog": "3.1.0",
"mkdirp": "^0.5.1",
"mkdirp": "^1.0.3",
"omggif": "1.0.9",
"papaparse": "4.6.2",
"papaparse": "5.1.1",
"postcss-import": "^12.0.0",
"postcss-loader": "^3.0.0",
"postcss-simple-vars": "^5.0.1",
@@ -108,13 +108,13 @@
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190925183642",
"scratch-l10n": "3.7.20191219145348",
"scratch-blocks": "0.1.0-prerelease.1576850350",
"scratch-paint": "0.2.0-prerelease.20191217213717",
"scratch-render": "0.1.0-prerelease.20191217212645",
"scratch-l10n": "3.7.20200205213636",
"scratch-blocks": "0.1.0-prerelease.1580911107",
"scratch-paint": "0.2.0-prerelease.20200204235639",
"scratch-render": "0.1.0-prerelease.20200128205403",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
"scratch-vm": "0.2.0-prerelease.20191119203901",
"scratch-svg-renderer": "0.2.0-prerelease.20200109070519",
"scratch-vm": "0.2.0-prerelease.20191227164934",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+9 -6
View File
@@ -92,14 +92,16 @@ class LibraryItem extends React.PureComponent {
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
const iconMd5Prop = this.props.iconMd5;
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex] &&
this.props.icons[this.state.iconIndex].baseLayerMD5) {
return this.props.icons[this.state.iconIndex].baseLayerMD5;
this.state.iconIndex < this.props.icons.length) {
const icon = this.props.icons[this.state.iconIndex] || {};
return icon.md5ext || // 3.0 library format
icon.baseLayerMD5 || // 2.0 library format, TODO GH-5084
iconMd5Prop;
}
return this.props.iconMd5;
return iconMd5Prop;
}
render () {
const iconMd5 = this.curIconMd5();
@@ -151,7 +153,8 @@ LibraryItem.propTypes = {
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string
baseLayerMD5: PropTypes.string, // 2.0 library format, TODO GH-5084
md5ext: PropTypes.string // 3.0 library format
})
),
id: PropTypes.number.isRequired,
+109 -84
View File
@@ -6,7 +6,12 @@ import VM from 'scratch-vm';
import {connect} from 'react-redux';
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
import {
computeChunkedRMS,
encodeAndAddSoundToVM,
downsampleIfNeeded,
dropEveryOtherSample
} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -39,7 +44,8 @@ class SoundEditor extends React.Component {
'paste',
'handleKeyPress',
'handleContainerClick',
'setRef'
'setRef',
'resampleBufferToRate'
]);
this.state = {
copyBuffer: null,
@@ -132,45 +138,32 @@ class SoundEditor extends React.Component {
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
// Encode the new sound into a wav so that it can be stored
let wavBuffer = null;
try {
wavBuffer = WavEncoder.encode.sync({
sampleRate: sampleRate,
channelData: [samples]
return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
WavEncoder.encode({
sampleRate: newSampleRate,
channelData: [newSamples]
}).then(wavBuffer => {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.getUndoItem());
}
this.resetState(newSamples, newSampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
new Uint8Array(wavBuffer));
return true; // Edit was successful
})
)
.catch(e => {
// Encoding failed, or the sound was too large to save so edit is rejected
log.error(`Encountered error while trying to encode sound update: ${e}`);
return false; // Edit was not applied
});
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
// Cancel the sound update by setting to null
wavBuffer = null;
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
}
} catch (e) {
// This error state is mostly for the mock sounds used during testing.
// Any incorrect sound buffer trying to get interpretd as a Wav file
// should yield this error.
// This can also happen if the sound is too be allocated in memory.
log.error(`Encountered error while trying to encode sound update: ${e}`);
}
// Do not submit sound if it could not be encoded (i.e. if too large)
if (wavBuffer) {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.getUndoItem());
}
this.resetState(samples, sampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
new Uint8Array(wavBuffer));
return true; // Update succeeded
}
return false; // Update failed
}
handlePlay () {
this.audioBufferPlayer.stop();
@@ -209,13 +202,15 @@ class SoundEditor extends React.Component {
newSamples.set(firstPart, 0);
newSamples.set(secondPart, firstPart.length);
}
this.submitNewSamples(newSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
this.submitNewSamples(newSamples, sampleRate).then(() => {
this.setState({
trimStart: null,
trimEnd: null
});
});
}
handleDeleteInverse () {
// Delete everything outside of the trimmers
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
@@ -224,10 +219,13 @@ class SoundEditor extends React.Component {
if (clippedSamples.length === 0) {
clippedSamples = new Float32Array(1);
}
this.submitNewSamples(clippedSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
this.submitNewSamples(clippedSamples, sampleRate).then(success => {
if (success) {
this.setState({
trimStart: null,
trimEnd: null
});
}
});
}
handleUpdateTrim (trimStart, trimEnd) {
@@ -257,14 +255,15 @@ class SoundEditor extends React.Component {
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
const samples = renderedBuffer.getChannelData(0);
const sampleRate = renderedBuffer.sampleRate;
const success = this.submitNewSamples(samples, sampleRate);
if (success) {
if (this.state.trimStart === null) {
this.handlePlay();
} else {
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
this.submitNewSamples(samples, sampleRate).then(success => {
if (success) {
if (this.state.trimStart === null) {
this.handlePlay();
} else {
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
}
}
}
});
});
}
tooLoud () {
@@ -287,16 +286,22 @@ class SoundEditor extends React.Component {
this.redoStack.push(this.getUndoItem());
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
if (samples) {
this.submitNewSamples(samples, sampleRate, true);
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
return this.submitNewSamples(samples, sampleRate, true).then(success => {
if (success) {
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
});
}
}
handleRedo () {
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
if (samples) {
this.undoStack.push(this.getUndoItem());
this.submitNewSamples(samples, sampleRate, true);
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
return this.submitNewSamples(samples, sampleRate, true).then(success => {
if (success) {
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
});
}
}
handleCopy () {
@@ -322,25 +327,39 @@ class SoundEditor extends React.Component {
});
}
resampleBufferToRate (buffer, newRate) {
return new Promise(resolve => {
if (window.OfflineAudioContext) {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
return new Promise((resolve, reject) => {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
let offlineContext;
// Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
// The constructors will throw if trying to resample at an unsupported rate
// (e.g. Safari/webkitOAC does not support lower than 44khz).
try {
if (window.OfflineAudioContext) {
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
} else if (window.webkitOfflineAudioContext) {
offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
}
} catch {
// If no OAC available and downsampling by 2, downsample by dropping every other sample.
if (newRate === buffer.sampleRate / 2) {
return resolve(dropEveryOtherSample(buffer));
}
return reject('Could not resample');
}
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
});
}
paste () {
@@ -351,8 +370,11 @@ class SoundEditor extends React.Component {
const newSamples = new Float32Array(newLength);
newSamples.set(samples, 0);
newSamples.set(this.state.copyBuffer.samples, samples.length);
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.handlePlay();
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
if (success) {
this.handlePlay();
}
});
} else {
// else replace the selection with the pasted sound
const trimStartSamples = this.state.trimStart * samples.length;
@@ -371,11 +393,14 @@ class SoundEditor extends React.Component {
const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.setState({
trimStart: adjustedTrimStart,
trimEnd: adjustedTrimEnd
}, this.handlePlay);
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
if (success) {
this.setState({
trimStart: adjustedTrimStart,
trimEnd: adjustedTrimEnd
}, this.handlePlay);
}
});
}
}
handlePaste () {
+49 -1
View File
@@ -59,9 +59,57 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
});
};
/**
@typedef SoundBuffer
@type {Object}
@property {Float32Array} samples Array of audio samples
@property {number} sampleRate Audio sample rate
*/
/**
* Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
* @param {SoundBuffer} buffer - Buffer to resample
* @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const downsampleIfNeeded = (buffer, resampler) => {
const {samples, sampleRate} = buffer;
const duration = samples.length / sampleRate;
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
// Resolve immediately if already within byte limit
if (encodedByteLength < SOUND_BYTE_LIMIT) {
return Promise.resolve({samples, sampleRate});
}
// If encodeable at 22khz, resample and call submitNewSamples again
if (duration * 22050 * 2 < SOUND_BYTE_LIMIT) {
return resampler({samples, sampleRate}, 22050);
}
// Cannot save this sound at 22khz, refuse to edit
// In the future we could introduce further compression here
return Promise.reject('Sound too large to save, refusing to edit');
};
/**
* Drop every other sample of an audio buffer as a last-resort way of downsampling.
* @param {SoundBuffer} buffer - Buffer to resample
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const dropEveryOtherSample = buffer => {
const newLength = Math.floor(buffer.samples.length / 2);
const newSamples = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
newSamples[i] = buffer.samples[i * 2];
}
return {
samples: newSamples,
sampleRate: buffer.sampleRate / 2
};
};
export {
computeRMS,
computeChunkedRMS,
encodeAndAddSoundToVM,
SOUND_BYTE_LIMIT
downsampleIfNeeded,
dropEveryOtherSample
};
+16 -12
View File
@@ -54,13 +54,10 @@ const cloudManagerHOC = function (WrappedComponent) {
canUseCloud (props) {
return !!(props.cloudHost && props.username && props.vm && props.projectId && props.hasCloudPermission);
}
shouldNotModifyCloudData (props) {
return (props.hasEverEnteredEditor && !props.canSave);
}
shouldConnect (props) {
return !this.isConnected() && this.canUseCloud(props) &&
props.isShowingWithId && props.vm.runtime.hasCloudData() &&
!this.shouldNotModifyCloudData(props);
props.canModifyCloudData;
}
shouldDisconnect (props, prevProps) {
return this.isConnected() &&
@@ -70,7 +67,7 @@ const cloudManagerHOC = function (WrappedComponent) {
(props.projectId !== prevProps.projectId) ||
(props.username !== prevProps.username) ||
// Editing someone else's project
this.shouldNotModifyCloudData(props)
!props.canModifyCloudData
);
}
isConnected () {
@@ -102,11 +99,11 @@ const cloudManagerHOC = function (WrappedComponent) {
render () {
const {
/* eslint-disable no-unused-vars */
canModifyCloudData,
cloudHost,
projectId,
username,
hasCloudPermission,
hasEverEnteredEditor,
isShowingWithId,
onShowCloudInfo,
/* eslint-enable no-unused-vars */
@@ -124,23 +121,30 @@ const cloudManagerHOC = function (WrappedComponent) {
}
CloudManager.propTypes = {
canSave: PropTypes.bool.isRequired,
canModifyCloudData: PropTypes.bool.isRequired,
cloudHost: PropTypes.string,
hasCloudPermission: PropTypes.bool,
hasEverEnteredEditor: PropTypes.bool,
isShowingWithId: PropTypes.bool,
isShowingWithId: PropTypes.bool.isRequired,
onShowCloudInfo: PropTypes.func,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
username: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = state => {
CloudManager.defaultProps = {
cloudHost: null,
hasCloudPermission: false,
onShowCloudInfo: () => {},
username: null
};
const mapStateToProps = (state, ownProps) => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
hasEverEnteredEditor: state.scratchGui.mode.hasEverEnteredEditor,
isShowingWithId: getIsShowingWithId(loadingState),
projectId: state.scratchGui.projectState.projectId
projectId: state.scratchGui.projectState.projectId,
// if you're editing someone else's project, you can't modify cloud data
canModifyCloudData: (!state.scratchGui.mode.hasEverEnteredEditor || ownProps.canSave)
};
};
+1 -1
View File
@@ -433,7 +433,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
onShowCreatingRemixAlert: () => showAlertWithTimeout(dispatch, 'creatingRemix'),
onShowSaveSuccessAlert: () => showAlertWithTimeout(dispatch, 'saveSuccess'),
onShowSavingAlert: () => showAlertWithTimeout(dispatch, 'saving'),
onUpdatedProject: (projectId, loadingState) => dispatch(doneUpdatingProject(projectId, loadingState)),
onUpdatedProject: loadingState => dispatch(doneUpdatingProject(loadingState)),
setAutoSaveTimeoutId: id => dispatch(setAutoSaveTimeoutId(id))
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
+3 -1
View File
@@ -26,7 +26,9 @@ const supportedBrowser = () => {
* always returns false
*/
const recommendedBrowser = () => !bowser.isUnsupportedBrowser(minVersions, true);
const recommendedBrowser = () =>
!bowser.isUnsupportedBrowser(minVersions, true) ||
window.navigator.userAgent.toLowerCase().indexOf('googlebot') !== -1;
export {
supportedBrowser as default,
+14 -1
View File
@@ -3,7 +3,10 @@ import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {getIsShowingWithoutId} from '../reducers/project-state';
import {
getIsAnyCreatingNewState,
getIsShowingWithoutId
} from '../reducers/project-state';
import {setProjectTitle} from '../reducers/project-title';
const messages = defineMessages({
@@ -27,6 +30,12 @@ const TitledHOC = function (WrappedComponent) {
if (this.props.projectTitle !== prevProps.projectTitle) {
this.handleReceivedProjectTitle(this.props.projectTitle);
}
// if project is a new default project, and has loaded,
if (this.props.isShowingWithoutId && prevProps.isAnyCreatingNewState) {
// reset title to default
const defaultProjectTitle = this.handleReceivedProjectTitle();
this.props.onUpdateProjectTitle(defaultProjectTitle);
}
// if the projectTitle hasn't changed, but the reduxProjectTitle
// HAS changed, we need to report that change to the projectTitle's owner
if (this.props.reduxProjectTitle !== prevProps.reduxProjectTitle &&
@@ -40,11 +49,13 @@ const TitledHOC = function (WrappedComponent) {
newTitle = this.props.intl.formatMessage(messages.defaultProjectTitle);
}
this.props.onChangedProjectTitle(newTitle);
return newTitle;
}
render () {
const {
/* eslint-disable no-unused-vars */
intl,
isAnyCreatingNewState,
isShowingWithoutId,
onChangedProjectTitle,
// for children, we replace onUpdateProjectTitle with our own
@@ -66,6 +77,7 @@ const TitledHOC = function (WrappedComponent) {
TitledComponent.propTypes = {
intl: intlShape,
isAnyCreatingNewState: PropTypes.bool,
isShowingWithoutId: PropTypes.bool,
onChangedProjectTitle: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
@@ -80,6 +92,7 @@ const TitledHOC = function (WrappedComponent) {
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
isAnyCreatingNewState: getIsAnyCreatingNewState(loadingState),
isShowingWithoutId: getIsShowingWithoutId(loadingState),
reduxProjectTitle: state.scratchGui.projectTitle
};
+4 -1
View File
@@ -14,7 +14,10 @@ export default class MockAudioEffects {
this.buffer = buffer;
this.name = name;
this.process = jest.fn(done => {
this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1);
this._finishProcessing = renderedBuffer => {
done(renderedBuffer, 0, 1);
return new Promise(resolve => setTimeout(resolve));
};
});
MockAudioEffects.instance = this;
}
+2
View File
@@ -28,6 +28,8 @@ class SeleniumHelper {
'loadUri',
'rightClickText'
]);
this.Key = webdriver.Key; // map Key constants, for sending special keys
}
elementIsVisible (element, timeoutMessage = 'elementIsVisible timed out') {
+45
View File
@@ -0,0 +1,45 @@
import path from 'path';
import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByXpath,
getDriver,
Key,
loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
let driver;
describe('Project state', () => {
beforeAll(() => {
driver = getDriver();
});
afterAll(async () => {
await driver.quit();
});
test('File->New resets project title', async () => {
const defaultProjectTitle = 'Scratch Project';
await loadUri(uri);
const inputEl = await findByXpath(`//input[@value="${defaultProjectTitle}"]`);
for (let i = 0; i < defaultProjectTitle.length; i++) {
inputEl.sendKeys(Key.BACK_SPACE);
}
inputEl.sendKeys('Changed title of project');
await clickText('Costumes'); // just to blur the input
// verify that project title has changed
await clickXpath('//input[@value="Changed title of project"]');
await clickXpath(
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
);
await clickXpath('//li[span[text()="New"]]');
// project title should be default again
await clickXpath(`//input[@value="${defaultProjectTitle}"]`);
});
});
+12 -12
View File
@@ -97,7 +97,7 @@ describe('Sound Editor Container', () => {
expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
});
test('it handles an effect by submitting the result and playing', () => {
test('it handles an effect by submitting the result and playing', async () => {
const wrapper = mountWithIntl(
<SoundEditor
soundIndex={soundIndex}
@@ -106,7 +106,7 @@ describe('Sound Editor Container', () => {
);
const component = wrapper.find(SoundEditorComponent);
component.props().onReverse(); // Could be any of the effects, just testing the end result
mockAudioEffects.instance._finishProcessing(soundBuffer);
await mockAudioEffects.instance._finishProcessing(soundBuffer);
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
expect(vm.updateSoundBuffer).toHaveBeenCalled();
});
@@ -202,7 +202,7 @@ describe('Sound Editor Container', () => {
expect(mockAudioEffects.instance.process).toHaveBeenCalled();
});
test('undo/redo stack state', () => {
test('undo/redo stack state', async () => {
const wrapper = mountWithIntl(
<SoundEditor
soundIndex={soundIndex}
@@ -216,40 +216,40 @@ describe('Sound Editor Container', () => {
// Submitting new samples should make it possible to undo
component.props().onFaster();
mockAudioEffects.instance._finishProcessing(soundBuffer);
await mockAudioEffects.instance._finishProcessing(soundBuffer);
wrapper.update();
component = wrapper.find(SoundEditorComponent);
expect(component.prop('canUndo')).toEqual(true);
expect(component.prop('canRedo')).toEqual(false);
// Undoing should make it possible to redo and not possible to undo again
component.props().onUndo();
await component.props().onUndo();
wrapper.update();
component = wrapper.find(SoundEditorComponent);
expect(component.prop('canUndo')).toEqual(false);
expect(component.prop('canRedo')).toEqual(true);
// Redoing should make it possible to undo and not possible to redo again
component.props().onRedo();
await component.props().onRedo();
wrapper.update();
component = wrapper.find(SoundEditorComponent);
expect(component.prop('canUndo')).toEqual(true);
expect(component.prop('canRedo')).toEqual(false);
// New submission should clear the redo stack
component.props().onUndo(); // Undo to go back to a state where redo is enabled
await component.props().onUndo(); // Undo to go back to a state where redo is enabled
wrapper.update();
component = wrapper.find(SoundEditorComponent);
expect(component.prop('canRedo')).toEqual(true);
component.props().onFaster();
mockAudioEffects.instance._finishProcessing(soundBuffer);
await mockAudioEffects.instance._finishProcessing(soundBuffer);
wrapper.update();
component = wrapper.find(SoundEditorComponent);
expect(component.prop('canRedo')).toEqual(false);
});
test('undo and redo submit new samples and play the sound', () => {
test('undo and redo submit new samples and play the sound', async () => {
const wrapper = mountWithIntl(
<SoundEditor
soundIndex={soundIndex}
@@ -260,12 +260,12 @@ describe('Sound Editor Container', () => {
// Set up an undoable state
component.props().onFaster();
mockAudioEffects.instance._finishProcessing(soundBuffer);
await mockAudioEffects.instance._finishProcessing(soundBuffer);
wrapper.update();
component = wrapper.find(SoundEditorComponent);
// Undo should update the sound buffer and play the new samples
component.props().onUndo();
await component.props().onUndo();
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
expect(vm.updateSoundBuffer).toHaveBeenCalled();
@@ -274,7 +274,7 @@ describe('Sound Editor Container', () => {
mockAudioBufferPlayer.instance.play.mockClear();
// Undo should update the sound buffer and play the new samples
component.props().onRedo();
await component.props().onRedo();
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
expect(vm.updateSoundBuffer).toHaveBeenCalled();
});
+51 -1
View File
@@ -1,4 +1,9 @@
import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util';
import {
computeRMS,
computeChunkedRMS,
downsampleIfNeeded,
dropEveryOtherSample
} from '../../../src/lib/audio/audio-util';
describe('computeRMS', () => {
test('returns 0 when given no samples', () => {
@@ -49,3 +54,48 @@ describe('computeChunkedRMS', () => {
expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55), Math.sqrt(1 / 0.55)]);
});
});
describe('downsampleIfNeeded', () => {
const samples = {length: 1};
const sampleRate = 44100;
test('returns given data when no downsampling needed', async () => {
samples.length = 1;
const res = await downsampleIfNeeded({samples, sampleRate}, null);
expect(res.samples).toEqual(samples);
expect(res.sampleRate).toEqual(sampleRate);
});
test('downsamples to 22050 if that puts it under the limit', async () => {
samples.length = 44100 * 3 * 60;
const resampler = jest.fn(() => 'TEST');
const res = await downsampleIfNeeded({samples, sampleRate}, resampler);
expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 22050);
expect(res).toEqual('TEST');
});
test('fails if resampling would not put it under the limit', async () => {
samples.length = 44100 * 4 * 60;
try {
await downsampleIfNeeded({samples, sampleRate}, null);
} catch (e) {
expect(e).toEqual('Sound too large to save, refusing to edit');
}
});
});
describe('dropEveryOtherSample', () => {
const buffer = {
samples: [1, 0, 2, 0, 3, 0],
sampleRate: 2
};
test('result is half the length', () => {
const {samples} = dropEveryOtherSample(buffer);
expect(samples.length).toEqual(Math.floor(buffer.samples.length / 2));
});
test('result contains only even-index items', () => {
const {samples} = dropEveryOtherSample(buffer);
expect(samples).toEqual(new Float32Array([1, 2, 3]));
});
test('result sampleRate is given sampleRate / 2', () => {
const {sampleRate} = dropEveryOtherSample(buffer);
expect(sampleRate).toEqual(buffer.sampleRate / 2);
});
});
+2 -43
View File
@@ -60,7 +60,6 @@ describe('CloudManagerHOC', () => {
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -80,7 +79,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
store={store}
username="user"
@@ -98,7 +96,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -115,7 +112,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@@ -132,7 +128,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
cloudHost="nonEmpty"
hasCloudPermission={false}
store={store}
@@ -153,7 +148,6 @@ describe('CloudManagerHOC', () => {
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@@ -182,7 +176,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={stillLoadingStore}
@@ -209,7 +202,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -235,7 +227,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -262,7 +253,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -293,7 +283,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -315,7 +304,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -343,7 +331,6 @@ describe('CloudManagerHOC', () => {
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -364,12 +351,11 @@ describe('CloudManagerHOC', () => {
});
// Editor Mode Connection/Disconnection Tests
test('Entering editor mode and can\'t save project should disconnect cloud provider # 1', () => {
test('Entering editor mode and can\'t save project should disconnect cloud provider', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
canSave
hasCloudPermission
cloudHost="nonEmpty"
store={store}
@@ -382,34 +368,7 @@ describe('CloudManagerHOC', () => {
const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection;
mounted.setProps({
canSave: false,
hasEverEnteredEditor: true
});
expect(vm.setCloudProvider.mock.calls.length).toBe(2);
expect(vm.setCloudProvider).toHaveBeenCalledWith(null);
expect(requestCloseConnection).toHaveBeenCalledTimes(1);
});
test('Entering editor mode and can\'t save project should disconnect cloud provider # 2', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
<WrappedComponent
hasCloudPermission
canSave={false}
cloudHost="nonEmpty"
store={store}
username="user"
vm={vm}
/>
);
expect(CloudProvider).toHaveBeenCalled();
const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection;
mounted.setProps({
hasEverEnteredEditor: true
canModifyCloudData: false
});
expect(vm.setCloudProvider.mock.calls.length).toBe(2);