diff --git a/package.json b/package.json index ca5de9869d7..122fb53e86b 100644 --- a/package.json +++ b/package.json @@ -1,183 +1,183 @@ { - "name": "scratch-gui", - "version": "3.2.37", - "description": "Graphical User Interface for creating and running Scratch 3.0 projects", - "author": "Massachusetts Institute of Technology", - "license": "GPL-3.0", - "homepage": "https://github.com/LLK/scratch-gui#readme", - "repository": { - "type": "git", - "url": "https://github.com/LLK/scratch-gui.git" - }, - "main": "./dist/scratch-gui.js", - "scripts": { - "build": "npm run clean && webpack --colors --bail", - "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist", - "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"[skip ci] Build for $(git log --pretty=format:%H -n1)\"", - "prepublish": "npm run prepare", - "prepare": "node scripts/prepublish.mjs", - "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/", - "start": "webpack-dev-server --port 3000 --disable-host-check", - "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration", - "test:integration": "jest --maxWorkers=4 test[\\\\/]integration", - "test:lint": "eslint . --ext .js,.jsx", - "test:unit": "jest test[\\\\/]unit[\\\\/]addons", - "test:smoke": "jest --runInBand test[\\\\/]smoke", - "watch": "webpack --colors --watch" - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "dependencies": { - "@microbit/microbit-universal-hex": "0.2.2", - "@turbowarp/jszip": "^3.12.0", - "@turbowarp/nanolog": "^1.0.1", - "@turbowarp/scratch-l10n": "github:PenguinMod-Port/scratch-l10n#new-builds", - "@turbowarp/scratch-svg-renderer": "github:PenguinMod-Port/scratch-svg-renderer#develop", - "@turbowarp/startaudiocontext": "^1.0.0", - "PenguinMod-MarkDown": "github:PenguinMod/PenguinMod-MarkDown#master", - "arraybuffer-loader": "^1.0.8", - "autoprefixer": "^9.8.8", - "balance-text": "3.3.1", - "base64-loader": "1.0.0", - "bowser": "1.9.4", - "classnames": "2.2.6", - "computed-style-to-inline-style": "3.0.0", - "cookie": "0.5.0", - "copy-webpack-plugin": "6.4.1", - "core-js": "2.5.7", - "css-loader": "^1.0.1", - "dapjs": "2.3.0", - "es6-object-assign": "1.1.0", - "file-loader": "2.0.0", - "get-float-time-domain-data": "0.1.0", - "get-user-media-promise": "1.1.4", - "intl": "1.2.5", - "intl-messageformat": "^2.2.0", - "js-base64": "2.4.9", - "jszip": "^3.10.2", - "keymirror": "0.1.1", - "localforage": "^1.10.0", - "lodash.bindall": "4.4.0", - "lodash.debounce": "4.0.8", - "lodash.defaultsdeep": "4.6.1", - "lodash.omit": "4.5.0", - "lodash.throttle": "4.0.1", - "minilog": "3.1.0", - "omggif": "1.0.9", - "papaparse": "5.3.0", - "pmp-protobuf": "^1.8.8", - "postcss-import": "^12.0.1", - "postcss-loader": "^3.0.0", - "postcss-simple-vars": "^5.0.2", - "prop-types": "^15.8.1", - "protobufjs": "^7.6.6", - "query-string": "^5.1.1", - "raw-loader": "^0.5.1", - "react": "^16.14.0", - "react-contextmenu": "2.9.4", - "react-dom": "^16.14.0", - "react-draggable": "3.0.5", - "react-ga": "2.5.3", - "react-intl": "2.9.0", - "react-intl-redux": "0.7.0", - "react-modal": "3.9.1", - "react-popover": "0.5.10", - "react-redux": "5.0.7", - "react-responsive": "5.0.0", - "react-string-replace": "^0.4.4", - "react-style-proptype": "3.2.2", - "react-tabs": "2.3.0", - "react-tooltip": "3.8.0", - "react-virtualized": "9.20.1", - "redux": "3.7.2", - "redux-throttle": "0.1.1", - "scratch-audio": "github:PenguinMod-Port/scratch-audio#develop", - "scratch-blocks": "github:PenguinMod-Port/scratch-blocks#develop-builds", - "scratch-paint": "github:PenguinMod-Port/scratch-paint#develop", - "scratch-render": "github:PenguinMod-Port/scratch-render#develop", - "scratch-render-fonts": "github:PenguinMod-Port/scratch-render-fonts#master", - "scratch-storage": "github:PenguinMod-Port/scratch-storage#develop", - "scratch-svg-renderer": "github:PenguinMod-Port/scratch-svg-renderer#develop", - "scratch-vm": "github:PenguinMod-Port/scratch-vm#develop", - "startaudiocontext": "1.2.1", - "style-loader": "^0.23.1", - "text-encoding": "0.7.0", - "to-style": "1.3.3", - "wav-encoder": "1.3.0", - "xhr": "2.5.0" - }, - "peerDependencies": { - "react": "^16.0.0", - "react-dom": "^16.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.29.7", - "@babel/core": "^7.29.7", - "@babel/plugin-proposal-object-rest-spread": "7.14.7", - "@babel/plugin-syntax-dynamic-import": "7.2.0", - "@babel/plugin-transform-async-to-generator": "7.14.5", - "@babel/preset-env": "7.14.8", - "@babel/preset-react": "7.14.5", - "babel-core": "7.0.0-bridge.0", - "babel-eslint": "10.1.0", - "babel-loader": "8.3.0", - "cache-loader": "^4.1.0", - "chromedriver": "117.0.3", - "cross-env": "^7.0.3", - "cross-fetch": "3.1.5", - "enzyme": "3.10.0", - "enzyme-adapter-react-16": "1.15.7", - "eslint": "8.55.0", - "eslint-config-scratch": "9.0.3", - "eslint-import-resolver-webpack": "0.11.1", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-jest": "22.17.0", - "eslint-plugin-react": "7.33.2", - "file-loader": "6.2.0", - "gh-pages": "3.2.3", - "hard-source-webpack-plugin": "^0.13.1", - "html-webpack-plugin": "^4.5.2", - "jest": "^29.7.0", - "jest-junit": "7.0.0", - "lodash.defaultsdeep": "4.6.1", - "mkdirp": "1.0.3", - "raf": "3.4.1", - "react-test-renderer": "16.14.0", - "redux-mock-store": "1.5.3", - "rimraf": "2.7.1", - "selenium-webdriver": "3.6.0", - "uglifyjs-webpack-plugin": "1.3.0", - "url-loader": "4.1.1", - "web-audio-test-api": "0.5.2", - "webpack": "4.47.0", - "webpack-cli": "3.3.12", - "webpack-dev-server": "3.11.2", - "webpack-hash-cache": "^0.4.8", - "webpack-sources": "3.2.3", - "yauzl": "2.10.0" - }, - "jest": { - "setupFiles": [ - "raf/polyfill", - "/test/helpers/enzyme-setup.js" - ], - "testPathIgnorePatterns": [ - "src/test.js" - ], - "moduleNameMapper": { - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", - "\\.(css|less)$": "/test/__mocks__/styleMock.js", - "editor-msgs(\\.js)?$": "/test/__mocks__/editor-msgs-mock.js" - } - }, - "overrides": { - "webpack@4.47.0": { - "terser-webpack-plugin": "^4.2.3" - } + "name": "scratch-gui", + "version": "3.2.37", + "description": "Graphical User Interface for creating and running Scratch 3.0 projects", + "author": "Massachusetts Institute of Technology", + "license": "GPL-3.0", + "homepage": "https://github.com/LLK/scratch-gui#readme", + "repository": { + "type": "git", + "url": "https://github.com/LLK/scratch-gui.git" + }, + "main": "./dist/scratch-gui.js", + "scripts": { + "build": "npm run clean && webpack --colors --bail", + "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist", + "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"[skip ci] Build for $(git log --pretty=format:%H -n1)\"", + "prepublish": "npm run prepare", + "prepare": "node scripts/prepublish.mjs", + "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/", + "start": "webpack-dev-server --port 3000 --disable-host-check", + "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration", + "test:integration": "jest --maxWorkers=4 test[\\\\/]integration", + "test:lint": "eslint . --ext .js,.jsx", + "test:unit": "jest test[\\\\/]unit[\\\\/]addons", + "test:smoke": "jest --runInBand test[\\\\/]smoke", + "watch": "webpack --colors --watch" + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" } -} + }, + "dependencies": { + "@microbit/microbit-universal-hex": "0.2.2", + "@turbowarp/jszip": "^3.12.0", + "@turbowarp/nanolog": "^1.0.1", + "@turbowarp/scratch-l10n": "github:PenguinMod-Port/scratch-l10n#new-builds", + "@turbowarp/scratch-svg-renderer": "github:PenguinMod-Port/scratch-svg-renderer#develop", + "@turbowarp/startaudiocontext": "^1.0.0", + "PenguinMod-MarkDown": "github:PenguinMod/PenguinMod-MarkDown#master", + "arraybuffer-loader": "^1.0.8", + "autoprefixer": "^9.8.8", + "balance-text": "3.3.1", + "base64-loader": "1.0.0", + "bowser": "1.9.4", + "classnames": "2.2.6", + "computed-style-to-inline-style": "3.0.0", + "cookie": "0.5.0", + "copy-webpack-plugin": "6.4.1", + "core-js": "2.5.7", + "css-loader": "^1.0.1", + "dapjs": "2.3.0", + "es6-object-assign": "1.1.0", + "file-loader": "2.0.0", + "get-float-time-domain-data": "0.1.0", + "get-user-media-promise": "1.1.4", + "intl": "1.2.5", + "intl-messageformat": "^2.2.0", + "js-base64": "2.4.9", + "jszip": "^3.10.2", + "keymirror": "0.1.1", + "localforage": "^1.10.0", + "lodash.bindall": "4.4.0", + "lodash.debounce": "4.0.8", + "lodash.defaultsdeep": "4.6.1", + "lodash.omit": "4.5.0", + "lodash.throttle": "4.0.1", + "minilog": "3.1.0", + "omggif": "1.0.9", + "papaparse": "5.3.0", + "pmp-protobuf": "^1.8.8", + "postcss-import": "^12.0.1", + "postcss-loader": "^3.0.0", + "postcss-simple-vars": "^5.0.2", + "prop-types": "^15.8.1", + "protobufjs": "^7.6.6", + "query-string": "^5.1.1", + "raw-loader": "^0.5.1", + "react": "^16.14.0", + "react-contextmenu": "2.9.4", + "react-dom": "^16.14.0", + "react-draggable": "3.0.5", + "react-ga": "2.5.3", + "react-intl": "2.9.0", + "react-intl-redux": "0.7.0", + "react-modal": "3.9.1", + "react-popover": "0.5.10", + "react-redux": "5.0.7", + "react-responsive": "5.0.0", + "react-string-replace": "^0.4.4", + "react-style-proptype": "3.2.2", + "react-tabs": "2.3.0", + "react-tooltip": "3.8.0", + "react-virtualized": "9.20.1", + "redux": "3.7.2", + "redux-throttle": "0.1.1", + "scratch-audio": "github:PenguinMod-Port/scratch-audio#develop", + "scratch-blocks": "github:PenguinMod-Port/scratch-blocks#develop-builds", + "scratch-paint": "github:PenguinMod-Port/scratch-paint#develop", + "scratch-render": "github:PenguinMod-Port/scratch-render#develop", + "scratch-render-fonts": "github:PenguinMod-Port/scratch-render-fonts#master", + "scratch-storage": "github:PenguinMod-Port/scratch-storage#develop", + "scratch-svg-renderer": "github:PenguinMod-Port/scratch-svg-renderer#develop", + "scratch-vm": "github:PenguinMod-Port/scratch-vm#develop", + "startaudiocontext": "1.2.1", + "style-loader": "^0.23.1", + "text-encoding": "0.7.0", + "to-style": "1.3.3", + "wav-encoder": "1.3.0", + "xhr": "2.5.0" + }, + "peerDependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0" + }, + "devDependencies": { + "@babel/cli": "^7.29.7", + "@babel/core": "^7.29.7", + "@babel/plugin-proposal-object-rest-spread": "7.14.7", + "@babel/plugin-syntax-dynamic-import": "7.2.0", + "@babel/plugin-transform-async-to-generator": "7.14.5", + "@babel/preset-env": "7.14.8", + "@babel/preset-react": "7.14.5", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "10.1.0", + "babel-loader": "8.3.0", + "cache-loader": "^4.1.0", + "chromedriver": "117.0.3", + "cross-env": "^7.0.3", + "cross-fetch": "3.1.5", + "enzyme": "3.10.0", + "enzyme-adapter-react-16": "1.15.7", + "eslint": "8.55.0", + "eslint-config-scratch": "9.0.3", + "eslint-import-resolver-webpack": "0.11.1", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-jest": "22.17.0", + "eslint-plugin-react": "7.33.2", + "file-loader": "6.2.0", + "gh-pages": "3.2.3", + "hard-source-webpack-plugin": "^0.13.1", + "html-webpack-plugin": "^4.5.2", + "jest": "^29.7.0", + "jest-junit": "7.0.0", + "lodash.defaultsdeep": "4.6.1", + "mkdirp": "1.0.3", + "raf": "3.4.1", + "react-test-renderer": "16.14.0", + "redux-mock-store": "1.5.3", + "rimraf": "2.7.1", + "selenium-webdriver": "3.6.0", + "uglifyjs-webpack-plugin": "1.3.0", + "url-loader": "4.1.1", + "web-audio-test-api": "0.5.2", + "webpack": "4.47.0", + "webpack-cli": "3.3.12", + "webpack-dev-server": "3.11.2", + "webpack-hash-cache": "^0.4.8", + "webpack-sources": "3.2.3", + "yauzl": "2.10.0" + }, + "jest": { + "setupFiles": [ + "raf/polyfill", + "/test/helpers/enzyme-setup.js" + ], + "testPathIgnorePatterns": [ + "src/test.js" + ], + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", + "\\.(css|less)$": "/test/__mocks__/styleMock.js", + "editor-msgs(\\.js)?$": "/test/__mocks__/editor-msgs-mock.js" + } + }, + "overrides": { + "webpack@4.47.0": { + "terser-webpack-plugin": "^4.2.3" + } + } +} \ No newline at end of file diff --git a/src/addons/addons-l10n/de.json b/src/addons/addons-l10n/de.json index 6de8c6c3336..d8741fc295e 100644 --- a/src/addons/addons-l10n/de.json +++ b/src/addons/addons-l10n/de.json @@ -1,5 +1,5 @@ { - "_general/blocks/anticlockwise": "im Gegenuhrzeigersinn", + "_general/blocks/anticlockwise": "entgegen des Uhrzeigersinnes", "_general/blocks/clockwise": "im Uhrzeigersinn", "_general/blocks/green-flag": "Flagge", "editor-devtools/clean-plus": "Blöcke aufräumen +", @@ -46,20 +46,24 @@ "mediarecorder/record-duration": "Aufnahmedauer (in Sekunden)", "mediarecorder/record-mic": "Mikrofonklänge einschließen", "mediarecorder/record-until-stop": "Aufnahme anhalten, wenn das Projekt gestoppt wird", - "mediarecorder/record-until-stop-disabled": "Du musst \"{afterFlagOption}\" aktivieren um diese Option zu verwenden.", + "mediarecorder/record-until-stop-disabled": "Du musst \"{afterFlagOption}\" aktivieren, um diese Option zu verwenden.", "mediarecorder/start": "Starten", "mediarecorder/start-delay": "Startverzögerung (in Sekunden)", "mediarecorder/starting-in": "Startet in {secs}...", "mediarecorder/stop": "Aufnahme stoppen", "debugger/block-breakpoint": "unterbrechen", - "debugger/block-error": "schreibe Fehler %s im Log", - "debugger/block-log": "schreibe %s im Log", - "debugger/block-warn": "schreibe Warnung %s im Log", - "debugger/cannot-pause-player": "Der \"unterbrechen\"-Block kann nur im Editor verwendet werden.", + "debugger/block-error": "schreibe Fehler %s im Protokoll", + "debugger/block-log": "schreibe %s im Protokoll", + "debugger/block-start-timer": "starte die Stoppuhr %s", + "debugger/block-stop-timer": "halte die Stoppuhr %s an", + "debugger/block-warn": "schreibe Warnung %s im Protokoll", + "debugger/cannot-pause-player": "Der \"unterbrechen\"-Block kann nur verwendet werden, während du im Editor bist..", "debugger/clear": "Löschen", "debugger/clone-of": "Klon von {sprite}", "debugger/close": "Schließen", + "debugger/console": "Protokolle", "debugger/debug": "Debuggen", + "debugger/default-timer-label": "Stoppuhr1", "debugger/empty-string": "(leerer Text)", "debugger/enter-format": "Exportformat eingeben:", "debugger/export": "Exportieren", @@ -67,16 +71,23 @@ "debugger/icon-error": "Fehler", "debugger/icon-warn": "Warnung", "debugger/log-msg-broadcasted": "Nachricht '{broadcast}' gesendet.", - "debugger/log-msg-clone-cap": "Klonen von '{sprite}' fehlgeschlagen, es können nicht mehr als 300 Klone erzeugt werden.", + "debugger/log-msg-clone-cap": "'{sprite}' konnte nicht geklont werden, weil nicht mehr als 300 Klone existieren können.", "debugger/log-msg-clone-created": "'{sprite}' geklont.", "debugger/log-msg-flag-clicked": "Grüne Flagge angeklickt.", - "debugger/no-logs": "Es gibt keine Logs zum Anzeigen.", - "debugger/no-threads-running": "Es werden momentan keine Threads ausgeführt.", + "debugger/no-logs": "Es gibt keine Protokolle zum Anzeigen.", + "debugger/no-threads-running": "Aktuell werden keine Threads ausgeführt.", "debugger/performance-clonecount-graph-tooltip": "Klone: {clones}", "debugger/performance-clonecount-title": "Klone", "debugger/step": "Schritt", "debugger/step-desc": "Führt einen Block aus.", + "debugger/tab-logs": "Protokolle", "debugger/tab-performance": "Leistung", + "debugger/timing-average-time": "Durchschnittliche Zeit", + "debugger/timing-call-count": "Aufrufanzahl", + "debugger/timing-no-timers": "Keine Stoppuhren gestartet.", + "debugger/timing-percent-time": "Zeitprozentsatz", + "debugger/timing-ratio-time": "Zeitverhältnis", + "debugger/timing-total-time": "Gesamte Zeit", "debugger/unknown-sprite": "(unbekannte Figur)", "debugger/unpause": "Fortsetzen", "pause/play": "Fortfahren", @@ -90,8 +101,8 @@ "onion-skinning/merge": "Mischen", "onion-skinning/mode": "Modus", "onion-skinning/next": "Nächstes Kostüm", - "onion-skinning/opacity": "Transparenz (%)", - "onion-skinning/opacityStep": "Transparenz-Abstufung (%)", + "onion-skinning/opacity": "Deckkraft (%)", + "onion-skinning/opacityStep": "Deckkraft-Abstufung (%)", "onion-skinning/previous": "Vorheriges Kostüm", "onion-skinning/settings": "Einstellungen für Onion Skinning", "onion-skinning/tint": "Färbung", @@ -140,10 +151,10 @@ "gamepad/key-space": "Leertaste", "gamepad/key-up": "Oben", "gamepad/keyinput-title": "Klicke und drücke eine Taste oder klicke, um Knopf zu wechseln. Drücke Esc zum abbrechen. Löschen oder Entf zum Löschen.", - "gamepad/no-controllers": "Keine Controller gefunden. Schließe einen an und drücke einen der Knöpfe.", + "gamepad/no-controllers": "Es wurde kein Controller gefunden. Versuche, einen anzuschließen und drücke einen der Knöpfe.", "gamepad/reset": "Alle Steuerungen zu Projektstandards zurücksetzen", "gamepad/settings": "Gamepad-Einstellungen", - "gamepad/store-in-project": "Die oben angeführten Mappings im Projekt speichern. Nach dem Speichern des Projekts werden sie die neue Standardkonfiguration sein.", + "gamepad/store-in-project": "Die oben aufgeführten Mappings im Projekt speichern. Nach dem Speichern des Projekts werden sie die neue Standardkonfiguration sein.", "folders/add-to-folder": "zu Ordner \"{folder}\" hinzufügen", "folders/closed-folder": "Ordner", "folders/confirm-backpack-folder": "Den ganzen Ordner im Lager speichern?", @@ -172,9 +183,9 @@ "block-switching/data_setvariableto": "setze Variable auf", "block-switching/data_showlist": "zeige Liste", "block-switching/data_showvariable": "zeige Variable", - "block-switching/debugger_error": "Fehler im Log schreiben", - "block-switching/debugger_log": "im Log schreiben", - "block-switching/debugger_warn": "Warnung im Log schreiben", + "block-switching/debugger_error": "schreibe Fehler im Protokoll", + "block-switching/debugger_log": "schreibe im Protokoll", + "block-switching/debugger_warn": "schreibe Warnung im Protokoll", "block-switching/event_broadcast": "sende an alle", "block-switching/event_broadcastandwait": "sende an alle und warte", "block-switching/looks_backdropnumbername": "Bühnenbildnummer oder -name", @@ -240,10 +251,10 @@ "move-to-top-layer/move-to-front-layer": "gehe zu vorderster Ebene", "rename-broadcasts/RENAME_BROADCAST": "Nachricht umbenennen", "rename-broadcasts/RENAME_BROADCAST_MODAL_TITLE": "Nachricht umbenennen", - "rename-broadcasts/RENAME_BROADCAST_TITLE": "Alle \"{name}\"-Nachrichten umbenennen nach:", - "swap-local-global/cant-convert-cloud": "Cloudvariablen können nicht in nur für diese Figur umgewandelt werden.", + "rename-broadcasts/RENAME_BROADCAST_TITLE": "Alle \"{name}\"-Nachrichten umbenennen in:", + "swap-local-global/cant-convert-cloud": "Cloudvariablen können nicht in \"nur für diese Figur\" umgewandelt werden.", "swap-local-global/cant-convert-conflict": "Konnte nicht umwandeln, weil Konflikte mit Variablen in einer anderen Figur auftreten würden: {sprites}", - "swap-local-global/cant-convert-stage": "Die Bühne kann nicht Variablen nur für diese Figur haben.", + "swap-local-global/cant-convert-stage": "Die Bühne kann keine Variablen nur für diese Figur haben.", "swap-local-global/cant-convert-to-local": "Konnte nicht umwandeln, weil sie von mehreren Figuren verwendet wird: {sprites}", "swap-local-global/cant-convert-used-elsewhere": "Konnte nicht umwandeln, weil sie von einer anderen Figur verwendet wird: {sprite}", "swap-local-global/edit": "Eigenschaften bearbeiten:", diff --git a/src/addons/addons-l10n/es.json b/src/addons/addons-l10n/es.json index 22040d4c5c0..a73b29979cc 100644 --- a/src/addons/addons-l10n/es.json +++ b/src/addons/addons-l10n/es.json @@ -174,7 +174,7 @@ "block-switching/looks_costumenumbername": "número o nombre de disfraz", "block-switching/looks_goforwardbackwardlayers": "avanzar o retroceder capas", "block-switching/looks_gotofrontback": "ir a la capa delantera o trasera", - "block-switching/looks_hide": "ocultar", + "block-switching/looks_hide": "esconder", "block-switching/looks_nextbackdrop": "siguiente fondo", "block-switching/looks_nextcostume": "siguiente disfraz", "block-switching/looks_say": "decir", @@ -227,7 +227,7 @@ "blocks2image/export_selected_to_PNG": "Exportar bloque como PNG", "blocks2image/export_selected_to_SVG": "Exportar bloque como SVG", "move-to-top-bottom/bottom": "mover al fondo", - "move-to-top-bottom/top": "movar a la cima", + "move-to-top-bottom/top": "mover a la cima", "move-to-top-layer/move-to-front-layer": "mover a capa delantera", "rename-broadcasts/RENAME_BROADCAST": "Renombrar mensaje", "rename-broadcasts/RENAME_BROADCAST_MODAL_TITLE": "Renombrar Mensaje", diff --git a/src/addons/addons-l10n/fi.json b/src/addons/addons-l10n/fi.json index 47830e8ebe2..f915ebb9734 100644 --- a/src/addons/addons-l10n/fi.json +++ b/src/addons/addons-l10n/fi.json @@ -54,6 +54,8 @@ "debugger/block-breakpoint": "keskeytyskohta", "debugger/block-error": "kirjaa virhe %s", "debugger/block-log": "kirjaa lokimerkintä %s", + "debugger/block-start-timer": "käynnistä ajastin %s", + "debugger/block-stop-timer": "pysäytä ajastin %s", "debugger/block-warn": "kirjaa varoitus %s", "debugger/cannot-pause-player": "Keskeytyskohtalohkoa voidaan käyttää vain editorissa.", "debugger/clear": "Tyhjennä", @@ -61,6 +63,7 @@ "debugger/close": "Sulje", "debugger/console": "Lokimerkinnät", "debugger/debug": "Virheenkorjaus", + "debugger/default-timer-label": "ajastin1", "debugger/empty-string": "(tyhjä merkkijono)", "debugger/enter-format": "Anna latausformaatti:", "debugger/export": "Lataa", @@ -77,12 +80,22 @@ "debugger/performance-clonecount-title": "Kloonit", "debugger/performance-framerate-graph-tooltip": "Kuvataajuus: {fps} ruutua sekunnissa", "debugger/performance-framerate-title": "Kuvataajuus", - "debugger/step": "Vaihe", + "debugger/step": "Askel", "debugger/step-desc": "Suorittaa yhden lohkon.", "debugger/tab-logs": "Lokimerkinnät", "debugger/tab-performance": "Suorituskyky", "debugger/tab-threads": "Säikeet", + "debugger/tab-timing": "Ajastus", "debugger/thread": "Säie {id}", + "debugger/timing-average-time": "Keskimääräinen aika", + "debugger/timing-call-count": "Suorituskerrat", + "debugger/timing-heatmap": "Lämpökartta", + "debugger/timing-label": "Nimi", + "debugger/timing-no-timers": "Yhtään ajastinta ei ole käynnistetty.", + "debugger/timing-percent-time": "Osuus suoritusajasta", + "debugger/timing-profiling": "Profilointi", + "debugger/timing-ratio-time": "Aikasuhde", + "debugger/timing-total-time": "Kokonaisaika", "debugger/unknown-sprite": "(tuntematon hahmo)", "debugger/unpause": "Jatka", "pause/pause": "Keskeytä", @@ -98,7 +111,7 @@ "onion-skinning/mode": "Tila", "onion-skinning/next": "Seuraavat asusteet", "onion-skinning/opacity": "Peittävyys (%)", - "onion-skinning/opacityStep": "Peittävyysvaihe (%)", + "onion-skinning/opacityStep": "Peittävyyspykälä (%)", "onion-skinning/previous": "Edelliset asusteet", "onion-skinning/settings": "Sipulin kuorinnan asetukset", "onion-skinning/tint": "Värjää", @@ -110,7 +123,7 @@ "paint-snap/objectCorners": "Objektien kulmiin", "paint-snap/objectEdges": "Objektien reunoihin", "paint-snap/objectMidlines": "Objektien keskilinjoille", - "paint-snap/off": "Pois päältä", + "paint-snap/off": "Poissa päältä", "paint-snap/on": "Päällä", "paint-snap/pageAxes": "Muokkausalueen x- ja y-koordinaatteihin", "paint-snap/pageCenter": "Muokkausalueen keskipisteeseen", diff --git a/src/addons/addons-l10n/fr.json b/src/addons/addons-l10n/fr.json index 35a98f35332..43f84043ecc 100644 --- a/src/addons/addons-l10n/fr.json +++ b/src/addons/addons-l10n/fr.json @@ -230,8 +230,8 @@ "blocks2image/export_selected_to_PNG": "Exporter le bloc au format PNG", "blocks2image/export_selected_to_SVG": "Exporter le bloc au format SVG", "editor-extra-keys/enter-key": "entrée", - "move-to-top-bottom/bottom": "Déplacé en bas", - "move-to-top-bottom/top": "Déplacé en haut", + "move-to-top-bottom/bottom": "Déplacer en bas", + "move-to-top-bottom/top": "Déplacer en haut", "move-to-top-layer/move-to-front-layer": "passer à la couche avant", "rename-broadcasts/RENAME_BROADCAST": "Renommer le message", "rename-broadcasts/RENAME_BROADCAST_MODAL_TITLE": "Renommer le message", diff --git a/src/addons/addons-l10n/hu.json b/src/addons/addons-l10n/hu.json index 7e2636b3d83..84f2a0bba41 100644 --- a/src/addons/addons-l10n/hu.json +++ b/src/addons/addons-l10n/hu.json @@ -8,7 +8,7 @@ "editor-devtools/orphaned": "{count, plural, one {Fejlesztői eszközök: Törölni szeretnél 1 kódon kívüli értékadó blokkot?} other {Fejlesztői eszközök: Törölni szeretnél # kódon kívüli értékadó blokkot?} }", "editor-devtools/paste": "Beillesztés", "editor-devtools/replace": "Fejlesztői eszközök: Lecserélsz minden {name} változót ebben a szereplőben egy másik változóra, amit úgy hívnak:", - "editor-devtools/swap": "{var} cserélye a Szereplőben", + "editor-devtools/swap": "{var} cseréje a Szereplőben", "editor-devtools/unused-list": "{count, plural, one {Fejlesztői eszközök: Törölni szeretnél 1 nem használt helyi listát? Itt van:\n} other {Fejlesztői eszközök: Törölni szeretnél # nem használt heliy listát? Itt vannak:\n} }", "editor-devtools/unused-var": "{count, plural, one {Fejlesztői eszközök: Törölni szeretnél 1 nem használt helyi változót? Itt van:\n} other {Fejlesztői eszközök: Törölni szeretnél # nem használt helyi változót? Itt vannak:\n} }", "editor-devtools/var-not-exist": "Ez a változó nem létezik...", diff --git a/src/addons/addons-l10n/it.json b/src/addons/addons-l10n/it.json index d5d9ff70f7c..063b70a48cd 100644 --- a/src/addons/addons-l10n/it.json +++ b/src/addons/addons-l10n/it.json @@ -49,13 +49,18 @@ "mediarecorder/start-delay": "Ritardo Avvio (in secondi)", "mediarecorder/starting-in": "Inizierà tra {secs}...", "mediarecorder/stop": "Arresta Registrazione", + "debugger/block-breakpoint": "pausa", "debugger/block-error": "errore %s", + "debugger/block-log": "annotazione %s", + "debugger/block-start-timer": "avvia cronometro %s", + "debugger/block-stop-timer": "arresta cronometro %s", "debugger/block-warn": "avviso %s", - "debugger/cannot-pause-player": "Il blocco breakpoint può essere usato solo nell'editor.", + "debugger/cannot-pause-player": "Il blocco pausa può essere usato solo nell'editor.", "debugger/clear": "Cancella", "debugger/clone-of": "Clone di {sprite}", "debugger/close": "Chiudi", "debugger/console": "Log", + "debugger/default-timer-label": "cronometro1", "debugger/empty-string": "(stringa vuota)", "debugger/enter-format": "Inserisci il formato di esportazione:", "debugger/export": "Esporta", @@ -66,14 +71,24 @@ "debugger/log-msg-clone-cap": "Creazione del clone di '{sprite}' fallita, non è possibile creare più di 300 cloni..", "debugger/log-msg-clone-created": "Creato clone di '{sprite}'.", "debugger/log-msg-flag-clicked": "Bandiera verde cliccata.", - "debugger/no-logs": "Non ci sono log da mostrare.", + "debugger/no-logs": "Non ci sono log annotazioni mostrare.", "debugger/no-threads-running": "Nessun thread in esecuzione.", "debugger/performance-clonecount-graph-tooltip": "Cloni: {clones}", "debugger/performance-clonecount-title": "Cloni", "debugger/step": "Passi", "debugger/step-desc": "Esegue un blocco.", - "debugger/tab-logs": "Log", + "debugger/tab-logs": "Annotazioni", "debugger/tab-threads": "Thread", + "debugger/tab-timing": "Cronometraggio", + "debugger/timing-average-time": "Tempo Medio", + "debugger/timing-call-count": "Numero Chiamate", + "debugger/timing-heatmap": "Zone Calde", + "debugger/timing-label": "Etichetta", + "debugger/timing-no-timers": "Nessun timer avviato.", + "debugger/timing-percent-time": "Tempo Percentuale", + "debugger/timing-profiling": "Profilazione", + "debugger/timing-ratio-time": "Tempo Relativo", + "debugger/timing-total-time": "Tempo Totale", "debugger/unknown-sprite": "(sprite sconosciuto)", "debugger/unpause": "Riprendi", "pause/pause": "Pausa", @@ -172,6 +187,7 @@ "block-switching/data_showlist": "mostra la lista", "block-switching/data_showvariable": "mostra la variabile", "block-switching/debugger_error": "errore", + "block-switching/debugger_log": "annotazione", "block-switching/debugger_warn": "avviso", "block-switching/event_broadcast": "Invia a tutti", "block-switching/event_broadcastandwait": "Invia a tutti e attendi", diff --git a/src/addons/addons-l10n/ko.json b/src/addons/addons-l10n/ko.json index aba5effcf71..c589dcec275 100644 --- a/src/addons/addons-l10n/ko.json +++ b/src/addons/addons-l10n/ko.json @@ -1,4 +1,6 @@ { + "_general/blocks/anticlockwise": "반시계방향", + "_general/blocks/clockwise": "시계방향", "editor-devtools/clean-plus": "블럭 정리하기 +", "editor-devtools/copy-all": "모두 복사하기", "editor-devtools/copy-block": "블럭 복사하기", @@ -57,6 +59,7 @@ "debugger/close": "닫기", "debugger/console": "기록", "debugger/debug": "디버그", + "debugger/default-timer-label": "타이머1", "debugger/empty-string": "(빈 문자열)", "debugger/enter-format": "내보낼 방법을 입력하세요:", "debugger/export": "내보내기", @@ -77,6 +80,11 @@ "debugger/tab-performance": "성능", "debugger/tab-threads": "실행 중인 작업", "debugger/thread": "작업 {id}", + "debugger/timing-average-time": "평균 시간", + "debugger/timing-label": "레이블", + "debugger/timing-percent-time": "퍼센트 시간", + "debugger/timing-ratio-time": "비율 시간", + "debugger/timing-total-time": "전체 시간", "debugger/unknown-sprite": "(알 수 없는 스프라이트)", "debugger/unpause": "재개하기", "pause/pause": "일시정지", @@ -238,6 +246,7 @@ "editor-extra-keys/enter-key": "엔터", "move-to-top-bottom/bottom": "맨 아래로 옮기기", "move-to-top-bottom/top": "맨 위로 옮기기", + "move-to-top-layer/move-to-front-layer": "맨앞으로", "rename-broadcasts/RENAME_BROADCAST": "메시지 이름 변경하기", "rename-broadcasts/RENAME_BROADCAST_MODAL_TITLE": "메시지 이름 변경하기", "rename-broadcasts/RENAME_BROADCAST_TITLE": "모든 \"{name}\" 메시지를 다음으로 변경:", diff --git a/src/addons/addons-l10n/ru.json b/src/addons/addons-l10n/ru.json index a7134d3a19a..0e4ad117cbb 100644 --- a/src/addons/addons-l10n/ru.json +++ b/src/addons/addons-l10n/ru.json @@ -1,4 +1,7 @@ { + "_general/blocks/anticlockwise": "против часовой стрелки", + "_general/blocks/clockwise": "по часовой стрелке", + "_general/blocks/green-flag": "флажок", "editor-devtools/clean-plus": "Очистить блоки +", "editor-devtools/copy-all": "Копировать все", "editor-devtools/copy-block": "Копировать блок", @@ -46,6 +49,8 @@ "debugger/block-breakpoint": "приостановить", "debugger/block-error": "ошибка %s", "debugger/block-log": "логировать %s", + "debugger/block-start-timer": "начало таймера %s", + "debugger/block-stop-timer": "конец таймера %s", "debugger/block-warn": "предупредить %s", "debugger/cannot-pause-player": "Блок точки остановки может быть использовать только в редакторе.", "debugger/clear": "Очистить", @@ -53,6 +58,7 @@ "debugger/close": "Закрыть", "debugger/console": "Логи", "debugger/debug": "Отладка", + "debugger/default-timer-label": "таймер1", "debugger/empty-string": "(пустая строка)", "debugger/enter-format": "Введите формат экспорта:", "debugger/export": "Экспорт", @@ -74,10 +80,21 @@ "debugger/tab-logs": "Логи", "debugger/tab-performance": "Производительность", "debugger/tab-threads": "Потоки", + "debugger/tab-timing": "Таймеры", "debugger/thread": "Поток {id}", + "debugger/timing-average-time": "Среднее Время", + "debugger/timing-call-count": "Количество Вызовов", + "debugger/timing-heatmap": "Тепловая Карта", + "debugger/timing-label": "Бирка", + "debugger/timing-no-timers": "Никаких таймеров не начато.", + "debugger/timing-percent-time": "Процент Времени", + "debugger/timing-profiling": "Сборка Отладочной Информации", + "debugger/timing-ratio-time": "Пропорция Времени", + "debugger/timing-total-time": "Сумма Времени", "debugger/unknown-sprite": "(неизвестный спрайт)", "debugger/unpause": "Продолжить", "pause/pause": "Пауза", + "pause/play": "Возобновить", "clones/clones": "клонов: {cloneCount}", "color-picker/hex": "цвет hex", "remove-sprite-confirm/confirm": "Вы хотите удалить спрайт?", @@ -218,6 +235,7 @@ "block-switching/pen_setPenHueToNumber": "задать цвет", "block-switching/pen_setPenShadeToNumber": "задать оттенок", "block-switching/pen_setPenSizeTo": "задать размер", + "block-switching/sensing_coloristouchingcolor": "цвет касается цвета", "block-switching/sensing_mousex": "x мыши", "block-switching/sensing_mousey": "y мыши", "block-switching/sensing_touchingcolor": "касается цвета?", diff --git a/src/addons/addons-l10n/sl.json b/src/addons/addons-l10n/sl.json index dc342ed199b..6ee76933c2a 100644 --- a/src/addons/addons-l10n/sl.json +++ b/src/addons/addons-l10n/sl.json @@ -51,6 +51,8 @@ "mediarecorder/stop": "Ustavi snemanje", "debugger/block-error": "napaka %s", "debugger/block-log": "zapiši %s", + "debugger/block-start-timer": "začni štoparico %s", + "debugger/block-stop-timer": "ustavi štoparico %s", "debugger/block-warn": "opozorilo %s", "debugger/cannot-pause-player": "Blok breakpoint deluje samo v urejevalniku.", "debugger/clear": "Počisti", @@ -58,6 +60,7 @@ "debugger/close": "Zapri", "debugger/console": "Razhroščevanje", "debugger/debug": "Razhroščevanje", + "debugger/default-timer-label": "štoparica1", "debugger/empty-string": "(prazen niz)", "debugger/enter-format": "Oblika datoteke za izvoz:", "debugger/export": "Izvozi", @@ -77,7 +80,17 @@ "debugger/tab-logs": "Zapiski", "debugger/tab-performance": "Učinkovitost", "debugger/tab-threads": "Niti", + "debugger/tab-timing": "Merjenje časa", "debugger/thread": "Nit {id}", + "debugger/timing-average-time": "Povprečni čas", + "debugger/timing-call-count": "Število klicev", + "debugger/timing-heatmap": "Toplotna karta", + "debugger/timing-label": "Oznaka", + "debugger/timing-no-timers": "Nobena štoparica ni bila začeta.", + "debugger/timing-percent-time": "Delež", + "debugger/timing-profiling": "Podrobno", + "debugger/timing-ratio-time": "Razmerje", + "debugger/timing-total-time": "Skupni čas", "debugger/unknown-sprite": "(neznana figura)", "debugger/unpause": "Nadaljuj", "pause/pause": "Premor", @@ -144,7 +157,7 @@ "gamepad/key-up": "Gor", "gamepad/keyinput-title": "Kliknite in pritisnite tipko ali kliknite z miško, da izberete gumb. Pritisnite tipko Escape za preklic ali Backspace/Delete, da izbrišete vnos.", "gamepad/no-controllers": "Noben igralni plošček ni bil zaznan. Poskusite ga priključiti in pritisniti gumb na njem.", - "gamepad/reset": "Povrni privzete nastavitve", + "gamepad/reset": "Povrni privzete nastavitve projekta", "gamepad/settings": "Nastavitve igralnih ploščkov", "gamepad/store-in-project": "Shrani zgornje nastavitve v projektu. Ko shranite projekt, bodo postale privzete nastavitve za ta projekt.", "folders/add-to-folder": "dodaj v mapo: {folder}", diff --git a/src/addons/addons-l10n/zh-cn.json b/src/addons/addons-l10n/zh-cn.json index 427e4a677e6..78bb038321d 100644 --- a/src/addons/addons-l10n/zh-cn.json +++ b/src/addons/addons-l10n/zh-cn.json @@ -8,12 +8,12 @@ "editor-devtools/cut-block": "剪切积木", "editor-devtools/lists": "列表", "editor-devtools/make-space": "留出空间", - "editor-devtools/orphaned": "{count, plural, other {} }", + "editor-devtools/orphaned": "{count, plural, other {开发者工具:要删除 # 个单独存在的积木块吗?} }", "editor-devtools/paste": "粘贴", "editor-devtools/replace": "开发者工具:将此角色中的所有 {name} 变量切换为:", "editor-devtools/swap": "交换角色中的 {var}", - "editor-devtools/unused-list": "{count, plural, other {} }", - "editor-devtools/unused-var": "{count, plural, other {} }", + "editor-devtools/unused-list": "{count, plural, other {开发者工具:要删除 # 个没有使用的列表吗?这些列表包括:\n} }", + "editor-devtools/unused-var": "{count, plural, other {开发者工具:要删除 # 个没有使用的变量吗?这些变量包括:\n} }", "editor-devtools/var-not-exist": "变量不存在……", "editor-devtools/variables": "变量", "find-bar/complex-broadcast": "(表达式)", diff --git a/src/addons/addons.js b/src/addons/addons.js index c3582f07e38..c83ef2ecc0e 100644 --- a/src/addons/addons.js +++ b/src/addons/addons.js @@ -68,7 +68,7 @@ const addons = [ ]; const newAddons = [ - 'expanded-backpack' + 'debugger' ]; // eslint-disable-next-line import/no-commonjs diff --git a/src/addons/addons/expanded-backpack/_manifest_entry.js b/src/addons/addons/expanded-backpack/_manifest_entry.js index 83599ed13dd..0dd07aceb8c 100644 --- a/src/addons/addons/expanded-backpack/_manifest_entry.js +++ b/src/addons/addons/expanded-backpack/_manifest_entry.js @@ -7,9 +7,7 @@ const manifest = { "name": "pumpkinhasapatch" } ], - "tags": [ - "new" - ], + "tags": [], "enabledByDefault": false, "settings": [ { diff --git a/src/addons/generated/upstream-meta.json b/src/addons/generated/upstream-meta.json index be1668b83ba..2a11d385513 100644 --- a/src/addons/generated/upstream-meta.json +++ b/src/addons/generated/upstream-meta.json @@ -1 +1 @@ -{"commit":"b206908"} \ No newline at end of file +{"commit":"a8b7945"} \ No newline at end of file diff --git a/src/addons/settings/translations.json b/src/addons/settings/translations.json index f2a5dc8e8ce..6e0a11b8b8a 100644 --- a/src/addons/settings/translations.json +++ b/src/addons/settings/translations.json @@ -279,7 +279,7 @@ "addonFeedback": "애드온 피드백하기", "confirmResetAll": "정말로 애드온 설정을 기본값으로 초기화하시겠습니까?", "credits": "도움을 준 사람들 :", - "dirty": "설정을 적용하려면 탭을 새로고침해야 합니다.", + "dirty": "설정을 적용하기 위해 탭을 새로고침해야 합니다.", "dirtyButton": "지금 재시작하기", "enableDangerous": "이 애드온은 위험하며 몇몇 기능을 의도적으로 비활성화할 수 있습니다. 일반적인 사용자는 이 애드온을 활성화하지 않는 것이 좋습니다. 정말로 활성화 하시겠습니까?", "export": "설정 내보내기", @@ -287,8 +287,8 @@ "groupNew": "새 기능 ({number})", "groupOthers": "기타 ({number})", "import": "설정 가져오기", - "noCompiler": "이 애드온은 메뉴의 고급 설정 > 컴파일러 비활성화를 설정했거나, \"에디터에서 컴파일러 비활성화\" 애드온을 사용해 컴파일러를 비활성화했을 때에만 동작합니다.", - "noResults": "결과가 없습니다.", + "noCompiler": "이 애드온은 [고급 설정] > [컴파일러 비활성화] 설정을 활성화했거나, '에디터에서 컴파일러 비활성화' 애드온을 사용해 컴파일러를 비활성화했을 때에만 동작합니다.", + "noResults": "결과 없음.", "presets": "프리셋", "reset": "초기화", "resetAll": "모두 초기화", @@ -299,7 +299,7 @@ "tagRecommended": "추천", "tagTheme": "테마", "title": "애드온 설정", - "unsupported": "몇몇 애드온은 다음의 브라우저에서 사용할 수 없습니다 :" + "unsupported": "다음의 브라우저에서 일부 애드온을 사용할 수 없습니다 :" }, "lt": { "addonFeedback": "Įskiepių atsiliepimas", diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 1b748cc194b..b945f9989ec 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -123,36 +123,32 @@ class Backpack extends React.Component { if (!payloader) return; // Creating the payload is async, so set loading before starting - this.setState({loading: true}, () => { - payloader(dragInfo.payload, this.props.vm) - .then(payload => { - // Force the asset to save to the asset server before storing in backpack - // Ensures any asset present in the backpack is also on the asset server - if (presaveAsset && !presaveAsset.clean && !this.props.host === LOCAL_API) { - return storage.store( - presaveAsset.assetType, - presaveAsset.dataFormat, - presaveAsset.data, - presaveAsset.assetId - ).then(() => payload); - } - return payload; - }) - .then(payload => saveBackpackObject({ + this.setState({loading: true}, async () => { + try { + const payload = await payloader(dragInfo.payload, this.props.vm); + // Force the asset to save to the asset server before storing in backpack + // Ensures any asset present in the backpack is also on the asset server + if (presaveAsset && !presaveAsset.clean && !this.props.host === LOCAL_API) { + await storage.store( + presaveAsset.assetType, + presaveAsset.dataFormat, + presaveAsset.data, + presaveAsset.assetId + ); + } + const item = await saveBackpackObject({ host: this.props.host, token: this.props.token, username: this.props.username, ...payload - })) - .then(item => { - this.setState({ - loading: false, - contents: [item].concat(this.state.contents) - }); - }) - .catch(error => { - this.handleError(error); }); + this.setState({ + loading: false, + contents: [item].concat(this.state.contents) + }); + } catch (error) { + this.handleError(error); + } }); } handleDelete (id) { diff --git a/src/containers/tw-security-manager.jsx b/src/containers/tw-security-manager.jsx index cb0ff38b2f2..f07357042f1 100644 --- a/src/containers/tw-security-manager.jsx +++ b/src/containers/tw-security-manager.jsx @@ -150,6 +150,16 @@ let rememberedExtensionInfo = { */ const notAllowedToAskUnsandbox = new Set(); +const resetPermissions = () => { + fetchHostsTrustedByUser.clear(); + embedHostsTrustedByUser.clear(); + allowedAudio = false; + allowedVideo = false; + allowedReadClipboard = false; + allowedNotify = false; + allowedGeolocation = false; +}; + const SECURITY_MANAGER_METHODS = [ 'getSandboxMode', 'canLoadExtensionFromProject', @@ -173,11 +183,16 @@ class TWSecurityManagerComponent extends React.Component { bindAll(this, [ 'handleAllowed', 'handleDenied', + 'handleRuntimeDisposed', 'projectWillChange' ]); bindAll(this, SECURITY_MANAGER_METHODS); this.nextModalCallbacks = []; this.modalLocked = false; + /** + * Incremented each time previous project disposed. + */ + this.generation = 0; this.state = { type: null, data: null, @@ -200,13 +215,24 @@ class TWSecurityManagerComponent extends React.Component { for (const method of SECURITY_MANAGER_METHODS) { vmSecurityManager[method] = propsSecurityManager[method] || this[method]; } - + this.props.vm.runtime.on('RUNTIME_DISPOSED', this.handleRuntimeDisposed); this.props.vm.runtime.on('RUNTIME_DISPOSED', this.projectWillChange); } - componentWillUnmount() { + + componentWillUnmount () { + this.props.vm.runtime.off('RUNTIME_DISPOSED', this.handleRuntimeDisposed); this.props.vm.runtime.off('RUNTIME_DISPOSED', this.projectWillChange); } + handleRuntimeDisposed () { + resetPermissions(); + this.generation++; + if (this.state.type) { + this.state.callback(false); + } + + } + // eslint-disable-next-line valid-jsdoc /** * @returns {Promise<() => Promise>} Resolves with a function that you can call to show the modal. @@ -218,6 +244,8 @@ class TWSecurityManagerComponent extends React.Component { // with just one click. This means that some places have to wait until previous modals are // closed before it knows if it needs to display another modal. + const generation = this.generation; + if (this.modalLocked) { await new Promise(resolve => { this.nextModalCallbacks.push(resolve); @@ -240,6 +268,11 @@ class TWSecurityManagerComponent extends React.Component { }; const showModal = async (type, data) => { + if (generation !== this.generation) { + // Request is from a previous project. + releaseLock(); + return false; + } const result = await new Promise(resolve => { this.setState(oldState => ({ type, @@ -249,7 +282,7 @@ class TWSecurityManagerComponent extends React.Component { })); }); releaseLock(); - return result; + return result && generation === this.generation; }; return { @@ -550,7 +583,10 @@ class TWSecurityManagerComponent extends React.Component { TWSecurityManagerComponent.propTypes = { vm: PropTypes.shape({ - runtime: PropTypes.any.isRequired, + runtime: PropTypes.shape({ + on: PropTypes.func.isRequired, + off: PropTypes.func.isRequired + }).isRequired, extensionManager: PropTypes.shape({ securityManager: PropTypes.shape( SECURITY_MANAGER_METHODS.reduce((obj, method) => { diff --git a/src/lib/audio/shared-audio-context.js b/src/lib/audio/shared-audio-context.js index d42856d78f1..f1f071c9c68 100644 --- a/src/lib/audio/shared-audio-context.js +++ b/src/lib/audio/shared-audio-context.js @@ -1,34 +1,35 @@ import StartAudioContext from '@turbowarp/startaudiocontext'; -import bowser from 'bowser'; import log from '../log'; let 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); +/** + * AudioContext can be initialized only when user interaction event happens + */ +const event = + typeof document.ontouchstart === 'undefined' ? + 'mousedown' : + 'touchstart'; +const initAudioContext = () => { + document.removeEventListener(event, initAudioContext); + if (AUDIO_CONTEXT) return; - try { - AUDIO_CONTEXT = new (window.AudioContext || window.webkitAudioContext)(); - StartAudioContext(AUDIO_CONTEXT); - } catch (e) { - log.error('could not create shared audio context; sound-related features will not be available', e); - } - }; - document.addEventListener(event, initAudioContext); -} + try { + AUDIO_CONTEXT = new (window.AudioContext || window.webkitAudioContext)(); + StartAudioContext(AUDIO_CONTEXT); + } catch (e) { + log.error('could not create shared audio context; sound-related features will not be available', e); + } +}; +document.addEventListener(event, initAudioContext); /** * Wrap browser AudioContext because we shouldn't create more than one * @return {AudioContext} The singleton AudioContext */ export default function () { + if (!AUDIO_CONTEXT) { + initAudioContext(); + } return AUDIO_CONTEXT; } diff --git a/src/lib/backpack/sprite-payload.js b/src/lib/backpack/sprite-payload.js index ce539005a44..b0c1f054b6b 100644 --- a/src/lib/backpack/sprite-payload.js +++ b/src/lib/backpack/sprite-payload.js @@ -2,7 +2,7 @@ import createThumbnail from './thumbnail'; const spritePayload = (id, vm) => { const target = vm.runtime.getTargetById(id); - if (!target) return null; + if (!target) return Promise.reject(new Error(`Sprite ${id} does not exist`)); return vm.exportSprite( id, diff --git a/src/lib/tw-local-backpack-api.js b/src/lib/tw-local-backpack-api.js index a7960490ca6..d3c8035e1b1 100644 --- a/src/lib/tw-local-backpack-api.js +++ b/src/lib/tw-local-backpack-api.js @@ -86,37 +86,68 @@ const openDB = () => new Promise((resolve, reject) => { }; }); +/** + * Run a transaction on the backpack store. + * @param {IDBTransactionMode} mode readonly or readwrite + * @param {string} errorDescription For error messages + * @param {(store: IDBObjectStore, withErrorHandling: () => void): void} callback What to run + * @returns {Promise} Resolves when the transaction commits. + */ +const runTransaction = async (mode, errorDescription, callback) => { + const db = await openDB(); + return new Promise((resolve, reject) => { + let handledError = null; + const transaction = db.transaction(STORE_NAME, mode); + + transaction.oncomplete = () => { + resolve(); + }; + + transaction.onabort = () => { + reject(handledError || new Error(`${errorDescription}: ${transaction.error}`)); + }; + + const withErrorHandling = fn => (...args) => { + try { + fn(...args); + } catch (e) { + handledError = e; + transaction.abort(); + } + }; + + const wrappedCallback = withErrorHandling(callback); + wrappedCallback(transaction.objectStore(STORE_NAME), withErrorHandling); + }); +}; + const getBackpackContents = async ({ limit, offset }) => { - const db = await openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readonly'); - transaction.onerror = event => { - reject(new Error(`Getting contents: ${event.target.error}`)); - }; - const store = transaction.objectStore(STORE_NAME); - const items = []; + const idbItems = []; + await runTransaction('readonly', 'Getting contents', (store, withErrorHandling) => { const request = store.openCursor(null, 'prev'); let first = true; - request.onsuccess = e => { - const cursor = e.target.result; + request.onsuccess = withErrorHandling(() => { + const cursor = request.result; + if (!cursor) { + return; + } if (first) { first = false; - if (cursor && offset !== 0) { + if (offset !== 0) { cursor.advance(offset); return; } } - if (cursor && items.length < limit) { - items.push(idbItemToBackpackItem(cursor.value)); + if (idbItems.length < limit) { + idbItems.push(cursor.value); cursor.continue(); - } else { - resolve(items); } - }; + }); }); + return idbItems.map(idbItemToBackpackItem); }; const saveBackpackObject = async ({ @@ -129,47 +160,33 @@ const saveBackpackObject = async ({ // User interaction -- fine to show a permission dialog requestPersistentStorage(); - const db = await openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readwrite'); - transaction.onerror = event => { - reject(new Error(`Sving object: ${event.target.error}`)); - }; - const store = transaction.objectStore(STORE_NAME); - const bodyData = base64ToArrayBuffer(body); - const bodyMD5 = md5(bodyData); - const idbItem = { - type, - mime, - name, - bodyData, - bodyMD5, - thumbnailData: base64ToArrayBuffer(thumbnail) - }; + const bodyData = base64ToArrayBuffer(body); + const idbItem = { + type, + mime, + name, + bodyData, + bodyMD5: md5(bodyData), + thumbnailData: base64ToArrayBuffer(thumbnail) + }; + let backpackItem; + await runTransaction('readwrite', 'Saving object', (store, withErrorHandling) => { const putRequest = store.put(idbItem); - putRequest.onsuccess = () => { + putRequest.onsuccess = withErrorHandling(() => { idbItem.id = putRequest.result; - resolve(idbItemToBackpackItem(idbItem)); - }; + backpackItem = idbItemToBackpackItem(idbItem); + }); }); + return backpackItem; }; const deleteBackpackObject = async ({ id }) => { + // Convert string IDs to number IDs id = +id; - const db = await openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readwrite'); - transaction.onerror = event => { - reject(new Error(`Deleting object: ${event.target.error}`)); - }; - const store = transaction.objectStore(STORE_NAME); - // Convert string IDs to number IDs - const deleteRequest = store.delete(id); - deleteRequest.onsuccess = () => { - resolve(); - }; + await runTransaction('readwrite', 'Deleting object', store => { + store.delete(id); }); }; @@ -178,25 +195,21 @@ const updateBackpackObject = async ({ name }) => { id = +id; - const db = await openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction(STORE_NAME, 'readwrite'); - transaction.onerror = event => { - reject(new Error(`Updating object: ${event.target.error}`)); - }; - const store = transaction.objectStore(STORE_NAME); + let backpackItem; + await runTransaction('readwrite', 'Updating object', (store, withErrorHandling) => { const getRequest = store.get(id); - getRequest.onsuccess = () => { + getRequest.onsuccess = withErrorHandling(() => { const newItem = { ...getRequest.result, name: name }; const putRequest = store.put(newItem); - putRequest.onsuccess = () => { - resolve(idbItemToBackpackItem(newItem)); - }; - }; + putRequest.onsuccess = withErrorHandling(() => { + backpackItem = idbItemToBackpackItem(newItem); + }); + }); }); + return backpackItem; }; export default { diff --git a/src/lib/tw-translations/generated-translations.json b/src/lib/tw-translations/generated-translations.json index 8effa6da70b..c1e6d633c15 100644 --- a/src/lib/tw-translations/generated-translations.json +++ b/src/lib/tw-translations/generated-translations.json @@ -2218,13 +2218,17 @@ "tw.blocks.mouseButton.secondary": "(2) 오른쪽", "tw.blocks.openDocs": "문서 열기", "tw.browserModal.desktopSettings": "데스크톱 설정 열기", + "tw.browserModal.desktopTitle": "지원되지 않는 실행 환경입니다", + "tw.browserModal.webgl1": "{APP_NAME}을(를) 실행하려면 WebGL을 지원해야 합니다. 현재 실행 환경에서는 WebGL이 지원되지 않습니다. 이 오류는 일시적으로 발생할 수 있으며 컴퓨터를 다시 시작하여 해결할 수 있습니다.", + "tw.browserModal.webglBrowser": "최신 버전의 웹 브라우저를 사용하고 그래픽카드 드라이버를 최신 버전으로 업데이트하시기 바랍니다. 또는 웹 브라우저의 [그래픽 가속] 혹은 [하드웨어 가속] 설정을 변경하여 문제를 해결할 수 있습니다.", + "tw.browserModal.webglDesktop": "또는 데스크톱 앱의 [그래픽 가속] 설정을 변경하여 문제를 해결할 수 있습니다.", "tw.cantUseCloud": "클라우드 변수를 생성할 수는 있으나, 해당 프로젝트가 Scratch에 업로드 하거나, {packager}와 같은 도구를 통해 변환하지 않는 한 클라우드 변수는 작동하지 않습니다.", "tw.changeUsername.cannotChangeWhileRunning": "'사용자 이름'은 프로젝트가 실행되는 동안 변경할 수 없습니다.", "tw.clipboard.danger": "클립보드에 비밀번호와 같은 민감한 정보가 포함되어 있을 경우, 프로젝트에 의해 해당 정보가 다른 사용자 또는 서버와 공유될 수 있습니다.", "tw.clipboard.permission": "일부 브라우저에서는 클립보드에 접근이 불가능할 수 있습니다. 권한을 허용할 경우, 추가적인 클립보드 읽기 요청이 자동으로 허용됩니다.", "tw.clipboard.title": "프로젝트가 클립보드의 데이터 읽기 권한을 요청합니다.", "tw.cloudProvider": "서버 제공자 : {name}", - "tw.cloudServers": "거주지 근처에 있는 서버를 선택하세요 :", + "tw.cloudServers": "가까운 지역의 서버를 선택해주세요 :", "tw.code": "소스코드", "tw.confirmIncompatibleExtension": "이 확장 기능은 스크래치와 호환되지 않습니다.\n해당 확장 기능을 활성화할 경우 프로젝트가 스크래치 웹사이트에 업로드되지 않을 수 있습니다. 계속하시겠습니까?", "tw.createdBy": "제작자 : ", diff --git a/src/playground/load-service-worker.js b/src/playground/load-service-worker.js index d7971f99045..0b888b06204 100644 --- a/src/playground/load-service-worker.js +++ b/src/playground/load-service-worker.js @@ -9,7 +9,15 @@ const actuallyLoadServiceWorker = () => { }); }; const loadServiceWorker = () => { - if (process.env.ENABLE_SERVICE_WORKER && 'serviceWorker' in navigator && !loaded) { + if ( + process.env.ENABLE_SERVICE_WORKER && + // there are weird browser extensions / configuration people can apply that leave: + // `'serviceWorker' in navigator` as true, but register() is not a function. so we should guard + // against those weird scenarios. Example is Windscribe's "Worker Block" option. + navigator.serviceWorker && + typeof navigator.serviceWorker.register === 'function' && + !loaded + ) { loaded = true; if (document.readyState === 'complete') { actuallyLoadServiceWorker(); diff --git a/test/.eslintrc.js b/test/.eslintrc.js deleted file mode 100644 index be26685849d..00000000000 --- a/test/.eslintrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - extends: ['scratch/react', 'scratch/es6', 'plugin:jest/recommended'], - env: { - browser: true, - jest: true - }, - plugins: ['jest'], - rules: { - 'react/prop-types': 0 - } -}; diff --git a/test/__mocks__/audio-buffer-player.js b/test/__mocks__/audio-buffer-player.js deleted file mode 100644 index 560bfb2d2f1..00000000000 --- a/test/__mocks__/audio-buffer-player.js +++ /dev/null @@ -1,15 +0,0 @@ -export default class MockAudioBufferPlayer { - constructor (samples, sampleRate) { - this.samples = samples; - this.sampleRate = sampleRate; - this.buffer = { - getChannelData: jest.fn(() => samples), - sampleRate: sampleRate - }; - this.play = jest.fn((trimStart, trimEnd, onUpdate) => { - this.onUpdate = onUpdate; - }); - this.stop = jest.fn(); - MockAudioBufferPlayer.instance = this; - } -} diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js deleted file mode 100644 index 9597eec1be0..00000000000 --- a/test/__mocks__/audio-effects.js +++ /dev/null @@ -1,24 +0,0 @@ -export default class MockAudioEffects { - static get effectTypes () { // @todo can this be imported from the real file? - return { - ROBOT: 'robot', - REVERSE: 'reverse', - LOUDER: 'higher', - SOFTER: 'lower', - FASTER: 'faster', - SLOWER: 'slower', - ECHO: 'echo' - }; - } - constructor (buffer, name) { - this.buffer = buffer; - this.name = name; - this.process = jest.fn(done => { - this._finishProcessing = renderedBuffer => { - done(renderedBuffer, 0, 1); - return new Promise(resolve => setTimeout(resolve)); - }; - }); - MockAudioEffects.instance = this; - } -} diff --git a/test/__mocks__/editor-msgs-mock.js b/test/__mocks__/editor-msgs-mock.js deleted file mode 100644 index 02ae1d6baaf..00000000000 --- a/test/__mocks__/editor-msgs-mock.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - en: {} -}; diff --git a/test/__mocks__/fileMock.js b/test/__mocks__/fileMock.js deleted file mode 100644 index 59890f6a201..00000000000 --- a/test/__mocks__/fileMock.js +++ /dev/null @@ -1,3 +0,0 @@ -// __mocks__/fileMock.js - -module.exports = 'test-file-stub'; diff --git a/test/__mocks__/styleMock.js b/test/__mocks__/styleMock.js deleted file mode 100644 index d988e23b739..00000000000 --- a/test/__mocks__/styleMock.js +++ /dev/null @@ -1,3 +0,0 @@ -// __mocks__/styleMock.js - -module.exports = {}; diff --git a/test/addons-settings.test.mjs b/test/addons-settings.test.mjs new file mode 100644 index 00000000000..87a31ff1138 --- /dev/null +++ b/test/addons-settings.test.mjs @@ -0,0 +1,707 @@ +import {test, beforeEach, mock} from 'node:test'; +import assert from 'node:assert/strict'; +import SettingStore from '../src/addons/settings-store.js'; +import upstreamMeta from '../src/addons/generated/upstream-meta.json' with {type: 'json'}; + +class LocalStorageShim { + constructor () { + this.storage = Object.create(null); + } + getItem (key) { + return this.storage[key]; + } + setItem (key, value) { + this.storage[key] = value.toString(); + } +} + +beforeEach(() => { + globalThis.localStorage = new LocalStorageShim(); +}); + +const lightTheme = { + isDark: () => false +}; + +const darkTheme = { + isDark: () => true +}; + +test('enabled, event', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + assert.equal(store.getAddonEnabled('editor-devtools'), true); + assert.equal('enabled' in store.store['editor-devtools'], false); + store.setAddonEnabled('editor-devtools', false); + assert.equal(store.getAddonEnabled('editor-devtools'), false); + assert.equal('enabled' in store.store['editor-devtools'], true); + store.setAddonEnabled('editor-devtools', true); + store.setAddonEnabled('cat-blocks', true); + assert.equal('enabled' in store.store['cat-blocks'], true); + store.setAddonEnabled('cat-blocks', null); + assert.equal('enabled' in store.store['cat-blocks'], false); + assert.equal(fn.mock.callCount(), 4); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'editor-devtools'); + assert.equal(fn.mock.calls[0].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[0].arguments[0].detail.value, false); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'editor-devtools'); + assert.equal(fn.mock.calls[1].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[1].arguments[0].detail.value, true); + assert.equal(fn.mock.calls[2].arguments[0].detail.addonId, 'cat-blocks'); + assert.equal(fn.mock.calls[2].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[2].arguments[0].detail.value, true); + assert.equal(fn.mock.calls[3].arguments[0].detail.addonId, 'cat-blocks'); + assert.equal(fn.mock.calls[3].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[3].arguments[0].detail.value, false); +}); + +test('settings, event, default values', () => { + const store = new SettingStore(); + const fn = mock.fn(); + assert.equal(store.getAddonSetting('onion-skinning', 'default'), false); + assert.equal('default' in store.store['onion-skinning'], false); + store.addEventListener('setting-changed', fn); + store.setAddonSetting('onion-skinning', 'default', true); + assert.equal(store.getAddonSetting('onion-skinning', 'default'), true); + assert.equal('default' in store.store['onion-skinning'], true); + store.setAddonSetting('onion-skinning', 'default', null); + assert.equal('default' in store.store['onion-skinning'], false); + assert.equal(store.getAddonSetting('onion-skinning', 'default'), false); + assert.equal(fn.mock.callCount(), 2); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[0].arguments[0].detail.settingId, 'default'); + assert.equal(fn.mock.calls[0].arguments[0].detail.value, true); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[1].arguments[0].detail.settingId, 'default'); + assert.equal(fn.mock.calls[1].arguments[0].detail.value, false); +}); + +test('no actual change emits no event', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + for (let i = 0; i < 5; i++) store.setAddonEnabled('cat-blocks', true); + assert.equal(fn.mock.callCount(), 1); + for (let i = 0; i < 5; i++) store.setAddonEnabled('cat-blocks', false); + assert.equal(fn.mock.callCount(), 2); + for (let i = 0; i < 5; i++) store.setAddonSetting('onion-skinning', 'default', true); + assert.equal(fn.mock.callCount(), 3); + for (let i = 0; i < 5; i++) store.setAddonSetting('onion-skinning', 'default', false); + assert.equal(fn.mock.callCount(), 4); +}); + +test('changing enabled throws on unknown addons', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + assert.throws(() => store.setAddonEnabled('egriohergoijergijregojiergdfoijre', true)); + assert.equal(fn.mock.callCount(), 0); +}); + +test('changing settings throws on unknown settings', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + assert.throws(() => store.setAddonSetting('onion-skinning', 'sdlkjfslkdjfljksd', true)); + assert.throws(() => store.setAddonSetting('ergfoijgi', 'sdflkjsfdlkj', true)); + assert.equal(fn.mock.callCount(), 0); +}); + +test('changing enabled throws on invalid values', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + assert.throws(() => store.setAddonEnabled('cat-blocks', 'sdfjlksdflk')); + assert.throws(() => store.setAddonEnabled('cat-blocks', 0)); + assert.throws(() => store.setAddonEnabled('cat-blocks', [])); + assert.throws(() => store.setAddonEnabled('cat-blocks', {})); + assert.equal(fn.mock.callCount(), 0); +}); + +test('changing settings checks value validity and throws', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + // boolean + assert.throws(() => store.setAddonSetting('onion-skinning', 'default', '#abcdef')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'default', [])); + assert.throws(() => store.setAddonSetting('onion-skinning', 'default', {})); + assert.throws(() => store.setAddonSetting('onion-skinning', 'default', '')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'default', 1)); + // integer + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', '#abcdef')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', [])); + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', {})); + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', '')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', '3')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'next', false)); + // select + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', '#abcdef')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', [])); + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', {})); + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', '')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', false)); + assert.throws(() => store.setAddonSetting('onion-skinning', 'mode', 1)); + assert.doesNotThrow(() => store.setAddonSetting('onion-skinning', 'mode', 'tint')); + assert.doesNotThrow(() => store.setAddonSetting('onion-skinning', 'mode', 'merge')); + // color + assert.doesNotThrow(() => store.setAddonSetting('onion-skinning', 'beforeTint', '#abcdef')); + assert.doesNotThrow(() => store.setAddonSetting('onion-skinning', 'beforeTint', '#abcDE1')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'beforeTint', [])); + assert.throws(() => store.setAddonSetting('onion-skinning', 'beforeTint', {})); + assert.throws(() => store.setAddonSetting('onion-skinning', 'beforeTint', '')); + assert.throws(() => store.setAddonSetting('onion-skinning', 'beforeTint', false)); + assert.throws(() => store.setAddonSetting('onion-skinning', 'beforeTint', 1)); + assert.equal(fn.mock.callCount(), 4); +}); + +test('colors with alpha channel', () => { + const store = new SettingStore(); + store.setAddonSetting('onion-skinning', 'beforeTint', '#123456'); + assert.equal(store.getAddonSetting('onion-skinning', 'beforeTint'), '#123456'); + store.setAddonSetting('onion-skinning', 'beforeTint', '#234567ff'); + assert.equal(store.getAddonSetting('onion-skinning', 'beforeTint'), '#234567'); + store.setAddonSetting('onion-skinning', 'beforeTint', '#abc67800'); + assert.equal(store.getAddonSetting('onion-skinning', 'beforeTint'), '#abc678'); + store.import({ + addons: { + 'onion-skinning': { + settings: { + beforeTint: '#56789aff' + } + } + } + }); + assert.equal(store.getAddonSetting('onion-skinning', 'beforeTint'), '#56789a'); +}); + +test('reset does not change enabled', () => { + const store = new SettingStore(); + store.setAddonEnabled('cat-blocks', true); + store.resetAddon('cat-blocks'); + assert.equal(store.getAddonEnabled('cat-blocks'), true); +}); + +test('reset settings, event', () => { + const store = new SettingStore(); + store.setAddonSetting('onion-skinning', 'default', true); + store.setAddonSetting('onion-skinning', 'next', 3); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + store.resetAddon('onion-skinning'); + assert.equal(store.getAddonSetting('onion-skinning', 'default'), false); + assert.equal(store.getAddonSetting('onion-skinning', 'next'), 0); + assert.equal(fn.mock.callCount(), 2); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[0].arguments[0].detail.settingId, 'default'); + assert.equal(fn.mock.calls[0].arguments[0].detail.value, false); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[1].arguments[0].detail.settingId, 'next'); + assert.equal(fn.mock.calls[1].arguments[0].detail.value, 0); +}); + +test('reset all addons', () => { + const store = new SettingStore(); + store.setAddonEnabled('cat-blocks', true); + store.setAddonSetting('onion-skinning', 'default', true); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + store.resetAllAddons(); + assert.equal(fn.mock.callCount(), 2); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'cat-blocks'); + assert.equal(fn.mock.calls[0].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[0].arguments[0].detail.value, false); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[1].arguments[0].detail.settingId, 'default'); + assert.equal(fn.mock.calls[1].arguments[0].detail.value, false); +}); + +test('apply preset', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + store.setAddonSetting('editor-theme3', 'motion-color', '#abcdef'); + store.applyAddonPreset('editor-theme3', 'original'); + assert.ok(fn.mock.callCount() > 5); + assert.equal(store.getAddonSetting('editor-theme3', 'motion-color'), '#4a6cd4'); + // TODO: test that settings not specified in the preset don't change +}); + +test('unknown preset throws', () => { + const store = new SettingStore(); + const fn = mock.fn(); + store.addEventListener('setting-changed', fn); + assert.throws(() => store.applyAddonPreset('alksdfjlksdf', 'jksdflkjsdf')); + assert.throws(() => store.applyAddonPreset('editor-theme3', 'jksdflkjsdf')); + assert.equal(fn.mock.callCount(), 0); +}); + +test('export core', () => { + const store = new SettingStore(); + const exported = store.export({theme: lightTheme}); + assert.match(exported.core.version, /tw/); + assert.equal(exported.core.lightTheme, true); + const dark = store.export({theme: darkTheme}); + assert.equal(dark.core.lightTheme, false); +}); + +test('export settings', () => { + const store = new SettingStore(); + let exported = store.export({theme: lightTheme}); + assert.equal(exported.addons['remove-sprite-confirm'].enabled, false); + assert.deepEqual(exported.addons['remove-sprite-confirm'].settings, {}); + assert.equal(exported.addons['onion-skinning'].enabled, true); + assert.deepEqual(exported.addons['onion-skinning'].settings.default, false); + store.setAddonEnabled('remove-sprite-confirm', true); + store.setAddonSetting('onion-skinning', 'default', true); + exported = store.export({theme: lightTheme}); + assert.equal(exported.addons['remove-sprite-confirm'].enabled, true); + assert.deepEqual(exported.addons['remove-sprite-confirm'].settings, {}); + assert.equal(exported.addons['onion-skinning'].enabled, true); + assert.deepEqual(exported.addons['onion-skinning'].settings.default, true); +}); + +test('export theme', () => { + const store = new SettingStore(); + const exported = store.export({theme: lightTheme}); + assert.equal(exported.core.lightTheme, true); + const exported2 = store.export({theme: darkTheme}); + assert.equal(exported2.core.lightTheme, false); +}); + +test('import, event', () => { + const store = new SettingStore(); + store.setAddonEnabled('onion-skinning', false); + store.setAddonSetting('onion-skinning', 'next', 5); + const newStore = new SettingStore(); + newStore.setAddonSetting('onion-skinning', 'next', 10); + const fn = mock.fn(); + newStore.addEventListener('setting-changed', fn); + newStore.import(store.export({theme: lightTheme})); + assert.equal(newStore.getAddonEnabled('onion-skinning'), false); + assert.equal(newStore.getAddonSetting('onion-skinning', 'next'), 5); + assert.equal(fn.mock.callCount(), 2); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[0].arguments[0].detail.settingId, 'enabled'); + assert.equal(fn.mock.calls[0].arguments[0].detail.value, false); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'onion-skinning'); + assert.equal(fn.mock.calls[1].arguments[0].detail.settingId, 'next'); + assert.equal(fn.mock.calls[1].arguments[0].detail.value, 5); +}); + +test('export is identical after import', () => { + const store = new SettingStore(); + const fn = mock.fn(); + const exported = store.export({theme: lightTheme}); + store.import(exported); + assert.equal(fn.mock.callCount(), 0); + assert.deepEqual(store.export({theme: lightTheme}), exported); +}); + +test('import format', () => { + const store = new SettingStore(); + store.setAddonEnabled('cat-blocks', true); + store.import({ + core: { + version: 'lksd', + lightTheme: false + }, + addons: { + 'onion-skinning': { + enabled: false, + settings: { + next: 7 + } + } + } + }); + assert.equal(store.getAddonEnabled('onion-skinning'), false); + assert.equal(store.getAddonSetting('onion-skinning', 'next'), 7); + assert.equal(store.getAddonEnabled('cat-blocks'), true); +}); + +test('invalid imports', () => { + const store = new SettingStore(); + assert.doesNotThrow(() => store.import({ + addons: {} + })); + assert.doesNotThrow(() => store.import({ + addons: { + 'onion-skinning': { + enabled: false, + settings: { + dsjfokosdfj: 5 + } + } + } + })); + assert.doesNotThrow(() => store.import({ + addons: { + grfdjiklk: { + enabled: true, + settings: {} + } + } + })); + assert.doesNotThrow(() => store.import({ + addons: { + 'onion-skinning': { + enabled: '4', + settings: { + default: '3' + } + } + } + })); + assert.equal(store.getAddonEnabled('onion-skinning'), false); + assert.equal(store.getAddonSetting('onion-skinning', 'default'), false); +}); + +test('local storage', () => { + const store = new SettingStore(); + store.setAddonEnabled('cat-blocks', true); + store.setAddonSetting('onion-skinning', 'default', true); + const newStore = new SettingStore(); + newStore.readLocalStorage(); + assert.deepEqual(newStore.store, store.store); +}); + +test('local storage is resistent to errors', () => { + globalThis.localStorage = new LocalStorageShim(); + const store = new SettingStore(); + localStorage.getItem = () => { + throw new Error(':('); + }; + store.readLocalStorage(); + localStorage.getItem = () => 'eoiru4jtg)(R('; + store.readLocalStorage(); + localStorage.setItem = () => { + throw new Error(':('); + }; + store.setAddonEnabled('cat-blocks', true); + // eslint-disable-next-line no-undefined + globalThis.localStorage = undefined; + store.readLocalStorage(); + store.setAddonEnabled('cat-blocks', false); +}); + +test('setStore diffing', () => { + const settingsStore = new SettingStore(); + const pageStore = new SettingStore(); + settingsStore.setAddonEnabled('editor-devtools', false); + pageStore.setAddonEnabled('editor-devtools', false); + const fn = mock.fn(); + pageStore.addEventListener('addon-changed', fn); + pageStore.setStore(settingsStore.store); + assert.equal(fn.mock.callCount(), 0); + settingsStore.setAddonEnabled('editor-devtools', true); + settingsStore.setAddonSetting('onion-skinning', 'next', 10); + pageStore.setStore(settingsStore.store); + assert.equal(fn.mock.callCount(), 2); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'editor-devtools'); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'onion-skinning'); +}); + +test('setStore dynamic enable/disable', () => { + const settingsStore = new SettingStore(); + const pageStore = new SettingStore(); + settingsStore.setAddonEnabled('block-palette-icons', false); + pageStore.setStore(settingsStore.store); + const fn = mock.fn(); + pageStore.addEventListener('addon-changed', fn); + settingsStore.setAddonEnabled('block-palette-icons', true); + pageStore.setStore(settingsStore.store); + assert.equal(fn.mock.calls[0].arguments[0].detail.addonId, 'block-palette-icons'); + assert.equal(fn.mock.calls[0].arguments[0].detail.dynamicEnable, true); + assert.equal(fn.mock.calls[0].arguments[0].detail.dynamicDisable, false); + settingsStore.setAddonEnabled('block-palette-icons', false); + pageStore.setStore(settingsStore.store); + assert.equal(fn.mock.calls[1].arguments[0].detail.addonId, 'block-palette-icons'); + assert.equal(fn.mock.calls[1].arguments[0].detail.dynamicEnable, false); + assert.equal(fn.mock.calls[1].arguments[0].detail.dynamicDisable, true); +}); + +test('setStore weird values', () => { + const settingsStore = new SettingStore(); + assert.equal(settingsStore.getAddonEnabled('pause'), true); + settingsStore.setAddonEnabled('pause', false); + settingsStore.setAddonEnabled('clones', true); + settingsStore.setStore({ + invalid0: {}, + invalid1: null, + pause: null + }); + assert.equal(settingsStore.getAddonEnabled('pause'), false); +}); + +test('resetting an addon through setStore', () => { + const store = new SettingStore(); + assert.equal(store.getAddonSetting('custom-block-shape', 'paddingSize'), 100); + store.setAddonSetting('custom-block-shape', 'paddingSize', 50); + assert.equal(store.getAddonSetting('custom-block-shape', 'paddingSize'), 50); + const store2 = new SettingStore(); + store.setStore(store2.store); + assert.equal(store.getAddonSetting('custom-block-shape', 'paddingSize'), 100); +}); + +test('setStoreWithVersionCheck', () => { + const store = new SettingStore(); + store.setStore = mock.fn(); + store.setStoreWithVersionCheck({ + store: '1234', + version: upstreamMeta.commit + }); + assert.equal(store.setStore.mock.callCount(), 1); + assert.deepEqual(store.setStore.mock.calls[0].arguments, ['1234']); + store.setStore = mock.fn(); + store.setStoreWithVersionCheck({ + store: '1234', + version: 'something invalid' + }); + assert.equal(store.setStore.mock.callCount(), 0); +}); + +test('parseUrlParameter', () => { + const store = new SettingStore(); + assert.equal(store.getAddonEnabled('pause'), true); + assert.equal(store.getAddonEnabled('mute-project'), true); + assert.equal(store.getAddonEnabled('remove-curved-stage-border'), false); + assert.equal(store.remote, false); + store.parseUrlParameter('pause,remove-curved-stage-border,,invalid addon??43t987(*&$'); + assert.equal(store.getAddonEnabled('pause'), true); + assert.equal(store.getAddonEnabled('mute-project'), false); + assert.equal(store.getAddonEnabled('remove-curved-stage-border'), true); + assert.equal(store.remote, true); +}); + +test('Settings migration 1 -> 2', () => { + const store = new SettingStore(); + + // eslint-disable-next-line max-len + globalThis.localStorage.getItem = () => `{"_":1,"tw-project-info":{"enabled":false},"tw-interface-customization":{"enabled":false,"removeFeedback":true,"removeBackpack":true}}`; + store.readLocalStorage(); + assert.equal(store.getAddonEnabled('block-count'), false); + assert.equal(store.getAddonEnabled('tw-remove-backpack'), false); + assert.equal(store.getAddonEnabled('tw-remove-feedback'), false); + + // eslint-disable-next-line max-len + globalThis.localStorage.getItem = () => `{"_":1,"tw-project-info":{"enabled":true},"tw-interface-customization":{"enabled":true,"removeFeedback":true,"removeBackpack":true}}`; + store.readLocalStorage(); + assert.equal(store.getAddonEnabled('block-count'), true); + assert.equal(store.getAddonEnabled('tw-remove-backpack'), true); + assert.equal(store.getAddonEnabled('tw-remove-feedback'), true); +}); + +test('Settings migration 2 -> 3', () => { + const store = new SettingStore(); + + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 2, + 'hide-flyout': { + enabled: true + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('hide-flyout', 'toggle'), 'hover'); +}); + +test('Settings migration 3 -> 4', () => { + const store = new SettingStore(); + + globalThis.localStorage.getItem = () => JSON.stringify({ + _: 3 + }); + store.readLocalStorage(); + assert.equal(store.getAddonEnabled('editor-devtools'), true); + assert.equal(store.getAddonEnabled('find-bar'), true); + assert.equal(store.getAddonEnabled('middle-click-popup'), true); + + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 3, + 'editor-devtools': { + enabled: false + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonEnabled('editor-devtools'), false); + assert.equal(store.getAddonEnabled('find-bar'), false); + assert.equal(store.getAddonEnabled('middle-click-popup'), false); +}); + +test('if', () => { + const store = new SettingStore(); + store.setAddonEnabled('editor-devtools', true); + store.setAddonEnabled('onion-skinning', false); + store.setAddonSetting('editor-theme3', 'motion-color', '#000000'); + store.setAddonSetting('editor-theme3', 'looks-color', '#FFFFFF'); + + // eslint-disable-next-line no-undefined + assert.equal(store.evaluateCondition('editor-theme3', undefined), true); + assert.equal(store.evaluateCondition('editor-theme3', null), true); + assert.equal(store.evaluateCondition('editor-theme3', {}), true); + + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['onion-skinning'] + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: 'onion-skinning' + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['editor-devtools'] + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: 'editor-devtools' + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['editor-devtools', 'onion-skinning'] + }), true); + + assert.equal(store.evaluateCondition('editor-theme3', { + settings: {} + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'motion-color': '#000000' + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': '#FFFFFF' + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': '#FFFFFE' + } + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'motion-color': '#000000', + 'looks-color': '#FFFFFF' + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'motion-color': '#000001', + 'looks-color': '#FFFFFF' + } + }), false); + + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': ['#FFFFFF'] + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': ['#FFFFFE', '#FFFFFF'] + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': ['#FFFFFF', '#FFFFFE'] + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': ['#FFFFFE'] + } + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': ['#FFFFFE', '#FFFFFD'] + } + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + settings: { + 'looks-color': [] + } + }), false); + + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['editor-devtools'], + settings: { + 'motion-color': '#000000' + } + }), true); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['onion-skinning'], + settings: { + 'motion-color': '#000000' + } + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['editor-devtools'], + settings: { + 'motion-color': '#000001' + } + }), false); + assert.equal(store.evaluateCondition('editor-theme3', { + addonEnabled: ['onion-skinning'], + settings: { + 'motion-color': '#000001' + } + }), false); +}); + +test('Settings migration 4 -> 5', () => { + const store = new SettingStore(); + + // implied default settings + globalThis.localStorage.getItem = () => JSON.stringify({ + _: 4 + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'show'); + + // also implied default settings + globalThis.localStorage.getItem = () => JSON.stringify({ + _: 4, + fullscreen: {} + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'show'); + + // explicit default settings + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 4, + 'fullscreen': { + hideToolbar: false + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'show'); + + // explicit hide, implied default hover setting + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 4, + 'fullscreen': { + hideToolbar: true + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'hover'); + + // explicit hide and default hover + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 4, + 'fullscreen': { + hideToolbar: true, + hoverToolbar: true + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'hover'); + + // explicit hide, no hover + globalThis.localStorage.getItem = () => JSON.stringify({ + '_': 4, + 'fullscreen': { + hideToolbar: true, + hoverToolbar: false + } + }); + store.readLocalStorage(); + assert.equal(store.getAddonSetting('fullscreen', 'toolbar'), 'hide'); +}); diff --git a/test/fixtures/100-100.svg b/test/fixtures/100-100.svg deleted file mode 100644 index 108f7aa1bc9..00000000000 --- a/test/fixtures/100-100.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - Artboard - Created with Sketch. - - - - - - - - - - - - - - - diff --git a/test/fixtures/bmpfile.bmp b/test/fixtures/bmpfile.bmp deleted file mode 100644 index e96d271c315..00000000000 Binary files a/test/fixtures/bmpfile.bmp and /dev/null differ diff --git a/test/fixtures/corrupt-bmp.sb3 b/test/fixtures/corrupt-bmp.sb3 deleted file mode 100644 index 0753a9491b7..00000000000 Binary files a/test/fixtures/corrupt-bmp.sb3 and /dev/null differ diff --git a/test/fixtures/corrupt-bmp.sprite3 b/test/fixtures/corrupt-bmp.sprite3 deleted file mode 100644 index 9108c9ea046..00000000000 Binary files a/test/fixtures/corrupt-bmp.sprite3 and /dev/null differ diff --git a/test/fixtures/corrupt-from-scratch3.svg b/test/fixtures/corrupt-from-scratch3.svg deleted file mode 100644 index 15436df5a69..00000000000 --- a/test/fixtures/corrupt-from-scratch3.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/test/fixtures/corrupt-svg.sb2 b/test/fixtures/corrupt-svg.sb2 deleted file mode 100644 index 3b036a09b7d..00000000000 Binary files a/test/fixtures/corrupt-svg.sb2 and /dev/null differ diff --git a/test/fixtures/corrupt-svg.sb3 b/test/fixtures/corrupt-svg.sb3 deleted file mode 100644 index 52fb93096f0..00000000000 Binary files a/test/fixtures/corrupt-svg.sb3 and /dev/null differ diff --git a/test/fixtures/corrupt-svg.sprite3 b/test/fixtures/corrupt-svg.sprite3 deleted file mode 100644 index 06bb51aec7c..00000000000 Binary files a/test/fixtures/corrupt-svg.sprite3 and /dev/null differ diff --git a/test/fixtures/corrupted-svg.sprite2 b/test/fixtures/corrupted-svg.sprite2 deleted file mode 100644 index 0db0dd70b95..00000000000 Binary files a/test/fixtures/corrupted-svg.sprite2 and /dev/null differ diff --git a/test/fixtures/gh-3582-png.png b/test/fixtures/gh-3582-png.png deleted file mode 100644 index cc1f1464e95..00000000000 Binary files a/test/fixtures/gh-3582-png.png and /dev/null differ diff --git a/test/fixtures/missing-bmp.sb3 b/test/fixtures/missing-bmp.sb3 deleted file mode 100644 index c7d7fef9f8f..00000000000 Binary files a/test/fixtures/missing-bmp.sb3 and /dev/null differ diff --git a/test/fixtures/missing-bmp.sprite3 b/test/fixtures/missing-bmp.sprite3 deleted file mode 100644 index d7c94ca350f..00000000000 Binary files a/test/fixtures/missing-bmp.sprite3 and /dev/null differ diff --git a/test/fixtures/missing-sprite-svg.sb3 b/test/fixtures/missing-sprite-svg.sb3 deleted file mode 100644 index e0956c36f06..00000000000 Binary files a/test/fixtures/missing-sprite-svg.sb3 and /dev/null differ diff --git a/test/fixtures/missing-svg.sb2 b/test/fixtures/missing-svg.sb2 deleted file mode 100644 index 45daf2cc806..00000000000 Binary files a/test/fixtures/missing-svg.sb2 and /dev/null differ diff --git a/test/fixtures/missing-svg.sprite2 b/test/fixtures/missing-svg.sprite2 deleted file mode 100644 index c8e3732de77..00000000000 Binary files a/test/fixtures/missing-svg.sprite2 and /dev/null differ diff --git a/test/fixtures/missing-svg.sprite3 b/test/fixtures/missing-svg.sprite3 deleted file mode 100644 index 5cc0986898c..00000000000 Binary files a/test/fixtures/missing-svg.sprite3 and /dev/null differ diff --git a/test/fixtures/monitor-variable.sb3 b/test/fixtures/monitor-variable.sb3 deleted file mode 100644 index c1f0bc592ce..00000000000 Binary files a/test/fixtures/monitor-variable.sb3 and /dev/null differ diff --git a/test/fixtures/movie.wav b/test/fixtures/movie.wav deleted file mode 100644 index 79c10d21462..00000000000 Binary files a/test/fixtures/movie.wav and /dev/null differ diff --git a/test/fixtures/paddleball.gif b/test/fixtures/paddleball.gif deleted file mode 100644 index b6207c7e3d7..00000000000 Binary files a/test/fixtures/paddleball.gif and /dev/null differ diff --git a/test/fixtures/project1.sb3 b/test/fixtures/project1.sb3 deleted file mode 100644 index 747ecd37a3a..00000000000 Binary files a/test/fixtures/project1.sb3 and /dev/null differ diff --git a/test/fixtures/scratch2-corrupted.svg b/test/fixtures/scratch2-corrupted.svg deleted file mode 100644 index 478d9ced8e8..00000000000 --- a/test/fixtures/scratch2-corrupted.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/test/fixtures/sneaker.wav b/test/fixtures/sneaker.wav deleted file mode 100644 index 01c4a17597b..00000000000 Binary files a/test/fixtures/sneaker.wav and /dev/null differ diff --git a/test/helpers/enzyme-setup.js b/test/helpers/enzyme-setup.js deleted file mode 100644 index 5374f679d38..00000000000 --- a/test/helpers/enzyme-setup.js +++ /dev/null @@ -1,4 +0,0 @@ -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -Enzyme.configure({adapter: new Adapter()}); diff --git a/test/helpers/intl-helpers.jsx b/test/helpers/intl-helpers.jsx deleted file mode 100644 index 8c9a057b4e5..00000000000 --- a/test/helpers/intl-helpers.jsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Helpers for using enzyme and react-test-renderer with react-intl - * Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl - */ -import React from 'react'; -import renderer from 'react-test-renderer'; -import {IntlProvider, intlShape} from 'react-intl'; -import {mount, shallow} from 'enzyme'; - -const intlProvider = new IntlProvider({locale: 'en'}, {}); -const {intl} = intlProvider.getChildContext(); - -const nodeWithIntlProp = node => React.cloneElement(node, {intl}); - -const shallowWithIntl = (node, {context} = {}) => shallow( - nodeWithIntlProp(node), - { - context: Object.assign({}, context, {intl}) - } -); - -const mountWithIntl = (node, {context, childContextTypes} = {}) => mount( - nodeWithIntlProp(node), - { - context: Object.assign({}, context, {intl}), - childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes) - } -); - -// react-test-renderer component for use with snapshot testing -const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create( - {children} -); - -export { - componentWithIntl, - shallowWithIntl, - mountWithIntl -}; diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js deleted file mode 100644 index debef3c0dd6..00000000000 --- a/test/helpers/selenium-helper.js +++ /dev/null @@ -1,378 +0,0 @@ -jest.setTimeout(30000); // eslint-disable-line no-undef - -import bindAll from 'lodash.bindall'; -import 'chromedriver'; // register path -import webdriver from 'selenium-webdriver'; - -const {Button, By, until} = webdriver; - -const USE_HEADLESS = process.env.USE_HEADLESS !== 'no'; - -// The main reason for this timeout is so that we can control the timeout message and report details; -// if we hit the Jasmine default timeout then we get a terse message that we can't control. -// The Jasmine default timeout is 30 seconds so make sure this is lower. -const DEFAULT_TIMEOUT_MILLISECONDS = 20 * 1000; - -/** - * Add more debug information to an error: - * - Merge a causal error into an outer error with valuable stack information - * - Add the causal error's message to the outer error's message. - * - Add debug information from the web driver, if available. - * The outerError compensates for the loss of context caused by `regenerator-runtime`. - * @param {Error} outerError The error to embed the cause into. - * @param {Error} cause The "inner" error to embed. - * @param {webdriver.ThenableWebDriver} [driver] Optional driver to capture debug info from. - * @returns {Promise} The outerError, with the cause embedded. - */ -const enhanceError = async (outerError, cause, driver) => { - if (cause) { - // This is the official way to nest errors in modern Node.js, but Jest ignores this field. - // It's here in case a future version uses it, or in case the caller does. - outerError.cause = cause; - } - if (cause && cause.message) { - outerError.message += `\n${['Cause:', ...cause.message.split('\n')].join('\n ')}`; - } else { - outerError.message += '\nCause: unknown'; - } - if (driver) { - const url = await driver.getCurrentUrl(); - const title = await driver.getTitle(); - const pageSource = await driver.getPageSource(); - const browserLogEntries = await driver.manage() - .logs() - .get('browser'); - const browserLogText = browserLogEntries.map(entry => entry.message).join('\n'); - outerError.message += `\nBrowser URL: ${url}`; - outerError.message += `\nBrowser title: ${title}`; - outerError.message += `\nBrowser logs:\n*****\n${browserLogText}\n*****\n`; - outerError.message += `\nBrowser page source:\n*****\n${pageSource}\n*****\n`; - } - return outerError; -}; - -class SeleniumHelper { - constructor () { - bindAll(this, [ - 'clickText', - 'clickButton', - 'clickXpath', - 'clickBlocksCategory', - 'elementIsVisible', - 'findByText', - 'textToXpath', - 'findByXpath', - 'textExists', - 'getDriver', - 'getSauceDriver', - 'getLogs', - 'loadUri', - 'rightClickText' - ]); - - this.Key = webdriver.Key; // map Key constants, for sending special keys - - // this type declaration suppresses IDE type warnings throughout this file - /** @type {webdriver.ThenableWebDriver} */ - this.driver = null; - } - - /** - * Set the browser window title. Useful for debugging. - * @param {string} title The title to set. - * @returns {Promise} A promise that resolves when the title is set. - */ - async setTitle (title) { - await this.driver.executeScript(`document.title = arguments[0];`, title); - } - - /** - * Wait for an element to be visible. - * @param {webdriver.WebElement} element The element to wait for. - * @returns {Promise} A promise that resolves when the element is visible. - */ - async elementIsVisible (element) { - const outerError = new Error('elementIsVisible failed'); - try { - await this.setTitle(`elementIsVisible ${await element.getId()}`); - await this.driver.wait(until.elementIsVisible(element), DEFAULT_TIMEOUT_MILLISECONDS); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * List of useful xpath scopes for finding elements. - * @returns {object} An object mapping names to xpath strings. - */ - get scope () { - return { - blocksTab: "*[@id='react-tabs-1']", - costumesTab: "*[@id='react-tabs-3']", - modal: '*[@class="ReactModalPortal"]', - reportedValue: '*[@class="blocklyDropDownContent"]', - soundsTab: "*[@id='react-tabs-5']", - spriteTile: '*[starts-with(@class,"react-contextmenu-wrapper")]', - menuBar: '*[contains(@class,"menu-bar_menu-bar_")]', - monitors: '*[starts-with(@class,"stage_monitor-wrapper")]', - contextMenu: '*[starts-with(@class,"react-contextmenu")]' - }; - } - - /** - * Instantiate a new Selenium driver. - * @returns {webdriver.ThenableWebDriver} The new driver. - */ - getDriver () { - const chromeCapabilities = webdriver.Capabilities.chrome(); - const args = []; - if (USE_HEADLESS) { - args.push('--headless'); - } - - // Stub getUserMedia to always not allow access - args.push('--use-fake-ui-for-media-stream=deny'); - - // Suppress complaints about AudioContext starting before a user gesture - // This is especially important on Windows, where Selenium directs JS console messages to stdout - args.push('--autoplay-policy=no-user-gesture-required'); - - chromeCapabilities.set('chromeOptions', {args}); - chromeCapabilities.setLoggingPrefs({ - performance: 'ALL' - }); - this.driver = new webdriver.Builder() - .forBrowser('chrome') - .withCapabilities(chromeCapabilities) - .build(); - return this.driver; - } - - /** - * Instantiate a new Selenium driver for Sauce Labs. - * @param {string} username The Sauce Labs username. - * @param {string} accessKey The Sauce Labs access key. - * @param {object} configs The Sauce Labs configuration. - * @param {string} configs.browserName The name of the desired browser. - * @param {string} configs.platform The name of the desired platform. - * @param {string} configs.version The desired browser version. - * @returns {webdriver.ThenableWebDriver} The new driver. - */ - getSauceDriver (username, accessKey, configs) { - this.driver = new webdriver.Builder() - .withCapabilities({ - browserName: configs.browserName, - platform: configs.platform, - version: configs.version, - username: username, - accessKey: accessKey - }) - .usingServer(`http://${username}:${accessKey}@ondemand.saucelabs.com:80/wd/hub`) - .build(); - return this.driver; - } - - /** - * Find an element by xpath. - * @param {string} xpath The xpath to search for. - * @returns {Promise} A promise that resolves to the element. - */ - async findByXpath (xpath) { - const outerError = new Error(`findByXpath failed with arguments:\n\txpath: ${xpath}`); - try { - await this.setTitle(`findByXpath ${xpath}`); - const el = await this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS); - // await this.driver.wait(() => el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS); - return el; - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Generate an xpath that finds an element by its text. - * @param {string} text The text to search for. - * @param {string} [scope] An optional xpath scope to search within. - * @returns {string} The xpath. - */ - textToXpath (text, scope) { - return `//body//${scope || '*'}//*[contains(text(), '${text}')]`; - } - - /** - * Find an element by its text. - * @param {string} text The text to search for. - * @param {string} [scope] An optional xpath scope to search within. - * @returns {Promise} A promise that resolves to the element. - */ - findByText (text, scope) { - return this.findByXpath(this.textToXpath(text, scope)); - } - - /** - * Check if an element exists by its text. - * @param {string} text The text to search for. - * @param {string} [scope] An optional xpath scope to search within. - * @returns {Promise} A promise that resolves to true if the element exists. - */ - async textExists (text, scope) { - const outerError = new Error(`textExists failed with arguments:\n\ttext: ${text}\n\tscope: ${scope}`); - try { - await this.setTitle(`textExists ${text}`); - const elements = await this.driver.findElements(By.xpath(this.textToXpath(text, scope))); - return elements.length > 0; - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Load a URI in the driver. - * @param {string} uri The URI to load. - * @returns {Promise} A promise that resolves when the URI is loaded. - */ - async loadUri (uri) { - const outerError = new Error(`loadUri failed with arguments:\n\turi: ${uri}`); - try { - await this.setTitle(`loadUri ${uri}`); - const WINDOW_WIDTH = 1024; - const WINDOW_HEIGHT = 768; - await this.driver - .get(`file://${uri}`); - await this.driver - .executeScript('window.onbeforeunload = undefined;'); - await this.driver.manage().window() - .setSize(WINDOW_WIDTH, WINDOW_HEIGHT); - await this.driver.wait( - async () => await this.driver.executeScript('return document.readyState;') === 'complete', - DEFAULT_TIMEOUT_MILLISECONDS - ); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Click an element by xpath. - * @param {string} xpath The xpath to click. - * @returns {Promise} A promise that resolves when the element is clicked. - */ - async clickXpath (xpath) { - const outerError = new Error(`clickXpath failed with arguments:\n\txpath: ${xpath}`); - try { - await this.setTitle(`clickXpath ${xpath}`); - const el = await this.findByXpath(xpath); - return el.click(); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Click an element by its text. - * @param {string} text The text to click. - * @param {string} [scope] An optional xpath scope to search within. - * @returns {Promise} A promise that resolves when the element is clicked. - */ - async clickText (text, scope) { - const outerError = new Error(`clickText failed with arguments:\n\ttext: ${text}\n\tscope: ${scope}`); - try { - await this.setTitle(`clickText ${text}`); - const el = await this.findByText(text, scope); - return el.click(); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Click a category in the blocks pane. - * @param {string} categoryText The text of the category to click. - * @returns {Promise} A promise that resolves when the category is clicked. - */ - async clickBlocksCategory (categoryText) { - const outerError = new Error(`clickBlocksCategory failed with arguments:\n\tcategoryText: ${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. - try { - await this.setTitle(`clickBlocksCategory ${categoryText}`); - 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 - } catch (cause) { - throw await enhanceError(outerError, cause); - } - } - - /** - * Right click an element by its text. - * @param {string} text The text to right click. - * @param {string} [scope] An optional xpath scope to search within. - * @returns {Promise} A promise that resolves when the element is right clicked. - */ - async rightClickText (text, scope) { - const outerError = new Error(`rightClickText failed with arguments:\n\ttext: ${text}\n\tscope: ${scope}`); - try { - await this.setTitle(`rightClickText ${text}`); - const el = await this.findByText(text, scope); - return this.driver.actions() - .click(el, Button.RIGHT) - .perform(); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Click a button by its text. - * @param {string} text The text to click. - * @returns {Promise} A promise that resolves when the button is clicked. - */ - async clickButton (text) { - const outerError = new Error(`clickButton failed with arguments:\n\ttext: ${text}`); - try { - await this.setTitle(`clickButton ${text}`); - await this.clickXpath(`//button//*[contains(text(), '${text}')]`); - } catch (cause) { - throw await enhanceError(outerError, cause, this.driver); - } - } - - /** - * Get selected browser log entries. - * @param {Array.} [whitelist] An optional list of log strings to allow. Default: see implementation. - * @returns {Promise>} A promise that resolves to the log entries. - */ - async getLogs (whitelist) { - const outerError = new Error(`getLogs failed with arguments:\n\twhitelist: ${whitelist}`); - try { - await this.setTitle(`getLogs ${whitelist}`); - if (!whitelist) { - // Default whitelist - whitelist = [ - 'The play() request was interrupted by a call to pause()' - ]; - } - const entries = await this.driver.manage() - .logs() - .get('browser'); - return entries.filter(entry => { - const message = entry.message; - for (const element of whitelist) { - if (message.indexOf(element) !== -1) { - return false; - } else if (entry.level !== 'SEVERE') { // WARNING: this doesn't do what it looks like it does! - return false; - } - } - return true; - }); - } catch (cause) { - throw await enhanceError(outerError, cause); - } - } -} - -export default SeleniumHelper; diff --git a/test/integration/backdrops.test.js b/test/integration/backdrops.test.js deleted file mode 100644 index 95a268b76c3..00000000000 --- a/test/integration/backdrops.test.js +++ /dev/null @@ -1,117 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with backdrops', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Adding a backdrop from the library should not switch to stage', async () => { - await loadUri(uri); - - // Start on the sounds tab of sprite1 to test switching behavior - await clickText('Sounds'); - - // Add a backdrop without selecting the stage first to test switching - await clickXpath('//button[@aria-label="Choose a Backdrop"]'); - const el = await findByXpath("//input[@placeholder='Search']"); - await el.sendKeys('blue'); - await clickText('Blue Sky'); // Adds the backdrop - - // Make sure the sprite is still selected, and that the tab has not changed - await clickText('Meow', scope.soundsTab); - - // Make sure the backdrop was actually added by going to the backdrops tab - await clickXpath('//span[text()="Stage"]'); - await clickText('Backdrops'); - await clickText('Blue Sky', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding backdrop via paint should switch to stage', async () => { - await loadUri(uri); - - const buttonXpath = '//button[@aria-label="Choose a Backdrop"]'; - const paintXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Paint"]`; - - const el = await findByXpath(buttonXpath); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath(paintXpath); - - // Stage should become selected and costume tab activated - await findByText('backdrop2', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding backdrop via surprise should not switch to stage', async () => { - await loadUri(uri); - - // Start on the sounds tab of sprite1 to test switching behavior - await clickText('Sounds'); - - const buttonXpath = '//button[@aria-label="Choose a Backdrop"]'; - const surpriseXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Surprise"]`; - - const el = await findByXpath(buttonXpath); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath(surpriseXpath); - - // Make sure the sprite is still selected, and that the tab has not changed - await clickText('Meow', scope.soundsTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding multiple backdrops from file should switch to stage', async () => { - const files = [ - path.resolve(__dirname, '../fixtures/gh-3582-png.png'), - path.resolve(__dirname, '../fixtures/100-100.svg') - ]; - await loadUri(uri); - - const buttonXpath = '//button[@aria-label="Choose a Backdrop"]'; - const fileXpath = `${buttonXpath}/following-sibling::div//input[@type="file"]`; - - const el = await findByXpath(buttonXpath); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath(fileXpath); - await input.sendKeys(files.join('\n')); - - // Should have been switched to stage/costume tab already - await findByText('gh-3582-png', scope.costumesTab); - await findByText('100-100', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/backpack.test.js b/test/integration/backpack.test.js deleted file mode 100644 index ce3125959bb..00000000000 --- a/test/integration/backpack.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - getDriver, - getLogs, - loadUri -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with the how-to library', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Backpack is "Coming Soon" without backpack host param', async () => { - await loadUri(uri); - // Check that the backpack header is visible and wrapped in a coming soon tooltip - await clickText('Backpack', '*[@data-for="backpack-tooltip"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Backpack can be expanded with backpack host param', async () => { - await loadUri(`${uri}?backpack_host=https://backpack.scratch.mit.edu`); - - // Try activating the backpack from the costumes tab to make sure it isn't pushed off - await clickText('Costumes'); - - // Check that the backpack header is visible and wrapped in a coming soon tooltip - await clickText('Backpack'); // Not wrapped in tooltip - await clickText('Backpack is empty'); // Make sure it can expand, is empty - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js deleted file mode 100644 index 25211007885..00000000000 --- a/test/integration/blocks.test.js +++ /dev/null @@ -1,339 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickBlocksCategory, - clickButton, - clickXpath, - findByText, - findByXpath, - textExists, - getDriver, - getLogs, - Key, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; - -describe('Working with the blocks', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Blocks report when clicked in the toolbox', async () => { - await loadUri(uri); - await clickText('Code'); - await clickBlocksCategory('Operators'); - await clickText('join', scope.blocksTab); // Click "join " block - await findByText('apple banana', scope.reportedValue); // Tooltip with result - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Switching sprites updates the block menus', async () => { - await loadUri(uri); - 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 clickBlocksCategory('Motion'); - await findByText('Stage selected: no motion blocks'); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Creating variables', async () => { - await loadUri(uri); - await clickText('Code'); - await clickBlocksCategory('Variables'); - - // Expect a default variable "my variable" to be visible - await clickText('my\u00A0variable', scope.blocksTab); - await findByText('0', scope.reportedValue); - - await clickText('Make a Variable'); - let el = await findByXpath("//input[@name='New variable name:']"); - await el.sendKeys('score'); - await clickButton('OK'); - await clickText('Make a Variable'); - el = await findByXpath("//input[@name='New variable name:']"); - await el.sendKeys('second variable'); - await clickButton('OK'); - - // Make sure reporting works on a new variable - await clickBlocksCategory('Variables'); - await clickText('score', scope.blocksTab); - await findByText('0', scope.reportedValue); // Tooltip with result - - // And there should be a monitor visible - await rightClickText('score', scope.monitors); - await clickText('slider'); - await findByXpath("//input[@step='1']"); - - // Changing the slider to a decimal should make it have a step size of 0.01 - await rightClickText('score', scope.monitors); - await clickText('change slider range'); - el = await findByXpath("//input[@name='Maximum value']"); - await el.sendKeys('.1'); - await clickButton('OK'); - await findByXpath("//input[@step='0.01'][@max='100.1']"); - - // Hiding the monitor via context menu should work - await rightClickText('score', scope.monitors); - await clickText('hide', scope.contextMenu); - await driver.sleep(100); - const monitorExists = await textExists('score', scope.monitors); - await expect(monitorExists).toBeFalsy(); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Creating a list', async () => { - await loadUri(uri); - await clickText('Code'); - await clickBlocksCategory('Variables'); - - await clickText('Make a List'); - let el = await findByXpath("//input[@name='New list name:']"); - await el.sendKeys('list1'); - await clickButton('OK'); - - // Click the "add to list" block 3 times - await clickText('add', scope.blocksTab); - await clickText('add', scope.blocksTab); - await clickText('add', scope.blocksTab); - await clickText('list1', scope.blocksTab); - await findByText('thing thing thing', scope.reportedValue); // Tooltip with result - - // Interact with the monitor, adding an item - await findByText('list1', scope.monitors); // Just to be sure it is there - await clickText('+', scope.monitors); - el = await findByXpath(`//body//${scope.monitors}//input`); - await el.sendKeys('thing2'); - await el.click(); // Regression for "clicking active input erases value" bug. - await clickText('list1', scope.monitors); // Blur the input to submit - - // Check that the list value has been propagated. - await clickText('list1', scope.blocksTab); - await findByText('thing thing thing thing2', scope.reportedValue); // Tooltip with result - - // Hiding the monitor via context menu should work - await rightClickText('list1', scope.monitors); - await clickText('hide', scope.contextMenu); - await driver.sleep(100); - const monitorExists = await textExists('list1', scope.monitors); - await expect(monitorExists).toBeFalsy(); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Custom procedures', async () => { - await loadUri(uri); - await clickBlocksCategory('My Blocks'); - await clickText('Make a Block'); - // Click on the "add an input" buttons - await clickText('number or text', scope.modal); - await clickText('boolean', scope.modal); - await clickText('Add a label', scope.modal); - await clickText('OK', scope.modal); - - // Make sure a "define" block has been added to the workspace - await findByText('define', scope.blocksTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding an extension', async () => { - await loadUri(uri); - await clickXpath('//button[@title="Add Extension"]'); - - await clickText('Pen'); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation - // Make sure toolbox has been scrolled to the pen extension - await findByText('stamp', scope.blocksTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Record option from sound block menu opens sound recorder', async () => { - await loadUri(uri); - await clickText('Code'); - await clickBlocksCategory('Sound'); - await clickText('Meow', scope.blocksTab); // Click "play sound 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 - await driver.sleep(1000); // getUserMedia requests are very slow to fail for some reason - await driver.switchTo().alert() - .accept(); - await findByText('Record Sound'); // Sound recorder is open - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Renaming costume changes the default costume name in the toolbox', async () => { - await loadUri(uri); - - // Rename the costume - await clickText('Costumes'); - 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 clickBlocksCategory('Looks'); - await clickText('newname', scope.blocksTab); - }); - - test('Renaming costume with a special character should not break toolbox', async () => { - await loadUri(uri); - - // Rename the costume - await clickText('Costumes'); - await clickText('costume2', scope.costumesTab); - const el = await findByXpath("//input[@value='costume2']"); - await el.sendKeys(''); - 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(), '')]"); - - // Make sure it is updated in the block menu - await clickText('Code'); - await clickBlocksCategory('Looks'); - await clickText('', 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 clickBlocksCategory('Looks'); - await clickText('costume2', scope.blocksTab); - - // Also check that adding a new costume does update the list - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .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'); - await clickText('costume3', scope.blocksTab); - }); - - // Skipped because it was flakey on travis, but seems to run locally ok - 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 clickBlocksCategory('Sound'); - await clickText('A\u00A0Bass', scope.blocksTab); // Need   for block text - }); - - // Regression test for switching between editor/player causing toolbox to stop updating - test('"See inside" after being on project page re-initializing variables', async () => { - const playerUri = path.resolve(__dirname, '../../build/player.html'); - await loadUri(playerUri); - await clickText('See inside'); - await clickBlocksCategory('Variables'); - await clickText('my\u00A0variable'); - - await clickText('See Project Page'); - await clickText('See inside'); - - await clickBlocksCategory('Variables'); - await clickText('my\u00A0variable'); - }); - - // Regression test for switching editor tabs causing toolbox to stop updating - test('Creating variables after adding extensions updates the toolbox', async () => { - await loadUri(uri); - await clickText('Costumes'); - await clickText('Code'); - await clickBlocksCategory('Variables'); - await clickText('Make a List'); - const el = await findByXpath("//input[@name='New list name:']"); - await el.sendKeys('list1'); - await clickButton('OK'); - await clickText('list1', scope.blocksTab); - }); - - test('Use variable blocks after switching languages', async () => { - const myVariable = 'my\u00A0variable'; - const changeVariableByScope = "*[@data-id='data_changevariableby']"; - - await loadUri(uri); - - await clickText('Code'); - await clickBlocksCategory('Variables'); - - // change "my variable" by 1 - await clickText('change', changeVariableByScope); - - // check reported value 1 - await clickText(myVariable, scope.blocksTab); - await findByText('1', scope.reportedValue); - - // change language - await clickXpath(SETTINGS_MENU_XPATH); - await clickText('Language', scope.menuBar); - await clickText('Deutsch'); - - await clickText('Skripte'); - await clickBlocksCategory('Variablen'); - - // make sure "my variable" is still 1 - await clickText(myVariable); - await findByText('1', scope.reportedValue); - - // change step from 1 to 10 - await clickText('1', changeVariableByScope); - await driver.actions() - .sendKeys('10') - .perform(); - - // change "my variable" by 10 - await clickText('ändere', changeVariableByScope); - - // check it is turned up to 11 - await clickText(myVariable); - await findByText('11', scope.reportedValue); - }); -}); diff --git a/test/integration/connection-modal.test.js b/test/integration/connection-modal.test.js deleted file mode 100644 index 48bff5004d5..00000000000 --- a/test/integration/connection-modal.test.js +++ /dev/null @@ -1,67 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - getDriver, - getLogs, - loadUri -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -// The tests below require Scratch Link to be unavailable, so we can trigger -// an error modal. To make sure this is always true, we came up with the idea of -// injecting javascript that overwrites the global Websocket object with one that -// attempts to connect to a fake socket address. -const websocketFakeoutJs = `var RealWebSocket = WebSocket; - WebSocket = function () { - return new RealWebSocket("wss://fake.fake"); - }`; - -describe('Hardware extension connection modal', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Message saying Scratch Link is unavailable (BLE)', async () => { - await driver.quit(); - driver = getDriver(); - - await loadUri(uri); - - await driver.executeScript(websocketFakeoutJs); - - await clickXpath('//button[@title="Add Extension"]'); - - await clickText('micro:bit'); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open - findByText('Scratch Link'); // Scratch Link is mentioned in the error modal - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Message saying Scratch Link is unavailable (BT)', async () => { - await loadUri(uri); - - await driver.executeScript(websocketFakeoutJs); - - await clickXpath('//button[@title="Add Extension"]'); - - await clickText('EV3'); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open - findByText('Scratch Link'); // Scratch Link is mentioned in the error modal - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js deleted file mode 100644 index 3dcd4485456..00000000000 --- a/test/integration/costumes.test.js +++ /dev/null @@ -1,262 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -// The costumes library is slow to load. Increase the timeout for these tests. -jest.setTimeout(60_000); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with costumes', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Adding a costume through the library', async () => { - await loadUri(uri); - await driver.sleep(500); - await clickText('Costumes'); - await clickXpath('//button[@aria-label="Choose a Costume"]'); - const el = await findByXpath("//input[@placeholder='Search']"); - await el.sendKeys('abb'); - await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector - await findByXpath("//input[@value='Abby-a']"); // Should show editor for new costume - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a costume by surprise button', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath('//button[@aria-label="Surprise"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a costume by paint button', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath('//button[@aria-label="Paint"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Duplicating a costume', async () => { - await loadUri(uri); - await clickText('Costumes'); - - await rightClickText('costume1', scope.costumesTab); - await clickText('duplicate', scope.costumesTab); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for duplication to finish - - // Make sure the duplicated costume is named correctly. - await clickText('costume3', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Converting bitmap/vector in paint editor', async () => { - await loadUri(uri); - await clickText('Costumes'); - - // Convert the first costume to bitmap. - await clickText('costume1', scope.costumesTab); - await clickText('Convert to Bitmap', scope.costumesTab); - - // Make sure mode switches back to vector for vector costume. - await clickText('costume2', scope.costumesTab); - await clickText('Convert to Bitmap', scope.costumesTab); - - // Make sure bitmap is saved by switching back and converting to vector. - await clickText('Sounds'); - await clickText('Costumes'); - await clickText('Convert to Vector', scope.costumesTab); // costume2 - await clickText('costume1', scope.costumesTab); - await clickText('Convert to Vector', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Undo/redo in the paint editor', async () => { - await loadUri(uri); - await clickText('Costumes'); - await clickText('costume1', scope.costumesTab); - await clickText('Convert to Bitmap', scope.costumesTab); - await clickXpath('//img[@alt="Undo"]'); - await clickText('Convert to Bitmap', scope.costumesTab); - await clickXpath('//img[@alt="Undo"]'); - await clickXpath('//img[@alt="Redo"]'); - await clickText('Convert to Vector', scope.costumesTab); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding an svg from file', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg')); - await clickText('100-100', scope.costumesTab); // Name from filename - await clickText('100 x 100', scope.costumesTab); // Size is right - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a png from file (gh-3582)', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png')); - await clickText('gh-3582-png', scope.costumesTab); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a bmp from file', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/bmpfile.bmp')); - await clickText('bmpfile', scope.costumesTab); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding several costumes with a gif', async () => { - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/paddleball.gif')); - - await findByText('paddleball', scope.costumesTab); - await findByText('paddleball2', scope.costumesTab); - await findByText('paddleball3', scope.costumesTab); - await findByText('paddleball4', scope.costumesTab); - await findByText('paddleball5', scope.costumesTab); - await findByText('paddleball6', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a letter costume through the Letters filter in the library', async () => { - await loadUri(uri); - await driver.manage() - .window() - .setSize(1244, 768); // Letters filter not visible at 1024 width - await clickText('Costumes'); - await clickXpath('//button[@aria-label="Choose a Costume"]'); - await clickText('Letters'); - await clickText('Block-a', scope.modal); // Closes modal - await rightClickText('Block-a', scope.costumesTab); // Make sure it is there - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Costumes animate on mouseover', async () => { - await loadUri(uri); - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - const searchElement = await findByXpath("//input[@placeholder='Search']"); - await searchElement.sendKeys('abb'); - const abbyElement = await findByXpath('//*[span[text()="Abby"]]'); - driver.actions() - .mouseMove(abbyElement) - .perform(); - // wait for one of Abby's alternate costumes to appear - await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/45de34b47a2ce22f6f5d28bb35a44ff5.svg/get/"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding multiple costumes at the same time', async () => { - const files = [ - path.resolve(__dirname, '../fixtures/gh-3582-png.png'), - path.resolve(__dirname, '../fixtures/100-100.svg') - ]; - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(files.join('\n')); - - await findByText('gh-3582-png', scope.costumesTab); - await findByText('100-100', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Load an invalid svg from scratch3 as costume', async () => { // eslint-disable-line no-disabled-tests - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-from-scratch3.svg')); - const costumeTile = await findByText('corrupt-from-scratch3', scope.costumesTab); // Name from filename - const tileVisible = await costumeTile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load an invalid svg from scratch2 as costume', async () => { // eslint-disable-line no-disabled-tests - await loadUri(uri); - await clickText('Costumes'); - const el = await findByXpath('//button[@aria-label="Choose a Costume"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/scratch2-corrupted.svg')); - const costumeTile = await findByText('scratch2-corrupted', scope.costumesTab); // Name from filename - const tileVisible = await costumeTile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); -}); diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js deleted file mode 100644 index a268455ca88..00000000000 --- a/test/integration/examples.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/* globals Promise */ - -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickButton, - clickText, - clickXpath, - findByXpath, - getDriver, - getLogs, - loadUri -} = new SeleniumHelper(); - -let driver; - -describe('player example', () => { - const uri = path.resolve(__dirname, '../../build/player.html'); - - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test.skip('Player: load a project by ID', async () => { - const projectId = '96708228'; - await loadUri(`${uri}#${projectId}`); - await clickXpath('//img[@title="Go"]'); - await new Promise(resolve => setTimeout(resolve, 2000)); - await clickXpath('//img[@title="Stop"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - const projectRequests = await driver.manage().logs() - .get('performance') - .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) - .filter(m => m.method === 'Network.requestWillBeSent') - .map(m => m.params.request.url) - .filter(url => url === 'https://projects.scratch.mit.edu/96708228') - ); - await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']); - }); -}); - -describe('blocks example', () => { - const uri = path.resolve(__dirname, '../../build/blocks-only.html'); - - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test.skip('Blocks: load a project by ID', async () => { - const projectId = '96708228'; - await loadUri(`${uri}#${projectId}`); - await new Promise(resolve => setTimeout(resolve, 2000)); - await clickXpath('//img[@title="Go"]'); - await new Promise(resolve => setTimeout(resolve, 2000)); - await clickXpath('//img[@title="Stop"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - const projectRequests = await driver.manage().logs() - .get('performance') - .then(pLogs => pLogs.map(log => JSON.parse(log.message).message) - .filter(m => m.method === 'Network.requestWillBeSent') - .map(m => m.params.request.url) - .filter(url => url === 'https://projects.scratch.mit.edu/96708228') - ); - await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']); - }); - - // skipping per https://github.com/LLK/scratch-gui/issues/4902 until we have better approach - test.skip('Change categories', async () => { - await loadUri(`${uri}`); - await clickText('Looks'); - await clickText('Sound'); - await clickText('Events'); - await clickText('Control'); - await clickText('Sensing'); - await clickText('Operators'); - await clickText('Variables'); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation - await clickText('Make a Variable'); - let el = await findByXpath("//input[@name='New variable name:']"); - await el.sendKeys('score'); - await clickButton('OK'); - await clickText('Make a Variable'); - el = await findByXpath("//input[@name='New variable name:']"); - await el.sendKeys('second variable'); - await clickButton('OK'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/how-tos.test.js b/test/integration/how-tos.test.js deleted file mode 100644 index 577031f37d6..00000000000 --- a/test/integration/how-tos.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByXpath, - getDriver, - getLogs, - loadUri -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with the how-to library', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Choosing a how-to', async () => { - await loadUri(uri); - await clickText('Costumes'); - await clickXpath('//*[@aria-label="Tutorials"]'); - await clickText('Getting Started'); // Modal should close - // Make sure YouTube video on first card appears - await findByXpath('//div[contains(@class, "step-video")]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // @todo navigating cards, etc. -}); diff --git a/test/integration/localization.test.js b/test/integration/localization.test.js deleted file mode 100644 index 00cae10ba49..00000000000 --- a/test/integration/localization.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; - -describe('Localization', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Switching languages', async () => { - await loadUri(uri); - - // Add a sprite to make sure it stays when switching languages - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - await clickText('Apple', scope.modal); // Closes modal - - await clickXpath(SETTINGS_MENU_XPATH); - await clickText('Language', scope.menuBar); - await clickText('Deutsch'); - await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh - - // Make sure the blocks are translating - await clickText('Fühlen'); // Sensing category in German - await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll - await clickText('Antwort'); // Find the "answer" block in German - - // Change to the costumes tab to confirm other parts of the GUI are translating - await clickText('Kostüme'); - - // After switching languages, make sure Apple sprite still exists - await rightClickText('Apple', scope.spriteTile); // Make sure it is there - - // Remounting re-attaches the beforeunload callback. Make sure to remove it - driver.executeScript('window.onbeforeunload = undefined;'); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // Regression test for #4476, blocks in wrong language when loaded with locale - test('Loading with locale shows correct blocks', async () => { - await loadUri(`${uri}?locale=de`); - await clickText('Fühlen'); // Sensing category in German - await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll - await clickText('Antwort'); // Find the "answer" block in German - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // test for #5445 - test('Loading with locale shows correct translation for string length block parameter', async () => { - await loadUri(`${uri}?locale=ja`); - await clickText('演算'); // Operators category in Japanese - await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll - await clickText('の長さ', scope.blocksTab); // Click "length " block - await findByText('3', scope.reportedValue); // Tooltip with result - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // Regression test for ENA-142, monitor can lag behind language selection - test('Monitor labels update on locale change', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/monitor-variable.sb3')); - - // Monitors are present - await findByText('username', scope.monitors); - await findByText('language', scope.monitors); - - // Change locale to ja - await clickXpath(SETTINGS_MENU_XPATH); - await clickText('Language', scope.menuBar); - await clickText('日本語'); - - // Monitor labels updated - await findByText('ユーザー名', scope.monitors); - await findByText('言語', scope.monitors); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js deleted file mode 100644 index 45d6ec4e7c9..00000000000 --- a/test/integration/menu-bar.test.js +++ /dev/null @@ -1,172 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; - -describe('Menu bar settings', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('File->New should be enabled', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]'); - }); - - test('File->Load should be enabled', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await findByXpath('//*[li[text()="Load from your computer"] and not(@data-tip="tooltip")]'); - }); - - test('File->Save should be enabled', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await findByXpath('//*[li[span[text()="Save to your computer"]] and not(@data-tip="tooltip")]'); - }); - - test('Share button should NOT be enabled', async () => { - await loadUri(uri); - await findByXpath('//div[span[div[span[text()="Share"]]] and @data-tip="tooltip"]'); - }); - - test('Logo should be clickable', async () => { - await loadUri(uri); - await clickXpath('//img[@alt="Scratch"]'); - const currentUrl = await driver.getCurrentUrl(); - await expect(currentUrl).toEqual('https://scratch.mit.edu/'); - }); - - test('(GH#4064) Project name should be editable', async () => { - await loadUri(uri); - const el = await findByXpath('//input[@value="Scratch Project"]'); - await el.sendKeys(' - Personalized'); - await clickText('Costumes'); // just to blur the input - await clickXpath('//input[@value="Scratch Project - Personalized"]'); - }); - - test('User is not warned before uploading project file over a fresh project', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3')); - // No replace alert since no changes were made - await findByText('project1-sprite'); - }); - - test('User is warned before uploading project file over an edited project', async () => { - await loadUri(uri); - - // Change the project by deleting a sprite - await rightClickText('Sprite1', scope.spriteTile); - await clickText('delete', scope.spriteTile); - - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3')); - await driver.switchTo().alert() - .accept(); - await findByText('project1-sprite'); - }); - - test('Theme picker shows themes', async () => { - await loadUri(uri); - await clickXpath(SETTINGS_MENU_XPATH); - await clickText('Color Mode', scope.menuBar); - - expect(await (await findByText('Original', scope.menuBar)).isDisplayed()).toBe(true); - expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true); - }); - - test('Theme picker switches to high contrast', async () => { - await loadUri(uri); - await clickXpath(SETTINGS_MENU_XPATH); - await clickText('Color Mode', scope.menuBar); - await clickText('High Contrast', scope.menuBar); - - // There is a tiny delay for the color theme to be applied to the categories. - await driver.wait(async () => { - const motionCategoryDiv = await findByXpath( - '//div[contains(@class, "scratchCategoryMenuItem") and ' + - 'contains(@class, "scratchCategoryId-motion")]/*[1]'); - const color = await motionCategoryDiv.getCssValue('background-color'); - - // Documentation for getCssValue says it depends on how the browser - // returns the value. Locally I am seeing 'rgba(128, 181, 255, 1)', - // but this is a bit flexible just in case. - return /128,\s?181,\s?255/.test(color) || color.includes('80B5FF'); - }, 5000, 'Motion category color does not match high contrast theme'); - }); - - test('Settings menu switches between submenus', async () => { - await loadUri(uri); - await clickXpath(SETTINGS_MENU_XPATH); - - // Language and theme options not visible yet - expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false); - expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false); - - await clickText('Color Mode', scope.menuBar); - - // Only theme options visible - expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true); - expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false); - - await clickText('Language', scope.menuBar); - - // Only language options visible - expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false); - expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(true); - }); - - test('Menu labels hidden when width is equal to 1024', async () => { - await loadUri(uri); - await driver.manage() - .window() - .setSize(1024, 768); - - const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials']; - for (const menu of collapsibleMenus) { - const settingsMenu = await findByText(menu, scope.menuBar); - expect(await settingsMenu.isDisplayed()).toBe(false); - } - }); - - test('Menu labels shown when width is greater than 1024', async () => { - await loadUri(uri); - await driver.manage() - .window() - .setSize(1200, 768); - - const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials']; - for (const menu of collapsibleMenus) { - const settingsMenu = await findByText(menu, scope.menuBar); - expect(await settingsMenu.isDisplayed()).toBe(true); - } - }); -}); diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js deleted file mode 100644 index 697b136ef87..00000000000 --- a/test/integration/project-loading.test.js +++ /dev/null @@ -1,117 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; - -describe('Loading scratch gui', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - describe('Loading projects by ID', () => { - - test('Nonexistent projects show error screen', async () => { - await loadUri(`${uri}#999999999999999999999`); - await clickText('Oops! Something went wrong.'); - }); - - // skipping because it relies on network speed, and tests a method - // of loading projects that we are not actively using anymore - test.skip('Load a project by ID directly through url', async () => { - await driver.quit(); // Reset driver to test hitting # url directly - driver = getDriver(); - - const projectId = '96708228'; - await loadUri(`${uri}#${projectId}`); - await clickXpath('//img[@title="Go"]'); - await new Promise(resolve => setTimeout(resolve, 2000)); - await clickXpath('//img[@title="Stop"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // skipping because it relies on network speed, and tests a method - // of loading projects that we are not actively using anymore - test.skip('Load a project by ID (fullscreen)', async () => { - await driver.quit(); // Reset driver to test hitting # url directly - driver = getDriver(); - - const prevSize = driver.manage() - .window() - .getSize(); - await new Promise(resolve => setTimeout(resolve, 2000)); - driver.manage() - .window() - .setSize(1920, 1080); - const projectId = '96708228'; - await loadUri(`${uri}#${projectId}`); - await clickXpath('//img[@title="Full Screen Control"]'); - await new Promise(resolve => setTimeout(resolve, 500)); - await clickXpath('//img[@title="Go"]'); - await new Promise(resolve => setTimeout(resolve, 1000)); - await clickXpath('//img[@title="Stop"]'); - prevSize.then(value => { - driver.manage() - .window() - .setSize(value.width, value.height); - }); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // skipping because this test fails frequently on CI; might need "wait(until.elementLocated" or similar - // error message is "stale element reference: element is not attached to the page document" - test.skip('Creating new project resets active tab to Code tab', async () => { - await loadUri(uri); - await findByXpath('//*[span[text()="Costumes"]]'); - await clickText('Costumes'); - await clickXpath(FILE_MENU_XPATH); - await clickXpath('//li[span[text()="New"]]'); - await findByXpath('//div[@class="scratchCategoryMenu"]'); - await clickText('Operators', scope.blocksTab); - }); - - test('Not logged in->made no changes to project->create new project should not show alert', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickXpath('//li[span[text()="New"]]'); - await findByXpath('//*[div[@class="scratchCategoryMenu"]]'); - await clickText('Operators', scope.blocksTab); - }); - - test.skip('Not logged in->made a change to project->create new project should show alert', 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 - await findByText('1.28'); // length of A Bass sound - await clickXpath(FILE_MENU_XPATH); - await clickXpath('//li[span[text()="New"]]'); - driver.switchTo() - .alert() - .accept(); - await findByXpath('//*[div[@class="scratchCategoryMenu"]]'); - await clickText('Operators', scope.blocksTab); - }); - }); -}); diff --git a/test/integration/project-state.test.js b/test/integration/project-state.test.js deleted file mode 100644 index cb1c245a496..00000000000 --- a/test/integration/project-state.test.js +++ /dev/null @@ -1,45 +0,0 @@ -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; - -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; - -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(FILE_MENU_XPATH); - await clickXpath('//li[span[text()="New"]]'); - // project title should be default again - await clickXpath(`//input[@value="${defaultProjectTitle}"]`); - }); -}); diff --git a/test/integration/sb-file-uploader-hoc.test.js b/test/integration/sb-file-uploader-hoc.test.js deleted file mode 100644 index 1b38f4978d7..00000000000 --- a/test/integration/sb-file-uploader-hoc.test.js +++ /dev/null @@ -1,114 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - loadUri -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + - '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; - -describe('Loading scratch gui', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Loading project file from computer succeeds, without opening failure alert', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3')); - await findByText('project1-sprite'); - // this test will fail if an alert appears, e.g. in SBFileUploaderHOC's onload() function - }); - - test('Loading project file from computer gives project the filename from file', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3')); - await findByText('project1-sprite'); - await clickXpath('//input[@value="project1"]'); - }); - - test('Load sb3 project with a missing svg costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-sprite-svg.sb3')); - const spriteTile = await findByText('Blue Square Guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); - - test('Load sb3 project with an invalid svg costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb3')); - const spriteTile = await findByText('Blue Square Guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); - - test('Load sb2 project with a missing svg costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sb2')); - const spriteTile = await findByText('Blue Guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); - - test('Load sb2 project with an invalid svg costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb2')); - const spriteTile = await findByText('Blue Guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); - - test('Load sb3 project with a missing bmp costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-bmp.sb3')); - const spriteTile = await findByText('green-bmp-guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); - - test('Load sb3 project with an invalid bmp costume', async () => { - await loadUri(uri); - await clickXpath(FILE_MENU_XPATH); - await clickText('Load from your computer'); - const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-bmp.sb3')); - const spriteTile = await findByText('green-bmp-guy'); - const tileVisible = await spriteTile.isDisplayed(); - expect(tileVisible).toBe(true); - }); -}); diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js deleted file mode 100644 index 27946e186fa..00000000000 --- a/test/integration/sounds.test.js +++ /dev/null @@ -1,189 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; -import {Key} from 'selenium-webdriver'; - -const { - clickText, - clickXpath, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with sounds', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Adding a sound through the library', async () => { - await loadUri(uri); - await clickText('Sounds'); - - // Delete the sound - await rightClickText('Meow', scope.soundsTab); - await driver.sleep(500); // Wait a moment for context menu; only needed for local testing - await clickText('delete', scope.soundsTab); - - // Add it back - await clickXpath('//button[@aria-label="Choose a Sound"]'); - let el = await findByXpath("//input[@placeholder='Search']"); - await el.sendKeys('meow'); - await clickText('Meow', scope.modal); // Should close the modal - - // Add a new sound - await clickXpath('//button[@aria-label="Choose a Sound"]'); - el = await findByXpath("//input[@placeholder='Search']"); - await el.sendKeys('chom'); - await clickText('Chomp'); // Should close the modal, then click the sounds in the selector - await findByXpath("//input[@value='Chomp']"); // Should show editor for new sound - - await clickXpath('//button[@title="Play"]'); - - await clickText('Louder'); - await clickText('Softer'); - await clickText('Faster'); - await clickText('Slower'); - await clickText('Robot'); - await clickText('Reverse'); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a sound by surprise button', async () => { - await loadUri(uri); - await clickText('Sounds'); - const el = await findByXpath('//button[@aria-label="Choose a Sound"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath('//button[@aria-label="Surprise"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Duplicating a sound', async () => { - await loadUri(uri); - await clickText('Sounds'); - - await rightClickText('Meow', scope.soundsTab); - await clickText('duplicate', scope.soundsTab); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error - - // Make sure the duplicated sound is named correctly. - await clickText('Meow2', scope.soundsTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // Regression test for gui issue #1320 - test('Switching sprites with different numbers of sounds', async () => { - await loadUri(uri); - - // Add a sound so this sprite has 2 sounds. - await clickText('Sounds'); - await clickXpath('//button[@aria-label="Choose a Sound"]'); - await clickText('A Bass'); // Closes the modal - - // Now add a sprite with only one sound. - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - await clickText('Abby'); // Doing this used to crash the editor. - - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error - - // Make sure the 'Oops' screen is not visible - const content = await driver.getPageSource(); - expect(content.indexOf('Oops')).toEqual(-1); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding multiple sounds at the same time', async () => { - const files = [ - path.resolve(__dirname, '../fixtures/movie.wav'), - path.resolve(__dirname, '../fixtures/sneaker.wav') - ]; - await loadUri(uri); - await clickText('Sounds'); - const el = await findByXpath('//button[@aria-label="Choose a Sound"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(files.join('\n')); - - await findByText('movie', scope.soundsTab); - await findByText('sneaker', scope.soundsTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Copy to new button adds a new sound', async () => { - await loadUri(uri); - await clickText('Sounds'); - await clickText('Copy to New', scope.soundsTab); - await clickText('Meow2', scope.soundsTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Copy and pasting within a sound changes its duration', async () => { - await loadUri(uri); - await clickText('Sounds'); - await findByText('0.85', scope.soundsTab); // Original meow sound duration - await clickText('Copy', scope.soundsTab); - await clickText('Paste', scope.soundsTab); - await findByText('1.70', scope.soundsTab); // Sound has doubled in duration - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Can copy a sound from a sprite and paste into a sound on the stage', async () => { - await loadUri(uri); - await clickText('Sounds'); - await clickText('Copy', scope.soundsTab); // Copy the meow sound - await clickXpath('//span[text()="Stage"]'); - await findByText('0.02', scope.soundsTab); // Original pop sound duration - await clickText('Paste', scope.soundsTab); - await findByText('0.87', scope.soundsTab); // Duration of pop + meow sound - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Keyboard shortcuts', async () => { - const cmdCtrl = process.platform.includes('darwin') ? Key.COMMAND : Key.CONTROL; - - await loadUri(uri); - await clickText('Sounds'); - const el = await findByXpath('//button[@aria-label="Choose a Sound"]'); - await el.sendKeys(Key.chord(cmdCtrl, 'a')); // Select all - await findByText('0.85', scope.soundsTab); // Meow sound duration - await el.sendKeys(Key.DELETE); - await findByText('0.00', scope.soundsTab); // Sound is now empty - await el.sendKeys(Key.chord(cmdCtrl, 'z')); // undo - await findByText('0.85', scope.soundsTab); // Meow sound is back - await el.sendKeys(Key.chord(cmdCtrl, Key.SHIFT, 'z')); // redo - await findByText('0.00', scope.soundsTab); // Sound is empty again - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js deleted file mode 100644 index 40677246cda..00000000000 --- a/test/integration/sprites.test.js +++ /dev/null @@ -1,299 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; -import {StaleElementReferenceError} from 'selenium-webdriver/lib/error'; -import until from 'selenium-webdriver/lib/until'; - -const { - clickText, - clickXpath, - elementIsVisible, - findByText, - findByXpath, - getDriver, - getLogs, - loadUri, - rightClickText, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Working with sprites', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Adding a sprite through the library', async () => { - await loadUri(uri); - await clickText('Costumes'); - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - await clickText('Apple', scope.modal); // Closes modal - await rightClickText('Apple', scope.spriteTile); // Make sure it is there - await clickText('Motion'); // Make sure we are back to the code tab - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a sprite by surprise button', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath('//button[@aria-label="Surprise"]'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a sprite by paint button', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - await clickXpath('//button[@aria-label="Paint"]'); - await findByText('Convert to Bitmap'); // Make sure we are on the paint editor - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Deleting only sprite does not crash', async () => { - await loadUri(uri); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation - await rightClickText('Sprite1', scope.spriteTile); - await clickText('delete', scope.spriteTile); - // Confirm that the stage has been switched to - await findByText('Stage selected: no motion blocks'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Deleting by x button on sprite tile', async () => { - await loadUri(uri); - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation - await clickXpath('//*[@aria-label="Delete"]'); // Only visible close button is on the sprite - // Confirm that the stage has been switched to - await findByText('Stage selected: no motion blocks'); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a sprite by uploading a png', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png')); - await clickText('gh-3582-png', scope.spriteTile); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - // This test fails because uploading an SVG as a sprite changes the scaling - // Enable when this is fixed issues/3608 - test('Adding a sprite by uploading an svg (gh-3608)', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg')); - await clickText('100-100', scope.spriteTile); // Sprite is named for costume filename - - // Check to make sure the size is right - await clickText('Costumes'); - await clickText('100-100', scope.costumesTab); // The name of the costume - await clickText('100 x 100', scope.costumesTab); // The size of the costume - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a sprite by uploading a gif', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/paddleball.gif')); - await clickText('paddleball', scope.spriteTile); // Sprite is named for costume filename - - await clickText('Costumes'); - await findByText('paddleball', scope.costumesTab); - await findByText('paddleball2', scope.costumesTab); - await findByText('paddleball3', scope.costumesTab); - await findByText('paddleball4', scope.costumesTab); - await findByText('paddleball5', scope.costumesTab); - await findByText('paddleball6', scope.costumesTab); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding a letter sprite through the Letters filter in the library', async () => { - await loadUri(uri); - await driver.manage() - .window() - .setSize(1244, 768); // Letters filter not visible at 1024 width - await clickText('Costumes'); - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - await clickText('Letters'); - await clickText('Block-B', scope.modal); // Closes modal - await rightClickText('Block-B', scope.spriteTile); // Make sure it is there - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Use browser back button to close library', async () => { - await loadUri(uri); - await clickText('Costumes'); - await clickXpath('//button[@aria-label="Choose a Sprite"]'); - const abbyElement = await findByText('Abby'); // Should show editor for new costume - await elementIsVisible(abbyElement); - await driver.navigate().back(); - // should throw error because library is no longer present - await expect(driver.wait(until.elementIsVisible(abbyElement))) - .rejects - .toBeInstanceOf(StaleElementReferenceError); - const costumesElement = await findByText('Costumes'); // Should show editor for new costume - await elementIsVisible(costumesElement); - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Adding multiple sprites at the same time', async () => { - const files = [ - path.resolve(__dirname, '../fixtures/gh-3582-png.png'), - path.resolve(__dirname, '../fixtures/100-100.svg') - ]; - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(files.join('\n')); - - await findByText('gh-3582-png', scope.spriteTile); - await findByText('100-100', scope.spriteTile); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); - - test('Load a sprite3 with a missing svg costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sprite3')); - const tile = await findByText('Blue Square Guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a sprite3 with a currupt svg costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sprite3')); - const tile = await findByText('Blue Square Guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a scratch3 corrupt svg as a sprite', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-from-scratch3.svg')); - const tile = await findByText('corrupt-from-scratch3', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a sprite2 with a missing svg costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sprite2')); - const tile = await findByText('Blue Guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a sprite2 with a currupt svg costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupted-svg.sprite2')); - const tile = await findByText('Blue Guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a corrupt scratch2 svg as a sprite', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/scratch2-corrupted.svg')); - const tile = await findByText('scratch2-corrupted', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a sprite3 with a missing bmp costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-bmp.sprite3')); - const tile = await findByText('green-bmp-guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - test('Load a sprite3 with a currupt bmp costume', async () => { - await loadUri(uri); - const el = await findByXpath('//button[@aria-label="Choose a Sprite"]'); - await driver.actions().mouseMove(el) - .perform(); - await driver.sleep(500); // Wait for thermometer menu to come up - const input = await findByXpath('//input[@type="file"]'); - await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-bmp.sprite3')); - const tile = await findByText('green-bmp-guy', scope.spriteTile); - const tileVisible = await tile.isDisplayed(); - await expect(tileVisible).toBe(true); - }); - - // TODO: uploading a corrupt bmp as a sprite should throw an error and not add a gray question mark - -}); diff --git a/test/integration/stage-size.test.js b/test/integration/stage-size.test.js deleted file mode 100644 index ca235905079..00000000000 --- a/test/integration/stage-size.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - clickXpath, - rightClickText, - getDriver, - getLogs, - loadUri, - scope -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html'); - -let driver; - -describe('Loading scratch gui', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('Switching small/large stage after highlighting and deleting sprite', async () => { - await loadUri(uri); - - // Highlight the sprite - await clickText('Sprite1', scope.spriteTile); - - // Delete it - await rightClickText('Sprite1', scope.spriteTile); - await clickText('delete', scope.spriteTile); - - // Go to small stage mode - await clickXpath('//button[@title="Switch to small stage"]'); - - // Confirm app still working - await clickXpath('//button[@title="Switch to large stage"]'); - - const logs = await getLogs(); - await expect(logs).toEqual([]); - }); -}); diff --git a/test/integration/tutorials-shortcut.test.js b/test/integration/tutorials-shortcut.test.js deleted file mode 100644 index 187e7252547..00000000000 --- a/test/integration/tutorials-shortcut.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import path from 'path'; -import SeleniumHelper from '../helpers/selenium-helper'; - -const { - clickText, - findByXpath, - getDriver, - loadUri -} = new SeleniumHelper(); - -const uri = path.resolve(__dirname, '../../build/index.html?tutorial=all'); -const uriPrefix = path.resolve(__dirname, '../../build/index.html?tutorial='); - -let driver; - -describe('Working with shortcut to Tutorials library', () => { - beforeAll(() => { - driver = getDriver(); - }); - - afterAll(async () => { - await driver.quit(); - }); - - test('opens with the Tutorial Library showing', async () => { - await loadUri(uri); - // make sure there is a tutorial visible that doesn't have a shortcut - await clickText('Make It Spin'); - await findByXpath('//div[contains(@class, "step-video")]'); - }); - - test('can open hidden tutorials', async () => { - await loadUri(`${uriPrefix}whatsnew`); - // should open the tutorial video immediately - await findByXpath('//div[contains(@class, "step-video")]'); - }); - // @todo navigating cards, etc. -}); diff --git a/test/register-hooks.mjs b/test/register-hooks.mjs new file mode 100644 index 00000000000..6f3f49aae1e --- /dev/null +++ b/test/register-hooks.mjs @@ -0,0 +1,30 @@ +/** + * @fileoverview + * Tiny bridge between webpack's permissive import rules and what Node.js wants. + */ + +import {registerHooks} from 'node:module'; + +registerHooks({ + resolve (specifier, context, nextResolve) { + let result; + try { + result = nextResolve(specifier, context); + } catch (e) { + if (e.code !== 'ERR_MODULE_NOT_FOUND' || !/^\.\.?\//.test(specifier)) { + throw e; + } + result = nextResolve(`${specifier}.js`, context); + } + if (result.url.endsWith('.json')) { + result = { + ...result, + importAttributes: { + ...result.importAttributes, + type: 'json' + } + }; + } + return result; + } +}); diff --git a/test/smoke/browser.test.js b/test/smoke/browser.test.js deleted file mode 100644 index 2eeb2cfccea..00000000000 --- a/test/smoke/browser.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import SeleniumHelper from '../helpers/selenium-helper'; -const {SAUCE_USERNAME, SAUCE_ACCESS_KEY, SMOKE_URL} = process.env; -const { - getSauceDriver, - findByText -} = new SeleniumHelper(); - -// Make the default timeout longer, Sauce tests take ~30s -jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 1000; // eslint-disable-line - -const SUPPORTED_MESSAGE = 'Welcome to the Scratch 3.0 Beta'; -const UNSUPPORTED_MESSAGE = 'Scratch 3.0 does not support Internet Explorer'; - -// Driver configs can be generated with the Sauce Platform Configurator -// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator -describe('Smoke tests on older browsers', () => { - let driver; - - afterEach(async () => { - if (driver) await driver.quit(); - }); - - test('Credentials should be provided', () => { - expect(SAUCE_USERNAME && SAUCE_ACCESS_KEY && SMOKE_URL).toBeTruthy(); - }); - - test('IE 11 should be unsupported', async () => { - const driverConfig = { - browserName: 'internet explorer', - platform: 'Windows 10', - version: '11.103' - }; - driver = await getSauceDriver( - process.env.SAUCE_USERNAME, - process.env.SAUCE_ACCESS_KEY, - driverConfig); - await driver.get(process.env.SMOKE_URL); - const el = await findByText(UNSUPPORTED_MESSAGE); - const isDisplayed = await el.isDisplayed(); - return expect(isDisplayed).toEqual(true); - }); - - test('Safari 9 should be supported', async () => { - const driverConfig = { - browserName: 'safari', - platform: 'OS X 10.11', - version: '9.0' - }; - driver = await getSauceDriver( - process.env.SAUCE_USERNAME, - process.env.SAUCE_ACCESS_KEY, - driverConfig); - await driver.get(process.env.SMOKE_URL); - const el = await findByText(SUPPORTED_MESSAGE); - const isDisplayed = await el.isDisplayed(); - return expect(isDisplayed).toEqual(true); - }); - - test('Safari 10 should be supported', async () => { - const driverConfig = { - browserName: 'safari', - platform: 'OS X 10.11', - version: '10.0' - }; - driver = await getSauceDriver( - process.env.SAUCE_USERNAME, - process.env.SAUCE_ACCESS_KEY, - driverConfig); - await driver.get(process.env.SMOKE_URL); - const el = await findByText(SUPPORTED_MESSAGE); - const isDisplayed = await el.isDisplayed(); - return expect(isDisplayed).toEqual(true); - }); -}); diff --git a/test/unit/addons/settings.test.js b/test/unit/addons/settings.test.js deleted file mode 100644 index 6f4ff1b9734..00000000000 --- a/test/unit/addons/settings.test.js +++ /dev/null @@ -1,705 +0,0 @@ -import SettingStore from '../../../src/addons/settings-store'; -import upstreamMeta from '../../../src/addons/generated/upstream-meta.json'; - -class LocalStorageShim { - constructor () { - this.storage = Object.create(null); - } - getItem (key) { - return this.storage[key]; - } - setItem (key, value) { - this.storage[key] = value.toString(); - } -} - -beforeEach(() => { - global.localStorage = new LocalStorageShim(); -}); - -const lightTheme = { - isDark: () => false -}; - -const darkTheme = { - isDark: () => true -}; - -test('enabled, event', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - expect(store.getAddonEnabled('editor-devtools')).toBe(true); - expect('enabled' in store.store['editor-devtools']).toBe(false); - store.setAddonEnabled('editor-devtools', false); - expect(store.getAddonEnabled('editor-devtools')).toBe(false); - expect('enabled' in store.store['editor-devtools']).toBe(true); - store.setAddonEnabled('editor-devtools', true); - store.setAddonEnabled('cat-blocks', true); - expect('enabled' in store.store['cat-blocks']).toBe(true); - store.setAddonEnabled('cat-blocks', null); - expect('enabled' in store.store['cat-blocks']).toBe(false); - expect(fn).toHaveBeenCalledTimes(4); - expect(fn.mock.calls[0][0].detail.addonId).toBe('editor-devtools'); - expect(fn.mock.calls[0][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[0][0].detail.value).toBe(false); - expect(fn.mock.calls[1][0].detail.addonId).toBe('editor-devtools'); - expect(fn.mock.calls[1][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[1][0].detail.value).toBe(true); - expect(fn.mock.calls[2][0].detail.addonId).toBe('cat-blocks'); - expect(fn.mock.calls[2][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[2][0].detail.value).toBe(true); - expect(fn.mock.calls[3][0].detail.addonId).toBe('cat-blocks'); - expect(fn.mock.calls[3][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[3][0].detail.value).toBe(false); -}); - -test('settings, event, default values', () => { - const store = new SettingStore(); - const fn = jest.fn(); - expect(store.getAddonSetting('onion-skinning', 'default')).toBe(false); - expect('default' in store.store['onion-skinning']).toBe(false); - store.addEventListener('setting-changed', fn); - store.setAddonSetting('onion-skinning', 'default', true); - expect(store.getAddonSetting('onion-skinning', 'default')).toBe(true); - expect('default' in store.store['onion-skinning']).toBe(true); - store.setAddonSetting('onion-skinning', 'default', null); - expect('default' in store.store['onion-skinning']).toBe(false); - expect(store.getAddonSetting('onion-skinning', 'default')).toBe(false); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn.mock.calls[0][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[0][0].detail.settingId).toBe('default'); - expect(fn.mock.calls[0][0].detail.value).toBe(true); - expect(fn.mock.calls[1][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[1][0].detail.settingId).toBe('default'); - expect(fn.mock.calls[1][0].detail.value).toBe(false); -}); - -test('no actual change emits no event', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - for (let i = 0; i < 5; i++) store.setAddonEnabled('cat-blocks', true); - expect(fn).toHaveBeenCalledTimes(1); - for (let i = 0; i < 5; i++) store.setAddonEnabled('cat-blocks', false); - expect(fn).toHaveBeenCalledTimes(2); - for (let i = 0; i < 5; i++) store.setAddonSetting('onion-skinning', 'default', true); - expect(fn).toHaveBeenCalledTimes(3); - for (let i = 0; i < 5; i++) store.setAddonSetting('onion-skinning', 'default', false); - expect(fn).toHaveBeenCalledTimes(4); -}); - -test('changing enabled throws on unknown addons', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - expect(() => store.setAddonEnabled('egriohergoijergijregojiergdfoijre', true)).toThrow(); - expect(fn).toHaveBeenCalledTimes(0); -}); - -test('changing settings throws on unknown settings', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - expect(() => store.setAddonSetting('onion-skinning', 'sdlkjfslkdjfljksd', true)).toThrow(); - expect(() => store.setAddonSetting('ergfoijgi', 'sdflkjsfdlkj', true)).toThrow(); - expect(fn).toHaveBeenCalledTimes(0); -}); - -test('changing enabled throws on invalid values', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - expect(() => store.setAddonEnabled('cat-blocks', 'sdfjlksdflk')).toThrow(); - expect(() => store.setAddonEnabled('cat-blocks', 0)).toThrow(); - expect(() => store.setAddonEnabled('cat-blocks', [])).toThrow(); - expect(() => store.setAddonEnabled('cat-blocks', {})).toThrow(); - expect(fn).toHaveBeenCalledTimes(0); -}); - -test('changing settings checks value validity and throws', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - // boolean - expect(() => store.setAddonSetting('onion-skinning', 'default', '#abcdef')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'default', [])).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'default', {})).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'default', '')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'default', 1)).toThrow(); - // integer - expect(() => store.setAddonSetting('onion-skinning', 'next', '#abcdef')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'next', [])).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'next', {})).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'next', '')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'next', '3')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'next', false)).toThrow(); - // select - expect(() => store.setAddonSetting('onion-skinning', 'mode', '#abcdef')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', [])).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', {})).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', '')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', false)).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', 1)).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', 'tint')).not.toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'mode', 'merge')).not.toThrow(); - // color - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', '#abcdef')).not.toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', '#abcDE1')).not.toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', [])).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', {})).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', '')).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', false)).toThrow(); - expect(() => store.setAddonSetting('onion-skinning', 'beforeTint', 1)).toThrow(); - expect(fn).toHaveBeenCalledTimes(4); -}); - -test('colors with alpha channel', () => { - const store = new SettingStore(); - store.setAddonSetting('onion-skinning', 'beforeTint', '#123456'); - expect(store.getAddonSetting('onion-skinning', 'beforeTint')).toBe('#123456'); - store.setAddonSetting('onion-skinning', 'beforeTint', '#234567ff'); - expect(store.getAddonSetting('onion-skinning', 'beforeTint')).toBe('#234567'); - store.setAddonSetting('onion-skinning', 'beforeTint', '#abc67800'); - expect(store.getAddonSetting('onion-skinning', 'beforeTint')).toBe('#abc678'); - store.import({ - addons: { - 'onion-skinning': { - settings: { - beforeTint: '#56789aff' - } - } - } - }); - expect(store.getAddonSetting('onion-skinning', 'beforeTint')).toBe('#56789a'); -}); - -test('reset does not change enabled', () => { - const store = new SettingStore(); - store.setAddonEnabled('cat-blocks', true); - store.resetAddon('cat-blocks'); - expect(store.getAddonEnabled('cat-blocks')).toBe(true); -}); - -test('reset settings, event', () => { - const store = new SettingStore(); - store.setAddonSetting('onion-skinning', 'default', true); - store.setAddonSetting('onion-skinning', 'next', 3); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - store.resetAddon('onion-skinning'); - expect(store.getAddonSetting('onion-skinning', 'default')).toBe(false); - expect(store.getAddonSetting('onion-skinning', 'next')).toBe(0); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn.mock.calls[0][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[0][0].detail.settingId).toBe('default'); - expect(fn.mock.calls[0][0].detail.value).toBe(false); - expect(fn.mock.calls[1][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[1][0].detail.settingId).toBe('next'); - expect(fn.mock.calls[1][0].detail.value).toBe(0); -}); - -test('reset all addons', () => { - const store = new SettingStore(); - store.setAddonEnabled('cat-blocks', true); - store.setAddonSetting('onion-skinning', 'default', true); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - store.resetAllAddons(); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn.mock.calls[0][0].detail.addonId).toBe('cat-blocks'); - expect(fn.mock.calls[0][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[0][0].detail.value).toBe(false); - expect(fn.mock.calls[1][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[1][0].detail.settingId).toBe('default'); - expect(fn.mock.calls[1][0].detail.value).toBe(false); -}); - -test('apply preset', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - store.setAddonSetting('editor-theme3', 'motion-color', '#abcdef'); - store.applyAddonPreset('editor-theme3', 'original'); - expect(fn.mock.calls.length).toBeGreaterThan(5); - expect(store.getAddonSetting('editor-theme3', 'motion-color')).toBe('#4a6cd4'); - // TODO: test that settings not specified in the preset don't change -}); - -test('unknown preset throws', () => { - const store = new SettingStore(); - const fn = jest.fn(); - store.addEventListener('setting-changed', fn); - expect(() => store.applyAddonPreset('alksdfjlksdf', 'jksdflkjsdf')).toThrow(); - expect(() => store.applyAddonPreset('editor-theme3', 'jksdflkjsdf')).toThrow(); - expect(fn).toHaveBeenCalledTimes(0); -}); - -test('export core', () => { - const store = new SettingStore(); - const exported = store.export({theme: lightTheme}); - expect(exported.core.version).toMatch(/tw/); - expect(exported.core.lightTheme).toBe(true); - const dark = store.export({theme: darkTheme}); - expect(dark.core.lightTheme).toBe(false); -}); - -test('export settings', () => { - const store = new SettingStore(); - let exported = store.export({theme: lightTheme}); - expect(exported.addons['remove-sprite-confirm'].enabled).toBe(false); - expect(exported.addons['remove-sprite-confirm'].settings).toEqual({}); - expect(exported.addons['onion-skinning'].enabled).toBe(true); - expect(exported.addons['onion-skinning'].settings.default).toEqual(false); - store.setAddonEnabled('remove-sprite-confirm', true); - store.setAddonSetting('onion-skinning', 'default', true); - exported = store.export({theme: lightTheme}); - expect(exported.addons['remove-sprite-confirm'].enabled).toBe(true); - expect(exported.addons['remove-sprite-confirm'].settings).toEqual({}); - expect(exported.addons['onion-skinning'].enabled).toBe(true); - expect(exported.addons['onion-skinning'].settings.default).toEqual(true); -}); - -test('export theme', () => { - const store = new SettingStore(); - const exported = store.export({theme: lightTheme}); - expect(exported.core.lightTheme).toBe(true); - const exported2 = store.export({theme: darkTheme}); - expect(exported2.core.lightTheme).toBe(false); -}); - -test('import, event', () => { - const store = new SettingStore(); - store.setAddonEnabled('onion-skinning', false); - store.setAddonSetting('onion-skinning', 'next', 5); - const newStore = new SettingStore(); - newStore.setAddonSetting('onion-skinning', 'next', 10); - const fn = jest.fn(); - newStore.addEventListener('setting-changed', fn); - newStore.import(store.export({theme: lightTheme})); - expect(newStore.getAddonEnabled('onion-skinning')).toBe(false); - expect(newStore.getAddonSetting('onion-skinning', 'next')).toBe(5); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn.mock.calls[0][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[0][0].detail.settingId).toBe('enabled'); - expect(fn.mock.calls[0][0].detail.value).toBe(false); - expect(fn.mock.calls[1][0].detail.addonId).toBe('onion-skinning'); - expect(fn.mock.calls[1][0].detail.settingId).toBe('next'); - expect(fn.mock.calls[1][0].detail.value).toBe(5); -}); - -test('export is identical after import', () => { - const store = new SettingStore(); - const fn = jest.fn(); - const exported = store.export({theme: lightTheme}); - store.import(exported); - expect(fn).toHaveBeenCalledTimes(0); - expect(store.export({theme: lightTheme})).toEqual(exported); -}); - -test('import format', () => { - const store = new SettingStore(); - store.setAddonEnabled('cat-blocks', true); - store.import({ - core: { - version: 'lksd', - lightTheme: false - }, - addons: { - 'onion-skinning': { - enabled: false, - settings: { - next: 7 - } - } - } - }); - expect(store.getAddonEnabled('onion-skinning')).toBe(false); - expect(store.getAddonSetting('onion-skinning', 'next')).toBe(7); - expect(store.getAddonEnabled('cat-blocks')).toBe(true); -}); - -test('invalid imports', () => { - const store = new SettingStore(); - expect(() => store.import({ - addons: {} - })).not.toThrow(); - expect(() => store.import({ - addons: { - 'onion-skinning': { - enabled: false, - settings: { - dsjfokosdfj: 5 - } - } - } - })).not.toThrow(); - expect(() => store.import({ - addons: { - grfdjiklk: { - enabled: true, - settings: {} - } - } - })).not.toThrow(); - expect(() => store.import({ - addons: { - 'onion-skinning': { - enabled: '4', - settings: { - default: '3' - } - } - } - })).not.toThrow(); - expect(store.getAddonEnabled('onion-skinning')).toBe(false); - expect(store.getAddonSetting('onion-skinning', 'default')).toBe(false); -}); - -test('local storage', () => { - const store = new SettingStore(); - store.setAddonEnabled('cat-blocks', true); - store.setAddonSetting('onion-skinning', 'default', true); - const newStore = new SettingStore(); - newStore.readLocalStorage(); - expect(newStore.store).toEqual(store.store); -}); - -test('local storage is resistent to errors', () => { - global.localStorage = new LocalStorageShim(); - const store = new SettingStore(); - localStorage.getItem = () => { - throw new Error(':('); - }; - store.readLocalStorage(); - localStorage.getItem = () => 'eoiru4jtg)(R('; - store.readLocalStorage(); - localStorage.setItem = () => { - throw new Error(':('); - }; - store.setAddonEnabled('cat-blocks', true); - // eslint-disable-next-line no-undefined - global.localStorage = undefined; - store.readLocalStorage(); - store.setAddonEnabled('cat-blocks', false); -}); - -test('setStore diffing', () => { - const settingsStore = new SettingStore(); - const pageStore = new SettingStore(); - settingsStore.setAddonEnabled('editor-devtools', false); - pageStore.setAddonEnabled('editor-devtools', false); - const fn = jest.fn(); - pageStore.addEventListener('addon-changed', fn); - pageStore.setStore(settingsStore.store); - expect(fn).toHaveBeenCalledTimes(0); - settingsStore.setAddonEnabled('editor-devtools', true); - settingsStore.setAddonSetting('onion-skinning', 'next', 10); - pageStore.setStore(settingsStore.store); - expect(fn).toHaveBeenCalledTimes(2); - expect(fn.mock.calls[0][0].detail.addonId).toBe('editor-devtools'); - expect(fn.mock.calls[1][0].detail.addonId).toBe('onion-skinning'); -}); - -test('setStore dynamic enable/disable', () => { - const settingsStore = new SettingStore(); - const pageStore = new SettingStore(); - settingsStore.setAddonEnabled('block-palette-icons', false); - pageStore.setStore(settingsStore.store); - const fn = jest.fn(); - pageStore.addEventListener('addon-changed', fn); - settingsStore.setAddonEnabled('block-palette-icons', true); - pageStore.setStore(settingsStore.store); - expect(fn.mock.calls[0][0].detail.addonId).toBe('block-palette-icons'); - expect(fn.mock.calls[0][0].detail.dynamicEnable).toBe(true); - expect(fn.mock.calls[0][0].detail.dynamicDisable).toBe(false); - settingsStore.setAddonEnabled('block-palette-icons', false); - pageStore.setStore(settingsStore.store); - expect(fn.mock.calls[1][0].detail.addonId).toBe('block-palette-icons'); - expect(fn.mock.calls[1][0].detail.dynamicEnable).toBe(false); - expect(fn.mock.calls[1][0].detail.dynamicDisable).toBe(true); -}); - -test('setStore weird values', () => { - const settingsStore = new SettingStore(); - expect(settingsStore.getAddonEnabled('pause')).toBe(true); - settingsStore.setAddonEnabled('pause', false); - settingsStore.setAddonEnabled('clones', true); - settingsStore.setStore({ - invalid0: {}, - invalid1: null, - pause: null - }); - expect(settingsStore.getAddonEnabled('pause')).toBe(false); -}); - -test('resetting an addon through setStore', () => { - const store = new SettingStore(); - expect(store.getAddonSetting('custom-block-shape', 'paddingSize')).toBe(100); - store.setAddonSetting('custom-block-shape', 'paddingSize', 50); - expect(store.getAddonSetting('custom-block-shape', 'paddingSize')).toBe(50); - const store2 = new SettingStore(); - store.setStore(store2.store); - expect(store.getAddonSetting('custom-block-shape', 'paddingSize')).toBe(100); -}); - -test('setStoreWithVersionCheck', () => { - const store = new SettingStore(); - store.setStore = jest.fn(); - store.setStoreWithVersionCheck({ - store: '1234', - version: upstreamMeta.commit - }); - expect(store.setStore).toHaveBeenCalledTimes(1); - expect(store.setStore).toHaveBeenCalledWith('1234'); - store.setStore = jest.fn(); - store.setStoreWithVersionCheck({ - store: '1234', - version: 'something invalid' - }); - expect(store.setStore).toHaveBeenCalledTimes(0); -}); - -test('parseUrlParameter', () => { - const store = new SettingStore(); - expect(store.getAddonEnabled('pause')).toBe(true); - expect(store.getAddonEnabled('mute-project')).toBe(true); - expect(store.getAddonEnabled('remove-curved-stage-border')).toBe(false); - expect(store.remote).toBe(false); - store.parseUrlParameter('pause,remove-curved-stage-border,,invalid addon??43t987(*&$'); - expect(store.getAddonEnabled('pause')).toBe(true); - expect(store.getAddonEnabled('mute-project')).toBe(false); - expect(store.getAddonEnabled('remove-curved-stage-border')).toBe(true); - expect(store.remote).toBe(true); -}); - -test('Settings migration 1 -> 2', () => { - const store = new SettingStore(); - - // eslint-disable-next-line max-len - global.localStorage.getItem = () => `{"_":1,"tw-project-info":{"enabled":false},"tw-interface-customization":{"enabled":false,"removeFeedback":true,"removeBackpack":true}}`; - store.readLocalStorage(); - expect(store.getAddonEnabled('block-count')).toBe(false); - expect(store.getAddonEnabled('tw-remove-backpack')).toBe(false); - expect(store.getAddonEnabled('tw-remove-feedback')).toBe(false); - - // eslint-disable-next-line max-len - global.localStorage.getItem = () => `{"_":1,"tw-project-info":{"enabled":true},"tw-interface-customization":{"enabled":true,"removeFeedback":true,"removeBackpack":true}}`; - store.readLocalStorage(); - expect(store.getAddonEnabled('block-count')).toBe(true); - expect(store.getAddonEnabled('tw-remove-backpack')).toBe(true); - expect(store.getAddonEnabled('tw-remove-feedback')).toBe(true); -}); - -test('Settings migration 2 -> 3', () => { - const store = new SettingStore(); - - global.localStorage.getItem = () => JSON.stringify({ - '_': 2, - 'hide-flyout': { - enabled: true - } - }); - store.readLocalStorage(); - expect(store.getAddonSetting('hide-flyout', 'toggle')).toBe('hover'); -}); - -test('Settings migration 3 -> 4', () => { - const store = new SettingStore(); - - global.localStorage.getItem = () => JSON.stringify({ - _: 3 - }); - store.readLocalStorage(); - expect(store.getAddonEnabled('editor-devtools')).toBe(true); - expect(store.getAddonEnabled('find-bar')).toBe(true); - expect(store.getAddonEnabled('middle-click-popup')).toBe(true); - - global.localStorage.getItem = () => JSON.stringify({ - '_': 3, - 'editor-devtools': { - enabled: false - } - }); - store.readLocalStorage(); - expect(store.getAddonEnabled('editor-devtools')).toBe(false); - expect(store.getAddonEnabled('find-bar')).toBe(false); - expect(store.getAddonEnabled('middle-click-popup')).toBe(false); -}); - -test('if', () => { - const store = new SettingStore(); - store.setAddonEnabled('editor-devtools', true); - store.setAddonEnabled('onion-skinning', false); - store.setAddonSetting('editor-theme3', 'motion-color', '#000000'); - store.setAddonSetting('editor-theme3', 'looks-color', '#FFFFFF'); - - // eslint-disable-next-line no-undefined - expect(store.evaluateCondition('editor-theme3', undefined)).toBe(true); - expect(store.evaluateCondition('editor-theme3', null)).toBe(true); - expect(store.evaluateCondition('editor-theme3', {})).toBe(true); - - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['onion-skinning'] - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: 'onion-skinning' - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['editor-devtools'] - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: 'editor-devtools' - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['editor-devtools', 'onion-skinning'] - })).toBe(true); - - expect(store.evaluateCondition('editor-theme3', { - settings: {} - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'motion-color': '#000000' - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': '#FFFFFF' - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': '#FFFFFE' - } - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'motion-color': '#000000', - 'looks-color': '#FFFFFF' - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'motion-color': '#000001', - 'looks-color': '#FFFFFF' - } - })).toBe(false); - - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': ['#FFFFFF'] - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': ['#FFFFFE', '#FFFFFF'] - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': ['#FFFFFF', '#FFFFFE'] - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': ['#FFFFFE'] - } - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': ['#FFFFFE', '#FFFFFD'] - } - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - settings: { - 'looks-color': [] - } - })).toBe(false); - - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['editor-devtools'], - settings: { - 'motion-color': '#000000' - } - })).toBe(true); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['onion-skinning'], - settings: { - 'motion-color': '#000000' - } - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['editor-devtools'], - settings: { - 'motion-color': '#000001' - } - })).toBe(false); - expect(store.evaluateCondition('editor-theme3', { - addonEnabled: ['onion-skinning'], - settings: { - 'motion-color': '#000001' - } - })).toBe(false); -}); - -test('Settings migration 4 -> 5', () => { - const store = new SettingStore(); - - // implied default settings - global.localStorage.getItem = () => JSON.stringify({ - _: 4 - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('show'); - - // also implied default settings - global.localStorage.getItem = () => JSON.stringify({ - _: 4, - fullscreen: {} - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('show'); - - // explicit default settings - global.localStorage.getItem = () => JSON.stringify({ - '_': 4, - 'fullscreen': { - hideToolbar: false - } - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('show'); - - // explicit hide, implied default hover setting - global.localStorage.getItem = () => JSON.stringify({ - '_': 4, - 'fullscreen': { - hideToolbar: true - } - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('hover'); - - // explicit hide and default hover - global.localStorage.getItem = () => JSON.stringify({ - '_': 4, - 'fullscreen': { - hideToolbar: true, - hoverToolbar: true - } - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('hover'); - - // explicit hide, no hover - global.localStorage.getItem = () => JSON.stringify({ - '_': 4, - 'fullscreen': { - hideToolbar: true, - hoverToolbar: false - } - }); - store.readLocalStorage(); - expect(store.getAddonSetting('fullscreen', 'toolbar')).toBe('hide'); -}); diff --git a/test/unit/components/__snapshots__/button.test.jsx.snap b/test/unit/components/__snapshots__/button.test.jsx.snap deleted file mode 100644 index a4633420483..00000000000 --- a/test/unit/components/__snapshots__/button.test.jsx.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ButtonComponent matches snapshot 1`] = ` - -
- -`; diff --git a/test/unit/components/__snapshots__/icon-button.test.jsx.snap b/test/unit/components/__snapshots__/icon-button.test.jsx.snap deleted file mode 100644 index 315390683df..00000000000 --- a/test/unit/components/__snapshots__/icon-button.test.jsx.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IconButtonComponent matches snapshot 1`] = ` -
- -
-
- Text -
-
-
-`; diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap deleted file mode 100644 index 877763cf950..00000000000 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ /dev/null @@ -1,565 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Sound Editor Component matches snapshot 1`] = ` -
-
-
- -
- - -
-
-
-
- -
- Copy -
-
-
- -
- Paste -
-
-
- -
- Copy to New -
-
-
-
- -
- Delete -
-
-
-
-
- - - - - -
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
- - Faster - -
-
-
- -
- - Slower - -
-
-
- -
- - Louder - -
-
-
- -
- - Softer - -
-
-
- -
- - Mute - -
-
-
- -
- - Fade in - -
-
-
- -
- - Fade out - -
-
-
- -
- - Reverse - -
-
-
- -
- - Robot - -
-
-
- -
- - Echo - -
-
-
-
-
-
- 00:00.30 / 00:00.60 -
-
- Hz - - Mono - - (10.51KB) -
-
-
-`; diff --git a/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap b/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap deleted file mode 100644 index f7f19434a61..00000000000 --- a/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap +++ /dev/null @@ -1,179 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SpriteSelectorItemComponent matches snapshot when given a number and details to show 1`] = ` -
-
- 5 -
-
-
- -
-
-
-
- Pony sprite -
-
- 480 x 360 -
-
-
-
- -
-
- -
-`; - -exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = ` -
-
-
- -
-
-
-
- Pony sprite -
-
-
-
- -
-
- -
-`; diff --git a/test/unit/components/button.test.jsx b/test/unit/components/button.test.jsx deleted file mode 100644 index 5445891ab1e..00000000000 --- a/test/unit/components/button.test.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import ButtonComponent from '../../../src/components/button/button'; -import renderer from 'react-test-renderer'; - -describe('ButtonComponent', () => { - test('matches snapshot', () => { - const onClick = jest.fn(); - const component = renderer.create( - - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - test('triggers callback when clicked', () => { - const onClick = jest.fn(); - const componentShallowWrapper = shallow( - - ); - componentShallowWrapper.simulate('click'); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/components/cards.test.jsx b/test/unit/components/cards.test.jsx deleted file mode 100644 index 617891e0201..00000000000 --- a/test/unit/components/cards.test.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; - -// Mock this utility because it uses dynamic imports that do not work with jest -jest.mock('../../../src/lib/libraries/decks/translate-image.js', () => {}); - -import Cards, {ImageStep, VideoStep} from '../../../src/components/cards/cards.jsx'; - -describe('Cards component', () => { - const defaultProps = () => ({ - activeDeckId: 'id1', - content: { - id1: { - name: 'id1 - name', - img: 'id1 - img', - steps: [{video: 'videoUrl'}] - } - }, - dragging: false, - expanded: true, - isRtl: false, - locale: 'en', - onActivateDeckFactory: jest.fn(), - onCloseCards: jest.fn(), - onDrag: jest.fn(), - onEndDrag: jest.fn(), - onNextStep: jest.fn(), - onPrevStep: jest.fn(), - onShowAll: jest.fn(), - onShrinkExpandCards: jest.fn(), - onStartDrag: jest.fn(), - showVideos: true, - step: 0, - x: 0, - y: 0 - }); - - test('showVideos=true shows the video step', () => { - const component = mountWithIntl( - - ); - expect(component.find(ImageStep).exists()).toEqual(false); - expect(component.find(VideoStep).exists()).toEqual(true); - }); - - test('showVideos=false shows the title image/name instead of video step', () => { - const component = mountWithIntl( - - ); - expect(component.find(VideoStep).exists()).toEqual(false); - - const imageStep = component.find(ImageStep); - expect(imageStep.props().image).toEqual('id1 - img'); - expect(imageStep.props().title).toEqual('id1 - name'); - }); -}); diff --git a/test/unit/components/controls.test.jsx b/test/unit/components/controls.test.jsx deleted file mode 100644 index 78abb474cdd..00000000000 --- a/test/unit/components/controls.test.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import Controls from '../../../src/components/controls/controls'; -import TurboMode from '../../../src/components/turbo-mode/turbo-mode'; -import GreenFlag from '../../../src/components/green-flag/green-flag'; -import StopAll from '../../../src/components/stop-all/stop-all'; - -describe('Controls component', () => { - const defaultProps = () => ({ - active: false, - onGreenFlagClick: jest.fn(), - onStopAllClick: jest.fn(), - turbo: false - }); - - test('shows turbo mode when in turbo mode', () => { - const component = mountWithIntl( - - ); - expect(component.find(TurboMode).exists()).toEqual(false); - component.setProps({turbo: true}); - expect(component.find(TurboMode).exists()).toEqual(true); - }); - - test('triggers the right callbacks when clicked', () => { - const props = defaultProps(); - const component = mountWithIntl( - - ); - component.find(GreenFlag).simulate('click'); - expect(props.onGreenFlagClick).toHaveBeenCalled(); - - component.find(StopAll).simulate('click'); - expect(props.onStopAllClick).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/components/error-boundary-hoc.test.jsx b/test/unit/components/error-boundary-hoc.test.jsx deleted file mode 100644 index 724165ee234..00000000000 --- a/test/unit/components/error-boundary-hoc.test.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import {Provider} from 'react-redux'; -const {mountWithIntl} = require('../../helpers/intl-helpers.jsx'); - -import configureStore from 'redux-mock-store'; - -import CrashMessageComponent from '../../../src/components/crash-message/crash-message.jsx'; -import ErrorBoundary from '../../../src/containers/error-boundary.jsx'; - -const ChildComponent = () =>
hello
; - -describe('ErrorBoundary', () => { - const mockStore = configureStore(); - let store; - - beforeEach(() => { - store = mockStore({ - locales: { - isRtl: false, - locale: 'en-US' - } - }); - }); - - test('ErrorBoundary shows children before error and CrashMessageComponent after', () => { - const child = ; - const wrapper = mountWithIntl( - {child} - ); - const errorSite = wrapper.childAt(0).childAt(0); - - // @ts-ignore: 'onReload' prop is absent because this component will only be used for pattern matching - const crashMessagePattern = ; - - expect(wrapper.containsMatchingElement(child)).toBeTruthy(); - expect(wrapper.containsMatchingElement(crashMessagePattern)).toBeFalsy(); - - errorSite.simulateError(new Error('fake error for testing purposes')); - - expect(wrapper.containsMatchingElement(child)).toBeFalsy(); - expect(wrapper.containsMatchingElement(crashMessagePattern)).toBeTruthy(); - }); -}); diff --git a/test/unit/components/icon-button.test.jsx b/test/unit/components/icon-button.test.jsx deleted file mode 100644 index 269dfad76fd..00000000000 --- a/test/unit/components/icon-button.test.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import IconButton from '../../../src/components/icon-button/icon-button'; -import renderer from 'react-test-renderer'; - -describe('IconButtonComponent', () => { - test('matches snapshot', () => { - const onClick = jest.fn(); - const title =
Text
; - const imgSrc = 'imgSrc'; - const className = 'custom-class-name'; - const component = renderer.create( - - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - test('triggers callback when clicked', () => { - const onClick = jest.fn(); - const title =
Text
; - const imgSrc = 'imgSrc'; - const componentShallowWrapper = shallow( - - ); - componentShallowWrapper.simulate('click'); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/components/menu-bar.test.jsx b/test/unit/components/menu-bar.test.jsx deleted file mode 100644 index 7222bcafdac..00000000000 --- a/test/unit/components/menu-bar.test.jsx +++ /dev/null @@ -1,64 +0,0 @@ -describe('no-op', () => { - test('no-op', () => {}); -}); -// tw: these seem to be hopelessly broken to the increasing scope of changes we make to the menu bar, disable for now... -/* -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 {DEFAULT_THEME} from '../../../src/lib/themes'; - -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 - }, - theme: { - theme: DEFAULT_THEME - }, - timeTravel: { - year: 'NOW' - }, - vm: new VM() - } - }); - - const getComponent = function (props = {}) { - return ; - }; - - 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); - }); -}); -*/ diff --git a/test/unit/components/monitor-list.test.jsx b/test/unit/components/monitor-list.test.jsx deleted file mode 100644 index b9576d46423..00000000000 --- a/test/unit/components/monitor-list.test.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import {OrderedMap} from 'immutable'; -import configureStore from 'redux-mock-store'; -import {Provider} from 'react-redux'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import MonitorList from '../../../src/components/monitor-list/monitor-list.jsx'; -import {DEFAULT_THEME} from '../../../src/lib/themes'; - -describe('MonitorListComponent', () => { - const store = configureStore()({scratchGui: { - monitorLayout: { - monitors: {}, - savedMonitorPositions: {} - }, - theme: { - theme: DEFAULT_THEME - }, - toolbox: { - toolboxXML: '' - }, - vm: { - runtime: { - requestUpdateMonitor: () => {}, - getLabelForOpcode: () => '' - } - } - }}); - const draggable = false; - const onMonitorChange = jest.fn(); - const stageSize = { - width: 100, - height: 100, - widthDefault: 100, - heightDefault: 100 - }; - - let monitors = OrderedMap({}); - - // Wrap this in a function so it gets test specific states and can be reused. - const getComponent = function () { - return ( - - - - ); - }; - - test('it renders the correct step size for discrete sliders', () => { - monitors = OrderedMap({ - id1: { - visible: true, - mode: 'slider', - isDiscrete: true - } - }); - const wrapper = mountWithIntl(getComponent()); - const input = wrapper.find('input'); - expect(input.props().step).toBe(1); - }); - - test('it renders the correct step size for non-discrete sliders', () => { - monitors = OrderedMap({ - id1: { - visible: true, - mode: 'slider', - isDiscrete: false - } - }); - const wrapper = mountWithIntl(getComponent()); - const input = wrapper.find('input'); - expect(input.props().step).toBe(0.01); - }); -}); diff --git a/test/unit/components/monitor.test.jsx b/test/unit/components/monitor.test.jsx deleted file mode 100644 index e52fa603abd..00000000000 --- a/test/unit/components/monitor.test.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import DefaultMonitor from '../../../src/components/monitor/default-monitor'; -import Monitor from '../../../src/components/monitor/monitor'; -import {DARK_THEME, DEFAULT_THEME} from '../../../src/lib/themes'; - -jest.mock('../../../src/lib/themes/default'); -jest.mock('../../../src/lib/themes/dark'); - -describe('Monitor Component', () => { - test('it selects the correct colors based on default theme', () => { - const noop = () => {}; - - const wrapper = shallow(); - - const defaultMonitor = wrapper.find(DefaultMonitor); - - // selects colors from mock value in src/lib/themes/__mocks__/default-colors.js - expect(defaultMonitor.props().categoryColor).toEqual({background: '#111111', text: '#444444'}); - }); - - test('it selects the correct colors based on dark mode theme', () => { - const noop = () => {}; - - const wrapper = shallow(); - - const defaultMonitor = wrapper.find(DefaultMonitor); - - // selects colors from mock value in src/lib/themes/__mocks__/dark-mode.js - expect(defaultMonitor.props().categoryColor).toEqual({background: '#AAAAAA', text: '#BBBBBB'}); - }); -}); diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx deleted file mode 100644 index 81a05563f6c..00000000000 --- a/test/unit/components/sound-editor.test.jsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx'; -import SoundEditor from '../../../src/components/sound-editor/sound-editor'; - -describe('Sound Editor Component', () => { - let props; - beforeEach(() => { - props = { - isStereo: false, - duration: 1, - size: 10507, - canUndo: true, - canRedo: false, - chunkLevels: [1, 2, 3], - name: 'sound name', - playhead: 0.5, - trimStart: 0.2, - trimEnd: 0.8, - onChangeName: jest.fn(), - onDelete: jest.fn(), - onPlay: jest.fn(), - onRedo: jest.fn(), - onReverse: jest.fn(), - onSofter: jest.fn(), - onLouder: jest.fn(), - onRobot: jest.fn(), - onEcho: jest.fn(), - onFaster: jest.fn(), - onSlower: jest.fn(), - onSetTrimEnd: jest.fn(), - onSetTrimStart: jest.fn(), - onStop: jest.fn(), - onUndo: jest.fn() - }; - }); - - test('matches snapshot', () => { - const component = componentWithIntl(); - expect(component.toJSON()).toMatchSnapshot(); - }); - - test('delete button appears when selection is not null', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('[children="Delete"]').simulate('click'); - expect(props.onDelete).toHaveBeenCalled(); - }); - - test('play button appears when playhead is null', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('button[title="Play"]').simulate('click'); - expect(props.onPlay).toHaveBeenCalled(); - }); - - test('stop button appears when playhead is not null', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('button[title="Stop"]').simulate('click'); - expect(props.onStop).toHaveBeenCalled(); - }); - - test('submitting name calls the callback', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('input') - .simulate('change', {target: {value: 'hello'}}) - .simulate('blur'); - expect(props.onChangeName).toHaveBeenCalled(); - }); - - test('effect buttons call the correct callbacks', () => { - const wrapper = mountWithIntl( - - ); - - wrapper.find('[children="Reverse"]').simulate('click'); - expect(props.onReverse).toHaveBeenCalled(); - - wrapper.find('[children="Robot"]').simulate('click'); - expect(props.onRobot).toHaveBeenCalled(); - - wrapper.find('[children="Faster"]').simulate('click'); - expect(props.onFaster).toHaveBeenCalled(); - - wrapper.find('[children="Slower"]').simulate('click'); - expect(props.onSlower).toHaveBeenCalled(); - - wrapper.find('[children="Louder"]').simulate('click'); - expect(props.onLouder).toHaveBeenCalled(); - - wrapper.find('[children="Softer"]').simulate('click'); - expect(props.onSofter).toHaveBeenCalled(); - }); - - test('undo and redo buttons can be disabled by canUndo/canRedo', () => { - let wrapper = mountWithIntl( - - ); - expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(false); - expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(true); - - wrapper = mountWithIntl( - - ); - expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(true); - expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(false); - }); - - test.skip('undo/redo buttons call the correct callback', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('button[title="Undo"]').simulate('click'); - expect(props.onUndo).toHaveBeenCalled(); - - wrapper.find('button[title="Redo"]').simulate('click'); - expect(props.onRedo).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/components/sprite-selector-item.test.jsx b/test/unit/components/sprite-selector-item.test.jsx deleted file mode 100644 index fb103b4efc9..00000000000 --- a/test/unit/components/sprite-selector-item.test.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx'; -import SpriteSelectorItemComponent from '../../../src/components/sprite-selector-item/sprite-selector-item'; -import DeleteButton from '../../../src/components/delete-button/delete-button'; - -describe('SpriteSelectorItemComponent', () => { - let className; - let costumeURL; - let name; - let onClick; - let onDeleteButtonClick; - let selected; - let number; - let details; - - // Wrap this in a function so it gets test specific states and can be reused. - const getComponent = function () { - return ( - - ); - }; - - beforeEach(() => { - className = 'ponies'; - costumeURL = 'https://scratch.mit.edu/foo/bar/pony'; - name = 'Pony sprite'; - onClick = jest.fn(); - onDeleteButtonClick = jest.fn(); - selected = true; - // Reset to undefined since they are optional props - number = undefined; // eslint-disable-line no-undefined - details = undefined; // eslint-disable-line no-undefined - }); - - test('matches snapshot when selected', () => { - const component = componentWithIntl(getComponent()); - expect(component.toJSON()).toMatchSnapshot(); - }); - - test('matches snapshot when given a number and details to show', () => { - number = 5; - details = '480 x 360'; - const component = componentWithIntl(getComponent()); - expect(component.toJSON()).toMatchSnapshot(); - }); - - test('does not have a close box when not selected', () => { - selected = false; - const wrapper = shallowWithIntl(getComponent()); - expect(wrapper.find(DeleteButton).exists()).toBe(false); - }); - - test('triggers callback when Box component is clicked', () => { - // Use `mount` here because of the way ContextMenuTrigger consumes onClick - const wrapper = mountWithIntl(getComponent()); - wrapper.simulate('click'); - expect(onClick).toHaveBeenCalled(); - }); - - test('triggers callback when CloseButton component is clicked', () => { - const wrapper = shallowWithIntl(getComponent()); - wrapper.find(DeleteButton).simulate('click'); - expect(onDeleteButtonClick).toHaveBeenCalled(); - }); - - test('it has a context menu with delete menu item and callback', () => { - const wrapper = mountWithIntl(getComponent()); - const contextMenu = wrapper.find('ContextMenu'); - expect(contextMenu.exists()).toBe(true); - - contextMenu.find('[children="delete"]').simulate('click'); - expect(onDeleteButtonClick).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/components/toggle-buttons.test.jsx b/test/unit/components/toggle-buttons.test.jsx deleted file mode 100644 index 547852461af..00000000000 --- a/test/unit/components/toggle-buttons.test.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import ToggleButtons from '../../../src/components/toggle-buttons/toggle-buttons'; - -describe('ToggleButtons', () => { - test('renders multiple buttons', () => { - const component = shallow( {}, - icon: 'Button 1 icon' - }, - { - title: 'Button 2', - handleClick: () => {}, - icon: 'Button 2 icon' - } - ]} - />); - - const buttons = component.find('button'); - - expect(buttons).toHaveLength(2); - expect(buttons.get(0).props.title).toBe('Button 1'); - expect(buttons.get(1).props.title).toBe('Button 2'); - }); - - test('calls correct click handler', () => { - const onClick1 = jest.fn(); - const onClick2 = jest.fn(); - const component = shallow(); - const button2 = component.find('button[title="Button 2"]'); - button2.simulate('click'); - - expect(onClick2).toHaveBeenCalled(); - expect(onClick1).not.toHaveBeenCalled(); - }); -}); diff --git a/test/unit/containers/menu-bar-hoc.test.jsx b/test/unit/containers/menu-bar-hoc.test.jsx deleted file mode 100644 index 4010665849e..00000000000 --- a/test/unit/containers/menu-bar-hoc.test.jsx +++ /dev/null @@ -1,170 +0,0 @@ -describe('no-op', () => { - test('no-op', () => {}); -}); -// tw: these seem to be hopelessly broken to the increasing scope of changes we make to the menu bar, disable for now... -/* -import React from 'react'; -import {mount} from 'enzyme'; -import configureStore from 'redux-mock-store'; -import MenuBarHOC from '../../../src/containers/menu-bar-hoc.jsx'; - -describe('Menu Bar HOC', () => { - const mockStore = configureStore(); - let store; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectChanged: true - } - }); - }); - - test('Logged in user who IS owner and HAS changed project will NOT be prompted to save', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - (false)} // eslint-disable-line react/jsx-no-bind - store={store} - /> - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().confirmReadyToReplaceProject('message')).toBe(true); - }); - - test('Logged in user who IS owner and has NOT changed project will NOT be prompted to save', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - (false)} // eslint-disable-line react/jsx-no-bind - projectChanged={false} - store={store} - /> - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().confirmReadyToReplaceProject('message')).toBe(true); - }); - - test('Logged in user who is NOT owner and HAS changed project will NOT be prompted to save', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - (false)} // eslint-disable-line react/jsx-no-bind - store={store} - /> - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().confirmReadyToReplaceProject('message')).toBe(true); - }); - - test('Logged OUT user who HAS changed project WILL be prompted to save', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - (false)} // eslint-disable-line react/jsx-no-bind - store={store} - /> - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().confirmReadyToReplaceProject('message')).toBe(false); - }); - - test('Logged OUT user who has NOT changed project WILL NOT be prompted to save', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - (false)} // eslint-disable-line react/jsx-no-bind - projectChanged={false} - store={store} - /> - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().confirmReadyToReplaceProject('message')).toBe(true); - }); - - test('Logged in user who IS owner and HAS changed project SHOULD save before transition to project page', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().shouldSaveBeforeTransition()).toBe(true); - }); - - test('Logged in user who IS owner and has NOT changed project should NOT save before transition', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().shouldSaveBeforeTransition()).toBe(false); - }); - - test('Logged in user who is NOT owner and HAS changed project should NOT save before transition', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().shouldSaveBeforeTransition()).toBe(false); - }); - - test('Logged in user who is NOT owner and has NOT changed project should NOT save before transition', () => { - const Component = () => (
); - const WrappedComponent = MenuBarHOC(Component); - const wrapper = mount( - - ); - const child = wrapper.find(Component); - expect(child.props().projectChanged).toBeUndefined(); - expect(child.props().shouldSaveBeforeTransition()).toBe(false); - }); - -}); -*/ diff --git a/test/unit/containers/save-status.test.jsx b/test/unit/containers/save-status.test.jsx deleted file mode 100644 index c426864d4e3..00000000000 --- a/test/unit/containers/save-status.test.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import {Provider} from 'react-redux'; -import configureStore from 'redux-mock-store'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import SaveStatus from '../../../src/components/menu-bar/save-status.jsx'; -import InlineMessages from '../../../src/containers/inline-messages.jsx'; -import {AlertTypes} from '../../../src/lib/alerts/index.jsx'; - -// Stub the manualUpdateProject action creator for later testing -jest.mock('../../../src/reducers/project-state', () => ({ - manualUpdateProject: jest.fn(() => ({type: 'stubbed'})) -})); - -describe('SaveStatus container', () => { - const mockStore = configureStore(); - - test('if there are inline messages, they are shown instead of save now', () => { - const store = mockStore({ - scratchGui: { - projectChanged: true, - alerts: { - alertsList: [ - {alertId: 'saveSuccess', alertType: AlertTypes.INLINE} - ] - } - } - }); - const wrapper = mountWithIntl( - - - - ); - expect(wrapper.find(InlineMessages).exists()).toBe(true); - expect(wrapper.contains('Save Now')).not.toBe(true); - }); - - test('save now is shown if there are project changes and no inline messages', () => { - const store = mockStore({ - scratchGui: { - projectChanged: true, - alerts: { - alertsList: [] - } - } - }); - const wrapper = mountWithIntl( - - - - ); - expect(wrapper.find(InlineMessages).exists()).not.toBe(true); - expect(wrapper.contains('Save Now')).toBe(true); - - // Clicking save now should dispatch the manualUpdateProject action (stubbed above) - wrapper.find('[children="Save Now"]').simulate('click'); - expect(store.getActions()[0].type).toEqual('stubbed'); - }); - - test('neither is shown if there are no project changes or inline messages', () => { - const store = mockStore({ - scratchGui: { - projectChanged: false, - alerts: { - alertsList: [] - } - } - }); - const wrapper = mountWithIntl( - - - - ); - expect(wrapper.find(InlineMessages).exists()).not.toBe(true); - expect(wrapper.contains('Save Now')).not.toBe(true); - }); -}); diff --git a/test/unit/containers/slider-prompt.test.jsx b/test/unit/containers/slider-prompt.test.jsx deleted file mode 100644 index 0c96c50267c..00000000000 --- a/test/unit/containers/slider-prompt.test.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import {shallow} from 'enzyme'; -import SliderPrompt from '../../../src/containers/slider-prompt.jsx'; -import SliderPromptComponent from '../../../src/components/slider-prompt/slider-prompt.jsx'; - -describe('Slider Prompt Container', () => { - let onCancel; - let onOk; - - beforeEach(() => { - onCancel = jest.fn(); - onOk = jest.fn(); - }); - - test('Min/max are shown with decimal when isDiscrete is false', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - expect(componentProps.minValue).toBe('0.00'); - expect(componentProps.maxValue).toBe('100.00'); - }); - - test('Min/max are NOT shown with decimal when isDiscrete is true', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - expect(componentProps.minValue).toBe('0'); - expect(componentProps.maxValue).toBe('100'); - }); - - test('Entering a number with a decimal submits with isDiscrete=false', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - componentProps.onChangeMin({target: {value: '1.0'}}); - componentProps.onOk(); - expect(onOk).toHaveBeenCalledWith(1, 100, false); - }); - - test('Entering integers submits with isDiscrete=true', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - componentProps.onChangeMin({target: {value: '1'}}); - componentProps.onChangeMax({target: {value: '2'}}); - componentProps.onOk(); - expect(onOk).toHaveBeenCalledWith(1, 2, true); - }); - - test('Enter button submits the form', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - componentProps.onChangeMin({target: {value: '1'}}); - componentProps.onChangeMax({target: {value: '2'}}); - componentProps.onKeyPress({key: 'Enter'}); - expect(onOk).toHaveBeenCalledWith(1, 2, true); - }); - - test('Validates number-ness before submitting', () => { - const wrapper = shallow( - - ); - const componentProps = wrapper.find(SliderPromptComponent).props(); - componentProps.onChangeMin({target: {value: 'hello'}}); - componentProps.onOk(); - expect(onOk).not.toHaveBeenCalled(); - expect(onCancel).toHaveBeenCalled(); - }); -}); diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx deleted file mode 100644 index 53f6e374f3c..00000000000 --- a/test/unit/containers/sound-editor.test.jsx +++ /dev/null @@ -1,306 +0,0 @@ -import React from 'react'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import configureStore from 'redux-mock-store'; -import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js'; -import mockAudioEffects from '../../__mocks__/audio-effects.js'; - -import SoundEditor from '../../../src/containers/sound-editor'; -import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor'; - -jest.mock('react-ga'); -jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer); -jest.mock('../../../src/lib/audio/audio-effects', () => mockAudioEffects); - -describe('Sound Editor Container', () => { - const mockStore = configureStore(); - let store; - let soundIndex; - let soundBuffer; - const samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef - let vm; - - beforeEach(() => { - soundIndex = 0; - soundBuffer = { - numberOfChannels: 1, - sampleRate: 0, - getChannelData: jest.fn(() => samples) - }; - vm = { - getSoundBuffer: jest.fn(() => soundBuffer), - renameSound: jest.fn(), - updateSoundBuffer: jest.fn(), - editingTarget: { - sprite: { - sounds: [{name: 'first name', id: 'first id'}] - } - } - }; - store = mockStore({scratchGui: {vm: vm, mode: {isFullScreen: false}}}); - }); - - test('should pass the correct data to the component from the store', () => { - const wrapper = mountWithIntl( - - ); - const componentProps = wrapper.find(SoundEditorComponent).props(); - // Data retreived and processed by the `connect` with the store - expect(componentProps.name).toEqual('first name'); - expect(componentProps.chunkLevels).toEqual([0]); - expect(mockAudioBufferPlayer.instance.samples).toEqual(samples); - // Initial data - expect(componentProps.playhead).toEqual(null); - expect(componentProps.trimStart).toEqual(null); - expect(componentProps.trimEnd).toEqual(null); - - }); - - test('it plays when clicked and stops when clicked again', () => { - const wrapper = mountWithIntl( - - ); - let component = wrapper.find(SoundEditorComponent); - // Ensure rendering doesn't start playing any sounds - expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]); - expect(mockAudioBufferPlayer.instance.stop.mock.calls).toEqual([]); - - component.props().onPlay(); - expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); - - // Mock the audio buffer player calling onUpdate - mockAudioBufferPlayer.instance.onUpdate(0.5); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - expect(component.props().playhead).toEqual(0.5); - - component.props().onStop(); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - expect(mockAudioBufferPlayer.instance.stop).toHaveBeenCalled(); - expect(component.props().playhead).toEqual(null); - }); - - test('it submits name changes to the vm', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onChangeName('hello'); - expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello'); - }); - - test('it handles an effect by submitting the result and playing', async () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onReverse(); // Could be any of the effects, just testing the end result - await mockAudioEffects.instance._finishProcessing(soundBuffer); - expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); - expect(vm.updateSoundBuffer).toHaveBeenCalled(); - }); - - test('it handles reverse effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onReverse(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.REVERSE); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles louder effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onLouder(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.LOUDER); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles softer effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onSofter(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SOFTER); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles faster effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onFaster(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.FASTER); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles slower effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onSlower(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SLOWER); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles echo effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onEcho(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ECHO); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('it handles robot effect correctly', () => { - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - component.props().onRobot(); - expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ROBOT); - expect(mockAudioEffects.instance.process).toHaveBeenCalled(); - }); - - test('undo/redo stack state', async () => { - const wrapper = mountWithIntl( - - ); - let component = wrapper.find(SoundEditorComponent); - // Undo and redo should be disabled initially - expect(component.prop('canUndo')).toEqual(false); - expect(component.prop('canRedo')).toEqual(false); - - // Submitting new samples should make it possible to undo - component.props().onFaster(); - 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 - 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 - 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 - 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(); - 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', async () => { - const wrapper = mountWithIntl( - - ); - let component = wrapper.find(SoundEditorComponent); - - // Set up an undoable state - component.props().onFaster(); - await mockAudioEffects.instance._finishProcessing(soundBuffer); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - - // Undo should update the sound buffer and play the new samples - await component.props().onUndo(); - expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); - expect(vm.updateSoundBuffer).toHaveBeenCalled(); - - // Clear the mocks call history to assert again for redo. - vm.updateSoundBuffer.mockClear(); - mockAudioBufferPlayer.instance.play.mockClear(); - - // Undo should update the sound buffer and play the new samples - await component.props().onRedo(); - expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); - expect(vm.updateSoundBuffer).toHaveBeenCalled(); - }); - - test('isStereo numberOfChannels=1', () => { - soundBuffer.numberOfChannels = 1; - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - expect(component.props().isStereo).toEqual(false); - }); - - test('isStereo numberOfChannels=2', () => { - soundBuffer.numberOfChannels = 2; - const wrapper = mountWithIntl( - - ); - const component = wrapper.find(SoundEditorComponent); - expect(component.props().isStereo).toEqual(true); - }); -}); diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx deleted file mode 100644 index 89018f5ef36..00000000000 --- a/test/unit/containers/sprite-selector-item.test.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import configureStore from 'redux-mock-store'; -import {Provider} from 'react-redux'; - -import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; -import DeleteButton from '../../../src/components/delete-button/delete-button'; - -describe('SpriteSelectorItem Container', () => { - const mockStore = configureStore(); - let className; - let costumeURL; - let name; - let onClick; - let dispatchSetHoveredSprite; - let onDeleteButtonClick; - let selected; - let id; - let store; - // Wrap this in a function so it gets test specific states and can be reused. - const getContainer = function () { - return ( - - - - ); - }; - - beforeEach(() => { - store = mockStore({scratchGui: { - hoveredTarget: {receivedBlocks: false, sprite: null}, - assetDrag: {dragging: false} - }}); - className = 'ponies'; - costumeURL = 'https://scratch.mit.edu/foo/bar/pony'; - id = 1337; - name = 'Pony sprite'; - onClick = jest.fn(); - onDeleteButtonClick = jest.fn(); - dispatchSetHoveredSprite = jest.fn(); - selected = true; - }); - - test('should delete the sprite', () => { - const wrapper = mountWithIntl(getContainer()); - wrapper.find(DeleteButton).simulate('click'); - expect(onDeleteButtonClick).toHaveBeenCalledWith(1337); - }); -}); diff --git a/test/unit/reducers/alerts-reducer.test.js b/test/unit/reducers/alerts-reducer.test.js deleted file mode 100644 index 8048d5aef6e..00000000000 --- a/test/unit/reducers/alerts-reducer.test.js +++ /dev/null @@ -1,208 +0,0 @@ -// TODO: add tests of extension alerts - -/* eslint-env jest */ -import {AlertTypes, AlertLevels} from '../../../src/lib/alerts/index.jsx'; -import alertsReducer, { - closeAlert, - closeAlertWithId, - filterInlineAlerts, - filterPopupAlerts, - showStandardAlert -} from '../../../src/reducers/alerts'; - -test('initialState', () => { - let defaultState; - /* alertsReducer(state, action) */ - expect(alertsReducer(defaultState, {type: 'anything'})).toBeDefined(); - expect(alertsReducer(defaultState, {type: 'anything'}).visible).toBe(true); - expect(alertsReducer(defaultState, {type: 'anything'}).alertsList).toEqual([]); -}); - -test('create one standard alert', () => { - let defaultState; - const action = showStandardAlert('creating'); - const resultState = alertsReducer(defaultState, action); - expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertId).toBe('creating'); - expect(resultState.alertsList[0].alertType).toBe(AlertTypes.STANDARD); - expect(resultState.alertsList[0].level).toBe(AlertLevels.SUCCESS); -}); - -test('add several standard alerts', () => { - const initialState = { - visible: true, - alertsList: [ - { - alertId: 'saving', - alertType: AlertTypes.INLINE, - level: AlertLevels.SUCCESS, - content: null, - iconURL: '/no_image_here.jpg' - } - ] - }; - const action = showStandardAlert('creating'); - let resultState = alertsReducer(initialState, action); - resultState = alertsReducer(resultState, action); - resultState = alertsReducer(resultState, action); - expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertType).toBe(AlertTypes.STANDARD); - expect(resultState.alertsList[0].iconURL).not.toBe('/no_image_here.jpg'); - expect(resultState.alertsList[0].alertId).toBe('creating'); -}); - -test('create one inline alert message', () => { - let defaultState; - const action = showStandardAlert('saving'); - const resultState = alertsReducer(defaultState, action); - expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertId).toBe('saving'); - expect(resultState.alertsList[0].alertType).toBe(AlertTypes.INLINE); - expect(resultState.alertsList[0].level).toBe(AlertLevels.INFO); -}); - -test('can close alerts by index', () => { - const initialState = { - visible: true, - alertsList: [ - { - alertId: 'saving', - alertType: AlertTypes.INLINE, - level: AlertLevels.SUCCESS, - content: null, - iconURL: '/no_image_here.jpg' - } - ] - }; - const closeAction = closeAlert(0); - let resultState = alertsReducer(initialState, closeAction); - expect(resultState.alertsList.length).toBe(0); - const createAction = showStandardAlert('creating'); - resultState = alertsReducer(resultState, createAction); - expect(resultState.alertsList.length).toBe(1); - resultState = alertsReducer(initialState, closeAction); - expect(resultState.alertsList.length).toBe(0); - resultState = alertsReducer(resultState, createAction); -}); - -test('can close a single alert by id', () => { - const initialState = { - visible: true, - alertsList: [ - {alertId: 'saving'}, - {alertId: 'creating'}, - {alertId: 'saving'}, - {alertId: 'saving'} - ] - }; - const closeAction = closeAlertWithId('saving'); - let resultState = alertsReducer(initialState, closeAction); - expect(resultState.alertsList.map(a => a.alertId)).toEqual([ - 'creating', 'saving', 'saving' - ]); - resultState = alertsReducer(resultState, closeAction); - expect(resultState.alertsList.map(a => a.alertId)).toEqual([ - 'creating', 'saving' - ]); - resultState = alertsReducer(resultState, closeAction); - expect(resultState.alertsList.map(a => a.alertId)).toEqual([ - 'creating' - ]); - resultState = alertsReducer(resultState, closeAction); - expect(resultState.alertsList.map(a => a.alertId)).toEqual([ - 'creating' - ]); -}); - -test('related alerts can clear each other', () => { - const initialState = { - visible: true, - alertsList: [ - { - alertId: 'saving', - alertType: AlertTypes.INLINE, - level: AlertLevels.SUCCESS, - content: null, - iconURL: '/no_image_here.jpg' - }, - { - alertId: 'creating', - alertType: AlertTypes.STANDARD, - level: AlertLevels.SUCCESS, - content: null, - iconURL: '/no_image_here.jpg' - } - ] - }; - const action = showStandardAlert('saveSuccess'); - const resultState = alertsReducer(initialState, action); - expect(resultState.alertsList.length).toBe(2); - expect(resultState.alertsList[0].alertId).toBe('creating'); - expect(resultState.alertsList[1].alertId).toBe('saveSuccess'); -}); - -test('several related alerts can be cleared at once', () => { - const initialState = { - visible: true, - alertsList: [] - }; - const createAction = showStandardAlert('creating'); - let resultState = alertsReducer(initialState, createAction); - resultState = alertsReducer(resultState, createAction); - resultState = alertsReducer(resultState, createAction); - const createSuccessAction = showStandardAlert('createSuccess'); - resultState = alertsReducer(resultState, createSuccessAction); - expect(resultState.alertsList.length).toBe(1); - expect(resultState.alertsList[0].alertId).toBe('createSuccess'); -}); - -test('filterInlineAlerts only returns inline type alerts', () => { - const alerts = [ - { - alertId: 'extension', - alertType: AlertTypes.EXTENSION - }, - { - alertId: 'inline', - alertType: AlertTypes.INLINE - }, - { - alertId: 'standard', - alertType: AlertTypes.STANDARD - }, - { - alertId: 'non-existent type', - alertType: 'wirly-burly' - } - ]; - - const filtered = filterInlineAlerts(alerts); - expect(filtered.length).toEqual(1); - expect(filtered[0].alertId).toEqual('inline'); -}); - -test('filterPopupAlerts returns standard and extension type alerts', () => { - const alerts = [ - { - alertId: 'extension', - alertType: AlertTypes.EXTENSION - }, - { - alertId: 'inline', - alertType: AlertTypes.INLINE - }, - { - alertId: 'standard', - alertType: AlertTypes.STANDARD - }, - { - alertId: 'non-existent type', - alertType: 'wirly-burly' - } - ]; - - const filtered = filterPopupAlerts(alerts); - expect(filtered.length).toEqual(2); - expect(filtered[0].alertId).toEqual('extension'); - expect(filtered[1].alertId).toEqual('standard'); -}); diff --git a/test/unit/reducers/mode-reducer.test.js b/test/unit/reducers/mode-reducer.test.js deleted file mode 100644 index 641398f2d5c..00000000000 --- a/test/unit/reducers/mode-reducer.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-env jest */ -import modeReducer from '../../../src/reducers/mode'; - -const SET_FULL_SCREEN = 'scratch-gui/mode/SET_FULL_SCREEN'; -const SET_PLAYER = 'scratch-gui/mode/SET_PLAYER'; - -test('initialState', () => { - let defaultState; - /* modeReducer(state, action) */ - expect(modeReducer(defaultState, {type: 'anything'})).toBeDefined(); -}); - -test('set full screen mode', () => { - const previousState = { - showBranding: false, - isFullScreen: false, - isPlayerOnly: false, - hasEverEnteredEditor: true - }; - const action = { - type: SET_FULL_SCREEN, - isFullScreen: true - }; - const newState = { - showBranding: false, - isFullScreen: true, - isPlayerOnly: false, - hasEverEnteredEditor: true - }; - /* modeReducer(state, action) */ - expect(modeReducer(previousState, action)).toEqual(newState); -}); - -test('set player mode', () => { - const previousState = { - showBranding: false, - isFullScreen: false, - isPlayerOnly: false, - hasEverEnteredEditor: true - }; - const action = { - type: SET_PLAYER, - isPlayerOnly: true - }; - const newState = { - showBranding: false, - isFullScreen: false, - isPlayerOnly: true, - hasEverEnteredEditor: true - }; - /* modeReducer(state, action) */ - expect(modeReducer(previousState, action)).toEqual(newState); -}); diff --git a/test/unit/reducers/monitor-layout-reducer.test.js b/test/unit/reducers/monitor-layout-reducer.test.js deleted file mode 100644 index 8aa01266c5a..00000000000 --- a/test/unit/reducers/monitor-layout-reducer.test.js +++ /dev/null @@ -1,302 +0,0 @@ -/* eslint-env jest */ -import monitorLayoutReducer, { - addMonitorRect, moveMonitorRect, - resizeMonitorRect, removeMonitorRect, - getInitialPosition, PADDING, SCREEN_WIDTH, SCREEN_HEIGHT -} from '../../../src/reducers/monitor-layout'; - -test('initialState', () => { - let defaultState; - - expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined(); - expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).monitors).toBeDefined(); - expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).savedMonitorPositions) - .toBeDefined(); -}); - -test('addMonitorRect', () => { - let defaultState; - const monitorId = 1; - const monitorId2 = 2; - const upperStart = {x: 100, y: 100}; - const lowerEnd = {x: 200, y: 200}; - - // Add a monitor rect - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd)); - expect(reduxState.monitors[monitorId]).toBeDefined(); - expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart); - expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd); - // Add monitor rect doesn't save position - expect(reduxState.savedMonitorPositions[monitorId]).toBeUndefined(); - const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0)); - - // Add a second monitor rect - const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd)); - expect(reduxState3.monitors[monitorId]).toBeDefined(); - expect(reduxState3.monitors[monitorId2]).toBeDefined(); - expect(reduxState3.monitors[monitorId2].upperStart).toEqual(upperStart); - expect(reduxState3.monitors[monitorId2].lowerEnd).toEqual(lowerEnd); - // Saved positions aren't changed by adding monitor - expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions); -}); - -test('addMonitorRectWithSavedPosition', () => { - let defaultState; - const monitorId = 1; - const upperStart = {x: 100, y: 100}; - const lowerEnd = {x: 200, y: 200}; - - // Add a monitor rect - const reduxState = monitorLayoutReducer(defaultState, - addMonitorRect(monitorId, upperStart, lowerEnd, true /* savePosition */)); - expect(reduxState.monitors[monitorId]).toBeDefined(); - expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart); - expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd); - // Save position - expect(reduxState.savedMonitorPositions[monitorId].x).toEqual(upperStart.x); - expect(reduxState.savedMonitorPositions[monitorId].y).toEqual(upperStart.y); -}); - -test('invalidRect', () => { - let defaultState; - const reduxState = monitorLayoutReducer(defaultState /* state */, {type: 'initialize'} /* action */); - - // Problem: x end is before x start - expect( - monitorLayoutReducer(reduxState, - addMonitorRect(1, {x: 100, y: 100}, {x: 10, y: 200}))) - .toEqual(reduxState); - - // Problem: y end is before y start - expect( - monitorLayoutReducer(reduxState, - addMonitorRect(1, {x: 100, y: 100}, {x: 200, y: 10}))) - .toEqual(reduxState); -}); - -test('invalidAddMonitorRect', () => { - let defaultState; - const monitorId = 1; - const upperStart = {x: 100, y: 100}; - const lowerEnd = {x: 200, y: 200}; - - // Add a monitor rect - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd)); - // Try to add the same one - expect(monitorLayoutReducer(reduxState, addMonitorRect(monitorId, upperStart, lowerEnd))) - .toEqual(reduxState); -}); - -test('moveMonitorRect', () => { - let defaultState; - const monitorId = 1; - const monitorId2 = 2; - const width = 102; - const height = 101; - const upperStart = {x: 100, y: 100}; - const lowerEnd = {x: upperStart.x + width, y: upperStart.y + height}; - const movedToPosition = {x: 0, y: 0}; - const movedToPosition2 = {x: 543, y: 2}; - - // Add a monitor rect and move it. Expect it to be in monitors state and saved positions. - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd)); - const reduxState2 = monitorLayoutReducer(reduxState, - moveMonitorRect(monitorId, movedToPosition.x, movedToPosition.y)); - expect(reduxState2.monitors[monitorId]).toBeDefined(); - expect(reduxState2.monitors[monitorId].upperStart).toEqual(movedToPosition); - expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(movedToPosition.x + width); - expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(movedToPosition.y + height); - expect(reduxState2.savedMonitorPositions[monitorId]).toBeDefined(); - expect(reduxState2.savedMonitorPositions[monitorId].x).toEqual(movedToPosition.x); - expect(reduxState2.savedMonitorPositions[monitorId].y).toEqual(movedToPosition.y); - - // Add a second monitor rect and move it. Expect there to now be 2 saved positions. - const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd)); - const reduxState4 = monitorLayoutReducer(reduxState3, - moveMonitorRect(monitorId2, movedToPosition2.x, movedToPosition2.y)); - expect(reduxState4.savedMonitorPositions[monitorId]).toEqual(reduxState2.savedMonitorPositions[monitorId]); - expect(reduxState4.savedMonitorPositions[monitorId2].x).toEqual(movedToPosition2.x); - expect(reduxState4.savedMonitorPositions[monitorId2].y).toEqual(movedToPosition2.y); -}); - -test('invalidMoveMonitorRect', () => { - let defaultState; - let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */); - const monitorId = 1; - - // Try to move a monitor rect that doesn't exist - expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */, 1 /* newY */))) - .toEqual(reduxState); - - // Add the monitor to move - reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200})); - - // Invalid newX - expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 'Oregon' /* newX */, 1 /* newY */))) - .toEqual(reduxState); - - // Invalid newY - expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */))) - .toEqual(reduxState); -}); - -test('resizeMonitorRect', () => { - let defaultState; - const monitorId = 1; - const upperStart = {x: 100, y: 100}; - const newWidth = 10; - const newHeight = 20; - - // Add a monitor rect and resize it - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, {x: 200, y: 200})); - const reduxState2 = monitorLayoutReducer(reduxState, - resizeMonitorRect(monitorId, newWidth, newHeight)); - expect(reduxState2.monitors[monitorId]).toBeDefined(); - expect(reduxState2.monitors[monitorId].upperStart).toEqual(upperStart); - expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(upperStart.x + newWidth); - expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(upperStart.y + newHeight); - // Saved positions aren't changed by resizing monitor - expect(reduxState2.savedMonitorPositions).toEqual(reduxState.savedMonitorPositions); -}); - -test('invalidResizeMonitorRect', () => { - let defaultState; - let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */); - const monitorId = 1; - - // Try to resize a monitor rect that doesn't exist - expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, 1 /* newHeight */))) - .toEqual(reduxState); - - // Add the monitor to resize - reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200})); - - // Invalid newWidth - expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 'Oregon' /* newWidth */, 1 /* newHeight */))) - .toEqual(reduxState); - - // Invalid newHeight - expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newWidth */))) - .toEqual(reduxState); - - // newWidth < 0 - expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, -1 /* newWidth */, 1 /* newHeight */))) - .toEqual(reduxState); - - // newHeight < 0 - expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, -1 /* newHeight */))) - .toEqual(reduxState); -}); - -test('removeMonitorRect', () => { - let defaultState; - const monitorId = 1; - - // Add a monitor rect, move it, and remove it - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, - {x: 100, y: 100}, - {x: 200, y: 200} - )); - const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0)); - const reduxState3 = monitorLayoutReducer(reduxState2, removeMonitorRect(monitorId)); - expect(reduxState3.monitors[monitorId]).toBeUndefined(); - // Check that saved positions aren't changed by removing monitor - expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions); -}); - -test('invalidRemoveMonitorRect', () => { - let defaultState; - const reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */); - - // Try to remove a monitor rect that doesn't exist - expect(monitorLayoutReducer(reduxState, resizeMonitorRect(1))) - .toEqual(reduxState); -}); - -test('getInitialPosition_lineUpTopLeft', () => { - let defaultState; - const width = 100; - const height = 200; - // Add monitors to right and bottom, but there is a space in the top left - let reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, - {x: width + PADDING, y: 0}, - {x: 100, y: height} - )); - reduxState = monitorLayoutReducer(defaultState, addMonitorRect(2, - {x: 0, y: height + PADDING}, - {x: width, y: 100} - )); - - // Check that the added monitor appears in the space - const rect = getInitialPosition(reduxState, 3, width, height); - expect(rect.upperStart).toBeDefined(); - expect(rect.lowerEnd).toBeDefined(); - expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width); - expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height); - expect(rect.upperStart.x).toEqual(PADDING); - expect(rect.upperStart.y).toEqual(PADDING); -}); - -test('getInitialPosition_savedPosition', () => { - const monitorId = 1; - const savedX = 100; - const savedY = 200; - const width = 7; - const height = 8; - const reduxState = { - monitors: {}, - savedMonitorPositions: {[monitorId]: {x: savedX, y: savedY}} - }; - - // Check that initial position uses saved state - const rect = getInitialPosition(reduxState, monitorId, width, height); - expect(rect.upperStart).toBeDefined(); - expect(rect.lowerEnd).toBeDefined(); - expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width); - expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height); - expect(rect.upperStart.x).toEqual(savedX); - expect(rect.upperStart.y).toEqual(savedY); -}); - -test('getInitialPosition_lineUpLeft', () => { - let defaultState; - const monitor1EndY = 60; - // Add a monitor that takes up the upper left corner - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, {x: 0, y: 0}, {x: 100, y: monitor1EndY})); - - // Check that added monitor is under it and lines up left - const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */); - expect(rect.upperStart.y >= monitor1EndY + PADDING).toBeTruthy(); -}); - -test('getInitialPosition_lineUpTop', () => { - let defaultState; - const monitor1EndX = 100; - // Add a monitor that takes up the whole left side - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, - {x: 0, y: 0}, - {x: monitor1EndX, y: SCREEN_HEIGHT} - )); - - // Check that added monitor is to the right of it and lines up top - const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */); - expect(rect.upperStart.y).toEqual(PADDING); - expect(rect.upperStart.x >= monitor1EndX + PADDING).toBeTruthy(); -}); - -test('getInitialPosition_noRoom', () => { - let defaultState; - const width = 7; - const height = 8; - // Add a monitor that takes up the whole screen - const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, - {x: 0, y: 0}, - {x: SCREEN_WIDTH, y: SCREEN_HEIGHT} - )); - - // Check that added monitor exists somewhere (we don't care where) - const rect = getInitialPosition(reduxState, 2, width, height); - expect(rect.upperStart).toBeDefined(); - expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width); - expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height); -}); diff --git a/test/unit/reducers/project-state-reducer.test.js b/test/unit/reducers/project-state-reducer.test.js deleted file mode 100644 index 8aabc107244..00000000000 --- a/test/unit/reducers/project-state-reducer.test.js +++ /dev/null @@ -1,508 +0,0 @@ -/* eslint-env jest */ -import projectStateReducer, { - LoadingState, - autoUpdateProject, - doneCreatingProject, - doneUpdatingProject, - manualUpdateProject, - onFetchedProjectData, - onLoadedProject, - projectError, - remixProject, - requestNewProject, - requestProjectUpload, - saveProjectAsCopy, - setProjectId -} from '../../../src/reducers/project-state'; - -test('initialState', () => { - let defaultState; - /* projectStateReducer(state, action) */ - expect(projectStateReducer(defaultState, {type: 'anything'})).toBeDefined(); - expect(projectStateReducer(defaultState, {type: 'anything'}).error).toBe(null); - expect(projectStateReducer(defaultState, {type: 'anything'}).projectData).toBe(null); - expect(projectStateReducer(defaultState, {type: 'anything'}).projectId).toBe(null); - expect(projectStateReducer(defaultState, {type: 'anything'}).loadingState).toBe(LoadingState.NOT_LOADED); -}); - -test('doneCreatingProject for new project with projectId type string shows project with that id', () => { - const initialState = { - projectId: null, - loadingState: LoadingState.CREATING_NEW - }; - const action = doneCreatingProject('100', initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); - expect(resultState.projectId).toBe('100'); -}); - -test('doneCreatingProject for new project with projectId type number shows project with id of type number', () => { - const initialState = { - projectId: null, - loadingState: LoadingState.CREATING_NEW - }; - const action = doneCreatingProject(100, initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); - expect(resultState.projectId).toBe(100); -}); - -test('doneCreatingProject for remix shows project with that id', () => { - const initialState = { - projectId: null, - loadingState: LoadingState.REMIXING - }; - const action = doneCreatingProject('100', initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); - expect(resultState.projectId).toBe('100'); -}); - -test('doneCreatingProject for save as copy shows project with that id', () => { - const initialState = { - projectId: null, - loadingState: LoadingState.CREATING_COPY - }; - const action = doneCreatingProject('100', initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); - expect(resultState.projectId).toBe('100'); -}); - -test('onFetchedProjectData with id loads project data into vm', () => { - const initialState = { - projectData: null, - loadingState: LoadingState.FETCHING_WITH_ID - }; - const action = onFetchedProjectData('1010101', initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_WITH_ID); - expect(resultState.projectData).toBe('1010101'); -}); - -test('onFetchedProjectData new loads project data into vm', () => { - const initialState = { - projectData: null, - loadingState: LoadingState.FETCHING_NEW_DEFAULT - }; - const action = onFetchedProjectData('1010101', initialState.loadingState); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.LOADING_VM_NEW_DEFAULT); - expect(resultState.projectData).toBe('1010101'); -}); - -// onLoadedProject: LOADING_VM_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(LOADING_VM_WITH_ID, false, true) results in state SHOWING_WITH_ID', () => { - const initialState = { - 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(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(LOADING_VM_NEW_DEFAULT, false, true) results in state SHOWING_WITHOUT_ID', () => { - const initialState = { - projectId: '0', - loadingState: LoadingState.LOADING_VM_NEW_DEFAULT - }; - 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(LOADING_VM_NEW_DEFAULT, false, false) results in ERROR state', () => { - const initialState = { - 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.ERROR); - expect(resultState.projectId).toBe('0'); -}); - -// 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 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, 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('calling setProjectId, using with same id as already showing, ' + - 'results in state SHOWING_WITH_ID', () => { - const initialState = { - projectId: '100', - loadingState: LoadingState.SHOWING_WITH_ID - }; - const action = setProjectId('100'); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); - expect(resultState.projectId).toBe('100'); -}); - -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 - }; - const action = setProjectId('100'); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.FETCHING_WITH_ID); - expect(resultState.projectId).toBe('100'); -}); - -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 - }; - const action = setProjectId(100); - const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.FETCHING_WITH_ID); - expect(resultState.projectId).toBe(100); -}); - -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/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 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 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 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', () => { - const startStates = [ - LoadingState.AUTO_UPDATING, - LoadingState.CREATING_NEW, - LoadingState.FETCHING_NEW_DEFAULT, - LoadingState.FETCHING_WITH_ID, - LoadingState.LOADING_VM_NEW_DEFAULT, - LoadingState.LOADING_VM_WITH_ID, - LoadingState.MANUAL_UPDATING, - LoadingState.REMIXING, - LoadingState.CREATING_COPY, - LoadingState.UPDATING_BEFORE_NEW - ]; - 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'); - } -}); - -test('fatal projectError should show error state', () => { - const startStates = [ - LoadingState.FETCHING_NEW_DEFAULT, - LoadingState.FETCHING_WITH_ID, - LoadingState.LOADING_VM_NEW_DEFAULT, - LoadingState.LOADING_VM_WITH_ID - ]; - 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'); - } -}); - -test('non-fatal projectError should show normal state', () => { - const startStates = [ - LoadingState.AUTO_UPDATING, - LoadingState.CREATING_COPY, - LoadingState.MANUAL_UPDATING, - LoadingState.REMIXING, - LoadingState.UPDATING_BEFORE_NEW - ]; - 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 ' + - 'go back to state SHOWING_WITH_ID', () => { - const initialState = { - error: null, - loadingState: LoadingState.CREATING_NEW, - projectId: '12345' - }; - 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 go back to state SHOWING_WITHOUT_ID', () => { - const initialState = { - error: null, - loadingState: LoadingState.CREATING_NEW, - projectId: '0' - }; - 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 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'); -}); diff --git a/test/unit/reducers/workspace-metrics-reducer.test.js b/test/unit/reducers/workspace-metrics-reducer.test.js deleted file mode 100644 index f1cf1a70de4..00000000000 --- a/test/unit/reducers/workspace-metrics-reducer.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-env jest */ -import workspaceMetricsReducer, {updateMetrics} from '../../../src/reducers/workspace-metrics'; - -test('initialState', () => { - let defaultState; - /* workspaceMetricsReducer(state, action) */ - expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toBeDefined(); - expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toEqual({targets: {}}); -}); - -test('updateMetrics action creator', () => { - let defaultState; - const action = updateMetrics({ - targetID: 'abcde', - scrollX: 225, - scrollY: 315, - scale: 1.25 - }); - const resultState = workspaceMetricsReducer(defaultState, action); - expect(Object.keys(resultState.targets).length).toBe(1); - expect(resultState.targets.abcde).toBeDefined(); - expect(resultState.targets.abcde.scrollX).toBe(225); - expect(resultState.targets.abcde.scrollY).toBe(315); - expect(resultState.targets.abcde.scale).toBe(1.25); -}); diff --git a/test/unit/util/audio-context.test.js b/test/unit/util/audio-context.test.js deleted file mode 100644 index c46d1317ddb..00000000000 --- a/test/unit/util/audio-context.test.js +++ /dev/null @@ -1,30 +0,0 @@ -/* global WebAudioTestAPI */ -import 'web-audio-test-api'; -WebAudioTestAPI.setState({ - 'AudioContext#resume': 'enabled' -}); - -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); - }); -}); diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js deleted file mode 100644 index 330fe12a18f..00000000000 --- a/test/unit/util/audio-effects.test.js +++ /dev/null @@ -1,91 +0,0 @@ -/* global WebAudioTestAPI */ -import 'web-audio-test-api'; -WebAudioTestAPI.setState({ - 'OfflineAudioContext#startRendering': 'promise' -}); - -import AudioEffects from '../../../src/lib/audio/audio-effects'; -import RobotEffect from '../../../src/lib/audio/effects/robot-effect'; -import EchoEffect from '../../../src/lib/audio/effects/echo-effect'; -import VolumeEffect from '../../../src/lib/audio/effects/volume-effect'; - -describe('Audio Effects manager', () => { - const audioContext = new AudioContext(); - const audioBuffer = audioContext.createBuffer(1, 400, 44100); - - test('changes buffer length and playback rate for faster effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'faster', 0, 1); - expect(audioEffects.audioContext._.length).toBeLessThan(400); - }); - - test('changes buffer length and playback rate for slower effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'slower', 0, 1); - expect(audioEffects.audioContext._.length).toBeGreaterThan(400); - }); - - test('changes buffer length for echo effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'echo', 0, 1); - expect(audioEffects.audioContext._.length).toBeGreaterThan(400); - }); - - test('updates the trim positions after an effect has changed the length of selection', () => { - const slowerEffect = new AudioEffects(audioBuffer, 'slower', 0.25, 0.75); - expect(slowerEffect.adjustedTrimStartSeconds).toEqual(slowerEffect.trimStartSeconds); - expect(slowerEffect.adjustedTrimEndSeconds).toBeGreaterThan(slowerEffect.trimEndSeconds); - - const fasterEffect = new AudioEffects(audioBuffer, 'faster', 0.25, 0.75); - expect(fasterEffect.adjustedTrimStartSeconds).toEqual(fasterEffect.trimStartSeconds); - expect(fasterEffect.adjustedTrimEndSeconds).toBeLessThan(fasterEffect.trimEndSeconds); - - // Some effects do not change the length of the selection - const fadeEffect = new AudioEffects(audioBuffer, 'fade in', 0.25, 0.75); - expect(fadeEffect.adjustedTrimStartSeconds).toEqual(fadeEffect.trimStartSeconds); - // Should be within one millisecond (flooring can change the duration by one sample) - expect(fadeEffect.adjustedTrimEndSeconds).toBeCloseTo(fadeEffect.trimEndSeconds, 3); - }); - - test.skip('process starts the offline rendering context and returns a promise', () => { - // @todo haven't been able to get web audio test api to actually run render - }); - - test('reverse effect strictly reverses the samples', () => { - const fakeSound = [1, 2, 3, 4, 5, 6, 7, 8]; - - const fakeBuffer = audioContext.createBuffer(1, 8, 44100); - const bufferData = fakeBuffer.getChannelData(0); - fakeSound.forEach((sample, index) => { - bufferData[index] = sample; - }); - - // Reverse the entire sound - const reverseAll = new AudioEffects(fakeBuffer, 'reverse', 0, 1); - expect(Array.from(reverseAll.buffer.getChannelData(0))).toEqual(fakeSound.reverse()); - - // Reverse part of the sound - const reverseSelection = new AudioEffects(fakeBuffer, 'reverse', 0.25, 0.75); - const selectionReversed = [1, 2, 6, 5, 4, 3, 7, 8]; - expect(Array.from(reverseSelection.buffer.getChannelData(0))).toEqual(selectionReversed); - }); -}); - -describe('Effects', () => { - let audioContext; - - beforeEach(() => { - audioContext = new AudioContext(); - }); - - test('all effects provide an input and output that are connected', () => { - const robotEffect = new RobotEffect(audioContext, 0, 1); - expect(robotEffect.input).toBeInstanceOf(AudioNode); - expect(robotEffect.output).toBeInstanceOf(AudioNode); - - const echoEffect = new EchoEffect(audioContext, 0, 1); - expect(echoEffect.input).toBeInstanceOf(AudioNode); - expect(echoEffect.output).toBeInstanceOf(AudioNode); - - const volumeEffect = new VolumeEffect(audioContext, 0.5, 0, 1); - expect(volumeEffect.input).toBeInstanceOf(AudioNode); - expect(volumeEffect.output).toBeInstanceOf(AudioNode); - }); -}); diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js deleted file mode 100644 index e571c0513d3..00000000000 --- a/test/unit/util/audio-util.test.js +++ /dev/null @@ -1,102 +0,0 @@ -import { - computeRMS, - computeChunkedRMS, - downsampleIfNeeded, - dropEveryOtherSample -} from '../../../src/lib/audio/audio-util'; - -describe('computeRMS', () => { - test('returns 0 when given no samples', () => { - expect(computeRMS([])).toEqual(0); - }); - test('returns the RMS scaled by the given unity value and square rooted', () => { - const unity = 0.5; - const samples = [3, 2, 1]; - expect(computeRMS(samples, unity)).toEqual( - Math.sqrt(Math.sqrt(((3 * 3) + (2 * 2) + (1 * 1)) / 3) / 0.5) - ); - }); - test('uses a default unity value of 0.55', () => { - const samples = [1, 1, 1]; - // raw rms is 1, scaled to (1 / 0.55) and square rooted - expect(computeRMS(samples)).toEqual(Math.sqrt(1 / 0.55)); - }); -}); - - -describe('computeChunkedRMS', () => { - test('computes the rms for each chunk based on chunk size', () => { - const samples = [2, 1, 3, 2, 5]; - const chunkedLevels = computeChunkedRMS(samples, 2); - // chunked to [2, 0], [3, 0], [5] - // rms scaled with default unity of 0.55 - expect(chunkedLevels.length).toEqual(3); - expect(chunkedLevels).toEqual([ - Math.sqrt(Math.sqrt(((2 * 2) + (1 * 1)) / 2) / 0.55), - Math.sqrt(Math.sqrt(((3 * 3) + (2 * 2)) / 2) / 0.55), - Math.sqrt(Math.sqrt((5 * 5) / 1) / 0.55) - ]); - }); - test('chunk size larger than sample size creates single chunk', () => { - const samples = [1, 1, 1]; - const chunkedLevels = computeChunkedRMS(samples, 7); - // chunked to [1, 1, 1] - // rms scaled with default unity of 0.55 - expect(chunkedLevels.length).toEqual(1); - expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55)]); - }); - test('chunk size as multiple is handled correctly', () => { - const samples = [1, 1, 1, 1, 1, 1]; - const chunkedLevels = computeChunkedRMS(samples, 3); - // chunked to [1, 1, 1], [1, 1, 1] - // rms scaled with default unity of 0.55 - expect(chunkedLevels.length).toEqual(2); - 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'); - }); - // TW: We allow resampling even if it would exceed the limit because our GUI handles this better. - test.skip('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.message).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); - }); -}); diff --git a/test/unit/util/cloud-manager-hoc.test.jsx b/test/unit/util/cloud-manager-hoc.test.jsx deleted file mode 100644 index 773fb38494c..00000000000 --- a/test/unit/util/cloud-manager-hoc.test.jsx +++ /dev/null @@ -1,401 +0,0 @@ -import 'web-audio-test-api'; - -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; -import VM from 'scratch-vm'; -import {LoadingState} from '../../../src/reducers/project-state'; -import CloudProvider from '../../../src/lib/cloud-provider'; -const mockCloudProviderInstance = { - connection: true, - requestCloseConnection: jest.fn() -}; -jest.mock('../../../src/lib/cloud-provider', () => - jest.fn().mockImplementation(() => mockCloudProviderInstance) -); - -import cloudManagerHOC from '../../../src/lib/cloud-manager-hoc.jsx'; - -describe.skip('CloudManagerHOC', () => { - const mockStore = configureStore(); - let store; - let vm; - let stillLoadingStore; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectState: { - projectId: '1234', - loadingState: LoadingState.SHOWING_WITH_ID - }, - mode: { - hasEverEnteredEditor: false - }, - tw: {} - } - }); - stillLoadingStore = mockStore({ - scratchGui: { - projectState: { - projectId: '1234', - loadingState: LoadingState.LOADING_WITH_ID - }, - mode: { - hasEverEnteredEditor: false - } - } - }); - vm = new VM(); - vm.setCloudProvider = jest.fn(); - vm.runtime = { - hasCloudData: jest.fn(() => true) - }; - vm.extensionManager = { - isExtensionLoaded: jest.fn(() => false) - }; - CloudProvider.mockClear(); - mockCloudProviderInstance.requestCloseConnection.mockClear(); - }); - test('when it mounts, the cloud provider is set on the vm', () => { - const Component = () => (
); - const WrappedComponent = cloudManagerHOC(Component); - const onShowCloudInfo = jest.fn(); - - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(1); - expect(CloudProvider).toHaveBeenCalledTimes(1); - expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance); - expect(onShowCloudInfo).not.toHaveBeenCalled(); - }); - - test('when cloudHost is missing, the cloud provider is not set on the vm', () => { - const Component = () => (
); - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - - }); - - test('when projectID is missing, the cloud provider is not set on the vm', () => { - - const Component = () => (
); - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - }); - - test('when project is not showingWithId, the cloud provider is not set on the vm', () => { - - const Component = () => (
); - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - }); - - test('when hasCloudPermission is false, the cloud provider is not set on the vm', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - }); - - test('when videoSensing extension is active, the cloud provider is not set on the vm', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - vm.extensionManager.isExtensionLoaded = jest.fn(extension => extension === 'videoSensing'); - - mount( - - ); - - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - }); - - test('if the isShowingWithId prop becomes true, it sets the cloud provider on the vm', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - const onShowCloudInfo = jest.fn(); - vm.runtime.hasCloudData = jest.fn(() => false); - - const mounted = mountWithIntl( - - ); - expect(onShowCloudInfo).not.toHaveBeenCalled(); - - vm.runtime.hasCloudData = jest.fn(() => true); - vm.emit('HAS_CLOUD_DATA_UPDATE', true); - - mounted.setProps({ - isShowingWithId: true, - loadingState: LoadingState.SHOWING_WITH_ID - }); - expect(vm.setCloudProvider.mock.calls.length).toBe(1); - expect(CloudProvider).toHaveBeenCalledTimes(1); - expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance); - expect(onShowCloudInfo).not.toHaveBeenCalled(); - }); - - test('projectId change should not trigger cloudProvider connection unless isShowingWithId becomes true', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - const mounted = mountWithIntl( - - ); - mounted.setProps({ - projectId: 'a different id' - }); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - mounted.setProps({ - isShowingWithId: true, - loadingState: LoadingState.SHOWING_WITH_ID - }); - expect(vm.setCloudProvider.mock.calls.length).toBe(1); - expect(CloudProvider).toHaveBeenCalledTimes(1); - expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance); - }); - - test('when it unmounts, the cloud provider is reset to null on the vm', () => { - const Component = () => (
); - const WrappedComponent = cloudManagerHOC(Component); - const mounted = mountWithIntl( - - ); - - expect(CloudProvider).toHaveBeenCalled(); - const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection; - - mounted.unmount(); - - // vm.setCloudProvider is called twice, - // once during mount and once during unmount - expect(vm.setCloudProvider.mock.calls.length).toBe(2); - expect(vm.setCloudProvider).toHaveBeenCalledWith(null); - expect(requestCloseConnection).toHaveBeenCalledTimes(1); - }); - - test('projectId changing should trigger cloudProvider disconnection', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - const mounted = mountWithIntl( - - ); - - expect(CloudProvider).toHaveBeenCalled(); - const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection; - - mounted.setProps({ - projectId: 'a different id' - }); - - expect(vm.setCloudProvider.mock.calls.length).toBe(2); - expect(vm.setCloudProvider).toHaveBeenCalledWith(null); - expect(requestCloseConnection).toHaveBeenCalledTimes(1); - - }); - - test('username changing should trigger cloudProvider disconnection', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - const mounted = mountWithIntl( - - ); - - expect(CloudProvider).toHaveBeenCalled(); - const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection; - - mounted.setProps({ - username: 'a different user' - }); - - expect(vm.setCloudProvider.mock.calls.length).toBe(3); // tw: the test is wrong. - expect(vm.setCloudProvider).toHaveBeenCalledWith(null); - expect(requestCloseConnection).toHaveBeenCalledTimes(1); - - }); - - test('project without cloud data should not trigger cloud connection', () => { - // Mock the vm runtime function so that has cloud data is not - // initially true - vm.runtime.hasCloudData = jest.fn(() => false); - - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - }); - - test('projectHasCloudData becoming true should trigger a cloud connection', () => { - // Mock the vm runtime function so that has cloud data is not - // initially true - vm.runtime.hasCloudData = jest.fn(() => false); - const onShowCloudInfo = jest.fn(); - - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - expect(vm.setCloudProvider.mock.calls.length).toBe(0); - expect(CloudProvider).not.toHaveBeenCalled(); - expect(onShowCloudInfo).not.toHaveBeenCalled(); - - // Mock VM hasCloudData becoming true and emitting an update - vm.runtime.hasCloudData = jest.fn(() => true); - vm.emit('HAS_CLOUD_DATA_UPDATE', true); - - expect(vm.setCloudProvider.mock.calls.length).toBe(1); - expect(CloudProvider).toHaveBeenCalledTimes(1); - expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance); - expect(onShowCloudInfo).toHaveBeenCalled(); - }); - - test('projectHasCloudDataUpdate becoming false should trigger cloudProvider disconnection', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - mountWithIntl( - - ); - - expect(CloudProvider).toHaveBeenCalled(); - const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection; - - vm.runtime.hasCloudData = jest.fn(() => false); - vm.emit('HAS_CLOUD_DATA_UPDATE', false); - - expect(vm.setCloudProvider.mock.calls.length).toBe(2); - expect(vm.setCloudProvider).toHaveBeenCalledWith(null); - expect(requestCloseConnection).toHaveBeenCalledTimes(1); - }); - - // Editor Mode Connection/Disconnection Tests - test('Entering editor mode and can\'t save project should disconnect cloud provider', () => { - const Component = () =>
; - const WrappedComponent = cloudManagerHOC(Component); - const mounted = mountWithIntl( - - ); - - expect(CloudProvider).toHaveBeenCalled(); - const requestCloseConnection = mockCloudProviderInstance.requestCloseConnection; - - mounted.setProps({ - canModifyCloudData: false - }); - - expect(vm.setCloudProvider.mock.calls.length).toBe(2); - expect(vm.setCloudProvider).toHaveBeenCalledWith(null); - expect(requestCloseConnection).toHaveBeenCalledTimes(1); - }); -}); diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js deleted file mode 100644 index a63c688cf67..00000000000 --- a/test/unit/util/cloud-provider.test.js +++ /dev/null @@ -1,211 +0,0 @@ -import CloudProvider from '../../../src/lib/cloud-provider'; - -let websocketConstructorCount = 0; - -// Stub the global websocket so we can call open/close/error/send on it -global.WebSocket = function (url) { - this._url = url; - this._sentMessages = []; - - // These are not real websocket methods, but used to trigger callbacks - this._open = () => this.onopen(); - this._error = e => this.onerror(e); - this._receive = msg => this.onmessage(msg); - - // Stub the real websocket.send to store sent messages - this.send = msg => this._sentMessages.push(msg); - this.close = () => this.onclose(); - - websocketConstructorCount++; -}; -global.WebSocket.CLOSING = 'CLOSING'; -global.WebSocket.CLOSED = 'CLOSED'; - -describe('CloudProvider', () => { - let cloudProvider = null; - let vmIOData = []; - let timeout = 0; - beforeEach(() => { - vmIOData = []; - cloudProvider = new CloudProvider(); - // Stub vm - cloudProvider.vm = { - postIOData: (_namespace, data) => { - vmIOData.push(data); - } - }; - // Stub setTimeout so this can run instantly. - cloudProvider.setTimeout = (fn, after) => { - timeout = after; - fn(); - }; - // Stub randomize to make it consistent for testing. - cloudProvider.randomizeDuration = t => t; - }); - - test('createVariable', () => { - cloudProvider.createVariable('hello', 1); - const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); - expect(obj.method).toEqual('create'); - expect(obj.name).toEqual('hello'); - expect(obj.value).toEqual(1); - }); - - test('updateVariable', () => { - cloudProvider.updateVariable('hello', 1); - const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); - expect(obj.method).toEqual('set'); - expect(obj.name).toEqual('hello'); - expect(obj.value).toEqual(1); - }); - - test('updateVariable with falsey value', () => { - cloudProvider.updateVariable('hello', 0); - const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); - expect(obj.method).toEqual('set'); - expect(obj.name).toEqual('hello'); - expect(obj.value).toEqual(0); - }); - - test('renameVariable', () => { - cloudProvider.renameVariable('oldName', 'newName'); - const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); - expect(obj.method).toEqual('rename'); - expect(obj.name).toEqual('oldName'); - expect(typeof obj.value).toEqual('undefined'); - expect(obj.new_name).toEqual('newName'); - }); - - test('deleteVariable', () => { - cloudProvider.deleteVariable('hello'); - const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); - expect(obj.method).toEqual('delete'); - expect(obj.name).toEqual('hello'); - expect(typeof obj.value).toEqual('undefined'); - }); - - - test('onMessage set', () => { - const msg = JSON.stringify({ - method: 'set', - name: 'name', - value: 'value' - }); - cloudProvider.connection._receive({data: msg}); - expect(vmIOData[0].varUpdate.name).toEqual('name'); - expect(vmIOData[0].varUpdate.value).toEqual('value'); - }); - - test('onMessage with newline at the end', () => { - const msg1 = JSON.stringify({ - method: 'set', - name: 'name1', - value: 'value' - }); - cloudProvider.onMessage({data: `${msg1}\n`}); - expect(vmIOData[0].varUpdate.name).toEqual('name1'); - }); - - test('onMessage with multiple commands', () => { - const msg1 = JSON.stringify({ - method: 'set', - name: 'name1', - value: 'value' - }); - const msg2 = JSON.stringify({ - method: 'set', - name: 'name2', - value: 'value2' - }); - cloudProvider.connection._receive({data: `${msg1}\n${msg2}`}); - expect(vmIOData[0].varUpdate.name).toEqual('name1'); - expect(vmIOData[1].varUpdate.name).toEqual('name2'); - }); - - test('connnection attempts set back to 1 when socket is opened', () => { - cloudProvider.connectionAttempts = 100; - cloudProvider.connection._open(); - expect(cloudProvider.connectionAttempts).toBe(1); - }); - - test('disconnect waits for a period equal to 2^k-1 before trying again', () => { - websocketConstructorCount = 1; // This is global, so set it back to 1 to start - // Constructor attempts to open connection, so attempts is initially 1 - expect(cloudProvider.connectionAttempts).toBe(1); - - // Make sure a close without a previous OPEN still waits 1s before reconnecting - cloudProvider.connection.close(); - expect(timeout).toEqual(1 * 1000); // 2^1 - 1 - expect(websocketConstructorCount).toBe(2); - expect(cloudProvider.connectionAttempts).toBe(2); - - cloudProvider.connection.close(); - expect(timeout).toEqual(3 * 1000); // 2^2 - 1 - expect(websocketConstructorCount).toBe(3); - expect(cloudProvider.connectionAttempts).toBe(3); - - cloudProvider.connection.close(); - expect(timeout).toEqual(7 * 1000); // 2^3 - 1 - expect(websocketConstructorCount).toBe(4); - expect(cloudProvider.connectionAttempts).toBe(4); - - cloudProvider.connection.close(); - expect(timeout).toEqual(15 * 1000); // 2^4 - 1 - expect(websocketConstructorCount).toBe(5); - expect(cloudProvider.connectionAttempts).toBe(5); - - cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // 2^5 - 1 - expect(websocketConstructorCount).toBe(6); - expect(cloudProvider.connectionAttempts).toBe(6); - - cloudProvider.connection.close(); - expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 - expect(websocketConstructorCount).toBe(7); - expect(cloudProvider.connectionAttempts).toBe(7); - }); - - test('close after connection is opened waits 1s before reconnecting', () => { - // This test is basically to check that opening the connection does not impact - // the time until reconnection for the first reconnect. - // It is easy to introduce a bug that causes reconnection time to be different - // based on whether an initial connection was made. - websocketConstructorCount = 1; - cloudProvider.connection._open(); - cloudProvider.connection.close(); - expect(timeout).toEqual(1 * 1000); // 2^1 - 1 - expect(websocketConstructorCount).toBe(2); - expect(cloudProvider.connectionAttempts).toBe(2); - }); - - test('exponentialTimeout caps connection attempt number', () => { - cloudProvider.connectionAttempts = 1000; - expect(cloudProvider.exponentialTimeout()).toEqual(31 * 1000); - }); - - test('requestCloseConnection does not try to reconnect', () => { - websocketConstructorCount = 1; // This is global, so set it back to 1 to start - cloudProvider.requestCloseConnection(); - expect(websocketConstructorCount).toBe(1); // No reconnection attempts - }); - - test('close with code 4002 triggers invalid username', () => { - cloudProvider.onInvalidUsername = jest.fn(); - cloudProvider.onClose({code: 4002}); - expect(cloudProvider.onInvalidUsername).toHaveBeenCalledTimes(1); - }); - - test('close with normal code does not trigger invalid username', () => { - cloudProvider.username = 'aaa'; - cloudProvider.onInvalidUsername = jest.fn(); - cloudProvider.onClose({code: 1000}); - expect(cloudProvider.onInvalidUsername).not.toHaveBeenCalled(); - }); -}); - -test('username anonymization', () => { - const anonymized = new CloudProvider('', null, 'player1234', ''); - expect(anonymized.username).toBe('player'); - const verbatim = new CloudProvider('', null, 'abcdef', ''); - expect(verbatim.username).toBe('abcdef'); -}); diff --git a/test/unit/util/code-payload.test.js b/test/unit/util/code-payload.test.js deleted file mode 100644 index 16264f5eb04..00000000000 --- a/test/unit/util/code-payload.test.js +++ /dev/null @@ -1,19 +0,0 @@ -jest.mock('../../../src/lib/backpack/block-to-image', () => () => Promise.resolve('block-image')); -jest.mock('../../../src/lib/backpack/thumbnail', () => () => Promise.resolve('thumbnail')); - -import codePayload from '../../../src/lib/backpack/code-payload'; -import {Base64} from 'js-base64'; - -describe('codePayload', () => { - test('base64 encodes the blocks as json', () => { - const blocks = '☁︎❤️🐻'; - const payload = codePayload({ - blockObjects: blocks - }); - return payload.then(p => { - expect( - JSON.parse(Base64.decode(p.body)) - ).toEqual(blocks); - }); - }); -}); diff --git a/test/unit/util/default-project.test.js b/test/unit/util/default-project.test.js deleted file mode 100644 index 726519c4160..00000000000 --- a/test/unit/util/default-project.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import defaultProjectGenerator from '../../../src/lib/default-project/index.js'; - -describe('defaultProject', () => { - // This test ensures that the assets referenced in the default project JSON - // do not get out of sync with the raw assets that are included alongside. - // see https://github.com/LLK/scratch-gui/issues/4844 - test('assets referenced by the project are included', () => { - const translatorFn = () => ''; - const defaultProject = defaultProjectGenerator(translatorFn); - const includedAssetIds = defaultProject.map(obj => obj.id); - const projectData = JSON.parse(defaultProject[0].data); - projectData.targets.forEach(target => { - target.costumes.forEach(costume => { - expect(includedAssetIds.includes(costume.assetId)).toBe(true); - }); - target.sounds.forEach(sound => { - expect(includedAssetIds.includes(sound.assetId)).toBe(true); - }); - }); - }); -}); diff --git a/test/unit/util/define-dynamic-block.test.js b/test/unit/util/define-dynamic-block.test.js deleted file mode 100644 index 257e5529bef..00000000000 --- a/test/unit/util/define-dynamic-block.test.js +++ /dev/null @@ -1,199 +0,0 @@ -import defineDynamicBlock from '../../../src/lib/define-dynamic-block'; - -import BlockType from 'scratch-vm/src/extension-support/block-type'; - -const MockScratchBlocks = { - OUTPUT_SHAPE_HEXAGONAL: 1, - OUTPUT_SHAPE_ROUND: 2, - OUTPUT_SHAPE_SQUARE: 3 -}; - -const categoryInfo = { - name: 'test category', - color1: '#111', - color2: '#222', - color3: '#333' -}; - -const penIconURI = 'data:image/svg+xml;base64,fake_pen_icon_svg_base64_data'; - -const testBlockInfo = { - commandWithIcon: { - blockType: BlockType.COMMAND, - blockIconURI: penIconURI, - text: 'command with icon' - }, - commandWithoutIcon: { - blockType: BlockType.COMMAND, - text: 'command without icon' - }, - terminalCommand: { - blockType: BlockType.COMMAND, - isTerminal: true, - text: 'terminal command' - }, - reporter: { - blockType: BlockType.REPORTER, - text: 'reporter' - }, - boolean: { - blockType: BlockType.BOOLEAN, - text: 'Boolean' - }, - hat: { - blockType: BlockType.HAT, - text: 'hat' - } -}; - -// similar to goog.mixin from the Closure library -const mixin = function (target, source) { - for (const x in source) { - target[x] = source[x]; - } -}; - -class MockBlock { - constructor (blockInfo, extendedOpcode) { - // mimic Closure-style inheritance by mixing in `defineDynamicBlock` output as this instance's prototype - // see also the `Blockly.Block` constructor - const prototype = defineDynamicBlock(MockScratchBlocks, categoryInfo, blockInfo, extendedOpcode); - mixin(this, prototype); - this.init(); - - // bootstrap the mutation<->DOM cycle - this.blockInfoText = JSON.stringify(blockInfo); - const xmlElement = this.mutationToDom(); - - // parse blockInfo from XML to fill dynamic properties - this.domToMutation(xmlElement); - } - - jsonInit (json) { - this.result = Object.assign({}, json); - } - interpolate_ () { - // TODO: add tests for this? - } - setCheckboxInFlyout (isEnabled) { - this.result.checkboxInFlyout_ = isEnabled; - } - setOutput (isEnabled) { - this.result.outputConnection = isEnabled; // Blockly calls `makeConnection_` here - } - setOutputShape (outputShape) { - this.result.outputShape_ = outputShape; - } - setNextStatement (isEnabled) { - this.result.nextConnection = isEnabled; // Blockly calls `makeConnection_` here - } - setPreviousStatement (isEnabled) { - this.result.previousConnection = isEnabled; // Blockly calls `makeConnection_` here - } -} - -describe('defineDynamicBlock', () => { - test('is a function', () => { - expect(typeof defineDynamicBlock).toBe('function'); - }); - test('can define a command block with an icon', () => { - const extendedOpcode = 'test.commandWithIcon'; - const block = new MockBlock(testBlockInfo.commandWithIcon, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - extensions: ['scratch_extension'], - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a command block without an icon', () => { - const extendedOpcode = 'test.commandWithoutIcon'; - const block = new MockBlock(testBlockInfo.commandWithoutIcon, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a terminal command', () => { - const extendedOpcode = 'test.terminal'; - const block = new MockBlock(testBlockInfo.terminalCommand, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: false, // terminal - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - previousConnection: true, - type: extendedOpcode - }); - }); - test('can define a reporter', () => { - const extendedOpcode = 'test.reporter'; - const block = new MockBlock(testBlockInfo.reporter, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - checkboxInFlyout_: true, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - // nextConnection: undefined, // reporter - outputConnection: true, // reporter - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_ROUND, // reporter - // previousConnection: undefined, // reporter - type: extendedOpcode - }); - }); - test('can define a Boolean', () => { - const extendedOpcode = 'test.boolean'; - const block = new MockBlock(testBlockInfo.boolean, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - // checkboxInFlyout_: undefined, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - // nextConnection: undefined, // reporter - outputConnection: true, // reporter - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_HEXAGONAL, // Boolean - // previousConnection: undefined, // reporter - type: extendedOpcode - }); - }); - test('can define a hat', () => { - const extendedOpcode = 'test.hat'; - const block = new MockBlock(testBlockInfo.hat, extendedOpcode); - expect(block.result).toEqual({ - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, - // extensions: undefined, // no icon means no extension - inputsInline: true, - nextConnection: true, - outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE, - // previousConnection: undefined, // hat - type: extendedOpcode - }); - }); -}); diff --git a/test/unit/util/detect-locale.test.js b/test/unit/util/detect-locale.test.js deleted file mode 100644 index f9b1f63fcea..00000000000 --- a/test/unit/util/detect-locale.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import {detectLocale} from '../../../src/lib/detect-locale.js'; - -const supportedLocales = ['en', 'es', 'pt-br', 'de', 'it']; - -Object.defineProperty(window.location, - 'search', - {value: '?name=val', configurable: true} -); -Object.defineProperty(window.navigator, - 'language', - {value: 'en-US', configurable: true} -); - -describe('detectLocale', () => { - test('uses locale from the URL when present', () => { - Object.defineProperty(window.location, - 'search', - {value: '?locale=pt-br'} - ); - expect(detectLocale(supportedLocales)).toEqual('pt-br'); - }); - - test('is case insensitive', () => { - Object.defineProperty(window.location, - 'search', - {value: '?locale=pt-BR'} - ); - expect(detectLocale(supportedLocales)).toEqual('pt-br'); - }); - - test('also accepts lang from the URL when present', () => { - Object.defineProperty(window.location, - 'search', - {value: '?lang=it'} - ); - expect(detectLocale(supportedLocales)).toEqual('it'); - }); - - test('ignores unsupported locales', () => { - Object.defineProperty(window.location, - 'search', - {value: '?lang=sv'} - ); - expect(detectLocale(supportedLocales)).toEqual('en'); - }); - - test('ignores other parameters', () => { - Object.defineProperty(window.location, - 'search', - {value: '?enable=language'} - ); - expect(detectLocale(supportedLocales)).toEqual('en'); - }); - - test('uses navigator language property for default if supported', () => { - Object.defineProperty(window.navigator, - 'language', - {value: 'pt-BR'} - ); - expect(detectLocale(supportedLocales)).toEqual('pt-br'); - }); - - test('ignores navigator language property if unsupported', () => { - Object.defineProperty(window.navigator, - 'language', - {value: 'da'} - ); - expect(detectLocale(supportedLocales)).toEqual('en'); - }); - - test('works with an empty locale', () => { - Object.defineProperty(window.location, - 'search', - {value: '?locale='} - ); - expect(detectLocale(supportedLocales)).toEqual('en'); - }); - - test('if multiple, uses the first locale', () => { - Object.defineProperty(window.location, - 'search', - {value: '?locale=de&locale=en'} - ); - expect(detectLocale(supportedLocales)).toEqual('de'); - }); -}); diff --git a/test/unit/util/drag-recognizer.test.js b/test/unit/util/drag-recognizer.test.js deleted file mode 100644 index 5a24b579b40..00000000000 --- a/test/unit/util/drag-recognizer.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import DragRecognizer from '../../../src/lib/drag-recognizer'; - -describe('DragRecognizer', () => { - let onDrag; - let onDragEnd; - let dragRecognizer; - - beforeEach(() => { - onDrag = jest.fn(); - onDragEnd = jest.fn(); - dragRecognizer = new DragRecognizer({onDrag, onDragEnd}); - }); - - afterEach(() => { - dragRecognizer.reset(); - }); - - test('start -> small drag', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('mousemove', {clientX: 101, clientY: 101})); - expect(onDrag).not.toHaveBeenCalled(); - }); - - test('start -> large vertical touch move -> scroll, not drag', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 106, clientY: 150})); - expect(onDrag).not.toHaveBeenCalled(); - }); - - test('start -> large vertical mouse move -> mouse moves always drag)', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 150})); - expect(onDrag).toHaveBeenCalled(); - }); - - test('start -> large horizontal touch move -> drag', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - expect(onDrag).toHaveBeenCalled(); - }); - - test('after starting a scroll, it cannot become a drag', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 110})); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 100})); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 110, clientY: 100})); - expect(onDrag).not.toHaveBeenCalled(); - }); - - test('start -> end unbinds', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - expect(onDrag).toHaveBeenCalledTimes(1); - window.dispatchEvent(new MouseEvent('touchend', {clientX: 150, clientY: 106})); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - expect(onDrag).toHaveBeenCalledTimes(1); // Still 1 - }); - - test('start -> end calls dragEnd callback after resetting internal state', done => { - onDragEnd = () => { - expect(dragRecognizer.gestureInProgress()).toBe(false); - done(); - }; - dragRecognizer = new DragRecognizer({onDrag, onDragEnd}); - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - window.dispatchEvent(new MouseEvent('touchend', {clientX: 150, clientY: 106})); - }); - - test('start -> reset unbinds', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - expect(onDrag).toHaveBeenCalledTimes(1); - dragRecognizer.reset(); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106})); - expect(onDrag).toHaveBeenCalledTimes(1); // Still 1 - }); - - test('scrolls do not call prevent default', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - const event = new MouseEvent('touchmove', {clientX: 100, clientY: 110}); - event.preventDefault = jest.fn(); - window.dispatchEvent(event); - expect(event.preventDefault).toHaveBeenCalledTimes(0); - }); - - test('confirmed drags have preventDefault called on them', () => { - dragRecognizer.start({clientX: 100, clientY: 100}); - const event = new MouseEvent('touchmove', {clientX: 150, clientY: 106}); - event.preventDefault = jest.fn(); - window.dispatchEvent(event); - expect(event.preventDefault).toHaveBeenCalled(); - }); - - test('multiple horizontal drag angles', () => { - // +45 from horizontal => drag - dragRecognizer.start({clientX: 0, clientY: 0}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: 10})); - expect(onDrag).toHaveBeenCalledTimes(1); - dragRecognizer.reset(); - - // -45 from horizontal => drag - dragRecognizer.start({clientX: 0, clientY: 0}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: -10})); - expect(onDrag).toHaveBeenCalledTimes(2); - dragRecognizer.reset(); - - // +135 from horizontal => drag - dragRecognizer.start({clientX: 0, clientY: 0}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: 10})); - expect(onDrag).toHaveBeenCalledTimes(3); - dragRecognizer.reset(); - - // -135 from horizontal => drag - dragRecognizer.start({clientX: 0, clientY: 0}); - window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: -10})); - expect(onDrag).toHaveBeenCalledTimes(4); - dragRecognizer.reset(); - }); -}); diff --git a/test/unit/util/drag-utils.test.js b/test/unit/util/drag-utils.test.js deleted file mode 100644 index e7fe2797f9a..00000000000 --- a/test/unit/util/drag-utils.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import {indexForPositionOnList} from '../../../src/lib/drag-utils'; - -const box = (top, right, bottom, left) => ({top, right, bottom, left}); - -describe('indexForPositionOnList', () => { - test('returns null when not given any boxes', () => { - expect(indexForPositionOnList({x: 0, y: 0}, [])).toEqual(null); - }); - - test('wrapped list with incomplete last row LTR', () => { - const boxes = [ - box(0, 100, 100, 0), // index: 0 - box(0, 200, 100, 100), // index: 1 - box(0, 300, 100, 200), // index: 2 - box(100, 100, 200, 0), // index: 3 (second row) - box(100, 200, 200, 100) // index: 4 (second row, left incomplete intentionally) - ]; - - // Inside the second box. - expect(indexForPositionOnList({x: 150, y: 50}, boxes, false)).toEqual(1); - - // On the border edge of the first and second box. Given to the first box. - expect(indexForPositionOnList({x: 100, y: 50}, boxes, false)).toEqual(0); - - // Off the top/left edge. - expect(indexForPositionOnList({x: -100, y: -100}, boxes, false)).toEqual(0); - - // Off the left edge, in the second row. - expect(indexForPositionOnList({x: -100, y: 175}, boxes, false)).toEqual(3); - - // Off the right edge, in the first row. - expect(indexForPositionOnList({x: 400, y: 75}, boxes, false)).toEqual(2); - - // Off the top edge, middle of second item. - expect(indexForPositionOnList({x: 150, y: -75}, boxes, false)).toEqual(1); - - // Within the right edge bounds, but on the second (incomplete) row. - // This tests that wrapped lists with incomplete final rows work correctly. - expect(indexForPositionOnList({x: 375, y: 175}, boxes, false)).toEqual(4); - }); - - test('wrapped list with incomplete last row RTL', () => { - const boxes = [ - box(0, 0, 100, -100), // index: 0 - box(0, -100, 100, -200), // index: 1 - box(0, -200, 100, -300), // index: 2 - box(100, 0, 200, -100), // index: 3 (second row) - box(100, -100, 200, -200) // index: 4 (second row, left incomplete intentionally) - ]; - - // Inside the second box. - expect(indexForPositionOnList({x: -150, y: 50}, boxes, true)).toEqual(1); - - // On the border edge of the first and second box. Given to the first box. - expect(indexForPositionOnList({x: -100, y: 50}, boxes, true)).toEqual(0); - - // Off the top/right edge. - expect(indexForPositionOnList({x: 100, y: -100}, boxes, true)).toEqual(0); - - // Off the right edge, in the second row. - expect(indexForPositionOnList({x: 100, y: 175}, boxes, true)).toEqual(3); - - // Off the left edge, in the first row. - expect(indexForPositionOnList({x: -400, y: 75}, boxes, true)).toEqual(2); - - // Off the top edge, middle of second item. - expect(indexForPositionOnList({x: -150, y: -75}, boxes, true)).toEqual(1); - - // Within the left edge bounds, but on the second (incomplete) row. - // This tests that wrapped lists with incomplete final rows work correctly. - expect(indexForPositionOnList({x: -375, y: 175}, boxes, true)).toEqual(4); - }); - -}); diff --git a/test/unit/util/get-costume-url.test.js b/test/unit/util/get-costume-url.test.js deleted file mode 100644 index 93154acca26..00000000000 --- a/test/unit/util/get-costume-url.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import {HAS_FONT_REGEXP} from '../../../src/lib/get-costume-url'; - -describe('SVG Font Parsing', () => { - test('Has font regexp works', () => { - expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); - expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); - expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); - - expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy(); - }); -}); diff --git a/test/unit/util/hash-project-loader-hoc.test.jsx b/test/unit/util/hash-project-loader-hoc.test.jsx deleted file mode 100644 index 208d1db16df..00000000000 --- a/test/unit/util/hash-project-loader-hoc.test.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mount} from 'enzyme'; - -import HashParserHOC from '../../../src/lib/hash-parser-hoc.jsx'; - -jest.mock('react-ga'); - -describe('HashParserHOC', () => { - const mockStore = configureStore(); - let store; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectState: {} - } - }); - }); - - test('when there is a hash, it passes the hash as projectId', () => { - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = HashParserHOC(Component); - window.location.hash = '#1234567'; - const mockSetProjectIdFunc = jest.fn(); - mount( - - ); - expect(mockSetProjectIdFunc.mock.calls[0][0]).toBe('1234567'); - }); - - test('when there is no hash, it passes 0 as the projectId', () => { - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = HashParserHOC(Component); - window.location.hash = ''; - const mockSetProjectIdFunc = jest.fn(); - mount( - - ); - expect(mockSetProjectIdFunc.mock.calls[0][0]).toBe('0'); - }); - - test('when the hash is not a number, it passes 0 as projectId', () => { - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = HashParserHOC(Component); - window.location.hash = '#winning'; - const mockSetProjectIdFunc = jest.fn(); - mount( - - ); - expect(mockSetProjectIdFunc.mock.calls[0][0]).toBe('0'); - }); - - test('when hash change happens, the projectId state is changed', () => { - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = HashParserHOC(Component); - window.location.hash = ''; - const mockSetProjectIdFunc = jest.fn(); - const mounted = mount( - - ); - window.location.hash = '#1234567'; - mounted - .childAt(0) - .instance() - .handleHashChange(); - expect(mockSetProjectIdFunc.mock.calls.length).toBe(2); - }); -}); diff --git a/test/unit/util/opcode-labels.test.js b/test/unit/util/opcode-labels.test.js deleted file mode 100644 index 78bdfeaa7ad..00000000000 --- a/test/unit/util/opcode-labels.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import opcodeLabels from '../../../src/lib/opcode-labels'; - -describe('Opcode Labels', () => { - test('day of week label', () => { - const labelFun = opcodeLabels.getLabel('sensing_current').labelFn; - expect(labelFun({CURRENTMENU: 'dayofweek'})).toBe('day of week'); - expect(labelFun({CURRENTMENU: 'DAYOFWEEK'})).toBe('day of week'); - }); - - test('unspecified opcodes default to extension category and opcode as label', () => { - const labelInfo = opcodeLabels.getLabel('music_getTempo'); - expect(labelInfo.label).toBe('music_getTempo'); - expect(labelInfo.category).toBe('extension'); - }); -}); diff --git a/test/unit/util/project-fetcher-hoc.test.jsx b/test/unit/util/project-fetcher-hoc.test.jsx deleted file mode 100644 index 0b34bafc8a9..00000000000 --- a/test/unit/util/project-fetcher-hoc.test.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import configureStore from 'redux-mock-store'; - -import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; - -import ProjectFetcherHOC from '../../../src/lib/project-fetcher-hoc.jsx'; -import storage from '../../../src/lib/storage'; -import {LoadingState} from '../../../src/reducers/project-state'; - -jest.mock('react-ga'); - -describe('ProjectFetcherHOC', () => { - const mockStore = configureStore(); - let store; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectState: {}, - vm: { - clear: () => {}, - stop: () => {} - } - } - }); - }); - - test.skip('when there is an id, it tries to update the store with that id', () => { - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = ProjectFetcherHOC(Component); - const mockSetProjectIdFunc = jest.fn(); - mountWithIntl( - - ); - expect(mockSetProjectIdFunc.mock.calls[0][0]).toBe('100'); - }); - test.skip('when there is a reduxProjectId and isFetchingWithProjectId is true, it loads the project', () => { - const mockedOnFetchedProject = jest.fn(); - const originalLoad = storage.load; - storage.load = jest.fn((type, id) => Promise.resolve({data: id})); - const Component = ({projectId}) =>
{projectId}
; - const WrappedComponent = ProjectFetcherHOC(Component); - const mounted = mountWithIntl( - - ); - mounted.setProps({ - reduxProjectId: '100', - isFetchingWithId: true, - loadingState: LoadingState.FETCHING_WITH_ID - }); - expect(storage.load).toHaveBeenLastCalledWith( - storage.AssetType.Project, '100', storage.DataFormat.JSON - ); - storage.load = originalLoad; - // nextTick needed since storage.load is async, and onFetchedProject is called in its then() - process.nextTick( - () => expect(mockedOnFetchedProject) - .toHaveBeenLastCalledWith('100', LoadingState.FETCHING_WITH_ID) - ); - }); -}); diff --git a/test/unit/util/project-saver-hoc.test.jsx b/test/unit/util/project-saver-hoc.test.jsx deleted file mode 100644 index f5ac7b680e6..00000000000 --- a/test/unit/util/project-saver-hoc.test.jsx +++ /dev/null @@ -1,491 +0,0 @@ -import 'web-audio-test-api'; - -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mount} from 'enzyme'; -import {LoadingState} from '../../../src/reducers/project-state'; -import VM from 'scratch-vm'; - -import projectSaverHOC from '../../../src/lib/project-saver-hoc.jsx'; - -describe('projectSaverHOC', () => { - const mockStore = configureStore(); - let store; - let vm; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectChanged: false, - projectState: {}, - projectTitle: 'Scratch Project', - timeout: { - autoSaveTimeoutId: null - } - }, - locales: { - locale: 'en' - } - }); - vm = new VM(); - jest.useFakeTimers(); - }); - - test('if canSave becomes true when showing a project with an id, project will be saved', () => { - const mockedUpdateProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - canSave: true, - isShowingSaveable: true - }); - expect(mockedUpdateProject).toHaveBeenCalled(); - }); - - 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 = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - canSave: true, - isShowingWithId: true, - loadingState: LoadingState.SHOWING_WITH_ID - }); - expect(mockedSaveProject).not.toHaveBeenCalled(); - }); - - test('if canSave is false when showing a project without an id, project will NOT be created', () => { - const mockedCreateProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - isShowingWithoutId: true, - loadingState: LoadingState.SHOWING_WITHOUT_ID - }); - expect(mockedCreateProject).not.toHaveBeenCalled(); - }); - - test('if canCreateNew becomes true when showing a project without an id, project will be created', () => { - const mockedCreateProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - canCreateNew: true - }); - expect(mockedCreateProject).toHaveBeenCalled(); - }); - - test('if canCreateNew is true and we transition to showing new project, project will be created', () => { - const mockedCreateProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - isShowingWithoutId: true, - loadingState: LoadingState.SHOWING_WITHOUT_ID - }); - expect(mockedCreateProject).toHaveBeenCalled(); - }); - - test('if we enter creating new state, vm project should be requested', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - // The first wrapper is redux's Connect HOC - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isCreatingNew: true, - loadingState: LoadingState.CREATING_NEW - }); - expect(mockedStoreProject).toHaveBeenCalled(); - }); - - test('if we enter remixing state, vm project should be requested, and alert should show', () => { - const mockedShowCreatingRemixAlert = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - // The first wrapper is redux's Connect HOC - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isRemixing: true, - loadingState: LoadingState.REMIXING - }); - expect(mockedStoreProject).toHaveBeenCalled(); - expect(mockedShowCreatingRemixAlert).toHaveBeenCalled(); - }); - - test('if we enter creating copy state, vm project should be requested, and alert should show', () => { - const mockedShowCreatingCopyAlert = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - // The first wrapper is redux's Connect HOC - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isCreatingCopy: true, - loadingState: LoadingState.CREATING_COPY - }); - expect(mockedStoreProject).toHaveBeenCalled(); - expect(mockedShowCreatingCopyAlert).toHaveBeenCalled(); - }); - - test('if we enter updating/saving state, vm project should be requested', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - // The first wrapper is redux's Connect HOC - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isUpdating: true, - loadingState: LoadingState.MANUAL_UPDATING - }); - expect(mockedStoreProject).toHaveBeenCalled(); - }); - - test('if we are already in updating/saving state, vm project ' + - 'should NOT requested, alert should NOT show', () => { - const mockedShowCreatingAlert = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - // The first wrapper is redux's Connect HOC - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isUpdating: true, - loadingState: LoadingState.AUTO_UPDATING, - reduxProjectId: '99' // random change to force a re-render and componentDidUpdate - }); - expect(mockedStoreProject).not.toHaveBeenCalled(); - expect(mockedShowCreatingAlert).not.toHaveBeenCalled(); - }); - - test('if user saves, inline saving alert should show', () => { - const mockedShowSavingAlert = jest.fn(); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - isManualUpdating: true, - isUpdating: true - }); - expect(mockedShowSavingAlert).toHaveBeenCalled(); - }); - - test('if project is changed, it should autosave after interval', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedAutoUpdate = jest.fn(() => Promise.resolve()); - const mounted = mount( - - ); - mounted.setProps({ - projectChanged: true - }); - // Fast-forward until all timers have been executed - jest.runAllTimers(); - expect(mockedAutoUpdate).toHaveBeenCalled(); - }); - - test('if project is changed several times in a row, it should only autosave once', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedAutoUpdate = jest.fn(() => Promise.resolve()); - const mounted = mount( - - ); - mounted.setProps({ - projectChanged: true, - reduxProjectTitle: 'a' - }); - mounted.setProps({ - projectChanged: true, - reduxProjectTitle: 'b' - }); - mounted.setProps({ - projectChanged: true, - reduxProjectTitle: 'c' - }); - // Fast-forward until all timers have been executed - jest.runAllTimers(); - expect(mockedAutoUpdate).toHaveBeenCalledTimes(1); - }); - - test('if project is not changed, it should not autosave after interval', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const mockedAutoUpdate = jest.fn(() => Promise.resolve()); - const mounted = mount( - - ); - mounted.setProps({ - projectChanged: false - }); - // Fast-forward until all timers have been executed - jest.runAllTimers(); - expect(mockedAutoUpdate).not.toHaveBeenCalled(); - }); - - test('when starting to remix, onRemixing should be called with param true', () => { - const mockedOnRemixing = jest.fn(); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isRemixing: true - }); - expect(mockedOnRemixing).toHaveBeenCalledWith(true); - }); - - test('when starting to remix, onRemixing should be called with param false', () => { - const mockedOnRemixing = jest.fn(); - const mockedStoreProject = jest.fn(() => Promise.resolve()); - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - WrappedComponent.WrappedComponent.prototype.storeProject = mockedStoreProject; - const mounted = mount( - - ); - mounted.setProps({ - isRemixing: false - }); - expect(mockedOnRemixing).toHaveBeenCalledWith(false); - }); - - test('uses onSetProjectThumbnailer on mount/unmount', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const setThumb = jest.fn(); - const mounted = mount( - - ); - // Set project thumbnailer should be called on mount - expect(setThumb).toHaveBeenCalledTimes(1); - - // And it should not pass that function on to wrapped element - expect(mounted.find(Component).props().onSetProjectThumbnailer).toBeUndefined(); - - // Unmounting should call it again with null - mounted.unmount(); - expect(setThumb).toHaveBeenCalledTimes(2); - expect(setThumb.mock.calls[1][0]).toBe(null); - }); - - test('uses onSetProjectSaver on mount/unmount', () => { - const Component = () =>
; - const WrappedComponent = projectSaverHOC(Component); - const setSaver = jest.fn(); - const mounted = mount( - - ); - // Set project saver should be called on mount - expect(setSaver).toHaveBeenCalledTimes(1); - - // And it should not pass that function on to wrapped element - expect(mounted.find(Component).props().onSetProjectSaver).toBeUndefined(); - - // Unmounting should call it again with null - mounted.unmount(); - expect(setSaver).toHaveBeenCalledTimes(2); - expect(setSaver.mock.calls[1][0]).toBe(null); - }); -}); diff --git a/test/unit/util/sb-file-uploader-hoc.test.jsx b/test/unit/util/sb-file-uploader-hoc.test.jsx deleted file mode 100644 index ab286ed4432..00000000000 --- a/test/unit/util/sb-file-uploader-hoc.test.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import 'web-audio-test-api'; - -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx'; -import {LoadingState} from '../../../src/reducers/project-state'; -import VM from 'scratch-vm'; - -import SBFileUploaderHOC from '../../../src/lib/sb-file-uploader-hoc.jsx'; - -describe('SBFileUploaderHOC', () => { - const mockStore = configureStore(); - let store; - let vm; - - // Wrap this in a function so it gets test specific states and can be reused. - const getContainer = function () { - const Component = () =>
; - return SBFileUploaderHOC(Component); - }; - - const shallowMountWithContext = component => ( - shallowWithIntl(component, {context: {store}}) - ); - - const unwrappedInstance = () => { - const WrappedComponent = getContainer(); - // default starting state: looking at a project you created, not logged in - const wrapper = shallowMountWithContext( - - ); - return wrapper - .dive() // unwrap intl - .dive() // unwrap redux Connect(SBFileUploaderComponent) - .instance(); // SBFileUploaderComponent - }; - - beforeEach(() => { - vm = new VM(); - store = mockStore({ - scratchGui: { - projectState: { - loadingState: LoadingState.SHOWING_WITHOUT_ID - }, - vm: {} - }, - locales: { - locale: 'en' - } - }); - }); - - test('correctly sets title with .sb3 filename', () => { - const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb3'); - expect(projectName).toBe('my project is great'); - }); - - test('correctly sets title with .sb2 filename', () => { - const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb2'); - expect(projectName).toBe('my project is great'); - }); - - test('correctly sets title with .sb filename', () => { - const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb'); - expect(projectName).toBe('my project is great'); - }); - - test('sets blank title with filename with no extension', () => { - const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great'); - expect(projectName).toBe(''); - }); - - /* tw: test is broken by flag required to fix issues with multiple instances - test('if isLoadingUpload becomes true, without fileToUpload set, will call cancelFileUpload', () => { - const mockedCancelFileUpload = jest.fn(); - const WrappedComponent = getContainer(); - const mounted = mountWithIntl( - - ); - mounted.setProps({ - isLoadingUpload: true - }); - expect(mockedCancelFileUpload).toHaveBeenCalled(); - }); - */ -}); diff --git a/test/unit/util/themes.test.js b/test/unit/util/themes.test.js deleted file mode 100644 index a815f9c5986..00000000000 --- a/test/unit/util/themes.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import { - DARK_THEME, - defaultColors, - DEFAULT_THEME, - getColorsForTheme, - HIGH_CONTRAST_THEME -} from '../../../src/lib/themes'; -import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../../../src/lib/themes/blockHelpers'; -import {detectTheme, persistTheme} from '../../../src/lib/themes/themePersistance'; - -jest.mock('../../../src/lib/themes/default'); -jest.mock('../../../src/lib/themes/dark'); - -describe('themes', () => { - let serializeToString; - - describe('core functionality', () => { - test('provides the default theme colors', () => { - expect(defaultColors.motion.primary).toEqual('#111111'); - }); - - test('returns the dark mode', () => { - const colors = getColorsForTheme(DARK_THEME); - - expect(colors.motion.primary).toEqual('#AAAAAA'); - }); - - test('uses default theme colors when not specified', () => { - const colors = getColorsForTheme(DARK_THEME); - - expect(colors.motion.secondary).toEqual('#222222'); - }); - }); - - describe('block helpers', () => { - beforeEach(() => { - serializeToString = jest.fn(() => 'mocked xml'); - - global.XMLSerializer = () => ({ - serializeToString - }); - }); - - test('updates extension block colors based on theme', () => { - const blockInfoJson = { - type: 'dummy_block', - colour: '#0FBD8C', - colourSecondary: '#0DA57A', - colourTertiary: '#0B8E69' - }; - - const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); - - expect(updated).toEqual({ - type: 'dummy_block', - colour: '#FFFFFF', - colourSecondary: '#EEEEEE', - colourTertiary: '#DDDDDD' - }); - // The original value was not modified - expect(blockInfoJson.colour).toBe('#0FBD8C'); - }); - - test('updates extension block icon based on theme', () => { - const blockInfoJson = { - type: 'pen_block', - args0: [ - { - type: 'field_image', - src: 'original' - } - ], - colour: '#0FBD8C', - colourSecondary: '#0DA57A', - colourTertiary: '#0B8E69' - }; - - const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); - - expect(updated).toEqual({ - type: 'pen_block', - args0: [ - { - type: 'field_image', - src: 'darkPenIcon' - } - ], - colour: '#FFFFFF', - colourSecondary: '#EEEEEE', - colourTertiary: '#DDDDDD' - }); - // The original value was not modified - expect(blockInfoJson.args0[0].src).toBe('original'); - }); - - test('bypasses updates if using the default theme', () => { - const blockInfoJson = { - type: 'dummy_block', - colour: '#0FBD8C', - colourSecondary: '#0DA57A', - colourTertiary: '#0B8E69' - }; - - const updated = injectExtensionBlockTheme(blockInfoJson, DEFAULT_THEME); - - expect(updated).toEqual({ - type: 'dummy_block', - colour: '#0FBD8C', - colourSecondary: '#0DA57A', - colourTertiary: '#0B8E69' - }); - }); - - test('updates extension category based on theme', () => { - const dynamicBlockXML = [ - { - id: 'pen', - xml: '' - } - ]; - - injectExtensionCategoryTheme(dynamicBlockXML, DARK_THEME); - - // XMLSerializer is not available outside the browser. - // Verify the mocked XMLSerializer.serializeToString is called with updated colors. - expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('colour')).toBe('#FFFFFF'); - expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('secondaryColour')).toBe('#DDDDDD'); - expect(serializeToString.mock.calls[0][0].documentElement.getAttribute('iconURI')).toBe('darkPenIcon'); - }); - }); - - describe('theme persistance', () => { - test('returns the theme stored in a cookie', () => { - window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; - - const theme = detectTheme(); - - expect(theme).toEqual(HIGH_CONTRAST_THEME); - }); - - test('returns the system theme when no cookie', () => { - window.document.cookie = 'scratchtheme='; - - const theme = detectTheme(); - - expect(theme).toEqual(DEFAULT_THEME); - }); - - test('persists theme to cookie', () => { - window.document.cookie = 'scratchtheme='; - - persistTheme(HIGH_CONTRAST_THEME); - - expect(window.document.cookie).toEqual(`scratchtheme=${HIGH_CONTRAST_THEME}`); - }); - - test('clears theme when matching system preferences', () => { - window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; - - persistTheme(DEFAULT_THEME); - - expect(window.document.cookie).toEqual('scratchtheme='); - }); - }); -}); diff --git a/test/unit/util/throttled-property-hoc.test.jsx b/test/unit/util/throttled-property-hoc.test.jsx deleted file mode 100644 index c24f937a357..00000000000 --- a/test/unit/util/throttled-property-hoc.test.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import {mount} from 'enzyme'; - -import ThrottledPropertyHOC from '../../../src/lib/throttled-property-hoc.jsx'; - -describe('VMListenerHOC', () => { - let mounted; - const throttleTime = 500; - beforeEach(() => { - const Component = ({propToThrottle, doNotThrottle}) => ( - - ); - const WrappedComponent = ThrottledPropertyHOC('propToThrottle', throttleTime)(Component); - - global.Date.now = () => 0; - - mounted = mount( - - ); - }); - - test('it passes the props on initial render ', () => { - expect(mounted.find('[value=0]').exists()).toEqual(true); - expect(mounted.find('[name="oldvalue"]').exists()).toEqual(true); - }); - - test('it does not rerender if throttled prop is updated too soon', () => { - global.Date.now = () => throttleTime / 2; - mounted.setProps({propToThrottle: 1}); - mounted.update(); - expect(mounted.find('[value=0]').exists()).toEqual(true); - }); - - test('it does rerender if throttled prop is updated after throttle timeout', () => { - global.Date.now = () => throttleTime * 2; - mounted.setProps({propToThrottle: 1}); - mounted.update(); - expect(mounted.find('[value=1]').exists()).toEqual(true); - }); - - test('it does rerender if a non-throttled prop is changed', () => { - global.Date.now = () => throttleTime / 2; - mounted.setProps({doNotThrottle: 'newvalue', propToThrottle: 2}); - mounted.update(); - expect(mounted.find('[name="newvalue"]').exists()).toEqual(true); - expect(mounted.find('[value=2]').exists()).toEqual(true); - }); -}); diff --git a/test/unit/util/translate-video.test.js b/test/unit/util/translate-video.test.js deleted file mode 100644 index 27d92d8425d..00000000000 --- a/test/unit/util/translate-video.test.js +++ /dev/null @@ -1,25 +0,0 @@ -describe('no-op', () => { - test('no-op', () => {}); -}); -// tw: we intentionally break this test -/* -import {translateVideo} from '../../../src/lib/libraries/decks/translate-video.js'; - -describe('translateVideo', () => { - test('returns the id if it is not found', () => { - expect(translateVideo('not-a-key', 'en')).toEqual('not-a-key'); - }); - - test('returns the expected id for Japanese', () => { - expect(translateVideo('intro-move-sayhello', 'ja')).toEqual('v2c2f3y2sc'); - }); - - test('returns the expected id for English', () => { - expect(translateVideo('intro-move-sayhello', 'en')).toEqual('rpjvs3v9gj'); - }); - - test('returns the English id for non-existent locales', () => { - expect(translateVideo('intro-move-sayhello', 'yum')).toEqual('rpjvs3v9gj'); - }); -}); -*/ diff --git a/test/unit/util/tutorial-from-url.test.js b/test/unit/util/tutorial-from-url.test.js deleted file mode 100644 index 454bc8725c9..00000000000 --- a/test/unit/util/tutorial-from-url.test.js +++ /dev/null @@ -1,47 +0,0 @@ -jest.mock('../../../src/lib/analytics.js', () => ({ - event: () => {} -})); - -jest.mock('../../../src/lib/libraries/decks/index.jsx', () => ({ - noUrlId: {}, - foo: {urlId: 'one'}, - noUrlIdSandwich: {} -})); - -import queryString from 'query-string'; -import {detectTutorialId} from '../../../src/lib/tutorial-from-url.js'; - -test('returns the tutorial ID if the urlId matches', () => { - const queryParams = queryString.parse('?tutorial=one'); - expect(detectTutorialId(queryParams)).toBe('foo'); -}); - -test('returns null if no matching urlId', () => { - const queryParams = queryString.parse('?tutorial=10'); - expect(detectTutorialId(queryParams)).toBe(null); -}); - -test('returns null if empty template', () => { - const queryParams = queryString.parse('?tutorial='); - expect(detectTutorialId(queryParams)).toBe(null); -}); - -test('returns null if no query param', () => { - const queryParams = queryString.parse(''); - expect(detectTutorialId(queryParams)).toBe(null); -}); - -test('returns null if unrecognized template', () => { - const queryParams = queryString.parse('?tutorial=asdf'); - expect(detectTutorialId(queryParams)).toBe(null); -}); - -test('takes the first of multiple', () => { - const queryParams = queryString.parse('?tutorial=one&tutorial=two'); - expect(detectTutorialId(queryParams)).toBe('foo'); -}); - -test('returns all for the tutorial library shortcut', () => { - const queryParams = queryString.parse('?tutorial=all'); - expect(detectTutorialId(queryParams)).toBe('all'); -}); diff --git a/test/unit/util/vm-listener-hoc.test.jsx b/test/unit/util/vm-listener-hoc.test.jsx deleted file mode 100644 index 407573c0bdd..00000000000 --- a/test/unit/util/vm-listener-hoc.test.jsx +++ /dev/null @@ -1,186 +0,0 @@ -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mount} from 'enzyme'; -import VM from 'scratch-vm'; - -import vmListenerHOC from '../../../src/lib/vm-listener-hoc.jsx'; - -describe('VMListenerHOC', () => { - const mockStore = configureStore(); - let store; - let vm; - - beforeEach(() => { - vm = new VM(); - store = mockStore({ - scratchGui: { - mode: {}, - modals: {}, - vm: vm, - tw: {hasCloudVariables: false} - } - }); - }); - - test('vm green flag event is bound to the passed in prop callback', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - const onGreenFlag = jest.fn(); - mount( - - ); - expect(onGreenFlag).not.toHaveBeenCalled(); - vm.emit('PROJECT_START'); - expect(onGreenFlag).toHaveBeenCalled(); - }); - - test('onGreenFlag is not passed to the children', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - const wrapper = mount( - - ); - const child = wrapper.find(Component); - expect(child.props().onGreenFlag).toBeUndefined(); - }); - - test('targetsUpdate event from vm triggers targets update action', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - mount( - - ); - const targetList = []; - const editingTarget = 'id'; - vm.emit('targetsUpdate', {targetList, editingTarget}); - const actions = store.getActions(); - expect(actions[0].type).toEqual('scratch-gui/targets/UPDATE_TARGET_LIST'); - expect(actions[0].targets).toEqual(targetList); - expect(actions[0].editingTarget).toEqual(editingTarget); - }); - - test('targetsUpdate does not dispatch if the sound recorder is visible', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - store = mockStore({ - scratchGui: { - mode: {}, - modals: {soundRecorder: true}, - vm: vm, - tw: {hasCloudVariables: false} - } - }); - mount( - - ); - const targetList = []; - const editingTarget = 'id'; - vm.emit('targetsUpdate', {targetList, editingTarget}); - const actions = store.getActions(); - expect(actions.length).toEqual(0); - }); - - test('PROJECT_CHANGED does dispatch if the sound recorder is visible', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - store = mockStore({ - scratchGui: { - mode: {}, - modals: {soundRecorder: true}, - vm: vm, - tw: {hasCloudVariables: false} - } - }); - mount( - - ); - vm.emit('PROJECT_CHANGED'); - const actions = store.getActions(); - expect(actions.length).toEqual(1); - }); - - test('PROJECT_CHANGED does not dispatch if in fullscreen mode', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - store = mockStore({ - scratchGui: { - mode: {isFullScreen: true}, - modals: {soundRecorder: true}, - vm: vm, - tw: {hasCloudVariables: false} - } - }); - mount( - - ); - vm.emit('PROJECT_CHANGED'); - const actions = store.getActions(); - expect(actions.length).toEqual(0); - }); - - test('keypresses go to the vm', () => { - const Component = () => (
); - const WrappedComponent = vmListenerHOC(Component); - - // Mock document.addEventListener so we can trigger keypresses manually - // Cannot use the enzyme simulate method because that only works on synthetic events - const eventTriggers = {}; - document.addEventListener = jest.fn((event, cb) => { - eventTriggers[event] = cb; - }); - - vm.postIOData = jest.fn(); - - store = mockStore({ - scratchGui: { - mode: {isFullScreen: true}, - modals: {soundRecorder: true}, - vm: vm, - tw: {hasCloudVariables: false} - } - }); - mount( - - ); - - // keyboard events that do not target the document or body are ignored - eventTriggers.keydown({key: 'A', target: null}); - expect(vm.postIOData).not.toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true}); - - // keydown/up with target as the document are sent to the vm via postIOData - eventTriggers.keydown({key: 'A', target: document}); - expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true}); - - eventTriggers.keyup({key: 'A', target: document}); - expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: false}); - - // When key is 'Dead' e.g. bluetooth keyboards on iOS, it sends keyCode instead - // because the VM can process both named keys or keyCodes as the `key` property - eventTriggers.keyup({key: 'Dead', keyCode: 10, target: document}); - expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 10, isDown: false, keyCode: 10}); - }); -}); diff --git a/test/unit/util/vm-manager-hoc.test.jsx b/test/unit/util/vm-manager-hoc.test.jsx deleted file mode 100644 index 17941c053b8..00000000000 --- a/test/unit/util/vm-manager-hoc.test.jsx +++ /dev/null @@ -1,199 +0,0 @@ -/* global WebAudioTestAPI */ -import 'web-audio-test-api'; -WebAudioTestAPI.setState({ - 'AudioContext#resume': 'enabled' -}); - -import React from 'react'; -import configureStore from 'redux-mock-store'; -import {mount} from 'enzyme'; -import VM from 'scratch-vm'; -import {LoadingState} from '../../../src/reducers/project-state'; - -import vmManagerHOC from '../../../src/lib/vm-manager-hoc.jsx'; - -describe('VMManagerHOC', () => { - const mockStore = configureStore(); - let store; - let vm; - - beforeEach(() => { - store = mockStore({ - scratchGui: { - projectState: {}, - mode: {}, - vmStatus: {} - }, - locales: { - locale: '', - messages: {} - } - }); - vm = new VM(); - vm.attachAudioEngine = jest.fn(); - vm.setCompatibilityMode = jest.fn(); - vm.setLocale = jest.fn(); - vm.start = jest.fn(); - }); - test('when it mounts in player mode, the vm is initialized but not started', () => { - const Component = () => (
); - const WrappedComponent = vmManagerHOC(Component); - mount( - - ); - expect(vm.attachAudioEngine.mock.calls.length).toBe(1); - expect(vm.setLocale.mock.calls.length).toBe(1); - expect(vm.initialized).toBe(true); - - // But vm should not be started automatically - expect(vm.start).not.toHaveBeenCalled(); - }); - test('when it mounts in editor mode, the vm is initialized and started', () => { - const Component = () => (
); - const WrappedComponent = vmManagerHOC(Component); - mount( - - ); - expect(vm.attachAudioEngine.mock.calls.length).toBe(1); - expect(vm.setLocale.mock.calls.length).toBe(1); - expect(vm.initialized).toBe(true); - - expect(vm.start).toHaveBeenCalled(); - }); - test('if it mounts with an initialized vm, it does not reinitialize the vm but will start it', () => { - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - vm.initialized = true; - mount( - - ); - expect(vm.attachAudioEngine.mock.calls.length).toBe(0); - expect(vm.setLocale.mock.calls.length).toBe(0); - expect(vm.initialized).toBe(true); - - expect(vm.start).toHaveBeenCalled(); - }); - - test('if it mounts without starting the VM, it can be started by switching to editor mode', () => { - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - vm.initialized = true; - const mounted = mount( - - ); - expect(vm.start).not.toHaveBeenCalled(); - mounted.setProps({ - isPlayerOnly: false - }); - expect(vm.start).toHaveBeenCalled(); - }); - test('if it mounts with an initialized and started VM, it does not start again', () => { - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - vm.initialized = true; - const mounted = mount( - - ); - expect(vm.start).not.toHaveBeenCalled(); - mounted.setProps({ - isPlayerOnly: false - }); - expect(vm.start).not.toHaveBeenCalled(); - }); - test('if the isLoadingWithId prop becomes true, it loads project data into the vm', () => { - vm.loadProject = jest.fn(() => Promise.resolve()); - const mockedOnLoadedProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - canSave: true, - isLoadingWithId: true, - loadingState: LoadingState.LOADING_VM_WITH_ID, - projectData: '100' - }); - expect(vm.loadProject).toHaveBeenLastCalledWith('100'); - // nextTick needed since vm.loadProject is async, and we have to wait for it :/ - process.nextTick(() => ( - expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID, true) - )); - }); - test('if the fontsLoaded prop becomes true, it loads project data into the vm', () => { - vm.loadProject = jest.fn(() => Promise.resolve()); - const mockedOnLoadedProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - canSave: false, - fontsLoaded: true, - loadingState: LoadingState.LOADING_VM_WITH_ID, - projectData: '100' - }); - expect(vm.loadProject).toHaveBeenLastCalledWith('100'); - // nextTick needed since vm.loadProject is async, and we have to wait for it :/ - process.nextTick(() => ( - expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID, false) - )); - }); - test('if the fontsLoaded prop is false, project data is never loaded', () => { - vm.loadProject = jest.fn(() => Promise.resolve()); - const mockedOnLoadedProject = jest.fn(); - const Component = () =>
; - const WrappedComponent = vmManagerHOC(Component); - const mounted = mount( - - ); - mounted.setProps({ - loadingState: LoadingState.LOADING_VM_WITH_ID, - projectData: '100' - }); - expect(vm.loadProject).toHaveBeenCalledTimes(0); - process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenCalledTimes(0)); - }); -});