Compare commits

...
153 Commits
Author SHA1 Message Date
Felix Rieseberg 26e054702a fix: Incorrect dropdowns 2019-12-12 14:23:11 -05:00
Felix Rieseberg 16f27132cb Revert "First basis for having Node in there, too"
This reverts commit 7613176f69.
2019-12-12 14:06:34 -05:00
Felix RiesebergandTierney Cyren 7613176f69 First basis for having Node in there, too
Co-authored-by: Tierney Cyren <ticyren@microsoft.com>
2019-12-11 12:19:16 -05:00
dependabot[bot] 470bea4fca build(deps): bump serialize-to-js from 3.0.0 to 3.0.1 (#280)
Bumps [serialize-to-js](https://github.com/commenthol/serialize-to-js) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/commenthol/serialize-to-js/releases)
- [Commits](https://github.com/commenthol/serialize-to-js/compare/v3.0.0...v3.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-10 00:30:59 -08:00
Felix Rieseberg 363febdb73 Merge pull request #279 from electron/notarize
build: Notarize the build with Apple
2019-12-04 13:43:38 -08:00
Felix Rieseberg 062ca4e205 build: Update to electron-forge b46 2019-12-04 10:44:13 -08:00
Felix Rieseberg 49053342a5 Merge pull request #278 from electron/electron-713
chore: Update Electron to 7.1.3
2019-12-04 10:09:10 -08:00
Felix Rieseberg fd582c67a8 build: Use a later macOS image 2019-12-03 18:08:51 -08:00
Felix Rieseberg 706b18808b build: Notarize the build with Apple 2019-12-03 17:52:52 -08:00
Felix Rieseberg a6ae8d5376 chore: Update Electron to 7.1.3 2019-12-03 17:45:02 -08:00
Erick Zhao 5034f8c853 feat: add bisect helper (#257) 2019-11-22 18:46:58 -05:00
Letra e916247013 fix: ensure dev tools only open in dev mode (#277) 2019-11-15 09:22:14 -05:00
Felix Rieseberg c052686de4 0.11.1 2019-11-12 14:39:38 +01:00
Erick Zhao 31aacef131 fix: update folder selection dialog for Electron 7 (#271) 2019-11-05 16:51:04 -05:00
Erick Zhao 932369b705 fix: update deprecated app.setName API (#273) 2019-11-05 16:24:43 -05:00
Shelley Vohr b835c34786 fix: update dialog example for promisification (#272) 2019-11-03 11:36:00 -05:00
Felix Rieseberg c9e1ba815a 0.11.0 2019-10-31 11:49:23 -04:00
Felix Rieseberg 374a85e2f6 Merge pull request #267 from electron/updates-updates
feat: Electron 7 (and more updates)
2019-10-27 11:26:14 -07:00
Felix Rieseberg 4282d0b93d fix: Better typing 2019-10-27 11:16:55 -07:00
Felix Rieseberg 549862b3bb chore: npm audit fix 2019-10-27 11:13:02 -07:00
Felix Rieseberg 4691c1b494 chore: Use a lockfile with latest npm 2019-10-27 11:10:36 -07:00
Felix Rieseberg 8125abc657 build: Update AppVeyor to Node 12 2019-10-27 11:01:39 -07:00
Felix Rieseberg 8cba7739f5 fix: Prepare for Electron 7 2019-10-25 11:59:05 -07:00
Felix Rieseberg 0b39fe40b1 chore: Update all the packages 2019-10-25 11:37:34 -07:00
Felix Rieseberg cae078ffe6 Merge pull request #266 from electron/ignored-commented-out-modules
fix: don't install commented-out requires
2019-10-15 18:14:33 -07:00
Ankit Arun bfb931435f fix: add whitespace to Welcome Tour (#263) 2019-10-14 20:48:40 -04:00
Shelley Vohr c878f72f2e fix: don't install commented-out requires 2019-10-14 13:59:00 -07:00
Felix Rieseberg 32494c057a Merge pull request #254 from electron/preload
feat: Enable support for a `preload.js`
2019-10-05 14:52:58 -10:00
Jason Zeng e732685fff fix: link to docs for content tracing show-me (#256) 2019-09-25 10:16:44 -04:00
Felix Rieseberg d9d5e0cbc9 test: Fix the tests 2019-09-17 16:14:38 -07:00
Felix Rieseberg 0e1d7b541e feat: If a file is newly empty, remove it 2019-09-17 15:59:27 -07:00
Felix Rieseberg 8a602d94b7 feat: Enable support for a preload.js 2019-09-17 15:50:49 -07:00
Erick Zhao 155889dbcc fix: ensure state consistency when replacing Fiddle (#253) 2019-09-16 14:53:53 -04:00
Felix Rieseberg 56ac6662b6 Merge pull request #248 from electron/add-new-themes
feat: ability to add new editor themes to Fiddle
2019-09-10 13:40:08 -04:00
Felix Rieseberg adb196549b chore: Bump version to 0.10 2019-09-08 15:45:59 -07:00
Felix Rieseberg 999facf062 Merge pull request #250 from electron/fix-console-stuff
fix: Console button / display issues
2019-09-08 15:39:55 -07:00
Felix Rieseberg fa0115f2db fix: Correctly toggle console state 2019-09-08 15:29:15 -07:00
Carlos Valdez c0a4c0e48d remove unused import 2019-09-07 11:05:13 -05:00
Carlos Valdez 3a5d2205fe remove unused imports 2019-09-07 11:03:27 -05:00
Carlos Valdez 1083b8f859 add warning dialog 2019-09-07 11:03:11 -05:00
Carlos Valdez 252d4c1de6 remove old code (yikes) 2019-09-07 10:25:10 -05:00
Carlos Valdez 0c44d2c9e4 remove check 2019-09-05 23:54:11 -05:00
Carlos Valdez 5eebc06514 add if statement inside try catch 2019-09-05 18:27:04 -05:00
Carlos Valdez eac1423177 remove comment 2019-09-05 18:18:44 -05:00
Carlos Valdez ae88780281 remove comment 2019-09-05 18:07:56 -05:00
Carlos Valdez 4bf9d48d70 remove commet 2019-09-05 18:07:36 -05:00
Carlos Valdez df6d4b5a9a remove monaco from observable variable name 2019-09-05 17:41:49 -05:00
Carlos Valdez 68202e2b23 change file name and change import 2019-09-05 17:23:40 -05:00
Felix Rieseberg acd5b35ca1 fix: Console button / display issues 2019-08-31 07:07:20 -07:00
Felix Rieseberg fae1ca3138 Merge pull request #249 from electron/update-dependencies
chore: Update all dependencies
2019-08-31 00:25:17 +02:00
Felix Rieseberg bd220d2690 chore: Update more dependencies 2019-08-30 15:07:07 -07:00
Felix Rieseberg d31f1016b8 test: Update all the tests 2019-08-30 15:05:07 -07:00
Felix Rieseberg 3b52be0f79 chore: Update templates for Standard 2019-08-28 09:44:10 +02:00
Felix Rieseberg 0e11c56f7f chore: Update all dependencies 2019-08-28 09:43:52 +02:00
Carlos Valdez 8019981d1a missing ; 2019-08-19 13:29:09 -07:00
Carlos Valdez 403a58e216 update snapshot 2019-08-19 13:28:42 -07:00
Carlos Valdez 3341d0ccaa fix json parsing 2019-08-19 11:13:32 -07:00
Carlos Valdez d42e83100a need to work on successful input 2019-08-16 17:58:27 -07:00
Carlos Valdez 50e9883d82 add new popup using react! 😰 2019-08-16 17:37:22 -07:00
Carlos Valdez f5aedac6a3 change wording of import monaco themes and add better formatting 2019-08-15 11:09:13 -07:00
Carlos Valdez a3f4aa8ed3 remove old code 2019-08-15 10:02:30 -07:00
Carlos Valdez 9c839aaad8 remove sad code 2019-08-15 09:57:30 -07:00
Carlos Valdez 3a7ca8ccd5 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-15 09:56:15 -07:00
Carlos Valdez fef42293bc account for user checking an incorrect file 2019-08-15 09:56:02 -07:00
Felix Rieseberg 1f9db42c80 Merge pull request #247 from electron/feat/local-version-intellisense
feat: intellisense for local builds
2019-08-14 14:51:18 -07:00
Carlos Valdez 8e5657ef2d the coolest recursive function thanks to Samuel 2019-08-14 13:34:43 -07:00
Carlos Valdez a94105dd27 the coolest recursive function thanks to Samuel 2019-08-14 13:34:29 -07:00
Carlos Valdez cf214dc0d0 remove bad method 2019-08-14 11:03:54 -07:00
Carlos Valdez 6dfdf00437 add in new function and button 2019-08-14 11:03:36 -07:00
Carlos Valdez b3743a4c73 Merge branch 'add-new-themes' of github.com:electron/fiddle into add-new-themes 2019-08-13 17:07:15 -07:00
Carlos Valdez 3c0d6c2293 fix my mess 2019-08-13 17:06:23 -07:00
CarlosandErick Zhao 89b589fd2e Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:48 -07:00
CarlosandErick Zhao 42c577988d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:53:37 -07:00
CarlosandErick Zhao d0e47b6b4d Update src/renderer/components/settings-general-appearance.tsx
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:34 -07:00
CarlosandErick Zhao f44a127012 Update tests/renderer/components/__snapshots__/settings-general-appearance-spec.tsx.snap
Co-Authored-By: Erick Zhao <erick@hotmail.ca>
2019-08-13 16:41:22 -07:00
Carlos Valdez fa2f5454ce don't throw! 2019-08-13 11:24:12 -07:00
Carlos Valdez 1021bb8b2b remove console.warn 2019-08-13 10:34:40 -07:00
Carlos Valdez cf4ad36ca2 remove optional arg 2019-08-13 10:33:38 -07:00
Carlos Valdez 4fe9c627b5 if json has name, set that as the filename otherwise take the filename 2019-08-13 10:25:21 -07:00
Carlos Valdez 262e636e8d update snapshot 2019-08-12 17:53:26 -07:00
Carlos Valdez bbe79ed6e2 added description 2019-08-12 15:32:46 -07:00
Carlos Valdez bc96a5013a added new VS Code option 2019-08-12 15:32:37 -07:00
Erick Zhao 66ba7b74d5 test: add spec 2019-08-12 10:13:08 -07:00
Erick Zhao 38e5aa98e3 test: remove '.only' 2019-08-09 13:06:28 -07:00
Erick Zhao a91504b173 feat: local typedefs 2019-08-09 13:05:07 -07:00
Erick Zhao 4685e88c86 style: run linter 2019-08-09 11:27:08 -07:00
Erick Zhao e0bb82d919 fix: throw appropriately if cannot fetch types 2019-08-08 15:43:14 -07:00
Erick Zhao 7035be3286 fix: adjust styling to avoid hiding intellisense (#243) 2019-07-30 10:43:44 -07:00
Erick Zhao c961cfadb8 chore: merge in settings UX improvements 2019-07-30 10:43:10 -07:00
Erick Zhao 7267a2ee88 fix: load empty files upon opening a fiddle (#241)
* fix: load empty files upon opening a fiddle
* test: add spec for no value passed to setValue
2019-07-30 10:42:16 -07:00
Erick Zhao bc03382dc5 test: test for event listener removal upon unmount 2019-07-29 08:48:59 -07:00
Erick Zhao 891c33ba68 fix: refresh theme list upon creation 2019-07-28 20:02:35 -07:00
Erick Zhao e7260c74a4 feat: close settings panel on Esc 2019-07-28 20:02:35 -07:00
Erick Zhao ed694a7044 chore: describe correct component in spec 2019-07-28 20:02:35 -07:00
Erick Zhao 14fc77c08c chore: 'Exection' -> 'Execution' 2019-07-28 20:02:35 -07:00
Erick Zhao f3079648cf fix: clean up typedefs when removing binaries (#240) 2019-07-26 18:03:57 -07:00
Felix Rieseberg 7272ec7899 Merge pull request #239 from electron/fix/consistent-tab-size
fix: enforce 2-space indents in all editors
2019-07-26 10:25:46 -07:00
Erick Zhao ac9ac701f7 test: add spec for forced tab size 2019-07-25 13:16:56 -07:00
Erick Zhao 7fce572823 fix: enforce tab-size 2 2019-07-25 12:40:53 -07:00
Erick Zhao 6186a80dad chore: 'monoaco' -> 'monaco' (#238) 2019-07-25 11:53:20 -07:00
Mark Lee ade6576285 build: upgrade Electron Forge to 6.0.0-beta.42 (#234) 2019-07-17 23:22:06 -07:00
Felix Rieseberg 61c58277f3 Merge pull request #233 from electron/chore/toogle
chore: `toogle` -> `toggle`
2019-07-17 19:48:00 -07:00
Erick Zhao c361d34a6d chore: toogle -> toggle 2019-07-16 17:54:02 -07:00
Erick Zhao 5887a6615a Merge pull request #232 from electron/feat/protocol-force-electron-version
feat: force electron version when loading from example protocol
2019-07-16 10:34:05 -07:00
Erick Zhao 7ba4cf5be7 style: run linter 2019-07-15 16:47:36 -07:00
Erick Zhao 0daf872d2b chore: conflict resolution w/ confirm prompt 2019-07-15 16:34:05 -07:00
Erick Zhao 75ec8ee9b1 chore: merge in master 2019-07-15 16:24:00 -07:00
Erick Zhao 183c152e40 test: create spec for remote-loader 2019-07-15 16:18:04 -07:00
Erick Zhao d6a807ba41 refactor: move version checking to its own fn 2019-07-15 15:23:30 -07:00
Carlos 24e0abf667 Merge pull request #230 from electron/warning-prompt
feat: Warning prompt
2019-07-15 14:05:37 -07:00
Carlos Valdez eb8261e240 remove obsolete snapshot 2019-07-15 13:38:20 -07:00
Erick Zhao 84cc07da90 feat: load examples with specific electron version 2019-07-15 13:21:44 -07:00
Erick Zhao b0f3837b47 refactor: move remote loading to its own file 2019-07-15 13:20:56 -07:00
Carlos Valdez e29664ca45 fix added event spec 2019-07-15 13:18:55 -07:00
Carlos Valdez ef36ee39f6 - 2019-07-15 11:14:56 -07:00
Carlos Valdez 73dadb815e removed spec test 2019-07-15 11:14:45 -07:00
Carlos Valdez 4945501703 fix test spec 2019-07-15 11:02:08 -07:00
Erick Zhao 77c87fbdeb feat: console resizing (#226)
Adds a `react-mosaic` wrapper to the console component to enable resizing in a similar
fashion to the editor components.
2019-07-15 10:33:01 -07:00
Carlos Valdez 2ef5b42f4a solved conflicts on git rebase 2019-07-15 10:31:07 -07:00
Carlos Valdez 9c4bc2ba52 fix test-spec failing 2019-07-15 10:30:31 -07:00
Carlos Valdez 80da4e73e3 fixed specs 2019-07-15 10:30:31 -07:00
Carlos Valdez eadd58d149 fix test 2019-07-15 10:28:09 -07:00
Carlos Valdez 1fc82f8f6d update specs 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 9bd7043b2c Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos aaeef6fd06 Update commands-address-bar.tsx 2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 1195902bd5 Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg c022d0f33d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
CarlosandFelix Rieseberg 996ae775f3 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-15 10:28:09 -07:00
Carlos Valdez ed44dda8fb better label 2019-07-15 10:28:09 -07:00
Carlos Valdez effba13e73 add to dialog class 2019-07-15 10:28:09 -07:00
Carlos Valdez 65886c3b52 added dialog-confirm prompt for users 2019-07-15 10:28:09 -07:00
Carlos Valdez 8bd4dd366a fix test-spec failing 2019-07-15 10:16:46 -07:00
Erick Zhao 4e19578e34 feat: add support for unchecked release channels 2019-07-14 18:45:18 -07:00
Erick Zhao c7fd0bc95e feat: force electron version when load w/ protocol 2019-07-14 18:45:18 -07:00
Erick Zhao 29e2f03b1a feat: allow version utility fn to accept a string 2019-07-14 18:45:18 -07:00
Felix Rieseberg 4cd6f330da Merge pull request #227 from electron/deermichel/error-msg
chore: add offline notice when loading fiddles
2019-07-13 16:10:27 -07:00
Carlos Valdez 79521eac7c fixed specs 2019-07-12 16:03:28 -07:00
Carlos Valdez 2736499d68 fix test 2019-07-12 15:36:14 -07:00
Carlos Valdez 1cbb781e8d update specs 2019-07-12 15:25:02 -07:00
CarlosandFelix Rieseberg e849a49f6f Update src/renderer/components/dialog-confirm.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:48:40 -07:00
Carlos 0114d2c1eb Update commands-address-bar.tsx 2019-07-12 14:48:09 -07:00
CarlosandFelix Rieseberg 365537012f Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:45:00 -07:00
CarlosandFelix Rieseberg 3e435cb64d Update src/renderer/components/dialog-warning.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:44:55 -07:00
CarlosandFelix Rieseberg 6981382793 Update src/renderer/components/commands-address-bar.tsx
Co-Authored-By: Felix Rieseberg <felix@felixrieseberg.com>
2019-07-12 14:42:53 -07:00
Felix Rieseberg f293ba84d1 Merge pull request #229 from electron/feat/protocol-dev-mode-support
feat: add dev mode support for protocol
2019-07-12 14:41:05 -07:00
Carlos Valdez 55cf507a96 better label 2019-07-12 14:23:21 -07:00
Carlos Valdez e71c2439cd add to dialog class 2019-07-12 13:49:52 -07:00
Carlos Valdez 9bd253951c added dialog-confirm prompt for users 2019-07-12 13:49:40 -07:00
Erick Zhao eda08c9aea feat: change args passed in to support yarn
NPM and Yarn both forward arguments via `process.env.npm_config_argv`.
The URL would be in the `remain` property in NPM, but in `original`
in both NPM and Yarn.
2019-07-12 11:50:35 -07:00
Felix Rieseberg 9414f66893 Merge pull request #228 from electron/no-prerelease
build: Don't mark releases as prereleases
2019-07-12 11:44:40 -07:00
Erick Zhao f4725b5dd8 feat: add dev mode support for protocol 2019-07-12 11:35:18 -07:00
Felix Rieseberg fd8be47494 build: Don't mark releases as prereleases 2019-07-12 11:10:09 -07:00
Micha Hanselmann 2cebe2ca7c add more verbose msg when offline 2019-07-12 11:07:51 -07:00
117 changed files with 8912 additions and 5677 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ os:
- linux
- osx
dist: trusty
osx_image: xcode8.3
osx_image: xcode10
sudo: false
cache:
+1 -1
View File
@@ -1,6 +1,6 @@
environment:
matrix:
- nodejs_version: "10"
- nodejs_version: "12"
init:
- git config --global core.symlinks true
+36 -3
View File
@@ -7,7 +7,7 @@ const packageJson = require('./package.json')
const { version } = packageJson
const iconDir = path.resolve(__dirname, 'assets', 'icons')
module.exports = {
const config = {
hooks: {
generateAssets: require('./tools/generateAssets')
},
@@ -32,7 +32,12 @@ module.exports = {
OriginalFilename: 'Electron Fiddle',
},
osxSign: {
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)'
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
'hardened-runtime': true,
'gatekeeper-assess': false,
'entitlements': 'static/entitlements.plist',
'entitlements-inherit': 'static/entitlements.plist',
'signature-flags': 'library'
}
},
makers: [
@@ -90,8 +95,36 @@ module.exports = {
name: 'fiddle'
},
draft: true,
prerelease: true
prerelease: false
}
}
]
}
function notarizeMaybe() {
if (process.platform !== 'darwin') {
return;
}
if (!process.env.CI) {
console.log(`Not in CI, skipping notarization`);
return;
}
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn('Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!');
return;
}
config.packagerConfig.osxNotarize = {
appBundleId: 'com.electron.fiddle',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
ascProvider: 'LT94ZKYDCJ'
}
}
notarizeMaybe()
// Finally, export it
module.exports = config
+5036 -4325
View File
File diff suppressed because it is too large Load Diff
+44 -44
View File
@@ -1,7 +1,7 @@
{
"name": "electron-fiddle",
"productName": "Electron Fiddle",
"version": "0.9.0",
"version": "0.11.1",
"description": "The easiest way to get started with Electron",
"repository": "https://github.com/electron/fiddle",
"main": "./dist/src/main/main",
@@ -36,9 +36,9 @@
"forge": "./forge.config.js"
},
"dependencies": {
"@blueprintjs/core": "^3.17.1",
"@blueprintjs/select": "^3.9.0",
"@octokit/rest": "^16.28.2",
"@blueprintjs/core": "^3.19.1",
"@blueprintjs/select": "^3.11.1",
"@octokit/rest": "^16.34.0",
"builtin-modules": "^3.1.0",
"classnames": "^2.2.6",
"electron-default-menu": "^1.0.1",
@@ -48,61 +48,61 @@
"extract-zip": "^1.6.7",
"fix-path": "^2.1.0",
"fs-extra": "^8.1.0",
"mobx": "^5.11.0",
"mobx-react": "^6.1.1",
"monaco-editor": "^0.17.1",
"mobx": "^5.14.2",
"mobx-react": "^6.1.4",
"monaco-editor": "^0.18.1",
"monaco-loader": "1.0.0",
"namor": "^1.1.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"namor": "^1.1.3",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-mosaic-component": "^3.2.0",
"semver": "^6.2.0",
"semver": "^6.3.0",
"tmp": "0.1.0",
"tslib": "^1.10.0",
"update-electron-app": "^1.4.2"
"update-electron-app": "^1.5.0"
},
"devDependencies": {
"@babel/core": "^7.4.5",
"@electron-forge/cli": "^6.0.0-beta.39",
"@electron-forge/maker-deb": "^6.0.0-beta.39",
"@electron-forge/maker-rpm": "^6.0.0-beta.39",
"@electron-forge/maker-squirrel": "^6.0.0-beta.39",
"@electron-forge/maker-zip": "^6.0.0-beta.39",
"@electron-forge/publisher-github": "^6.0.0-beta.39",
"@babel/core": "^7.6.4",
"@electron-forge/cli": "^6.0.0-beta.46",
"@electron-forge/maker-deb": "^6.0.0-beta.46",
"@electron-forge/maker-rpm": "^6.0.0-beta.46",
"@electron-forge/maker-squirrel": "^6.0.0-beta.46",
"@electron-forge/maker-zip": "^6.0.0-beta.46",
"@electron-forge/publisher-github": "^6.0.0-beta.46",
"@types/builtin-modules": "^3.1.1",
"@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^24.0.15",
"@types/enzyme": "^3.10.3",
"@types/fs-extra": "^8.0.1",
"@types/jest": "^24.0.19",
"@types/log-symbols": "^3.0.0",
"@types/node": "^10.11.1",
"@types/react": "^16.8.17",
"@types/react-dom": "^16.8.4",
"@types/semver": "^6.0.0",
"@types/node": "^12.11.7",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/semver": "^6.0.2",
"@types/tmp": "0.1.0",
"chokidar": "^2.1.6",
"coveralls": "^3.0.4",
"electron": "5.0.6",
"chokidar": "^3.2.2",
"coveralls": "^3.0.7",
"electron": "7.1.3",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.3.5",
"jest": "^24.8.0",
"enzyme-adapter-react-16": "^1.15.1",
"enzyme-to-json": "^3.4.3",
"jest": "^24.9.0",
"jest-fetch-mock": "^2.1.2",
"less": "^3.9.0",
"less": "^3.10.3",
"log-symbols": "^3.0.0",
"node-abi": "^2.9.0",
"node-abi": "^2.12.0",
"node-fetch": "^2.6.0",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.3",
"react-test-renderer": "^16.8.6",
"rimraf": "^2.6.3",
"standard": "^12.0.1",
"stylelint": "^10.1.0",
"stylelint-config-standard": "^18.3.0",
"ts-jest": "^24.0.2",
"tslint": "^5.18.0",
"parcel-bundler": "^1.12.4",
"react-test-renderer": "^16.11.0",
"rimraf": "^3.0.0",
"standard": "^14.3.1",
"stylelint": "^11.1.1",
"stylelint-config-standard": "^19.0.0",
"ts-jest": "^24.1.0",
"tslint": "^5.20.0",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-react": "^4.0.0",
"typescript": "^3.5.2"
"tslint-react": "^4.1.0",
"typescript": "^3.6.4"
}
}
+5 -3
View File
@@ -24,6 +24,7 @@ export interface EditorValues {
main: string;
renderer: string;
html: string;
preload: string;
package?: string;
}
@@ -33,9 +34,9 @@ export interface ElectronVersion extends NpmVersion {
}
export interface SetFiddleOptions {
values: EditorValues;
filePath?: string;
templateName?: string;
gistId?: string;
}
export interface OutputEntry {
@@ -63,7 +64,8 @@ export interface Templates {
export const enum EditorId {
'main' = 'main',
'renderer' = 'renderer',
'html' = 'html'
'html' = 'html',
'preload' = 'preload'
}
// Panels that can show up as a mosaic
@@ -73,7 +75,7 @@ export const enum PanelId {
export type MosaicId = EditorId | PanelId;
export const ALL_EDITORS = [ EditorId.main, EditorId.renderer, EditorId.html ];
export const ALL_EDITORS = [ EditorId.main, EditorId.renderer, EditorId.preload, EditorId.html ];
export const ALL_PANELS = [ PanelId.docsDemo ];
export const ALL_MOSAICS = [ ...ALL_EDITORS, ...ALL_PANELS ];
+11 -3
View File
@@ -17,14 +17,20 @@ export enum IpcEvents {
FS_SAVE_FIDDLE_DIALOG = 'FS_SAVE_FIDDLE_DIALOG',
FS_SAVE_FIDDLE_ERROR = 'FS_SAVE_FIDDLE_ERROR',
SHOW_WARNING_DIALOG = 'SHOW_WARNING_DIALOG',
SHOW_CONFIRMATION_DIALOG = 'SHOW_CONFIRMATION_DIALOG',
SHOW_WELCOME_TOUR = 'SHOW_WELCOME_TOUR',
CLEAR_CONSOLE = 'CLEAR_CONSOLE'
CLEAR_CONSOLE = 'CLEAR_CONSOLE',
LOAD_LOCAL_VERSION_FOLDER = 'LOAD_LOCAL_VERSION_FOLDER',
SHOW_LOCAL_VERSION_FOLDER_DIALOG = 'SHOW_LOCAL_VERSION_FOLDER_DIALOG',
BISECT_COMMANDS_TOGGLE = 'BISECT_COMMANDS_TOGGLE',
}
export const ipcMainEvents = [
IpcEvents.FS_SAVE_FIDDLE_DIALOG,
IpcEvents.FS_SAVE_FIDDLE,
IpcEvents.SHOW_WARNING_DIALOG
IpcEvents.SHOW_WARNING_DIALOG,
IpcEvents.SHOW_CONFIRMATION_DIALOG,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG
];
export const ipcRendererEvents = [
@@ -44,7 +50,9 @@ export const ipcRendererEvents = [
IpcEvents.FS_SAVE_FIDDLE_FORGE,
IpcEvents.FS_SAVE_FIDDLE_GIST,
IpcEvents.SHOW_WELCOME_TOUR,
IpcEvents.CLEAR_CONSOLE
IpcEvents.CLEAR_CONSOLE,
IpcEvents.LOAD_LOCAL_VERSION_FOLDER,
IpcEvents.BISECT_COMMANDS_TOGGLE
];
export const WEBCONTENTS_READY_FOR_IPC_SIGNAL = 'WEBCONTENTS_READY_FOR_IPC_SIGNAL';
@@ -1,3 +0,0 @@
button.version-chooser.bp3-button {
border-radius: 3px 0 0 3px;
}
+1 -1
View File
@@ -2,7 +2,6 @@
header {
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
//border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background-color: @background-3;
-webkit-app-region: drag;
}
@@ -20,6 +19,7 @@ header {
select {
-webkit-app-region: no-drag;
margin-bottom: 0;
white-space: nowrap;
}
select {
+3 -8
View File
@@ -7,20 +7,15 @@
color: var(--text-color-1);
background: var(--background-2);
box-shadow: inset 0 0 100px 100px rgba(0, 0, 0, 0.15);
border-top: @border;
top: 10vh;
padding: 0;
padding-top: 10px;
padding-left: 10px;
height: 0;
height: 100%;
overflow-y: scroll;
transition: height 0.2s linear, padding 0.2s linear;
-webkit-app-region: no-drag;
&.showing {
border-top: @border;
padding-top: 10px;
height: 150px;
}
p {
margin-bottom: 0;
white-space: pre;
-1
View File
@@ -188,7 +188,6 @@
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
}
-1
View File
@@ -16,5 +16,4 @@
@import "components/chrome-mac.less";
@import "components/editors.less";
@import "components/tour.less";
@import "components/commands-version-chooser.less";
@import "components/show-me.less";
+1 -1
View File
@@ -7,7 +7,7 @@ import { isDevMode } from '../utils/devmode';
* @returns {Promise<void>}
*/
export async function setupDevTools(): Promise<void> {
if (!isDevMode) return;
if (!isDevMode()) return;
const {
default: installExtension,
+35 -1
View File
@@ -1,4 +1,4 @@
import { dialog } from 'electron';
import { dialog, IpcMainEvent } from 'electron';
import { IpcEvents } from '../ipc-events';
import { ipcMainManager } from './ipc';
import { getOrCreateMainWindow } from './windows';
@@ -12,6 +12,15 @@ export function setupDialogs() {
ipcMainManager.on(IpcEvents.SHOW_WARNING_DIALOG, (_event, args) => {
showWarningDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_CONFIRMATION_DIALOG, (_event, args) => {
showConfirmationDialog(args);
});
ipcMainManager.on(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, async (event) => {
await showOpenDialog(event);
});
}
/**
@@ -25,3 +34,28 @@ function showWarningDialog(args: Electron.MessageBoxOptions) {
...args
});
}
/**
* Shows a confirmation dialog
*
* @param {Electron.MessageBoxOptions} args
*/
function showConfirmationDialog(args: Electron.MessageBoxOptions) {
dialog.showMessageBox(getOrCreateMainWindow(), {
type: 'warning',
...args
});
}
async function showOpenDialog(event: IpcMainEvent) {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Folder',
properties: ['openDirectory']
});
if (!filePaths || filePaths.length < 1) {
return;
}
event.reply(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, [filePaths[0]]);
}
+21 -21
View File
@@ -19,41 +19,41 @@ export function setupFileListeners() {
* Shows the "Open Fiddle" dialog and forwards
* the path to the renderer
*/
export function showOpenDialog() {
dialog.showOpenDialog({
export async function showOpenDialog() {
const { filePaths } = await dialog.showOpenDialog({
title: 'Open Fiddle',
properties: ['openDirectory']
}, (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
});
if (!filePaths || filePaths.length < 1) {
return;
}
ipcMainManager.send(IpcEvents.FS_OPEN_FIDDLE, [ filePaths[0] ]);
}
/**
* Shows the "Save Fiddle" dialog and forwards
* the path to the renderer
*/
export function showSaveDialog(event?: IpcEvents, as?: string) {
export async function showSaveDialog(event?: IpcEvents, as?: string) {
// We want to save to a folder, so we'll use an open dialog here
dialog.showOpenDialog({
const { filePaths } = await dialog.showOpenDialog({
buttonLabel: 'Save here',
properties: ['openDirectory', 'createDirectory'],
title: `Save Fiddle${as ? ` as ${as}` : ''}`
}, async (filePaths) => {
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
});
if (!filePaths || filePaths.length < 1) {
return;
}
console.log(`Asked to save to ${filePaths[0]}`);
// Let's confirm real quick if we want this
if (await ensureSaveTargetEmpty(filePaths[0])) {
ipcMainManager.send(event || IpcEvents.FS_SAVE_FIDDLE, [ filePaths[0] ]);
}
}
/**
+8 -8
View File
@@ -7,9 +7,9 @@ import { isDevMode } from '../utils/devmode';
* Is this the first run of Fiddle? If so, perform
* tasks that we only want to do in this case.
*/
export function onFirstRunMaybe() {
export async function onFirstRunMaybe() {
if (isFirstRun()) {
promptMoveToApplicationsFolder();
await promptMoveToApplicationsFolder();
}
}
@@ -17,18 +17,18 @@ export function onFirstRunMaybe() {
* Ask the user if the app should be moved to the
* applications folder.
*/
function promptMoveToApplicationsFolder(): void {
async function promptMoveToApplicationsFolder(): Promise<void> {
if (process.platform !== 'darwin') return;
if (isDevMode() || app.isInApplicationsFolder()) return;
dialog.showMessageBox({
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Move to Applications Folder', 'Do Not Move'],
defaultId: 0,
message: 'Move to Applications Folder?',
}, (response) => {
if (response === 0) {
app.moveToApplicationsFolder();
}
});
if (response === 0) {
app.moveToApplicationsFolder();
}
}
+3 -1
View File
@@ -1,5 +1,6 @@
import { ipcMain } from 'electron';
import { EventEmitter } from 'events';
import { IpcEvents, ipcMainEvents, WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { getOrCreateMainWindow } from './windows';
@@ -22,8 +23,9 @@ export class IpcMainManager extends EventEmitter {
ipcMain.on(name, (...args: Array<any>) => this.emit(name, ...args));
});
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.Event) => {
ipcMain.on(WEBCONTENTS_READY_FOR_IPC_SIGNAL, (event: Electron.IpcMainEvent) => {
this.readyWebContents.add(event.sender);
const queue = this.messageQueue.get(event.sender);
this.messageQueue.delete(event.sender);
if (!queue) return;
+2 -2
View File
@@ -15,7 +15,7 @@ import { getOrCreateMainWindow } from './windows';
* the method that takes care of booting the application.
*/
export async function onReady() {
onFirstRunMaybe();
await onFirstRunMaybe();
if (!isDevMode()) process.env.NODE_ENV = 'production';
getOrCreateMainWindow();
@@ -68,7 +68,7 @@ export function main() {
}
// Set the app's name
app.setName('Electron Fiddle');
app.name = 'Electron Fiddle';
// Ensure that there's only ever one Fiddle running
listenForProtocolHandler();
+13 -8
View File
@@ -144,7 +144,7 @@ function getShowMeMenuItem(key: string, item: string | Templates): MenuItemConst
if (typeof item === 'string') {
return {
label: key,
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [ key ])
click: () => ipcMainManager.send(IpcEvents.FS_OPEN_TEMPLATE, [key])
};
}
@@ -160,10 +160,10 @@ function getShowMeMenu(): MenuItemConstructorOptions {
const showMeMenu: Array<MenuItemConstructorOptions> = Object.keys(SHOW_ME_TEMPLATES)
.map((key) => getShowMeMenuItem(key, SHOW_ME_TEMPLATES[key]));
return {
label: 'Show Me',
submenu: showMeMenu
};
return {
label: 'Show Me',
submenu: showMeMenu
};
}
/**
@@ -248,14 +248,19 @@ export function setupMenu() {
// Tweak "View" menu
if (label === 'View' && isSubmenu(item.submenu)) {
item.submenu = item.submenu.filter((subItem) => subItem.label !== 'Toggle Developer Tools'); // Remove "Toggle Developer Tools"
item.submenu.push({ type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }); // Add zooming actions
item.submenu.push({ type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }); // Add zooming actions
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Soft Wrap',
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'wordWrap' ]),
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['wordWrap']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Mini Map',
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, [ 'minimap.enabled' ]),
click: () => ipcMainManager.send(IpcEvents.MONACO_TOGGLE_OPTION, ['minimap.enabled']),
});
item.submenu.push({ type: 'separator' }, {
label: 'Toggle Bisect Helper',
click: () => ipcMainManager.send(IpcEvents.BISECT_COMMANDS_TOGGLE),
accelerator: 'CommandorControl+Shift+B',
});
}
+11 -1
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import * as nodeUrl from 'url';
import { IpcEvents } from '../ipc-events';
import { isDevMode } from '../utils/devmode';
import { ipcMainManager } from './ipc';
const PROTOCOL = 'electron-fiddle';
@@ -65,6 +66,12 @@ export const scanArgv = (argv: Array<string>) => {
}
};
export const scanNpmArgv = (argv: string) => {
const parsedArgv = JSON.parse(argv);
const { original: args } = parsedArgv;
scanArgv(args);
};
export const listenForProtocolHandler = () => {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -75,7 +82,10 @@ export const listenForProtocolHandler = () => {
}
});
if (process.platform === 'win32') {
// pass protocol URL via npm start args in dev mode
if (isDevMode() && process.env.npm_config_argv) {
scanNpmArgv(process.env.npm_config_argv);
} else if (process.platform === 'win32') {
scanArgv(process.argv);
}
};
+75 -38
View File
@@ -2,13 +2,20 @@ import { when } from 'mobx';
import * as MonacoType from 'monaco-editor';
import { ipcRenderer } from 'electron';
import { ALL_EDITORS, EditorId, EditorValues } from '../interfaces';
import {
ALL_EDITORS,
EditorId,
EditorValues,
SetFiddleOptions
} from '../interfaces';
import { WEBCONTENTS_READY_FOR_IPC_SIGNAL } from '../ipc-events';
import { updateEditorLayout } from '../utils/editor-layout';
import { getEditorValue } from '../utils/editor-value';
import { getPackageJson, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { isEditorBackup } from '../utils/type-checks';
import { FileManager } from './file-manager';
import { RemoteLoader } from './remote-loader';
import { Runner } from './runner';
import { appState } from './state';
import { getTheme } from './themes';
@@ -25,32 +32,29 @@ export class App {
public monaco: typeof MonacoType | null = null;
public state = appState;
public fileManager = new FileManager(appState);
public remoteLoader = new RemoteLoader(appState);
public runner = new Runner(appState);
public touchBarManager: TouchBarManager | undefined;
constructor() {
this.getValues = this.getValues.bind(this);
this.setValues = this.setValues.bind(this);
this.getEditorValues = this.getEditorValues.bind(this);
this.setEditorValues = this.setEditorValues.bind(this);
if (process.platform === 'darwin') {
this.touchBarManager = new TouchBarManager(appState);
}
}
/**
* Sets the values on all three editors.
*
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async setValues(values: Partial<EditorValues>, warn: boolean = true): Promise<boolean> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
if (this.state.isUnsaved && warn) {
public async replaceFiddle(
editorValues: Partial<EditorValues>,
{ filePath, gistId, templateName }: Partial<SetFiddleOptions>
) {
// if unsaved, prompt user to make sure they're okay with overwriting and changing directory
if (this.state.isUnsaved) {
this.state.setWarningDialogTexts({
label: `Opening this Fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
this.state.isWarningDialogShowing = true;
await when(() => !this.state.isWarningDialogShowing);
@@ -59,31 +63,58 @@ export class App {
}
}
for (const name of ALL_EDITORS) {
const editor = fiddle.editors[name];
const backup = this.state.closedPanels[name];
// set values once prompt approves
await this.setEditorValues(editorValues);
if (isEditorBackup(backup) && backup.model && values[name]) {
// The editor does not exist, attempt to set it on the backup
backup.model.setValue(values[name]!);
} else if (editor && editor.setValue && values[name]) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
}
}
document.title = getTitle(this.state);
this.state.gistId = gistId || '';
this.state.localPath = filePath;
this.state.templateName = templateName;
// once loaded, we have a "saved" state
this.state.isUnsaved = false;
this.setupUnsavedOnChangeListener();
return true;
}
/**
* Sets the values on all three editors.
*
* @param {EditorValues} values
* @param {warn} warn - Should we warn before overwriting unsaved data?
*/
public async setEditorValues(values: Partial<EditorValues>): Promise<void> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
throw new Error('Fiddle not ready');
}
for (const name of ALL_EDITORS) {
const editor = fiddle.editors[name];
const backup = this.state.closedPanels[name];
if (typeof values[name] !== 'undefined') {
if (isEditorBackup(backup) && backup.model) {
// The editor does not exist, attempt to set it on the backup
backup.model.setValue(values[name]!);
} else if (editor && editor.setValue) {
// The editor exists, set the value directly
editor.setValue(values[name]!);
}
}
}
}
/**
* Gets the values on all three editors.
*
* @returns {EditorValues}
*/
public async getValues(options?: PackageJsonOptions): Promise<EditorValues> {
public async getEditorValues(
options?: PackageJsonOptions
): Promise<EditorValues> {
const { ElectronFiddle: fiddle } = window;
if (!fiddle) {
@@ -93,10 +124,11 @@ export class App {
const values: EditorValues = {
html: getEditorValue(EditorId.html),
main: getEditorValue(EditorId.main),
renderer: getEditorValue(EditorId.renderer),
preload: getEditorValue(EditorId.preload),
renderer: getEditorValue(EditorId.renderer)
};
if (options && options.include !== false) {
if (options && options.include !== false) {
values.package = await getPackageJson(this.state, values, options);
}
@@ -112,16 +144,18 @@ export class App {
const React = await import('react');
const { render } = await import('react-dom');
const { Header } = await import('./components/header');
const { Dialogs } = await import('./components/dialogs');
const { Editors } = await import('./components/editors');
const { OutputEditorsWrapper } = await import(
'./components/output-editors-wrapper'
);
const { Header } = await import('./components/header');
const className = `${process.platform} container`;
const app = (
<div className={className}>
<Header appState={this.state} />
<Dialogs appState={this.state} />
<Editors appState={this.state} />
<Header appState={this.state} />
<OutputEditorsWrapper appState={this.state} />
</div>
);
@@ -131,7 +165,7 @@ export class App {
ipcRenderer.send(WEBCONTENTS_READY_FOR_IPC_SIGNAL);
// Todo: A timer here is terrible. Let's fix this
// TODO: A timer here is terrible. Let's fix this
// and ensure we actually do it once Editors have mounted.
setTimeout(() => {
this.setupUnsavedOnChangeListener();
@@ -161,7 +195,9 @@ export class App {
* @returns {Promise<void>}
*/
public async setupTheme(): Promise<void> {
const tag: HTMLStyleElement | null = document.querySelector('style#fiddle-theme');
const tag: HTMLStyleElement | null = document.querySelector(
'style#fiddle-theme'
);
const theme = await getTheme(this.state.theme);
if (tag && theme.css) {
@@ -185,6 +221,7 @@ export class App {
}
window.ElectronFiddle = window.ElectronFiddle || {};
window.ElectronFiddle.contentChangeListeners = window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.contentChangeListeners =
window.ElectronFiddle.contentChangeListeners || [];
window.ElectronFiddle.app = window.ElectronFiddle.app || new App();
window.ElectronFiddle.app.setup();
+45 -12
View File
@@ -4,6 +4,7 @@ import * as path from 'path';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { USER_DATA_PATH } from './constants';
import { getOfflineTypeDefinitionPath } from './fetch-types';
/**
* The binary manager takes care of downloading Electron versions
@@ -19,31 +20,48 @@ export class BinaryManager {
* four times before giving up if an error occurs.
*
* @param {string} iVersion
* @param {number} iteration
* @returns {Promise<void>}
*/
public async remove(iVersion: string, i: number = 0): Promise<void> {
public async remove(iVersion: string): Promise<void> {
const version = normalizeVersion(iVersion);
const fs = await fancyImport<typeof fsType>('fs-extra');
let isDeleted = false;
try {
// utility to re-run removal functions upon failure
// due to windows filesystem lockfile jank
const rerunner = async (func: () => Promise<void>, counter: number = 1) => {
try {
await func();
} catch (error) {
console.warn(`Binary Manager: failed to run ${func.name} for ${version}, but failed`, error);
if (counter < 4) {
console.log(`Binary Manager: Trying again to run ${func.name}`);
await rerunner(func, counter + 1);
}
}
};
const binaryCleaner = async () => {
if (await this.getIsDownloaded(version)) {
// This is necessary since we're messing with .asar files inside
// the Electron binaries. Electron, powering Fiddle, will try to
// "correct" our calls, but we don't want that right here.
process.noAsar = true;
await fs.remove(this.getDownloadPath(version));
process.noAsar = false;
}
} catch (error) {
console.warn(`Binary Manager: Tried to remove ${version}, but failed`, error);
if (i < 3) {
console.log(`Binary Manager: Trying again`);
return this.remove(version, i + 1);
isDeleted = true;
}
};
const typeDefsCleaner = async () => {
await this.removeTypeDefsForVersion(version);
};
await rerunner(binaryCleaner);
if (isDeleted) {
await rerunner(typeDefsCleaner);
}
}
@@ -89,7 +107,7 @@ export class BinaryManager {
} catch (error) {
console.warn(`Failure while unzipping ${version}`, error);
// Todo: Handle this case
// TODO: Handle this case
}
this.state[version] = 'ready';
@@ -160,6 +178,20 @@ export class BinaryManager {
return fs.existsSync(expectedPath);
}
public async removeTypeDefsForVersion(version: string) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const _version = normalizeVersion(version);
const typeDefsDir = path.dirname(getOfflineTypeDefinitionPath(_version));
if (fs.existsSync(typeDefsDir)) {
try {
await fs.remove(typeDefsDir);
} catch (error) {
throw error;
}
}
}
/**
* Gets the expected path for a given Electron version
*
@@ -170,6 +202,7 @@ export class BinaryManager {
return path.join(USER_DATA_PATH, 'electron-bin', version);
}
/**
* Unzips an electron package so that we can actually use it.
*
+58
View File
@@ -0,0 +1,58 @@
import { ElectronVersion } from '../interfaces';
export class Bisector {
public revList: Array<ElectronVersion>;
public minRev: number;
public maxRev: number;
private pivot: number;
constructor(revList: Array<ElectronVersion>) {
this.getCurrentVersion = this.getCurrentVersion.bind(this);
this.continue = this.continue.bind(this);
this.calculatePivot = this.calculatePivot.bind(this);
this.revList = revList;
this.minRev = 0;
this.maxRev = revList.length - 1;
this.calculatePivot();
}
public getCurrentVersion() {
return this.revList[this.pivot];
}
public continue(isGoodVersion: boolean) {
let isBisectOver = false;
if (this.maxRev - this.minRev <= 1) {
isBisectOver = true;
}
if (isGoodVersion) {
const upPivot = Math.floor((this.maxRev - this.pivot) / 2) + this.pivot;
this.minRev = this.pivot;
if (upPivot !== this.maxRev && upPivot !== this.pivot) {
this.pivot = upPivot;
} else {
isBisectOver = true;
}
} else {
const downPivot = Math.floor((this.pivot - this.minRev) / 2) + this.minRev;
this.maxRev = this.pivot;
if (downPivot !== this.minRev && downPivot !== this.pivot) {
this.pivot = downPivot;
} else {
isBisectOver = true;
}
}
if (isBisectOver) {
return [this.revList[this.minRev], this.revList[this.maxRev]];
} else {
return this.revList[this.pivot];
}
}
private calculatePivot() {
this.pivot = Math.floor((this.maxRev - this.minRev) / 2);
}
}
+24 -174
View File
@@ -1,16 +1,11 @@
import { Button, InputGroup, Intent } from '@blueprintjs/core';
import classnames from 'classnames';
import { reaction, when } from 'mobx';
import { reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { EditorId, EditorValues } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../shared-constants';
import { getTitle } from '../../utils/get-title';
import { idFromUrl, urlFromId } from '../../utils/gist';
import { getOctokit } from '../../utils/octokit';
import { getContent } from '../content';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
@@ -20,26 +15,32 @@ export interface AddressBarProps {
export interface AddressBarState {
value: string;
loaders: {
gist: any;
example: any;
};
}
@observer
export class AddressBar extends React.Component<AddressBarProps, AddressBarState> {
constructor(props: AddressBarProps) {
super(props);
this.fetchExampleAndLoad = this.fetchExampleAndLoad.bind(this);
this.fetchGistAndLoad = this.fetchGistAndLoad.bind(this);
this.loadFiddleFromGist = this.loadFiddleFromGist.bind(this);
this.loadFiddleFromElectronExample = this.loadFiddleFromElectronExample.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.submit = this.submit.bind(this);
this.verifyRemoteLoad = this.verifyRemoteLoad.bind(this);
const { gistId } = this.props.appState;
const value = gistId ? urlFromId(gistId) : '';
this.state = { value };
const { remoteLoader } = window.ElectronFiddle.app;
this.state = {
value,
loaders: {
gist: remoteLoader.loadFiddleFromGist.bind(remoteLoader),
example: remoteLoader.loadFiddleFromElectronExample.bind(remoteLoader)
}
};
}
/**
@@ -59,8 +60,9 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
* @memberof AddressBar
*/
public submit() {
const { remoteLoader } = window.ElectronFiddle.app;
if (this.state.value) {
this.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
remoteLoader.fetchGistAndLoad(idFromUrl(this.state.value) || this.state.value);
}
}
@@ -68,48 +70,20 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
* Once the component mounts, we'll subscribe to gistId changes
*/
public componentDidMount() {
const { appState } = this.props;
const { loaders } = this.state;
reaction(
() => this.props.appState.gistId,
() => appState.gistId,
(gistId: string) => this.setState({ value: urlFromId(gistId) })
);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, this.loadFiddleFromGist);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, this.loadFiddleFromElectronExample);
ipcRendererManager.on(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.on(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
public componentWillUnmount() {
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, this.loadFiddleFromGist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, this.loadFiddleFromElectronExample);
}
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const ok = await this.verifyRemoteLoad('example from the Electron docs');
if (!ok) return;
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const ok = await this.verifyRemoteLoad('gist');
if (!ok) return;
this.fetchGistAndLoad(gistInfo.id);
}
/**
* Verifies from the user that we should be loading this fiddle
*
* @param what What are we loading from (gist, example, etc.)
*/
public async verifyRemoteLoad(what: string): Promise<boolean> {
const { appState } = this.props;
appState.setWarningDialogTexts({
label: `Are you sure you sure you want to load this ${what}? Only load and run it if you trust the source`
});
appState.isWarningDialogShowing = true;
await when(() => !appState.isWarningDialogShowing);
return !!appState.warningDialogLastResult;
const { loaders } = this.state;
ipcRendererManager.removeListener(IpcEvents.LOAD_GIST_REQUEST, loaders.gist);
ipcRendererManager.removeListener(IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, loaders.example);
}
/**
@@ -121,85 +95,6 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
this.setState({ value: event.target.value });
}
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
const { appState } = this.props;
try {
const octo = await getOctokit(this.props.appState);
// TODO: Maybe we should fetch the package.json for the given ref to use the correct version of Electron?
const folder = await octo.repos.getContents({
owner: 'electron',
repo: 'electron',
ref,
path,
});
const values = {
html: await getContent(EditorId.html, appState.version),
renderer: await getContent(EditorId.renderer, appState.version),
main: await getContent(EditorId.main, appState.version),
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.main = t; })
);
break;
case INDEX_HTML_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.html = t; })
);
break;
case RENDERER_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.renderer = t; })
);
break;
default:
break;
}
}
await Promise.all(loaders);
return this.handleLoadingSuccess(values, '');
} catch (error) {
return this.handleLoadingFailed(error);
}
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof AddressBar
*/
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
try {
const octo = await getOctokit(this.props.appState);
const gist = await octo.gists.get({ gist_id: gistId });
return this.handleLoadingSuccess({
html: gist.data.files[INDEX_HTML_NAME].content,
main: gist.data.files[MAIN_JS_NAME].content,
renderer: gist.data.files[RENDERER_JS_NAME].content,
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
}
public renderLoadButton(isValueCorrect: boolean): JSX.Element {
return (
<Button
@@ -233,49 +128,4 @@ export class AddressBar extends React.Component<AddressBarProps, AddressBarState
</form>
);
}
/**
* Loading a fiddle from GitHub succeeded, let's move on.
*
* @private
* @param {Partial<EditorValues>} values
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
const { appState } = this.props;
appState.setWarningDialogTexts({
label: 'Loading the fiddle will replace your current unsaved changes. Do you want to discard them?'
});
await window.ElectronFiddle.app.setValues(values);
document.title = getTitle(appState);
appState.gistId = gistId;
appState.localPath = undefined;
appState.templateName = undefined;
return true;
}
/**
* Loading a fiddle from GitHub failed - this method handles this case
* gracefully.
*
* @private
* @param {Error} error
* @returns {boolean}
*/
private handleLoadingFailed(error: Error): false {
this.props.appState.setWarningDialogTexts({
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
this.props.appState.toogleWarningDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
@@ -0,0 +1,68 @@
import { Button } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
interface BisectHandlerProps {
appState: AppState;
}
@observer
export class BisectHandler extends React.Component<BisectHandlerProps> {
constructor(props: BisectHandlerProps) {
super(props);
this.continueBisect = this.continueBisect.bind(this);
this.terminateBisect = this.terminateBisect.bind(this);
}
public continueBisect(isGood: boolean) {
const { appState } = this.props;
const response = appState.Bisector!.continue(isGood);
if (Array.isArray(response)) {
this.terminateBisect();
const [minRev, maxRev] = response;
appState.pushOutput(`[BISECT] Complete: Check between versions ${minRev.version} and ${maxRev.version}.`);
} else {
appState.setVersion(response.version);
}
}
public terminateBisect() {
const { appState } = this.props;
appState.Bisector = undefined;
}
public render() {
const { appState } = this.props;
if (!!appState.Bisector) {
return (
<>
<Button
icon={'thumbs-up'}
onClick={() => this.continueBisect(true)}
/>
<Button
icon={'thumbs-down'}
onClick={() => this.continueBisect(false)}
/>
<Button
icon={'cross'}
onClick={this.terminateBisect}
>
Cancel Bisect
</Button>
</>
);
} else {
return (
<Button
icon='git-branch'
text='Bisect'
onClick={appState.toggleBisectDialog}
/>
);
}
}
}
@@ -71,7 +71,7 @@ export class PublishButton extends React.Component<PublishButtonProps> {
const octo = await getOctokit(this.props.appState);
const { gitHubPublishAsPublic } = this.props.appState;
const options = { includeDependencies: true, includeElectron: true };
const values = await window.ElectronFiddle.app.getValues(options);
const values = await window.ElectronFiddle.app.getEditorValues(options);
try {
const gist = await octo.gists.create({
@@ -1,4 +1,4 @@
import { Button, MenuItem } from '@blueprintjs/core';
import { Button, ButtonGroup, MenuItem } from '@blueprintjs/core';
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -71,7 +71,7 @@ export const filterItem: ItemPredicate<ElectronVersion> = (query, { version }) =
*/
export const renderItem: ItemRenderer<ElectronVersion> = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
return null;
}
return (
@@ -95,6 +95,30 @@ export interface VersionChooserProps {
appState: AppState;
}
export const getVersionsFromAppState = (appState: AppState) => {
const { versions, versionsToShow, statesToShow } = appState;
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return false;
}
return true;
});
};
/**
* A dropdown allowing the selection of Electron versions. The actual
* download is managed in the state.
@@ -120,48 +144,29 @@ export class VersionChooser extends React.Component<VersionChooserProps, Version
this.props.appState.setVersion(version);
}
public getItems(): Array<ElectronVersion> {
const { versions, versionsToShow, statesToShow } = this.props.appState;
return sortedElectronMap<ElectronVersion>(versions, (_key, item) => item)
.filter((item) => {
if (!item) {
return false;
}
// Check if we want to show the version
if (!versionsToShow.includes(getReleaseChannel(item))) {
return false;
}
// Check if we want to show the state
if (!statesToShow.includes(item.state)) {
return false;
}
return true;
});
}
public render() {
const { currentElectronVersion } = this.props.appState;
const { currentElectronVersion, Bisector } = this.props.appState;
const { version } = currentElectronVersion;
return (
<ElectronVersionSelect
filterable={true}
items={this.getItems()}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
/>
</ElectronVersionSelect>
<ButtonGroup>
<ElectronVersionSelect
filterable={true}
items={getVersionsFromAppState(this.props.appState)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!!Bisector}
>
<Button
className='version-chooser'
text={`Electron v${version}`}
icon={getItemIcon(currentElectronVersion)}
disabled={!!Bisector}
/>
</ElectronVersionSelect>
</ButtonGroup>
);
}
}
+11
View File
@@ -4,6 +4,7 @@ import * as React from 'react';
import { AppState } from '../state';
import { AddressBar } from './commands-address-bar';
import { BisectHandler } from './commands-bisect';
import { EditorDropdown } from './commands-editors';
import { PublishButton } from './commands-publish-button';
import { Runner } from './commands-runner';
@@ -28,6 +29,7 @@ export class Commands extends React.Component<CommandsProps, {}> {
public render() {
const { appState } = this.props;
const { isBisectCommandShowing: isBisectCommandShowing } = appState;
return (
<div className='commands'>
@@ -36,6 +38,15 @@ export class Commands extends React.Component<CommandsProps, {}> {
<VersionChooser appState={appState} />
<Runner appState={appState} />
</ControlGroup>
{
// tslint:disable-next-line jsx-no-multiline-js
isBisectCommandShowing &&
(
<ControlGroup fill={true} vertical={false}>
<BisectHandler appState={appState} />
</ControlGroup>
)
}
<ControlGroup fill={true} vertical={false}>
<Button
active={appState.isConsoleShowing}
@@ -0,0 +1,160 @@
import { Button, Dialog, FileInput } from '@blueprintjs/core';
import { shell } from 'electron';
import * as fsType from 'fs-extra';
import { observer } from 'mobx-react';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import * as React from 'react';
import { getTheme, THEMES_PATH } from '../themes';
import { AppState } from '../state';
import { defaultDark, LoadedFiddleTheme } from '../themes-defaults';
export interface AddThemeDialogProps {
appState: AppState;
}
export interface AddThemeDialogState {
file?: File;
}
/**
* The "add monaco theme" dialog allows users to add custom editor themes.
*
* @class AddThemeDialog
* @extends {React.Component<AddThemeDialogProps, AddThemeDialogState>}
*/
@observer
export class AddThemeDialog extends React.Component<AddThemeDialogProps, AddThemeDialogState> {
public resetState = { file: undefined };
constructor(props: AddThemeDialogProps) {
super(props);
this.state = this.resetState;
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.reset = this.reset.bind(this);
}
/**
* Handles a change of the file input
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
const file = files && files[0] ? files[0] : undefined;
this.setState({ file });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file } = this.state;
const defaultTheme = !!this.props.appState.theme ? await getTheme(this.props.appState.theme) : defaultDark;
if (!file) return;
try {
const editor = fsType.readJSONSync(file.path);
if (!editor.base && !editor.rules) throw Error('File does not match specifications'); // has to have these attributes
defaultTheme.editor = editor as Partial<MonacoType.editor.IStandaloneThemeData>;
const newTheme = defaultTheme;
const name = editor.name ? editor.name : file.name;
await this.createNewThemeFromMonaco(name, newTheme);
} catch (error) {
this.props.appState.setWarningDialogTexts({label: `Error: ${error}, please pick a different file.`});
this.props.appState.isWarningDialogShowing = true;
return;
}
this.onClose();
return;
}
public async createNewThemeFromMonaco(name: string, newTheme: LoadedFiddleTheme): Promise<boolean> {
if (!name) return false;
const themePath = path.join(THEMES_PATH, `${name}`);
await fsType.outputJSON(themePath, {
...newTheme,
name,
}, {spaces: 2});
this.props.appState.setTheme(themePath);
shell.showItemInFolder(themePath);
return true;
}
get buttons() {
const canSubmit = !!this.state.file;
return [
(
<Button
icon='add'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Add'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
public onClose() {
this.props.appState.isThemeDialogShowing = false;
this.reset();
}
public render() {
const { isThemeDialogShowing } = this.props.appState;
const inputProps = { accept: '.json' };
const { file } = this.state;
const text = file && file.path ? file.path : `Select the Monaco file...`;
return (
<Dialog
isOpen={isThemeDialogShowing}
onClose={this.onClose}
title='Add theme'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
inputProps={inputProps as any}
text={text}
/>
<br />
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
/**
* Reset this component's state
*/
private reset(): void {
this.setState(this.resetState);
return;
}
}
+31 -24
View File
@@ -6,7 +6,9 @@ import * as React from 'react';
import * as semver from 'semver';
import { NpmVersion } from '../../interfaces';
import { IpcEvents } from '../../ipc-events';
import { getElectronNameForPlatform } from '../../utils/electron-name';
import { ipcRendererManager } from '../ipc';
import { AppState } from '../state';
export interface AddVersionDialogProps {
@@ -16,7 +18,7 @@ export interface AddVersionDialogProps {
export interface AddVersionDialogState {
isValidElectron: boolean;
isValidVersion: boolean;
file?: File;
folderPath?: string;
version: string;
}
@@ -39,8 +41,15 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onChangeFile = this.onChangeFile.bind(this);
this.onChangeVersion = this.onChangeVersion.bind(this);
ipcRendererManager.on(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, (_event, [file]) => {
this.setFolderPath(file);
});
}
public componentWillUnmount() {
ipcRendererManager.removeAllListeners(IpcEvents.LOAD_LOCAL_VERSION_FOLDER);
}
/**
@@ -48,16 +57,11 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
*
* @param {React.ChangeEvent<HTMLInputElement>} event
*/
public async onChangeFile(event: React.FormEvent<HTMLInputElement>) {
const { files } = event.target as any;
public async setFolderPath(folderPath: string) {
const { binaryManager } = this.props.appState;
const file = files && files[0] ? files[0] : undefined;
const isValidElectron = !!await binaryManager.getIsDownloaded('custom', folderPath);
const isValidElectron = !!(file
&& file.path
&& await binaryManager.getIsDownloaded('custom', file.path));
this.setState({ file, isValidElectron });
this.setState({ folderPath, isValidElectron });
}
public onChangeVersion(event: React.ChangeEvent<HTMLInputElement>) {
@@ -67,7 +71,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
this.setState({
version,
isValidVersion,
});
});
}
/**
@@ -76,18 +80,18 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { file, version } = this.state;
const { folderPath, version } = this.state;
if (!file) return;
if (!folderPath) return;
const name = file.path
const name = folderPath
.slice(-20)
.split(path.sep)
.slice(1)
.join(path.sep);
const toAdd: NpmVersion = {
localPath: file.path,
localPath: folderPath,
version,
name
};
@@ -129,12 +133,16 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
public render() {
const { isAddVersionDialogShowing } = this.props.appState;
const inputProps = { webkitdirectory: 'true' };
const { file } = this.state;
const inputProps = {
onClick: (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
ipcRendererManager.send(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
}
};
const { folderPath } = this.state;
const text = file && file.path
? file.path
: `Select the folder containing ${getElectronNameForPlatform()}...`;
const text = folderPath ||
`Select the folder containing ${getElectronNameForPlatform()}...`;
return (
<Dialog
@@ -145,7 +153,6 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
>
<div className='bp3-dialog-body'>
<FileInput
onInputChange={this.onChangeFile}
id='custom-electron-version'
inputProps={inputProps as any}
text={text}
@@ -163,9 +170,9 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
}
private renderPath(): JSX.Element | null {
const { file, isValidElectron } = this.state;
const { folderPath, isValidElectron } = this.state;
if (!file || !file.path) return null;
if (!folderPath) return null;
const info = isValidElectron
? `We found an ${getElectronNameForPlatform()} in this folder.`
@@ -208,7 +215,7 @@ export class AddVersionDialog extends React.Component<AddVersionDialogProps, Add
isValidElectron: false,
isValidVersion: false,
version: '',
file: undefined
folderPath: undefined
});
}
}
+164
View File
@@ -0,0 +1,164 @@
import { Button, Dialog, Label, MenuItem } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ElectronVersion } from '../../interfaces';
import { Bisector } from '../bisect';
import { AppState } from '../state';
import { filterItem, getItemIcon, getVersionsFromAppState, renderItem } from './commands-version-chooser';
const ElectronVersionSelect = Select.ofType<ElectronVersion>();
export interface BisectDialogProps {
appState: AppState;
}
export interface BisectDialogState {
startIndex?: number;
endIndex?: number;
allVersions: Array<ElectronVersion>;
}
/**
* The "add version" dialog allows users to add custom builds of Electron.
*
* @class AddVersionDialog
* @extends {React.Component<BisectDialogProps, BisectDialogState>}
*/
@observer
export class BisectDialog extends React.Component<BisectDialogProps, BisectDialogState> {
constructor(props: BisectDialogProps) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
this.onClose = this.onClose.bind(this);
this.onBeginSelect = this.onBeginSelect.bind(this);
this.onEndSelect = this.onEndSelect.bind(this);
const allVersions = getVersionsFromAppState(this.props.appState);
this.state = { allVersions };
}
public onBeginSelect(version: ElectronVersion) {
this.setState({ startIndex: this.state.allVersions.indexOf(version) });
}
public onEndSelect(version: ElectronVersion) {
this.setState({ endIndex: this.state.allVersions.indexOf(version) });
}
/**
* Handles the submission of the dialog
*
* @returns {Promise<void>}
*/
public async onSubmit(): Promise<void> {
const { endIndex, startIndex, allVersions } = this.state;
const { appState } = this.props;
if (endIndex === undefined || startIndex === undefined) {
return;
}
const bisectRange = allVersions
.slice(endIndex, startIndex + 1)
.reverse();
appState.Bisector = new Bisector(bisectRange);
const initialBisectPivot = appState.Bisector.getCurrentVersion().version;
appState.setVersion(initialBisectPivot);
this.onClose();
}
/**
* Closes the dialog
*/
public onClose() {
this.props.appState.isBisectDialogShowing = false;
}
get buttons() {
const canSubmit =
!!this.state.startIndex &&
!!this.state.endIndex &&
this.state.startIndex > this.state.endIndex;
return [
(
<Button
icon='play'
key='submit'
disabled={!canSubmit}
onClick={this.onSubmit}
text='Begin'
/>
), (
<Button
icon='cross'
key='cancel'
onClick={this.onClose}
text='Cancel'
/>
)
];
}
public render() {
const { isBisectDialogShowing } = this.props.appState;
const { startIndex, endIndex, allVersions } = this.state;
return (
<Dialog
isOpen={isBisectDialogShowing}
onClose={this.onClose}
title='Start a bisect session'
className='dialog-add-version'
>
<div className='bp3-dialog-body'>
<Label>
Earliest Version
<ElectronVersionSelect
filterable={true}
items={allVersions}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onBeginSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
>
<Button
text={startIndex ? `v${allVersions[startIndex].version}` : ``}
icon={startIndex ? getItemIcon(allVersions[startIndex]) : 'small-minus'}
fill={true}
/>
</ElectronVersionSelect>
</Label>
<Label>
Latest Version
<ElectronVersionSelect
filterable={true}
items={allVersions.slice(0, startIndex!)}
itemRenderer={renderItem}
itemPredicate={filterItem}
onItemSelect={this.onEndSelect}
noResults={<MenuItem disabled={true} text='No results.' />}
disabled={!startIndex}
>
<Button
text={endIndex ? `v${allVersions[endIndex].version}` : ``}
icon={endIndex ? getItemIcon(allVersions[endIndex]) : 'small-minus'}
fill={true}
disabled={!startIndex}
/>
</ElectronVersionSelect>
</Label>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
{this.buttons}
</div>
</div>
</Dialog>
);
}
}
@@ -0,0 +1,51 @@
import { Alert, Intent } from '@blueprintjs/core';
import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
export interface ConfirmDialogProps {
appState: AppState;
}
export interface ConfirmDialogState {
}
/**
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class ConfirmDialog
* @extends {React.Component<ConfirmDialogProps, ConfirmDialogState>}
*/
@observer
export class ConfirmDialog extends React.Component<ConfirmDialogProps, ConfirmDialogState> {
constructor(props: ConfirmDialogProps) {
super(props);
this.onClose = this.onClose.bind(this);
}
public onClose(result: boolean) {
this.props.appState.confirmationPromptLastResult = result;
this.props.appState.toggleConfirmationPromptDialog();
}
public render() {
const { isConfirmationPromptShowing, confirmationDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isConfirmationPromptShowing}
onClose={this.onClose}
icon='help'
confirmButtonText={confirmationDialogTexts.ok}
cancelButtonText={confirmationDialogTexts.cancel}
intent={Intent.PRIMARY}
>
<p>{confirmationDialogTexts.label}</p>
</Alert>
);
}
}
+2 -4
View File
@@ -13,8 +13,7 @@ export interface WarningDialogState {
}
/**
* The token dialog asks the user for a GitHub Personal Access Token.
* It's also responsible for checking if the token is correct.
* The token dialog prompts the user to either continue or cancel the operation.
*
* @export
* @class WarningDialog
@@ -30,12 +29,11 @@ export class WarningDialog extends React.Component<WarningDialogProps, WarningDi
public onClose(result: boolean) {
this.props.appState.warningDialogLastResult = result;
this.props.appState.toogleWarningDialog();
this.props.appState.toggleWarningDialog();
}
public render() {
const { isWarningDialogShowing, warningDialogTexts } = this.props.appState;
return (
<Alert
isOpen={isWarningDialogShowing}
+21 -2
View File
@@ -2,7 +2,10 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { AppState } from '../state';
import { AddThemeDialog } from './dialog-add-theme';
import { AddVersionDialog } from './dialog-add-version';
import { BisectDialog } from './dialog-bisect';
import { ConfirmDialog } from './dialog-confirm';
import { TokenDialog } from './dialog-token';
import { WarningDialog } from './dialog-warning';
import { Settings } from './settings';
@@ -21,7 +24,13 @@ export interface DialogsProps {
export class Dialogs extends React.Component<DialogsProps, {}> {
public render() {
const { appState } = this.props;
const { isTokenDialogShowing, isSettingsShowing, isAddVersionDialogShowing } = appState;
const {
isTokenDialogShowing,
isSettingsShowing,
isAddVersionDialogShowing,
isThemeDialogShowing,
isBisectDialogShowing
} = appState;
const maybeToken = isTokenDialogShowing
? <TokenDialog key='dialogs' appState={appState} />
: null;
@@ -31,13 +40,23 @@ export class Dialogs extends React.Component<DialogsProps, {}> {
const maybeAddLocalVersion = isAddVersionDialogShowing
? <AddVersionDialog key='add-version-dialog' appState={appState} />
: null;
const maybeMonaco = isThemeDialogShowing ? <AddThemeDialog appState={appState} />
: null;
const maybeBisect = isBisectDialogShowing
? <BisectDialog key='bisect-dialog' appState={appState} />
: null;
const eitherWarningOrPrompt = appState.isWarningDialogShowing
? <WarningDialog appState={appState} />
: <ConfirmDialog appState={appState} />;
return (
<div key='dialogs' className='dialogs'>
{maybeToken}
{maybeSettings}
{maybeAddLocalVersion}
<WarningDialog appState={appState} />
{maybeMonaco}
{maybeBisect}
{eitherWarningOrPrompt}
</div>
);
}
+6 -3
View File
@@ -11,7 +11,7 @@ import { AppState } from '../state';
export interface EditorProps {
appState: AppState;
monaco: typeof MonacoType;
monoacoOptions: MonacoType.editor.IEditorOptions;
monacoOptions: MonacoType.editor.IEditorOptions;
id: EditorId;
options?: Partial<MonacoType.editor.IEditorConstructionOptions>;
editorDidMount?: (editor: MonacoType.editor.IStandaloneCodeEditor) => void;
@@ -68,7 +68,7 @@ export class Editor extends React.Component<EditorProps> {
* Initialize Monaco.
*/
public async initMonaco() {
const { monaco, monoacoOptions } = this.props;
const { monaco, monacoOptions: monacoOptions } = this.props;
const ref = this.containerRef.current;
if (ref) {
@@ -77,7 +77,7 @@ export class Editor extends React.Component<EditorProps> {
theme: 'main',
contextmenu: false,
model: null,
...monoacoOptions
...monacoOptions
});
await this.editorDidMount(this.editor);
@@ -121,6 +121,9 @@ export class Editor extends React.Component<EditorProps> {
} else {
const value = await getContent(id, version);
const model = monaco.editor.createModel(value, this.language);
model.updateOptions({
tabSize: 2
});
this.editor.setModel(model);
}
+8 -13
View File
@@ -30,8 +30,9 @@ const defaultMonacoOptions: MonacoType.editor.IEditorOptions = {
export const TITLE_MAP: Record<MosaicId, string> = {
main: 'Main Process (main.js)',
renderer: 'Renderer Process (renderer.js)',
preload: 'Preload (preload.js)',
html: 'HTML (index.html)',
docsDemo: 'Docs & Demos'
docsDemo: 'Docs & Demos',
};
export interface EditorsProps {
@@ -79,17 +80,11 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
ipcRendererManager.on(IpcEvents.FS_NEW_FIDDLE, async (_event) => {
const { version } = this.props.appState;
this.props.appState.setWarningDialogTexts({
label: 'Your current fiddle is unsaved. Do you want to discard it?'
});
window.ElectronFiddle.app.fileManager.setFiddle({
values: {
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
}
});
await window.ElectronFiddle.app.replaceFiddle({
html: await getContent(EditorId.html, version),
renderer: await getContent(EditorId.renderer, version),
main: await getContent(EditorId.main, version),
}, {});
});
ipcRendererManager.on(IpcEvents.MONACO_TOGGLE_OPTION, (_event, cmd: string) => {
@@ -241,7 +236,7 @@ export class Editors extends React.Component<EditorsProps, EditorsState> {
id={id}
monaco={monaco!}
appState={appState}
monoacoOptions={defaultMonacoOptions}
monacoOptions={defaultMonacoOptions}
/>
);
}
-2
View File
@@ -3,7 +3,6 @@ import * as React from 'react';
import { AppState } from '../state';
import { ChromeMac } from './chrome-mac';
import { Commands } from './commands';
import { Output } from './output';
import { WelcomeTour } from './tour-welcome';
export interface HeaderProps {
@@ -23,7 +22,6 @@ export class Header extends React.Component<HeaderProps, {}> {
<ChromeMac appState={this.props.appState} />
<header id='header'>
<Commands key='commands' appState={this.props.appState} />
<Output key='output' appState={this.props.appState} />
</header>
<WelcomeTour appState={this.props.appState} />
</>
@@ -0,0 +1,59 @@
import * as React from 'react';
import { Mosaic, MosaicNode } from 'react-mosaic-component';
import { AppState } from '../state';
import { Editors } from './editors';
import { Output } from './output';
export interface WrapperProps {
appState: AppState;
}
export interface WrapperState {
mosaicArrangement: MosaicNode<WrapperMosaicId>;
}
export type WrapperMosaicId = 'output' | 'editors';
export class OutputEditorsWrapper extends React.Component<WrapperProps, WrapperState> {
private MOSAIC_ELEMENTS = {
output: <Output appState={this.props.appState} />,
editors: <Editors appState={this.props.appState} />
};
constructor(props: any) {
super(props);
this.state = {
mosaicArrangement: {
direction: 'column',
first: 'output',
second: 'editors',
splitPercentage: 25,
}
};
}
public render() {
return (
<>
<Mosaic<WrapperMosaicId>
renderTile={(id: string) => this.MOSAIC_ELEMENTS[id]}
resize={{minimumPaneSizePercentage: 0}}
value={this.state.mosaicArrangement}
onChange={this.onChange}
/>
</>
);
}
private onChange = (currentNode: any) => {
const isConsoleShowing = currentNode.splitPercentage !== 0;
if (isConsoleShowing !== this.props.appState.isConsoleShowing) {
this.props.appState.isConsoleShowing = isConsoleShowing;
}
this.setState({mosaicArrangement: currentNode});
}
}
+30 -4
View File
@@ -1,8 +1,12 @@
import { autorun } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MosaicContext } from 'react-mosaic-component';
import { OutputEntry } from '../../interfaces';
import { AppState } from '../state';
import { WrapperMosaicId } from './output-editors-wrapper';
export interface CommandsProps {
appState: AppState;
@@ -19,6 +23,8 @@ export interface CommandsProps {
*/
@observer
export class Output extends React.Component<CommandsProps, {}> {
public static contextType = MosaicContext;
public context: MosaicContext<WrapperMosaicId>;
private outputRef = React.createRef<HTMLDivElement>();
constructor(props: CommandsProps) {
@@ -28,6 +34,23 @@ export class Output extends React.Component<CommandsProps, {}> {
this.renderEntry = this.renderEntry.bind(this);
}
public componentDidMount() {
autorun(() => {
const { isConsoleShowing } = this.props.appState;
// this context should always exist, but mocking context in enzyme
// is not fully supported, so this condition makes the tests pass
if (this.context.mosaicActions && this.context.mosaicActions.expand) {
if (!isConsoleShowing) {
this.context.mosaicActions.expand(['first'], 0);
} else {
this.context.mosaicActions.expand(['first'], 25);
}
}
});
}
public componentDidUpdate() {
if (this.outputRef.current) {
this.outputRef.current.scrollTop = this.outputRef.current.scrollHeight;
@@ -71,9 +94,12 @@ export class Output extends React.Component<CommandsProps, {}> {
));
}
public render() {
const { isConsoleShowing, output } = this.props.appState;
const className = isConsoleShowing ? 'output showing' : 'output';
public render(): JSX.Element | null {
const { output, isConsoleShowing } = this.props.appState;
if (!isConsoleShowing) {
return null;
}
// The last 1000 lines
const lines = output
@@ -81,7 +107,7 @@ export class Output extends React.Component<CommandsProps, {}> {
.map(this.renderEntry);
return (
<div className={className} ref={this.outputRef}>
<div className='output' ref={this.outputRef}>
{lines}
</div>
);
@@ -63,7 +63,7 @@ export class ExecutionSettings extends React.Component<ExecutionSettingsProps, {
return (
<div>
<h2>Exection</h2>
<h2>Execution</h2>
<Callout>
These advanced settings control how Electron Fiddle executes your fiddles.
</Callout>
@@ -76,6 +76,7 @@ export class AppearanceSettings extends React.Component<
this.handleChange = this.handleChange.bind(this);
this.openThemeFolder = this.openThemeFolder.bind(this);
this.handleAddTheme = this.handleAddTheme.bind(this);
this.state = {
themes: []
@@ -111,8 +112,9 @@ export class AppearanceSettings extends React.Component<
* @memberof AppearanceSettings
*/
public async createNewThemeFromCurrent(): Promise<boolean> {
const { appState } = this.props;
const fs = await fancyImport<typeof fsType>('fs-extra');
const theme = await getTheme(this.props.appState.theme);
const theme = await getTheme(appState.theme);
try {
const namor = await fancyImport<any>('namor');
@@ -128,6 +130,8 @@ export class AppearanceSettings extends React.Component<
shell.showItemInFolder(themePath);
this.setState({themes: await getAvailableThemes()});
return true;
} catch (error) {
console.warn(`Themes: Failed to create new theme from current`, error);
@@ -190,13 +194,29 @@ export class AppearanceSettings extends React.Component<
</a>. The easiest way to get started is to clone one of the two existing
themes and to add your own colors.
</p>
<p>
Additionally, if you wish to import a Monaco Editor theme, pick your JSON file and Fiddle will attempt to import it.
</p>
<Button
onClick={this.createNewThemeFromCurrent}
text='Create theme from current selection'
icon='duplicate'
/>
<Button
icon='document-open'
onClick={this.handleAddTheme}
text='Add a Monaco Editor theme'
/>
</Callout>
</div>
);
}
/**
* Opens the "add monaco theme" dialog
*/
public handleAddTheme(): void {
this.props.appState.toggleAddMonacoThemeDialog();
}
}
+20
View File
@@ -44,6 +44,16 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
this.state = {
section: SettingsSections.General
};
this.closeSettingsPanel = this.closeSettingsPanel.bind(this);
}
public componentDidMount() {
window.addEventListener('keyup', this.closeSettingsPanel, true);
}
public componentWillUnmount() {
window.removeEventListener('keyup', this.closeSettingsPanel);
}
/**
@@ -138,4 +148,14 @@ export class Settings extends React.Component<SettingsProps, SettingsState> {
return 'cog';
}
/**
* Trigger closing of the settings panel upon Esc
*/
private closeSettingsPanel(event: KeyboardEvent) {
const { appState } = this.props;
if (event.code === 'Escape') {
appState.isSettingsShowing = false;
}
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ export function ShowMeApp(_props: any): JSX.Element {
let result = '';
pathsToQuery.forEach((item) => {
result += `${item}: ${remote.app.getPath(item)}\n`;
result += `${item}: ${remote.app.getPath(item as any)}\n`;
});
setPaths(result);
+4 -4
View File
@@ -115,7 +115,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
a Node.js application is started. The main script runs in the "main
process". To display a user interface, the main process creates renderer
processes  usually in the form of windows, which Electron calls
<code>BrowserWindow</code>.
&nbsp;<code>BrowserWindow</code>.
</p>
<p>
To get started, pretend that the main process is just like a Node.js
@@ -137,10 +137,10 @@ export function getWelcomeTour(): Set<TourScriptStep> {
content: (
<p>
In the default fiddle, this HTML file is loaded in the
<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
&nbsp;<code>BrowserWindow</code>. Any HTML, CSS, or JavaScript that works
in a browser will work here, too. In addition, Electron allows you
to execute Node.js code. Take a close look at the
<code>&lt;script /&gt;</code> tag and notice how we can call <code>
&nbsp;<code>&lt;script /&gt;</code> tag and notice how we can call <code>
require()</code> like we would in Node.js.
</p>
)
@@ -157,7 +157,7 @@ export function getWelcomeTour(): Set<TourScriptStep> {
</p>
<p>
By the way: If you want to use an <code>npm</code> module here, just
<code>require</code> it. Electron Fiddle will automatically detect that you
&nbsp;<code>require</code> it. Electron Fiddle will automatically detect that you
requested a module and install it as soon as you run your fiddle.
</p>
</>
+3
View File
@@ -18,3 +18,6 @@ export const DEFAULT_MOSAIC_ARRANGEMENT: MosaicNode<MosaicId> = {
second: EditorId.html,
}
};
export const ELECTRON_ORG = 'electron';
export const ELECTRON_REPO = 'electron';
+2 -2
View File
@@ -22,7 +22,7 @@ export async function getContent(
if (name === EditorId.main) {
// We currently only distinguish between loadFile
// and loadURL. Todo: Properly version the quick-start.
// and loadURL. TODO: Properly version the quick-start.
if (version && version.startsWith('1.')) {
return (await import('../content/main-1-x-x')).main;
}
@@ -42,7 +42,7 @@ export async function getContent(
export async function isContentUnchanged(name: EditorId): Promise<boolean> {
if (!window.ElectronFiddle || !window.ElectronFiddle.app) return false;
const values = await window.ElectronFiddle.app.getValues({ include: false });
const values = await window.ElectronFiddle.app.getEditorValues({ include: false });
// Handle main case, which needs to check both possible versions
if (name === EditorId.main) {
+58 -21
View File
@@ -2,28 +2,38 @@ import * as fsType from 'fs-extra';
import * as MonacoType from 'monaco-editor';
import * as path from 'path';
import { ElectronVersion, ElectronVersionSource } from '../interfaces';
import { callIn } from '../utils/call-in';
import { fancyImport } from '../utils/import';
import { USER_DATA_PATH } from './constants';
const definitionPath = path.join(USER_DATA_PATH, 'electron-typedef');
/**
* Fetch TypeScript definitions for the current version of Electron (online)
*
* @param {string} version
* @returns {Promise<string>}
*/
export function fetchTypeDefinitions(version: string): Promise<string> {
export async function fetchTypeDefinitions(version: string): Promise<string> {
const url = `https://unpkg.com/electron@${version}/electron.d.ts`;
return window.fetch(url)
.then((response) => response.text())
.catch((error) => {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
});
let text: string;
try {
const response = await window.fetch(url);
text = await response.text();
} catch (error) {
console.warn(`Fetch Types: Could not fetch definitions`, error);
return '';
}
// for invalid packa
if (text.includes('Cannot find package')) {
console.warn(`Fetch Types: ${text}`);
return '';
} else {
return text;
}
}
/**
@@ -48,27 +58,25 @@ export async function getOfflineTypeDefinitions(version: string): Promise<boolea
}
/**
* Get TypeScript defintions for a version of Electron. If none can't be
* Get TypeScript definitions for a version of Electron. If none can be
* found, returns null.
*
* @param {string} version
* @returns {void}
*/
export async function getTypeDefinitions(version: string): Promise<string | null> {
export async function getDownloadedVersionTypeDefs(version: ElectronVersion): Promise<string | null> {
const fs = await fancyImport<typeof fsType>('fs-extra');
await fs.mkdirp(definitionPath);
await fs.mkdirp(definitionPath);
const offlinePath = getOfflineTypeDefinitionPath(version.version);
const offlinePath = getOfflineTypeDefinitionPath(version);
if (await getOfflineTypeDefinitions(version)) {
if (await getOfflineTypeDefinitions(version.version)) {
try {
return await fs.readFile(offlinePath, 'utf-8');
} catch (error) {
return null;
}
} else {
const typeDefs = await fetchTypeDefinitions(version);
const typeDefs = await fetchTypeDefinitions(version.version);
if (typeDefs && typeDefs.length > 0) {
try {
await fs.outputFile(offlinePath, typeDefs);
@@ -79,17 +87,27 @@ export async function getTypeDefinitions(version: string): Promise<string | null
return typeDefs;
}
}
return null;
}
}
export async function getLocalVersionTypeDefs(version: ElectronVersion) {
if (version.source === ElectronVersionSource.local && !!version.localPath) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typesPath = getLocalTypePathForVersion(version);
if (!!typesPath && fs.existsSync(typesPath)) {
return fs.readFile(typesPath, 'utf-8');
}
}
return null;
}
/**
* Tries to update the editor with type definitions.
*
* @param {string} version
*/
export async function updateEditorTypeDefinitions(version: string, i: number = 0): Promise<void> {
export async function updateEditorTypeDefinitions(version: ElectronVersion, i: number = 0): Promise<void> {
const defer = async (): Promise<void> => {
if (i > 10) {
console.warn(`Fetch Types: Failed, dependencies do not exist`);
@@ -106,17 +124,36 @@ export async function updateEditorTypeDefinitions(version: string, i: number = 0
const { app } = window.ElectronFiddle;
const monaco: typeof MonacoType = app.monaco!;
const typeDefDisposable: MonacoType.IDisposable = app.typeDefDisposable!;
const typeDefs = await getTypeDefinitions(version);
const getTypeDefs = (version.source === ElectronVersionSource.local) ?
getLocalVersionTypeDefs : getDownloadedVersionTypeDefs;
const typeDefs = await getTypeDefs(version);
if (typeDefDisposable) {
typeDefDisposable.dispose();
}
if (typeDefs) {
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version}`);
console.log(`Fetch Types: Updating Monaco types with electron.d.ts@${version.version}`);
const disposable = monaco.languages.typescript.javascriptDefaults.addExtraLib(typeDefs);
window.ElectronFiddle.app.typeDefDisposable = disposable;
} else {
console.log(`Fetch Types: No type definitions for ${version} 😢`);
console.log(`Fetch Types: No type definitions for ${version.version} 😢`);
}
}
export function getLocalTypePathForVersion(version: ElectronVersion) {
if (version.localPath) {
return path.join(
version.localPath,
'gen',
'electron',
'tsc',
'typings',
'electron.d.ts'
);
} else {
return null;
}
}
+38 -32
View File
@@ -1,11 +1,10 @@
import * as fsType from 'fs-extra';
import * as path from 'path';
import { EditorValues, Files, FileTransform, SetFiddleOptions } from '../interfaces';
import { EditorValues, Files, FileTransform } from '../interfaces';
import { IpcEvents } from '../ipc-events';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PACKAGE_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { DEFAULT_OPTIONS, PackageJsonOptions } from '../utils/get-package';
import { getTitle } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { ipcRendererManager } from './ipc';
import { AppState } from './state';
@@ -38,13 +37,12 @@ export class FileManager {
/**
* Opens a template.
*
* @param {string} name
* @param {string} templateName
* @memberof FileManager
*/
public async openTemplate(name: string) {
const values = await getTemplateValues(name);
this.setFiddle({ values, templateName: name });
public async openTemplate(templateName: string) {
const editorValues = await getTemplateValues(templateName);
await window.ElectronFiddle.app.replaceFiddle(editorValues, {templateName});
}
/**
@@ -60,34 +58,15 @@ export class FileManager {
// We'll do our best and will likely have to
// rewrite this once we support multiple files
const values: EditorValues = {
const editorValues: EditorValues = {
html: await this.readFile(path.join(filePath, INDEX_HTML_NAME)),
main: await this.readFile(path.join(filePath, MAIN_JS_NAME)),
renderer: await this.readFile(path.join(filePath, RENDERER_JS_NAME)),
preload: await this.readFile(path.join(filePath, PRELOAD_JS_NAME)),
};
this.setFiddle({ values, filePath });
}
/**
* Got values? This method will load them.
*
* @param {SetFiddleOptions} options
* @memberof FileManager
*/
public async setFiddle({ values, filePath, templateName }: SetFiddleOptions) {
this.appState.gistId = '';
this.appState.isMyGist = false;
this.appState.localPath = filePath;
this.appState.templateName = templateName;
this.appState.setWarningDialogTexts({
label: `Opening this fiddle will replace your unsaved changes. Do you want to proceed?`,
ok: 'Yes'
});
await window.ElectronFiddle.app.setValues(values);
document.title = getTitle(this.appState);
window.ElectronFiddle.app.replaceFiddle(editorValues, {filePath});
}
/**
@@ -109,7 +88,15 @@ export class FileManager {
const files = await this.getFiles(undefined, ...transforms);
for (const [fileName, content] of files) {
await this.saveFile(path.join(pathToSave, fileName), content);
const savePath = path.join(pathToSave, fileName);
// If the file has content, save it to disk. If there's no
// content in the file, remove a file that possibly exists.
if (content) {
await this.saveFile(savePath, content);
} else {
await this.removeFile(savePath);
}
}
if (pathToSave !== localPath) {
@@ -132,12 +119,13 @@ export class FileManager {
*/
public async getFiles(options?: PackageJsonOptions, ...transforms: Array<FileTransform>): Promise<Files> {
const pOptions = typeof options === 'object' ? options : DEFAULT_OPTIONS;
const values = await window.ElectronFiddle.app.getValues(pOptions);
const values = await window.ElectronFiddle.app.getEditorValues(pOptions);
let output: Files = new Map();
output.set(RENDERER_JS_NAME, values.renderer);
output.set(MAIN_JS_NAME, values.main);
output.set(INDEX_HTML_NAME, values.html);
output.set(PRELOAD_JS_NAME, values.preload);
output.set(PACKAGE_NAME, values.package!);
for (const transform of transforms) {
@@ -240,4 +228,22 @@ export class FileManager {
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
/**
* Safely attempts to remove a file, doesn't crash the app if
* it fails.
*
* @param {string} filePath
* @returns {string}
* @memberof FileManager
*/
private async removeFile(filePath: string): Promise<void> {
try {
const fs = await fancyImport<typeof fsType>('fs-extra');
return await fs.remove(filePath);
} catch (error) {
console.log(`FileManager: Could not remove ${filePath}`, error);
ipcRendererManager.send(IpcEvents.FS_SAVE_FIDDLE_ERROR, [filePath]);
}
}
}
+6 -3
View File
@@ -16,7 +16,7 @@ const ignoredModules: Array<string> = [
];
/* regular expression to both match and extract module names */
const requiregx = /require\(['"](.*?)['"]\)/gm;
const requiregx = /^.*require\(['"](.*?)['"]\)/gm;
/*
@@ -91,8 +91,11 @@ export function findModules(input: string): Array<string> {
/* grab all global require matches in the text */
// tslint:disable-next-line:no-conditional-assignment
while (match = (requiregx.exec(input) || null)) {
const mod = match[1];
modules.push(mod);
// ensure commented-out requires aren't downloaded
if (!match[0].startsWith('//')) {
const mod = match[1];
modules.push(mod);
}
}
/* map and reduce */
+259
View File
@@ -0,0 +1,259 @@
import * as Octokit from '@octokit/rest';
import { when } from 'mobx';
import { EditorId, EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { getOctokit } from '../utils/octokit';
import { sortedElectronMap } from '../utils/sorted-electron-map';
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
import { getContent } from './content';
import { AppState } from './state';
import { ElectronReleaseChannel, getReleaseChannel } from './versions';
export class RemoteLoader {
constructor(private readonly appState: AppState) {
this.loadFiddleFromElectronExample.bind(this);
this.loadFiddleFromGist.bind(this);
this.verifyRemoteLoad.bind(this);
this.verifyReleaseChannelEnabled.bind(this);
this.fetchExampleAndLoad.bind(this);
this.fetchGistAndLoad.bind(this);
this.setElectronVersionWithRef.bind(this);
this.getPackageVersionFromRef.bind(this);
this.handleLoadingSuccess.bind(this);
this.handleLoadingFailed.bind(this);
}
public async loadFiddleFromElectronExample(_: any, exampleInfo: { path: string; ref: string }) {
console.log(`Loading fiddle from Electron example`, _, exampleInfo);
const ok = await this.verifyRemoteLoad('example from the Electron docs', exampleInfo.ref);
if (!ok) return;
this.fetchExampleAndLoad(exampleInfo.ref, exampleInfo.path);
}
public async loadFiddleFromGist(_: any, gistInfo: { id: string }) {
const ok = await this.verifyRemoteLoad('gist');
if (!ok) return;
this.fetchGistAndLoad(gistInfo.id);
}
public async fetchExampleAndLoad(ref: string, path: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
const folder = await octo.repos.getContents({
owner: ELECTRON_REPO,
repo: ELECTRON_ORG,
ref,
path,
});
const ok = await this.setElectronVersionWithRef(ref);
if (!ok) return false;
const values = {
html: await getContent(EditorId.html, this.appState.version),
renderer: await getContent(EditorId.renderer, this.appState.version),
main: await getContent(EditorId.main, this.appState.version),
preload: await getContent(EditorId.preload, this.appState.version),
};
const loaders: Array<Promise<void>> = [];
if (!Array.isArray(folder.data)) {
throw new Error('The example Fiddle tried to launch is not a valid Electron example');
}
for (const child of folder.data) {
if (!child.download_url) {
console.warn(`Could not find download_url for ${child.name}`);
continue;
}
switch (child.name) {
case MAIN_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.main = t; })
);
break;
case INDEX_HTML_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.html = t; })
);
break;
case RENDERER_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.renderer = t; })
);
break;
case PRELOAD_JS_NAME:
loaders.push(fetch(child.download_url)
.then((r) => r.text()).then((t) => { values.preload = t; })
);
break;
default:
break;
}
}
await Promise.all(loaders);
return this.handleLoadingSuccess(values, '');
} catch (error) {
return this.handleLoadingFailed(error);
}
}
/**
* Get data from a gist. If it doesn't exist, return an empty string.
*
* @param {Octokit.Response<Octokit.GistsGetResponse>} gist
* @param {string} name
* @returns {string}
* @memberof RemoteLoader
*/
public getContentOrEmpty(gist: Octokit.Response<Octokit.GistsGetResponse>, name: string): string {
try {
return gist.data.files[name].content;
} catch (error) {
return '';
}
}
/**
* Load a fiddle
*
* @returns {Promise<boolean>}
* @memberof RemoteLoader
*/
public async fetchGistAndLoad(gistId: string): Promise<boolean> {
try {
const octo = await getOctokit(this.appState);
const gist = await octo.gists.get({ gist_id: gistId });
return this.handleLoadingSuccess({
html: this.getContentOrEmpty(gist, INDEX_HTML_NAME),
main: this.getContentOrEmpty(gist, MAIN_JS_NAME),
renderer: this.getContentOrEmpty(gist, RENDERER_JS_NAME),
preload: this.getContentOrEmpty(gist, PRELOAD_JS_NAME)
}, gistId);
} catch (error) {
return this.handleLoadingFailed(error);
}
}
public async setElectronVersionWithRef(ref: string): Promise<boolean> {
const version = await this.getPackageVersionFromRef(ref);
const supportedVersions = sortedElectronMap(this.appState.versions, (k) => k);
if (!supportedVersions.includes(version)) {
this.handleLoadingFailed(new Error('Version of Electron in example not supported'));
return false;
}
// check if version is part of release channel
const versionReleaseChannel: ElectronReleaseChannel = getReleaseChannel(version);
if (!this.appState.versionsToShow.includes(versionReleaseChannel)) {
const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
if (!ok) return false;
this.appState.versionsToShow.push(versionReleaseChannel);
}
this.appState.setVersion(version);
return true;
}
public async getPackageVersionFromRef(ref: string): Promise<string> {
const octo = await getOctokit(this.appState);
const { data: packageJsonData } = await octo.repos.getContents({
owner: ELECTRON_ORG,
repo: ELECTRON_REPO,
ref,
path: 'package.json'
});
if (!Array.isArray(packageJsonData) && !!packageJsonData.content) {
const packageJsonString = Buffer.from(packageJsonData.content, 'base64').toString('utf8');
const { version } = JSON.parse(packageJsonString);
return version;
} else {
console.error(`getPackageVersionFromRef: Received unexpected response from GitHub, could not parse version`, {
packageJsonData
});
return '0.0.0';
}
}
/**
* Verifies from the user that we should be loading this fiddle
*
* @param what What are we loading from (gist, example, etc.)
*/
public async verifyRemoteLoad(what: string, fiddlePath?: string): Promise<boolean> {
this.appState.setConfirmationPromptTexts({
label: `Are you sure you sure you want to load this '${what}' from fiddle path '${fiddlePath}'? Only load and run it if you trust the source.`
});
this.appState.isConfirmationPromptShowing = true;
await when(() => !this.appState.isConfirmationPromptShowing);
return !!this.appState.confirmationPromptLastResult;
}
public async verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
this.appState.setWarningDialogTexts({
label: `You're loading an example with a version of Electron with an unincluded release
channel (${channel}). Do you want to enable the release channel to load the
version of Electron from the example?`
});
this.appState.isWarningDialogShowing = true;
await when(() => !this.appState.isWarningDialogShowing);
return !!this.appState.warningDialogLastResult;
}
/**
* Loading a fiddle from GitHub succeeded, let's move on.
*
* @param {Partial<EditorValues>} values
* @param {string} gistId
* @returns {boolean}
*/
private async handleLoadingSuccess(values: Partial<EditorValues>, gistId: string): Promise<boolean> {
await window.ElectronFiddle.app.replaceFiddle(values, {gistId});
return true;
}
/**
* Loading a fiddle from GitHub failed - this method handles this case
* gracefully.
*
* @param {Error} error
* @returns {boolean}
*/
private handleLoadingFailed(error: Error): false {
if (navigator.onLine) {
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed: ${error}`,
cancel: undefined
});
} else {
this.appState.setWarningDialogTexts({
label: `Loading the fiddle failed. Your computer seems to be offline. Error: ${error}`,
cancel: undefined
});
}
this.appState.toggleWarningDialog();
console.warn(`Loading Fiddle failed`, error);
return false;
}
}
+4 -4
View File
@@ -37,7 +37,7 @@ export class Runner {
* @returns {Promise<boolean>}
*/
public async run(): Promise<boolean> {
const { fileManager, getValues } = window.ElectronFiddle.app;
const { fileManager, getEditorValues } = window.ElectronFiddle.app;
const options = { includeDependencies: false, includeElectron: false };
const { binaryManager, currentElectronVersion } = this.appState;
const { version, localPath } = currentElectronVersion;
@@ -47,7 +47,7 @@ export class Runner {
}
this.appState.isConsoleShowing = true;
const values = await getValues(options);
const values = await getEditorValues(options);
const dir = await this.saveToTemp(options);
if (!dir) return false;
@@ -200,8 +200,8 @@ export class Runner {
this.appState.isRunning = true;
pushOutput(`Electron v${version} started.`);
this.child.stdout.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stdout!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.stderr!.on('data', (data) => pushOutput(data, { bypassBuffer: false }));
this.child.on('close', async (code) => {
const withCode = typeof code === 'number'
? ` with code ${code.toString()}.`
+103 -40
View File
@@ -1,3 +1,4 @@
import * as fsType from 'fs-extra';
import { action, autorun, computed, observable, when } from 'mobx';
import { MosaicNode } from 'react-mosaic-component';
@@ -19,14 +20,17 @@ import { arrayToStringMap } from '../utils/array-to-stringmap';
import { EditorBackup, getEditorBackup } from '../utils/editor-backup';
import { createMosaicArrangement, getVisibleMosaics } from '../utils/editors-mosaic-arrangement';
import { getName } from '../utils/get-title';
import { fancyImport } from '../utils/import';
import { normalizeVersion } from '../utils/normalize-version';
import { isEditorBackup, isEditorId, isPanelId } from '../utils/type-checks';
import { BinaryManager } from './binary';
import { Bisector } from './bisect';
import { DEFAULT_MOSAIC_ARRANGEMENT } from './constants';
import { getContent, isContentUnchanged } from './content';
import { updateEditorTypeDefinitions } from './fetch-types';
import { getLocalTypePathForVersion, updateEditorTypeDefinitions } from './fetch-types';
import { ipcRendererManager } from './ipc';
import { activateTheme } from './themes';
import {
addLocalVersion,
ElectronReleaseChannel,
@@ -63,7 +67,6 @@ export class AppState {
// -- Persisted settings ------------------
@observable public version: string = defaultVersion;
@observable public theme: string | null = localStorage.getItem('theme');
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
@observable public gitHubAvatarUrl: string | null = localStorage.getItem('gitHubAvatarUrl');
@observable public gitHubName: string | null = localStorage.getItem('gitHubName');
@observable public gitHubLogin: string | null = localStorage.getItem('gitHubLogin');
@@ -71,37 +74,45 @@ export class AppState {
@observable public gitHubPublishAsPublic: boolean = !!this.retrieve('gitHubPublishAsPublic');
@observable public versionsToShow: Array<ElectronReleaseChannel> =
this.retrieve('versionsToShow') as Array<ElectronReleaseChannel>
|| [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta ];
|| [ElectronReleaseChannel.stable, ElectronReleaseChannel.beta];
@observable public statesToShow: Array<ElectronVersionState> =
this.retrieve('statesToShow') as Array<ElectronVersionState>
|| [ ElectronVersionState.downloading, ElectronVersionState.ready, ElectronVersionState.unknown ];
this.retrieve('statesToShow') as Array<ElectronVersionState>
|| [ElectronVersionState.downloading, ElectronVersionState.ready, ElectronVersionState.unknown];
@observable public isKeepingUserDataDirs: boolean = !!this.retrieve('isKeepingUserDataDirs');
@observable public isEnablingElectronLogging: boolean = !!this.retrieve('isEnablingElectronLogging');
@observable public binaryManager: BinaryManager = new BinaryManager();
@observable public isClearingConsoleOnRun: boolean = !!this.retrieve('isClearingConsoleOnRun');
// -- Various session-only state ------------------
@observable public gistId: string = '';
@observable public isMyGist: boolean = false;
@observable public isPublishing: boolean = false;
@observable public versions: Record<string, ElectronVersion> = arrayToStringMap(knownVersions);
@observable public output: Array<OutputEntry> = [];
@observable public localPath: string | undefined;
@observable public isUpdatingElectronVersions = false;
@observable public warningDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public confirmationDialogTexts = { label: '', ok: 'Okay', cancel: 'Cancel' };
@observable public warningDialogLastResult: boolean | null = null;
@observable public isRunning = false;
@observable public confirmationPromptLastResult: boolean | null = null;
@observable public mosaicArrangement: MosaicNode<MosaicId> | null = DEFAULT_MOSAIC_ARRANGEMENT;
@observable public templateName: string | undefined;
@observable public currentDocsDemoPage: DocsDemoPage = DocsDemoPage.DEFAULT;
@observable public localTypeWatcher: fsType.FSWatcher | undefined;
@observable public binaryManager: BinaryManager = new BinaryManager();
@observable public Bisector: Bisector | undefined;
@observable public isPublishing: boolean = false;
@observable public isRunning: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isUpdatingElectronVersions: boolean = false;
// -- Various "isShowing" settings ------------------
@observable public isBisectCommandShowing: boolean;
@observable public isConsoleShowing: boolean = false;
@observable public isTokenDialogShowing: boolean = false;
@observable public isWarningDialogShowing: boolean = false;
@observable public isConfirmationPromptShowing: boolean = false;
@observable public isSettingsShowing: boolean = false;
@observable public isUnsaved: boolean = false;
@observable public isBisectDialogShowing: boolean = false;
@observable public isAddVersionDialogShowing: boolean = false;
@observable public isThemeDialogShowing: boolean = false;
@observable public isTourShowing: boolean = !localStorage.getItem('hasShownTour');
// -- Editor Values stored when we close the editor ------------------
@@ -121,15 +132,18 @@ export class AppState {
this.setVersion = this.setVersion.bind(this);
this.showTour = this.showTour.bind(this);
this.signOutGitHub = this.signOutGitHub.bind(this);
this.toggleBisectCommands = this.toggleBisectCommands.bind(this);
this.toggleAuthDialog = this.toggleAuthDialog.bind(this);
this.toggleConsole = this.toggleConsole.bind(this);
this.clearConsole = this.clearConsole.bind(this);
this.toggleSettings = this.toggleSettings.bind(this);
this.toggleBisectDialog = this.toggleBisectDialog.bind(this);
this.updateElectronVersions = this.updateElectronVersions.bind(this);
ipcRendererManager.on(IpcEvents.OPEN_SETTINGS, this.toggleSettings);
ipcRendererManager.on(IpcEvents.SHOW_WELCOME_TOUR, this.showTour);
ipcRendererManager.on(IpcEvents.CLEAR_CONSOLE, this.clearConsole);
ipcRendererManager.on(IpcEvents.BISECT_COMMANDS_TOGGLE, this.toggleBisectCommands);
// Setup auto-runs
autorun(() => this.save('theme', this.theme));
@@ -235,15 +249,26 @@ export class AppState {
this.output = [];
}
@action public toggleBisectCommands() {
// guard against hiding the commands when executing a bisect
if (!this.Bisector && !this.isBisectDialogShowing) {
this.isBisectCommandShowing = !this.isBisectCommandShowing;
}
}
@action public toggleAddVersionDialog() {
this.isAddVersionDialogShowing = !this.isAddVersionDialogShowing;
}
@action public toggleAddMonacoThemeDialog() {
this.isThemeDialogShowing = !this.isThemeDialogShowing;
}
@action public toggleAuthDialog() {
this.isTokenDialogShowing = !this.isTokenDialogShowing;
}
@action public toogleWarningDialog() {
@action public toggleWarningDialog() {
this.isWarningDialogShowing = !this.isWarningDialogShowing;
if (this.isWarningDialogShowing) {
@@ -251,6 +276,18 @@ export class AppState {
}
}
@action public toggleBisectDialog() {
this.isBisectDialogShowing = !this.isBisectDialogShowing;
}
@action public toggleConfirmationPromptDialog() {
this.isConfirmationPromptShowing = !this.isConfirmationPromptShowing;
if (this.isConfirmationPromptShowing) {
this.confirmationPromptLastResult = null;
}
}
@action public toggleSettings() {
// We usually don't lose editor focus,
// so you can still type. Let's force-blur.
@@ -284,6 +321,14 @@ export class AppState {
};
}
@action public setConfirmationPromptTexts(input: WarningDialogTexts) {
this.confirmationDialogTexts = {
ok: 'Okay',
cancel: 'Cancel',
...input
};
}
@action public addLocalVersion(input: NpmVersion) {
addLocalVersion(input);
@@ -291,12 +336,12 @@ export class AppState {
this.updateDownloadedVersionState();
}
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Remove a version of Electron
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async removeVersion(input: string) {
const version = normalizeVersion(input);
const release = this.versions[version];
@@ -330,12 +375,12 @@ export class AppState {
this.updateDownloadedVersionState();
}
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Download a version of Electron.
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async downloadVersion(input: string) {
const version = normalizeVersion(input);
console.log(`State: Downloading Electron ${version}`);
@@ -359,12 +404,12 @@ export class AppState {
}
}
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
/**
* Select a version of Electron (and download it if necessary).
*
* @param {string} input
* @returns {Promise<void>}
*/
@action public async setVersion(input: string) {
const version = normalizeVersion(input);
@@ -382,21 +427,38 @@ export class AppState {
// Should we update the editor?
if (await isContentUnchanged(EditorId.main)) {
const main = await getContent(EditorId.main, version);
window.ElectronFiddle.app.setValues({ main }, false);
window.ElectronFiddle.app.setEditorValues({ main });
}
// Update TypeScript definitions
updateEditorTypeDefinitions(version);
const versionObject = this.versions[version];
if (versionObject.source === ElectronVersionSource.local) {
const fs = await fancyImport<typeof fsType>('fs-extra');
const typePath = getLocalTypePathForVersion(versionObject);
console.info(`TypeDefs: Watching file for local version ${version} at path ${typePath}`);
this.localTypeWatcher = fs.watch(typePath!, async () => {
console.info(`TypeDefs: Noticed file change at ${typePath}. Updating editor typedefs.`);
await updateEditorTypeDefinitions(versionObject);
});
} else {
if (!!this.localTypeWatcher) {
console.info(`TypeDefs: Switched to downloaded version ${version}. Unwatching local typedefs.`);
this.localTypeWatcher.close();
this.localTypeWatcher = undefined;
}
}
await updateEditorTypeDefinitions(versionObject);
// Fetch new binaries, maybe?
await this.downloadVersion(version);
}
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
/**
* Go and check which versions have already been downloaded.
*
* @returns {Promise<void>}
*/
@action public async updateDownloadedVersionState(): Promise<void> {
const downloadedVersions = await this.binaryManager.getDownloadedVersions();
const updatedVersions = { ...this.versions };
@@ -435,7 +497,7 @@ export class AppState {
let strData = data.toString();
const { isNotPre, bypassBuffer } = options;
// Todo: This drops the first part of the buffer... is that fully expected?
// TODO: This drops the first part of the buffer... is that fully expected?
if (process.platform === 'win32' && bypassBuffer === false) {
this.outputBuffer += strData;
strData = this.outputBuffer;
@@ -533,7 +595,7 @@ export class AppState {
*/
@action public showMosaic(id: MosaicId) {
const currentlyVisible = getVisibleMosaics(this.mosaicArrangement);
this.setVisibleMosaics([ ...currentlyVisible, id ]);
this.setVisibleMosaics([...currentlyVisible, id]);
}
/**
@@ -548,6 +610,7 @@ export class AppState {
this.isTourShowing = false;
this.isConsoleShowing = false;
this.isAddVersionDialogShowing = false;
this.isThemeDialogShowing = false;
if (additionalOptions) {
for (const key in additionalOptions) {
+4 -4
View File
@@ -2,12 +2,11 @@ import * as fsExtraType from 'fs-extra';
import * as pathType from 'path';
import { EditorValues } from '../interfaces';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { INDEX_HTML_NAME, MAIN_JS_NAME, PRELOAD_JS_NAME, RENDERER_JS_NAME } from '../shared-constants';
import { fancyImport } from '../utils/import';
/**
* Returns expected content for a given name. Currently synchronous,
* but probably shouldn't be.
* Returns expected content for a given name.
*
* @param {string} name
* @returns {Promise<EditorValues>}
@@ -41,6 +40,7 @@ export async function getTemplateValues(name: string): Promise<EditorValues> {
return {
renderer: await getFile(RENDERER_JS_NAME),
main: await getFile(MAIN_JS_NAME),
html: await getFile(INDEX_HTML_NAME)
html: await getFile(INDEX_HTML_NAME),
preload: await getFile(PRELOAD_JS_NAME)
};
}
+4 -3
View File
@@ -45,13 +45,14 @@ export function getDefaultVersion(
* Return the release channel for a given input
* version.
*
* @param {NpmVersion} input
* @param {NpmVersion | string} input
* @returns {ElectronReleaseChannel}
*/
export function getReleaseChannel(
input: NpmVersion
input: NpmVersion | string
): ElectronReleaseChannel {
const tag = input.version || '';
const tag = (typeof input === 'string') ? input : (input.version || '');
if (tag.includes('beta')) {
return ElectronReleaseChannel.beta;
+1
View File
@@ -1,4 +1,5 @@
export const INDEX_HTML_NAME = 'index.html';
export const MAIN_JS_NAME = 'main.js';
export const RENDERER_JS_NAME = 'renderer.js';
export const PRELOAD_JS_NAME = 'preload.js';
export const PACKAGE_NAME = 'package.json';
+5 -5
View File
@@ -3,11 +3,11 @@
* a debounced version below.
*/
function _updateEditorLayout() {
const { main, renderer, html } = window.ElectronFiddle.editors;
if (main) main.layout();
if (renderer) renderer.layout();
if (html) html.layout();
Object.keys(window.ElectronFiddle.editors).forEach((key) => {
if (window.ElectronFiddle.editors[key]) {
window.ElectronFiddle.editors[key].layout();
}
});
}
let handle: number;
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
+1 -1
View File
@@ -14,7 +14,7 @@ app.on('ready', () => {
win = null
})
let view = new BrowserView({
const view = new BrowserView({
webPreferences: {
nodeIntegration: false
}
+1 -1
View File
@@ -6,7 +6,7 @@
// the result.
//
// For more info, see:
// https://electronjs.org/docs/api/contentTracing
// https://electronjs.org/docs/api/content-tracing
const { app, contentTracing } = require('electron')
+1 -1
View File
@@ -18,6 +18,6 @@ app.on('ready', () => {
mainWindow.loadFile('index.html')
mainWindow.webContents.on('crashed', () => {
console.log(`Window crashed!`)
console.log('Window crashed!')
})
})
+10 -7
View File
@@ -12,13 +12,16 @@ app.on('ready', () => {
// Show an "Open File" dialog and attempt to open
// the chosen file in our window.
//
// If you provide a callback, the method will be asynchronous,
// but in thise case, we'll just use the synchronous variant.
const file = dialog.showOpenDialog({
title: 'Hello!',
dialog.showOpenDialog(mainWindow, {
properties: ['openFile']
}).then(result => {
if (result.canceled) {
console.log('Dialog was canceled')
} else {
const file = result.filePaths[0]
mainWindow.loadURL(`file://${file}`)
}
}).catch(err => {
console.log(err)
})
mainWindow.loadURL(`file://${file}`)
})
+3 -3
View File
@@ -22,13 +22,13 @@ app.on('ready', () => {
// CommandOrControl which represents Command on macOS and Control on
// Linux and Windows to define some accelerators.
globalShortcut.register('CommandOrControl+Y', () => {
console.log(`The global shortkey was pressed!`)
console.log('The global shortkey was pressed!')
})
// It supports "special names". Check out the API documentation for a full
// list.
globalShortcut.register('VolumeUp', () => console.log(`Turn it up!`))
globalShortcut.register('VolumeDown', () => console.log(`Turn it down!`))
globalShortcut.register('VolumeUp', () => console.log('Turn it up!'))
globalShortcut.register('VolumeDown', () => console.log('Turn it down!'))
})
app.on('will-quit', () => {
+5 -5
View File
@@ -20,21 +20,21 @@ app.on('ready', () => {
if (Notification.isSupported()) {
const notification = new Notification({
title: 'Hello World!',
subtitle: `Nice to see you`,
subtitle: 'Nice to see you',
body: 'Are you having a good day?',
hasReply: true
})
notification.on('show', () => console.log(`Notification shown`))
notification.on('click', () => console.log(`Notification clicked`))
notification.on('close', () => console.log(`Notification closed`))
notification.on('show', () => console.log('Notification shown'))
notification.on('click', () => console.log('Notification clicked'))
notification.on('close', () => console.log('Notification closed'))
notification.on('reply', (event, reply) => {
console.log(`Reply: ${reply}`)
})
notification.show()
} else {
console.log(`Hm, are notifications supported on this system?`)
console.log('Hm, are notifications supported on this system?')
}
mainWindow.loadFile('index.html')
+2 -2
View File
@@ -19,10 +19,10 @@ app.on('ready', () => {
})
powerMonitor.on('on-ac', () => {
console.log(`We're on AC power`)
console.log('We\'re on AC power')
})
powerMonitor.on('on-battery', () => {
console.log(`We're on battery power`)
console.log('We\'re on battery power')
})
})
+19 -12
View File
@@ -1,12 +1,6 @@
import { setupDevTools } from '../../src/main/devtools';
let mockIsDevMode = false;
jest.mock('../../src/utils/devmode', () => ({
get isDevMode() {
return mockIsDevMode;
}
}));
import { isDevMode } from '../../src/utils/devmode';
jest.mock('../../src/utils/devmode');
jest.mock('electron-devtools-installer', () => ({
default: jest.fn(),
@@ -17,18 +11,31 @@ jest.mock('electron-devtools-installer', () => ({
describe('devtools', () => {
it('does not set up developer tools if not in dev mode', () => {
const devtools = require('electron-devtools-installer');
(isDevMode as jest.Mock).mockReturnValue(false);
setupDevTools();
expect(devtools.default).toHaveBeenCalledTimes(0);
});
it('sets up developer tools if not in dev mode', () => {
it('sets up developer tools if in dev mode', () => {
const devtools = require('electron-devtools-installer');
mockIsDevMode = true;
(isDevMode as jest.Mock).mockReturnValue(true);
setupDevTools();
expect(devtools.default).toHaveBeenCalledTimes(1);
});
it('catch error in setting up developer tools', async (done) => {
const devtools = require('electron-devtools-installer');
// throw devtool error
devtools.default.mockRejectedValue(new Error('devtool error'));
(isDevMode as jest.Mock).mockReturnValue(true);
try {
await setupDevTools();
done();
} catch (e) {
expect(e).toMatch('error');
}
});
});
+80 -9
View File
@@ -3,25 +3,96 @@ import { setupDialogs } from '../../src/main/dialogs';
import { ipcMainManager } from '../../src/main/ipc';
import { dialog } from 'electron';
import { flushPromises } from '../utils';
jest.mock('../../src/main/windows');
describe('dialogs', () => {
it('sets up dialogs', () => {
setupDialogs();
beforeEach(() => {
setupDialogs();
});
it('sets up dialogs', () => {
expect(ipcMainManager.eventNames()).toEqual([
IpcEvents.SHOW_WARNING_DIALOG
IpcEvents.SHOW_WARNING_DIALOG,
IpcEvents.SHOW_CONFIRMATION_DIALOG,
IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG
]);
});
it('tries to show a dialog if triggered', () => {
setupDialogs();
describe('warning dialog', () => {
it('shows dialog when triggering IPC event', () => {
ipcMainManager.emit(IpcEvents.SHOW_WARNING_DIALOG, {}, { hi: 'hello' });
expect(dialog.showMessageBox).toHaveBeenCalledWith(undefined, {
type: 'warning',
hi: 'hello'
});
});
});
ipcMainManager.emit(IpcEvents.SHOW_WARNING_DIALOG, {}, { hi: 'hello' });
expect(dialog.showMessageBox).toHaveBeenCalledWith(undefined, {
type: 'warning',
hi: 'hello'
describe('confirmation dialog', () => {
it('shows dialog when triggering IPC event', () => {
ipcMainManager.emit(IpcEvents.SHOW_CONFIRMATION_DIALOG, {}, { hi: 'hello' });
expect(dialog.showMessageBox).toHaveBeenCalledWith(undefined, {
type: 'warning',
hi: 'hello'
});
});
});
describe('local version folder dialog', () => {
it('shows dialog when triggering IPC event', () => {
(dialog.showOpenDialog as jest.Mock).mockResolvedValue({
filePaths: []
});
ipcMainManager.emit(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, {
reply: jest.fn()
});
expect(dialog.showOpenDialog).toHaveBeenCalledWith(expect.objectContaining({
properties: ['openDirectory']
}));
});
it('triggers IPC load local version event', async () => {
const replyFn = jest.fn();
const paths = ['/test/path/'];
(dialog.showOpenDialog as jest.Mock).mockResolvedValue({
filePaths: paths
});
ipcMainManager.emit(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, {
reply: replyFn
});
await flushPromises();
expect(replyFn).toHaveBeenCalledWith(IpcEvents.LOAD_LOCAL_VERSION_FOLDER, paths);
});
it('does nothing if not given a path', async () => {
const replyFn = jest.fn();
// empty array
(dialog.showOpenDialog as jest.Mock).mockResolvedValue({
filePaths: []
});
ipcMainManager.emit(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, {
reply: replyFn
});
await flushPromises();
expect(replyFn).not.toHaveBeenCalled();
// nothing in response
(dialog.showOpenDialog as jest.Mock).mockResolvedValue({});
ipcMainManager.emit(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG, {
reply: replyFn
});
await flushPromises();
expect(replyFn).not.toHaveBeenCalled();
});
});
});
+29 -56
View File
@@ -15,7 +15,22 @@ jest.mock('fs-extra', () => ({
existsSync: jest.fn()
}));
const mockTarget = {
webContents: {
send: jest.fn()
}
};
describe('files', () => {
beforeEach(() => {
(dialog.showOpenDialog as jest.Mock<any>).mockResolvedValue({
filePaths: [ 'my/fake/path' ]
});
(getOrCreateMainWindow as jest.Mock<any>).mockReturnValue(mockTarget);
ipcMainManager.readyWebContents.add(mockTarget.webContents as any);
});
describe('setupFileListeners()', () => {
it('sets up the listener', () => {
@@ -31,8 +46,8 @@ describe('files', () => {
});
describe('showOpenDialog', () => {
it('tries to open an "open" dialog', () => {
showOpenDialog();
it('tries to open an "open" dialog', async () => {
await showOpenDialog();
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
@@ -44,22 +59,10 @@ describe('files', () => {
});
it('notifies the main window of the event', async () => {
showOpenDialog();
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
const cb = call[1];
const mockTarget = {
webContents: {
send: jest.fn()
}
};
(getOrCreateMainWindow as jest.Mock<any>).mockReturnValue(mockTarget);
ipcMainManager.readyWebContents.add(mockTarget.webContents as any);
await cb('/my/fake/path');
expect(mockTarget.webContents.send).toHaveBeenCalledTimes(1);
await cb('');
await showOpenDialog();
expect(mockTarget.webContents.send).toHaveBeenCalledTimes(1);
});
});
@@ -92,12 +95,11 @@ describe('files', () => {
});
it('handles not getting a path returned', async (done) => {
showSaveDialog();
(dialog.showOpenDialog as jest.Mock<any>).mockResolvedValueOnce({
filePaths: []
});
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
const cb = call[1];
await cb();
await showSaveDialog();
process.nextTick(() => {
expect(fs.existsSync).toHaveBeenCalledTimes(0);
@@ -106,68 +108,39 @@ describe('files', () => {
});
it('ensures that the target is empty on save', async () => {
showSaveDialog();
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
const cb = call[1];
const mockTarget = {
webContents: {
send: jest.fn()
}
};
(dialog.showMessageBox as jest.Mock<any>).mockImplementation(async () => true);
(getOrCreateMainWindow as jest.Mock<any>).mockReturnValue(mockTarget);
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
ipcMainManager.readyWebContents.add(mockTarget.webContents as any);
await cb('/my/fake/path');
await showSaveDialog();
expect(dialog.showMessageBox).toHaveBeenCalled();
expect(mockTarget.webContents.send).toHaveBeenCalled();
});
it('does not overwrite files without consent', async () => {
showSaveDialog();
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
const cb = call[1];
const mockTarget = {
webContents: {
send: jest.fn()
}
};
(dialog.showMessageBox as jest.Mock<any>).mockImplementation(async () => false);
(getOrCreateMainWindow as jest.Mock<any>).mockReturnValue(mockTarget);
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
ipcMainManager.readyWebContents.add(mockTarget.webContents as any);
await cb('/my/fake/path');
await showSaveDialog();
expect(dialog.showMessageBox).toHaveBeenCalled();
expect(mockTarget.webContents.send).toHaveBeenCalledTimes(0);
});
it('does not overwrite files if an error happens', async () => {
showSaveDialog();
const call = (dialog.showOpenDialog as jest.Mock<any>).mock.calls[0];
const cb = call[1];
const mockTarget = {
webContents: {
send: jest.fn()
}
};
(dialog.showMessageBox as jest.Mock<any>).mockImplementation(async () => {
throw new Error('Nope');
});
(getOrCreateMainWindow as jest.Mock<any>).mockReturnValue(mockTarget);
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
let errored = false;
try {
await cb('/fake/path');
await showSaveDialog();
} catch (error) {
errored = error;
}
+14 -13
View File
@@ -7,6 +7,10 @@ jest.mock('../../src/utils/check-first-run', () => ({
isFirstRun: jest.fn()
}));
const mockDialogResponse = {
response: 1
};
describe('first-run', () => {
const oldDefaultApp = process.defaultApp;
const oldPlatform = process.platform;
@@ -15,6 +19,8 @@ describe('first-run', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin'
});
(dialog.showMessageBox as jest.Mock<any>).mockResolvedValue(mockDialogResponse);
});
afterEach(() => {
@@ -24,7 +30,7 @@ describe('first-run', () => {
});
afterEach(() => {
process.defaultApp = oldDefaultApp;
(process as any).defaultApp = oldDefaultApp;
});
describe('onFirstRunMaybe()', () => {
@@ -44,7 +50,7 @@ describe('first-run', () => {
it(`doesn't run unless required (dev mode)`, () => {
(isFirstRun as jest.Mock).mockReturnValueOnce(true);
process.defaultApp = true;
(process as any).defaultApp = true;
(app.isInApplicationsFolder as jest.Mock).mockReturnValue(false);
onFirstRunMaybe();
@@ -65,21 +71,16 @@ describe('first-run', () => {
expect(dialog.showMessageBox).toHaveBeenCalledTimes(0);
});
it(`moves the app when requested to do so`, () => {
(isFirstRun as jest.Mock).mockReturnValueOnce(true);
it(`moves the app when requested to do so`, async () => {
(isFirstRun as jest.Mock).mockReturnValue(true);
(app.isInApplicationsFolder as jest.Mock).mockReturnValue(false);
onFirstRunMaybe();
const call = (dialog.showMessageBox as jest.Mock<any>).mock.calls[0];
const cb = call[1];
cb(1);
mockDialogResponse.response = 1;
await onFirstRunMaybe();
expect(app.moveToApplicationsFolder).toHaveBeenCalledTimes(0);
cb(0);
mockDialogResponse.response = 0;
await onFirstRunMaybe();
expect(app.moveToApplicationsFolder).toHaveBeenCalledTimes(1);
});
});
+17 -2
View File
@@ -12,6 +12,7 @@ jest.mock('../../src/main/ipc');
describe('menu', () => {
beforeEach(() => {
(electron.app.getName as any).mockReturnValue('Electron Fiddle');
(electron.dialog.showOpenDialog as any).mockReturnValue(Promise.resolve({}));
});
afterEach(() => {
@@ -77,6 +78,20 @@ describe('menu', () => {
(toggleMap as any).click();
expect(ipcMainManager.send).toHaveBeenCalledTimes(2);
});
it('adds Bisect toggle', () => {
overridePlatform('linux');
setupMenu();
const result = (electron.Menu.buildFromTemplate as any).mock.calls[0][0];
const submenu = result[2].submenu as Array<Electron.MenuItemConstructorOptions>;
const toggleSoftWrap = submenu.find(({ label }) => label === 'Toggle Bisect Helper');
(toggleSoftWrap as any).click();
expect(ipcMainManager.send).toHaveBeenCalledWith(IpcEvents.BISECT_COMMANDS_TOGGLE);
});
});
describe('menu groups', () => {
@@ -173,7 +188,7 @@ describe('menu', () => {
it('attempts to open a template on click', () => {
showMe.submenu[0].submenu[0].click();
expect(ipcMainManager.send)
.toHaveBeenCalledWith(IpcEvents.FS_OPEN_TEMPLATE, [ 'App' ]);
.toHaveBeenCalledWith(IpcEvents.FS_OPEN_TEMPLATE, ['App']);
});
});
@@ -181,7 +196,7 @@ describe('menu', () => {
let tasks: any;
beforeEach(() => {
const mock = (electron.Menu.buildFromTemplate as any).mock;
const mock = (electron.Menu.buildFromTemplate as any).mock;
const menu = mock.calls[0][0];
tasks = menu[menu.length - 3];
});
+5 -2
View File
@@ -1,10 +1,12 @@
import { FileManager } from './file-manager';
import { RemoteLoader } from './remote-loader';
import { RunnerMock } from './runner';
export class AppMock {
public setupUnsavedOnChangeListener = jest.fn();
public setValues = jest.fn();
public getValues = jest.fn(() => ({
public replaceFiddle = jest.fn();
public setEditorValues = jest.fn();
public getEditorValues = jest.fn(() => ({
main: 'main-content',
renderer: 'renderer-content',
html: 'html-content'
@@ -18,6 +20,7 @@ export class AppMock {
public fileManager = new FileManager();
public runner = new RunnerMock();
public remoteLoader = new RemoteLoader();
public monaco = {
editor: {
+2 -3
View File
@@ -116,7 +116,6 @@ const app = {
quit: jest.fn(),
relaunch: jest.fn(),
setJumpList: jest.fn(),
setName: jest.fn(),
requestSingleInstanceLock: jest.fn(),
on: jest.fn(),
once: jest.fn()
@@ -157,8 +156,8 @@ const electronMock = {
writeImage: jest.fn()
},
dialog: {
showOpenDialog: jest.fn(),
showMessageBox: jest.fn()
showOpenDialog: jest.fn(() => Promise.resolve({})),
showMessageBox: jest.fn(() => Promise.resolve({}))
},
ipcMain: new MockIPC(),
ipcRenderer: new MockIPC(),
-1
View File
@@ -1,5 +1,4 @@
export class FileManager {
public saveToTemp = jest.fn(() => '/mock/temp/dir');
public setFiddle = jest.fn();
public cleanup = jest.fn();
}
+5
View File
@@ -0,0 +1,5 @@
export class RemoteLoader {
public loadFiddleFromGist = jest.fn();
public loadFiddleFromElectronExample = jest.fn();
public fetchGistAndLoad = jest.fn();
}
+2
View File
@@ -4,4 +4,6 @@ export class MockState {
@observable public isWarningDialogShowing = false;
@observable public gistId = '';
@observable public closedPanels = {};
@observable public isConsoleShowing = true;
@observable public output = [];
}
+195 -83
View File
@@ -1,10 +1,13 @@
import { App } from '../../src/renderer/app';
import { AppState } from '../../src/renderer/state';
import { EditorBackup } from '../../src/utils/editor-backup';
import { ElectronFiddleMock } from '../mocks/electron-fiddle';
import { MockState } from '../mocks/state';
import { overridePlatform, resetPlatform } from '../utils';
jest.mock('../../src/renderer/file-manager', () => require('../mocks/file-manager'));
jest.mock('../../src/renderer/file-manager', () =>
require('../mocks/file-manager')
);
jest.mock('../../src/renderer/state', () => ({
appState: {
theme: 'defaultDark',
@@ -18,8 +21,8 @@ jest.mock('../../src/renderer/components/header', () => ({
jest.mock('../../src/renderer/components/dialogs', () => ({
Dialogs: () => 'Dialogs;'
}));
jest.mock('../../src/renderer/components/editors', () => ({
Editors: () => 'Editors;'
jest.mock('../../src/renderer/components/output-editors-wrapper', () => ({
OutputEditorsWrapper: () => 'OutputEditorsWrapper;'
}));
describe('Editors component', () => {
@@ -33,11 +36,11 @@ describe('Editors component', () => {
jest.useFakeTimers();
const app = new App();
const result = await app.setup() as HTMLDivElement;
const result = (await app.setup()) as HTMLDivElement;
app.setupUnsavedOnChangeListener = jest.fn();
jest.runAllTimers();
expect(result.innerHTML).toBe('Header;Dialogs;Editors;');
expect(result.innerHTML).toBe('Dialogs;Header;OutputEditorsWrapper;');
expect(app.setupUnsavedOnChangeListener).toHaveBeenCalled();
jest.useRealTimers();
@@ -54,19 +57,19 @@ describe('Editors component', () => {
it('does not create a touch bar manager on Windows and Linux', () => {
overridePlatform('win32');
expect((new App()).touchBarManager).toBeFalsy();
expect(new App().touchBarManager).toBeFalsy();
overridePlatform('linux');
expect((new App()).touchBarManager).toBeFalsy();
expect(new App().touchBarManager).toBeFalsy();
resetPlatform();
});
});
describe('getValues()', () => {
describe('getEditorValues()', () => {
it('gets values', async () => {
const app = new App();
const b = await app.getValues({});
const b = await app.getEditorValues({});
expect(b.html).toBe('editor-value');
expect(b.main).toBe('editor-value');
@@ -81,7 +84,7 @@ describe('Editors component', () => {
(window as any).ElectronFiddle.editors.renderer = null;
const app = new App();
const result = await app.getValues({});
const result = await app.getEditorValues({});
expect(result.html).toBe('');
expect(result.main).toBe('');
@@ -96,7 +99,7 @@ describe('Editors component', () => {
const app = new App();
let threw = false;
try {
await app.getValues({});
await app.getEditorValues({});
} catch (error) {
threw = true;
}
@@ -105,61 +108,57 @@ describe('Editors component', () => {
});
});
describe('setValues()', () => {
it('attempts to set values', () => {
describe('replaceFiddle()', () => {
it('sets editor values and source info', (done) => {
const app = new App();
app.setValues({
(app.state as Partial<AppState>) = new MockState();
app.state.isUnsaved = false;
app.setEditorValues = jest.fn();
const editorValues = {
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
});
renderer: 'renderer-value',
preload: ''
};
expect((window as any).ElectronFiddle.editors.html.setValue)
.toHaveBeenCalledWith('html-value');
expect((window as any).ElectronFiddle.editors.main.setValue)
.toHaveBeenCalledWith('main-value');
expect((window as any).ElectronFiddle.editors.renderer.setValue)
.toHaveBeenCalledWith('renderer-value');
app.replaceFiddle(editorValues, {
gistId: 'gistId',
templateName: 'templateName',
filePath: 'localPath'
})
.then(() => {
expect(app.setEditorValues).toHaveBeenCalledWith(editorValues);
expect(app.state.gistId).toBe('gistId');
expect(app.state.templateName).toBe('templateName');
expect(app.state.localPath).toBe('localPath');
done();
});
});
it('attempts to set values for closed editors', () => {
const oldMainEditor = window.ElectronFiddle.editors.main;
delete window.ElectronFiddle.editors.main;
it('unsets state of previous source when called', (done) => {
const app = new App();
(app.state.closedPanels as any).main = { model: { setValue: jest.fn() } };
app.setValues({
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
});
expect((app.state.closedPanels.main as EditorBackup)!.model!.setValue)
.toHaveBeenCalledWith('main-value');
window.ElectronFiddle.editors.main = oldMainEditor;
});
it('warns when the contents are unsaved, does not proceed if denied', (done) => {
const app = new App();
(app.state as any) = new MockState();
(app.state as Partial<AppState>) = new MockState();
app.state.isUnsaved = true;
app.state.localPath = '/fake/path';
app.state.setWarningDialogTexts = jest.fn();
app.setEditorValues = jest.fn();
app.setValues({
const editorValues = {
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
}).then((result) => {
expect(result).toBe(true);
expect((window as any).ElectronFiddle.editors.html.setValue)
.toHaveBeenCalledWith('html-value');
expect((window as any).ElectronFiddle.editors.main.setValue)
.toHaveBeenCalledWith('main-value');
expect((window as any).ElectronFiddle.editors.renderer.setValue)
.toHaveBeenCalledWith('renderer-value');
};
done();
});
expect(app.state.localPath).toBe('/fake/path');
app.replaceFiddle(editorValues, {
gistId: 'gistId',
})
.then(() => {
expect(app.state.localPath).toBeUndefined();
done();
});
setTimeout(() => {
expect(app.state.isWarningDialogShowing).toBe(true);
@@ -168,32 +167,135 @@ describe('Editors component', () => {
});
});
it('warns when the contents are unsaved, does proceed if allowed', (done) => {
it('marks the new Fiddle as Saved', (done) => {
const app = new App();
(app.state as any) = new MockState();
app.state.isUnsaved = true;
(app.state as Partial<AppState>) = new MockState();
app.state.isUnsaved = false;
app.setupUnsavedOnChangeListener = jest.fn();
app.setEditorValues = jest.fn();
app.setValues({
const editorValues = {
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
}).then((result) => {
expect(result).toBe(false);
expect((window as any).ElectronFiddle.editors.html.setValue)
.toHaveBeenCalledTimes(0);
expect((window as any).ElectronFiddle.editors.main.setValue)
.toHaveBeenCalledTimes(0);
expect((window as any).ElectronFiddle.editors.renderer.setValue)
.toHaveBeenCalledTimes(0);
};
done();
app.replaceFiddle(editorValues, {
gistId: 'gistId',
templateName: 'templateName',
filePath: 'localPath'
})
.then(() => {
expect(app.state.isUnsaved).toBe(false);
expect(app.setupUnsavedOnChangeListener).toHaveBeenCalled();
done();
});
});
describe('when current Fiddle is unsaved and prompt appears', () => {
it('takes no action if prompt is rejected', (done) => {
const app = new App();
(app.state as Partial<AppState>) = new MockState();
app.state.isUnsaved = true;
app.state.setWarningDialogTexts = jest.fn();
app.setEditorValues = jest.fn();
expect(app.state.localPath).toBeUndefined();
expect(app.state.gistId).toBe('');
expect(app.state.templateName).toBeUndefined();
app.replaceFiddle({}, {
gistId: 'gistId',
templateName: 'templateName',
filePath: 'localPath'
})
.then(() => {
expect(app.setEditorValues).not.toHaveBeenCalled();
expect(app.state.localPath).toBeUndefined();
expect(app.state.gistId).toBe('');
expect(app.state.templateName).toBeUndefined();
done();
});
setTimeout(() => {
expect(app.state.isWarningDialogShowing).toBe(true);
app.state.warningDialogLastResult = false;
app.state.isWarningDialogShowing = false;
});
});
setTimeout(() => {
expect(app.state.isWarningDialogShowing).toBe(true);
app.state.isWarningDialogShowing = false;
it('sets editor values and source info if prompt is accepted', (done) => {
const app = new App();
(app.state as Partial<AppState>) = new MockState();
app.state.isUnsaved = true;
app.state.setWarningDialogTexts = jest.fn();
app.setEditorValues = jest.fn();
const editorValues = {
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
};
app.replaceFiddle(editorValues, {
gistId: 'gistId',
templateName: 'templateName',
filePath: 'localPath'
})
.then(() => {
expect(app.setEditorValues).toHaveBeenCalledWith(editorValues);
expect(app.state.gistId).toBe('gistId');
expect(app.state.templateName).toBe('templateName');
expect(app.state.localPath).toBe('localPath');
done();
});
setTimeout(() => {
expect(app.state.isWarningDialogShowing).toBe(true);
app.state.warningDialogLastResult = true;
app.state.isWarningDialogShowing = false;
});
});
});
});
describe('setEditorValues()', () => {
it('attempts to set values', () => {
const app = new App();
app.setEditorValues({
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
});
expect(
(window as any).ElectronFiddle.editors.html.setValue
).toHaveBeenCalledWith('html-value');
expect(
(window as any).ElectronFiddle.editors.main.setValue
).toHaveBeenCalledWith('main-value');
expect(
(window as any).ElectronFiddle.editors.renderer.setValue
).toHaveBeenCalledWith('renderer-value');
});
it('attempts to set values for closed editors', () => {
const oldMainEditor = window.ElectronFiddle.editors.main;
delete window.ElectronFiddle.editors.main;
const app = new App();
(app.state.closedPanels as any).main = { model: { setValue: jest.fn() } };
app.setEditorValues({
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value',
});
expect(
(app.state.closedPanels.main as EditorBackup)!.model!.setValue
).toHaveBeenCalledWith('main-value');
window.ElectronFiddle.editors.main = oldMainEditor;
});
it('throws if the Fiddle object is not present', async () => {
(window as any).ElectronFiddle = null;
@@ -201,26 +303,35 @@ describe('Editors component', () => {
const app = new App();
let threw = false;
try {
await app.setValues({ html: '', main: '', renderer: ''});
await app.setEditorValues({ html: '', main: '', renderer: '' });
} catch (error) {
threw = true;
}
expect(threw).toBe(true);
});
it('does not set a value if none passed in', async () => {
const app = new App();
await app.setEditorValues({
html: 'html-value',
main: 'main-value'
});
expect(
(window as any).ElectronFiddle.editors.renderer.setValue
).not.toHaveBeenCalled();
});
});
describe('setupUnsavedOnChangeListener()', () => {
it('listens for model change events', async () => {
const app = new App();
await app.setValues({
html: 'html-value',
main: 'main-value',
renderer: 'renderer-value'
});
app.setupUnsavedOnChangeListener();
const fn = window.ElectronFiddle.editors!.renderer!.onDidChangeModelContent;
const fn = window.ElectronFiddle.editors!.renderer!
.onDidChangeModelContent;
const call = (fn as jest.Mock<any>).mock.calls[0];
const cb = call[0];
@@ -237,16 +348,16 @@ describe('Editors component', () => {
const app = new App();
app.setupResizeListener();
expect(window.addEventListener)
.toHaveBeenCalled();
expect((window.addEventListener as jest.Mock).mock.calls[0][0])
.toBe('resize');
expect(window.addEventListener).toHaveBeenCalled();
expect((window.addEventListener as jest.Mock).mock.calls[0][0]).toBe(
'resize'
);
});
});
describe('setupTheme()', () => {
it(`adds the current theme's css to the document`, async () => {
document.head!.innerHTML = '<style id="fiddle-theme"></style>';
document.head!.innerHTML = "<style id='fiddle-theme'></style>";
const app = new App();
await app.setupTheme();
@@ -272,8 +383,9 @@ describe('Editors component', () => {
--error-color: #df3434;
--fonts-common: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
</style>`.replace(/ /gm, ''));
// tslint:enable:max-line-length
</style>`.replace(/ /gm, '')
);
// tslint:enable:max-line-length
});
it('removes the dark theme option if required', async () => {
+50
View File
@@ -1,4 +1,5 @@
import { BinaryManager } from '../../src/renderer/binary';
import { USER_DATA_PATH } from '../../src/renderer/constants';
import { overridePlatform, resetPlatform } from '../utils';
import * as path from 'path';
@@ -57,6 +58,55 @@ describe('binary', () => {
await binaryManager.remove('v3.0.0');
expect(fs.remove).toHaveBeenCalledTimes(4);
});
it('attempts to clean up the version\'s associated typedefs', async () => {
const fs = require('fs-extra');
binaryManager.removeTypeDefsForVersion = jest.fn();
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
await binaryManager.remove('v3.0.0');
expect(fs.remove).toHaveBeenCalled();
expect(binaryManager.removeTypeDefsForVersion).toHaveBeenCalled();
});
it('retries typedef cleanup upon failure', async () => {
const fs = require('fs-extra');
binaryManager.removeTypeDefsForVersion = jest.fn().mockImplementation(() => {
throw new Error('Bwap bwap');
});
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
await binaryManager.remove('v3.0.0');
expect(binaryManager.removeTypeDefsForVersion).toHaveBeenCalledTimes(4);
});
});
describe('removeTypeDefsForVersion()', () => {
it('removes a version\'s typedefs', async () => {
const fs = require('fs-extra');
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
await binaryManager.removeTypeDefsForVersion('v3.0.0');
expect(fs.remove).toHaveBeenCalledWith(path.join(USER_DATA_PATH, 'electron-typedef', '3.0.0'));
});
it('throws upon fs failure', async () => {
const fs = require('fs-extra');
(fs.existsSync as jest.Mock<any>).mockReturnValue(true);
(fs.remove as jest.Mock<any>).mockImplementation(() => {
throw new Error('Bwap bwap');
});
expect.assertions(1);
try {
await binaryManager.removeTypeDefsForVersion('v3.0.0');
} catch (e) {
expect(e).toEqual(new Error('Bwap bwap'));
}
});
});
describe('getDownloadedVersions()', () => {
+70
View File
@@ -0,0 +1,70 @@
import { ElectronVersionSource, ElectronVersionState } from '../../src/interfaces';
import { Bisector } from '../../src/renderer/bisect';
const generateVersionRange = (rangeLength: number) =>
(new Array(rangeLength)).fill(0).map((_, i) => ({
state: ElectronVersionState.ready,
version: `${i + 1}.0.0`,
source: ElectronVersionSource.local
}));
describe('bisect', () => {
let bisector: Bisector;
beforeEach(() => {
const versions = generateVersionRange(9);
bisector = new Bisector(versions);
});
it('selects a pivot in the middle of the array', () => {
const pivot = bisector.getCurrentVersion();
const middleIndex = Math.floor(bisector.revList.length / 2);
expect(pivot).toBe(bisector.revList[middleIndex]);
});
describe('continue()', () => {
it('returns the current version', () => {
const result = bisector.continue(true);
const version = bisector.getCurrentVersion();
expect(result).toBe(version);
});
it('discards lower half of the range if pivot is good version', () => {
const pivotVersion = bisector.getCurrentVersion();
bisector.continue(true);
expect(bisector.revList[bisector.minRev]).toBe(pivotVersion);
});
it('discards upper half of the range if pivot is bad version', () => {
const pivotVersion = bisector.getCurrentVersion();
bisector.continue(false);
expect(bisector.revList[bisector.maxRev]).toBe(pivotVersion);
});
it('terminates if fewer than 2 items are left', () => {
const versions = generateVersionRange(2);
bisector = new Bisector(versions);
expect(bisector.revList.length).toBe(2);
const responseGood = bisector.continue(true);
expect(responseGood).toHaveLength(2);
expect(versions).toContain(responseGood[0]);
expect(versions).toContain(responseGood[1]);
bisector = new Bisector(versions);
expect(bisector.revList.length).toBe(2);
const responseBad = bisector.continue(false);
expect(responseBad).toHaveLength(2);
expect(versions).toContain(responseBad[0]);
expect(versions).toContain(responseBad[1]);
});
});
describe('getCurrentVersion()', () => {
it('returns a version within the range', () => {
const version = bisector.getCurrentVersion();
expect(bisector.revList).toContain(version);
});
});
});
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Bisect commands component renders bisect dialog button if no bisect instance 1`] = `
<Blueprint3.Button
icon="git-branch"
text="Bisect"
/>
`;
exports[`Bisect commands component renders helper buttons if bisect instance is active 1`] = `
<Fragment>
<Blueprint3.Button
icon="thumbs-up"
onClick={[Function]}
/>
<Blueprint3.Button
icon="thumbs-down"
onClick={[Function]}
/>
<Blueprint3.Button
icon="cross"
onClick={[Function]}
>
Cancel Bisect
</Blueprint3.Button>
</Fragment>
`;
@@ -27,6 +27,16 @@ exports[`EditorDropdown component renders 1`] = `
shouldDismissPopover={true}
text="Renderer Process (renderer.js)"
/>
<Blueprint3.MenuItem
disabled={false}
icon="eye-off"
id="preload"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Preload (preload.js)"
/>
<Blueprint3.MenuItem
disabled={false}
icon="eye-open"
@@ -98,6 +108,16 @@ exports[`EditorDropdown component renders the extra button if the FIDDLE_DOCS_DE
shouldDismissPopover={true}
text="Renderer Process (renderer.js)"
/>
<Blueprint3.MenuItem
disabled={false}
icon="eye-off"
id="preload"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Preload (preload.js)"
/>
<Blueprint3.MenuItem
disabled={false}
icon="eye-open"
@@ -1,46 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VersionChooser component handles corrupt data 1`] = `
<Blueprint3.Select
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "remote",
"state": "ready",
"version": "2.0.2",
},
Object {
"source": "remote",
"state": "ready",
"version": "2.0.1",
},
Object {
"source": "remote",
"state": "ready",
"version": "1.8.7",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
<Blueprint3.ButtonGroup>
<Blueprint3.Select
disabled={false}
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "remote",
"state": "ready",
"version": "2.0.2",
},
Object {
"source": "remote",
"state": "ready",
"version": "2.0.1",
},
Object {
"source": "remote",
"state": "ready",
"version": "1.8.7",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
className="version-chooser"
disabled={false}
icon="saved"
text="Electron v2.0.2"
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
className="version-chooser"
icon="saved"
text="Electron v2.0.2"
/>
</Blueprint3.Select>
</Blueprint3.Select>
</Blueprint3.ButtonGroup>
`;
exports[`VersionChooser component renderItem() renders an item 1`] = `
@@ -62,44 +66,48 @@ exports[`VersionChooser component renderItem() renders an item 1`] = `
`;
exports[`VersionChooser component renders 1`] = `
<Blueprint3.Select
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "remote",
"state": "ready",
"version": "2.0.2",
},
Object {
"source": "remote",
"state": "ready",
"version": "2.0.1",
},
Object {
"source": "remote",
"state": "ready",
"version": "1.8.7",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
<Blueprint3.ButtonGroup>
<Blueprint3.Select
disabled={false}
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "remote",
"state": "ready",
"version": "2.0.2",
},
Object {
"source": "remote",
"state": "ready",
"version": "2.0.1",
},
Object {
"source": "remote",
"state": "ready",
"version": "1.8.7",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
className="version-chooser"
disabled={false}
icon="saved"
text="Electron v2.0.2"
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
className="version-chooser"
icon="saved"
text="Electron v2.0.2"
/>
</Blueprint3.Select>
</Blueprint3.Select>
</Blueprint3.ButtonGroup>
`;
@@ -16,10 +16,9 @@ exports[`AddVersionDialog component renders 1`] = `
id="custom-electron-version"
inputProps={
Object {
"webkitdirectory": "true",
"onClick": [Function],
}
}
onInputChange={[Function]}
text="/test/file"
/>
<br />
@@ -79,10 +78,9 @@ exports[`AddVersionDialog component renders 2`] = `
id="custom-electron-version"
inputProps={
Object {
"webkitdirectory": "true",
"onClick": [Function],
}
}
onInputChange={[Function]}
text="/test/file"
/>
<br />
@@ -0,0 +1,413 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BisectDialog component renders 1`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="dialog-add-version"
isOpen={false}
onClose={[Function]}
title="Start a bisect session"
>
<div
className="bp3-dialog-body"
>
<Component>
Earliest Version
<Blueprint3.Select
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "4.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "5.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
fill={true}
icon="saved"
text="v4.0.0"
/>
</Blueprint3.Select>
</Component>
<Component>
Latest Version
<Blueprint3.Select
disabled={false}
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
disabled={false}
fill={true}
icon="small-minus"
text=""
/>
</Blueprint3.Select>
</Component>
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
disabled={true}
icon="play"
key="submit"
onClick={[Function]}
text="Begin"
/>
<Blueprint3.Button
icon="cross"
key="cancel"
onClick={[Function]}
text="Cancel"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
exports[`BisectDialog component renders 2`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="dialog-add-version"
isOpen={false}
onClose={[Function]}
title="Start a bisect session"
>
<div
className="bp3-dialog-body"
>
<Component>
Earliest Version
<Blueprint3.Select
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "4.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "5.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
fill={true}
icon="small-minus"
text=""
/>
</Blueprint3.Select>
</Component>
<Component>
Latest Version
<Blueprint3.Select
disabled={true}
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "4.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "5.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
disabled={true}
fill={true}
icon="small-minus"
text=""
/>
</Blueprint3.Select>
</Component>
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
disabled={true}
icon="play"
key="submit"
onClick={[Function]}
text="Begin"
/>
<Blueprint3.Button
icon="cross"
key="cancel"
onClick={[Function]}
text="Cancel"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
exports[`BisectDialog component renders 3`] = `
<Blueprint3.Dialog
canOutsideClickClose={true}
className="dialog-add-version"
isOpen={false}
onClose={[Function]}
title="Start a bisect session"
>
<div
className="bp3-dialog-body"
>
<Component>
Earliest Version
<Blueprint3.Select
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "4.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "5.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
fill={true}
icon="saved"
text="v4.0.0"
/>
</Blueprint3.Select>
</Component>
<Component>
Latest Version
<Blueprint3.Select
disabled={false}
filterable={true}
itemPredicate={[Function]}
itemRenderer={[Function]}
items={
Array [
Object {
"source": "local",
"state": "ready",
"version": "1.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "2.0.0",
},
Object {
"source": "local",
"state": "ready",
"version": "3.0.0",
},
]
}
noResults={
<Blueprint3.MenuItem
disabled={true}
multiline={false}
popoverProps={Object {}}
shouldDismissPopover={true}
text="No results."
/>
}
onItemSelect={[Function]}
>
<Blueprint3.Button
disabled={false}
fill={true}
icon="small-minus"
text=""
/>
</Blueprint3.Select>
</Component>
</div>
<div
className="bp3-dialog-footer"
>
<div
className="bp3-dialog-footer-actions"
>
<Blueprint3.Button
disabled={true}
icon="play"
key="submit"
onClick={[Function]}
text="Begin"
/>
<Blueprint3.Button
icon="cross"
key="cancel"
onClick={[Function]}
text="Cancel"
/>
</div>
</div>
</Blueprint3.Dialog>
`;
@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dialogs component renders initially without visible dialogs 1`] = `
<div
className="dialogs"
key="dialogs"
>
<WarningDialog
appState={
Object {
"isAddVersionDialogShowing": false,
"isSettingsShowing": false,
"isTokenDialogShowing": false,
"warningDialogTexts": Object {
"cancel": "",
"label": "",
"ok": "",
},
}
}
/>
</div>
`;
@@ -56,7 +56,7 @@ exports[`Editors component renders 1`] = `
maximize
</desc>
<path
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 0 0-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 0 0-.29.7 1.003 1.003 0 0 0 1.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 00-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 00-.29.7 1.003 1.003 0 001.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
fillRule="evenodd"
key="0"
/>
@@ -84,7 +84,7 @@ exports[`Editors component renders 1`] = `
cross
</desc>
<path
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L9.41 8z"
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
fillRule="evenodd"
key="0"
/>
@@ -229,7 +229,7 @@ exports[`Editors component renders 1`] = `
maximize
</desc>
<path
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 0 0-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 0 0-.29.7 1.003 1.003 0 0 0 1.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 00-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 00-.29.7 1.003 1.003 0 001.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
fillRule="evenodd"
key="0"
/>
@@ -257,7 +257,7 @@ exports[`Editors component renders 1`] = `
cross
</desc>
<path
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L9.41 8z"
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
fillRule="evenodd"
key="0"
/>
@@ -355,7 +355,7 @@ exports[`Editors component renders 1`] = `
</div>
<div
className="mosaic-tile"
key="html"
key="preload"
style={
Object {
"bottom": "50%",
@@ -364,6 +364,179 @@ exports[`Editors component renders 1`] = `
"top": "0%",
}
}
>
<div
className="mosaic-window mosaic-drop-target preload"
>
<div
className="mosaic-window-toolbar draggable"
>
<div>
<div>
<h5>
Preload (preload.js)
</h5>
</div>
<div />
<div
className="mosaic-controls"
>
<button
className="bp3-button bp3-small"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
type="button"
>
<span
className="bp3-icon bp3-icon-maximize"
icon="maximize"
>
<svg
data-icon="maximize"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
maximize
</desc>
<path
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 00-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 00-.29.7 1.003 1.003 0 001.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
fillRule="evenodd"
key="0"
/>
</svg>
</span>
</button>
<button
className="bp3-button bp3-small"
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
type="button"
>
<span
className="bp3-icon bp3-icon-cross"
icon="cross"
>
<svg
data-icon="cross"
height={16}
viewBox="0 0 16 16"
width={16}
>
<desc>
cross
</desc>
<path
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
fillRule="evenodd"
key="0"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
<div
className="mosaic-window-body"
>
Editor
</div>
<div
className="mosaic-window-body-overlay"
onClick={[Function]}
/>
<div
className="mosaic-window-additional-actions-bar"
/>
<div
className="mosaic-preview"
>
<div
className="mosaic-window-toolbar"
>
<div
className="mosaic-window-title"
>
Preload (preload.js)
</div>
</div>
<div
className="mosaic-window-body"
>
<h4>
Preload (preload.js)
</h4>
<span
className="bp3-icon bp3-icon-application"
icon="application"
>
<svg
data-icon="application"
height={72}
viewBox="0 0 20 20"
width={72}
>
<desc>
application
</desc>
<path
d="M3.5 9h9c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-9c-.28 0-.5.22-.5.5s.22.5.5.5zm0 2h5c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-5c-.28 0-.5.22-.5.5s.22.5.5.5zM19 1H1c-.55 0-1 .45-1 1v16c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V2c0-.55-.45-1-1-1zm-1 16H2V6h16v11zM3.5 13h7c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-7c-.28 0-.5.22-.5.5s.22.5.5.5z"
fillRule="evenodd"
key="0"
/>
</svg>
</span>
</div>
</div>
<div
className="drop-target-container"
>
<div
className="drop-target top"
/>
<div
className="drop-target bottom"
/>
<div
className="drop-target left"
/>
<div
className="drop-target right"
/>
</div>
</div>
</div>
<div
className="mosaic-split -column"
onMouseDown={[Function]}
style={
Object {
"bottom": "0%",
"left": "50%",
"right": "0%",
"top": "50%",
}
}
>
<div
className="mosaic-split-line"
/>
</div>
<div
className="mosaic-tile"
key="html"
style={
Object {
"bottom": "25%",
"left": "50%",
"right": "0%",
"top": "50%",
}
}
>
<div
className="mosaic-window mosaic-drop-target html"
@@ -402,7 +575,7 @@ exports[`Editors component renders 1`] = `
maximize
</desc>
<path
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 0 0-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 0 0-.29.7 1.003 1.003 0 0 0 1.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 00-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 00-.29.7 1.003 1.003 0 001.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
fillRule="evenodd"
key="0"
/>
@@ -430,7 +603,7 @@ exports[`Editors component renders 1`] = `
cross
</desc>
<path
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L9.41 8z"
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
fillRule="evenodd"
key="0"
/>
@@ -518,7 +691,7 @@ exports[`Editors component renders 1`] = `
"bottom": "0%",
"left": "50%",
"right": "0%",
"top": "50%",
"top": "75%",
}
}
>
@@ -534,7 +707,7 @@ exports[`Editors component renders 1`] = `
"bottom": "0%",
"left": "50%",
"right": "0%",
"top": "50%",
"top": "75%",
}
}
>
@@ -575,7 +748,7 @@ exports[`Editors component renders 1`] = `
home
</desc>
<path
d="M2 10v5c0 .55.45 1 1 1h3v-5h4v5h3c.55 0 1-.45 1-1v-5L8 4l-6 6zm13.71-2.71L14 5.59V2c0-.55-.45-1-1-1s-1 .45-1 1v1.59L8.71.29C8.53.11 8.28 0 8 0s-.53.11-.71.29l-7 7a1.003 1.003 0 0 0 1.42 1.42L8 2.41l6.29 6.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71z"
d="M2 10v5c0 .55.45 1 1 1h3v-5h4v5h3c.55 0 1-.45 1-1v-5L8 4l-6 6zm13.71-2.71L14 5.59V2c0-.55-.45-1-1-1s-1 .45-1 1v1.59L8.71.29C8.53.11 8.28 0 8 0s-.53.11-.71.29l-7 7a1.003 1.003 0 001.42 1.42L8 2.41l6.29 6.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71z"
fillRule="evenodd"
key="0"
/>
@@ -609,7 +782,7 @@ exports[`Editors component renders 1`] = `
maximize
</desc>
<path
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 0 0-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 0 0-.29.7 1.003 1.003 0 0 0 1.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
d="M5.99 8.99c-.28 0-.53.11-.71.29l-3.29 3.29v-1.59c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .55.45 1 1 1h4c.55 0 1-.45 1-1s-.45-1-1-1H3.41L6.7 10.7a1.003 1.003 0 00-.71-1.71zm9-9h-4c-.55 0-1 .45-1 1s.45 1 1 1h1.59l-3.3 3.3a.99.99 0 00-.29.7 1.003 1.003 0 001.71.71l3.29-3.29V5c0 .55.45 1 1 1s1-.45 1-1V1c0-.56-.45-1.01-1-1.01z"
fillRule="evenodd"
key="0"
/>
@@ -637,7 +810,7 @@ exports[`Editors component renders 1`] = `
cross
</desc>
<path
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L9.41 8z"
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
fillRule="evenodd"
key="0"
/>
@@ -715,7 +888,7 @@ exports[`Editors component renders 1`] = `
play
</desc>
<path
d="M12 8c0-.35-.19-.64-.46-.82l.01-.02-6-4-.01.02A.969.969 0 0 0 5 3c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1 .21 0 .39-.08.54-.18l.01.02 6-4-.01-.02c.27-.18.46-.47.46-.82z"
d="M12 8c0-.35-.19-.64-.46-.82l.01-.02-6-4-.01.02A.969.969 0 005 3c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1 .21 0 .39-.08.54-.18l.01.02 6-4-.01-.02c.27-.18.46-.47.46-.82z"
fillRule="evenodd"
key="0"
/>
@@ -869,8 +1042,12 @@ exports[`Editors component renders a toolbar 1`] = `
},
"second": Object {
"direction": "column",
"first": "html",
"second": "docsDemo",
"first": "preload",
"second": Object {
"direction": "column",
"first": "html",
"second": "docsDemo",
},
},
},
"setWarningDialogTexts": [Function],
@@ -893,8 +1070,12 @@ exports[`Editors component renders a toolbar 1`] = `
},
"second": Object {
"direction": "column",
"first": "html",
"second": "docsDemo",
"first": "preload",
"second": Object {
"direction": "column",
"first": "html",
"second": "docsDemo",
},
},
},
"setWarningDialogTexts": [Function],
@@ -12,10 +12,6 @@ exports[`Header component renders 1`] = `
appState={Object {}}
key="commands"
/>
<output
appState={Object {}}
key="output"
/>
</header>
<welcome-tour
appState={Object {}}
@@ -2,7 +2,7 @@
exports[`Output component renders with output 1`] = `
<div
className="output showing"
className="output"
>
<p
key="1532704073130--0--0"
@@ -3,7 +3,7 @@
exports[`ExecutionSettings component renders 1`] = `
<div>
<h2>
Exection
Execution
</h2>
<Blueprint3.Callout>
These advanced settings control how Electron Fiddle executes your fiddles.
@@ -63,11 +63,19 @@ exports[`AppearanceSettings component renders 1`] = `
</a>
. The easiest way to get started is to clone one of the two existing themes and to add your own colors.
</p>
<p>
Additionally, if you wish to import a Monaco Editor theme, pick your JSON file and Fiddle will attempt to import it.
</p>
<Blueprint3.Button
icon="duplicate"
onClick={[Function]}
text="Create theme from current selection"
/>
<Blueprint3.Button
icon="document-open"
onClick={[Function]}
text="Add a Monaco Editor theme"
/>
</Blueprint3.Callout>
</div>
`;
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditsSettings component renders only the menu if page unknown 1`] = `
exports[`Settings component renders only the menu if page unknown 1`] = `
<div
className="settings"
>
@@ -72,7 +72,7 @@ exports[`CreditsSettings component renders only the menu if page unknown 1`] = `
</div>
`;
exports[`CreditsSettings component renders the Credits page after a click 1`] = `
exports[`Settings component renders the Credits page after a click 1`] = `
<div
className="settings"
>
@@ -151,7 +151,7 @@ exports[`CreditsSettings component renders the Credits page after a click 1`] =
</div>
`;
exports[`CreditsSettings component renders the Electron page after a click 1`] = `
exports[`Settings component renders the Electron page after a click 1`] = `
<div
className="settings"
>
@@ -230,7 +230,7 @@ exports[`CreditsSettings component renders the Electron page after a click 1`] =
</div>
`;
exports[`CreditsSettings component renders the Electron page by default 1`] = `
exports[`Settings component renders the Electron page by default 1`] = `
<div
className="settings"
>
@@ -309,7 +309,7 @@ exports[`CreditsSettings component renders the Electron page by default 1`] = `
</div>
`;
exports[`CreditsSettings component renders the Execution page after a click 1`] = `
exports[`Settings component renders the Execution page after a click 1`] = `
<div
className="settings"
>
@@ -388,7 +388,7 @@ exports[`CreditsSettings component renders the Execution page after a click 1`]
</div>
`;
exports[`CreditsSettings component renders the General page after a click 1`] = `
exports[`Settings component renders the General page after a click 1`] = `
<div
className="settings"
>
@@ -120,7 +120,7 @@ exports[`Header component renders the tour once started 1`] = `
Object {
"content": <React.Fragment>
<p>
Every Electron app starts with a main script, very similar to how a Node.js application is started. The main script runs in the "main process". To display a user interface, the main process creates renderer processes  usually in the form of windows, which Electron calls
Every Electron app starts with a main script, very similar to how a Node.js application is started. The main script runs in the "main process". To display a user interface, the main process creates renderer processes  usually in the form of windows, which Electron calls  
<code>
BrowserWindow
</code>
@@ -147,11 +147,11 @@ exports[`Header component renders the tour once started 1`] = `
},
Object {
"content": <p>
In the default fiddle, this HTML file is loaded in the
In the default fiddle, this HTML file is loaded in the  
<code>
BrowserWindow
</code>
. Any HTML, CSS, or JavaScript that works in a browser will work here, too. In addition, Electron allows you to execute Node.js code. Take a close look at the
. Any HTML, CSS, or JavaScript that works in a browser will work here, too. In addition, Electron allows you to execute Node.js code. Take a close look at the  
<code>
&lt;script /&gt;
</code>
@@ -179,7 +179,7 @@ exports[`Header component renders the tour once started 1`] = `
<code>
npm
</code>
module here, just
module here, just  
<code>
require
</code>
@@ -3,58 +3,23 @@ import * as React from 'react';
import { observable } from 'mobx';
import { AddressBar } from '../../../src/renderer/components/commands-address-bar';
import { INDEX_HTML_NAME, MAIN_JS_NAME, RENDERER_JS_NAME } from '../../../src/shared-constants';
import { getOctokit } from '../../../src/utils/octokit';
import { ElectronFiddleMock } from '../../mocks/electron-fiddle';
import { MockState } from '../../mocks/state';
jest.mock('../../../src/utils/octokit');
const mockGists = {
get: async () => ({
data: {
files: {
'renderer.js': {
content: 'renderer-content'
},
'main.js': {
content: 'main-content'
},
'index.html': {
content: 'html'
}
}
}
})
};
const mockRepos = {
getContents: async () => ({
data: [
{
name: MAIN_JS_NAME,
download_url: 'https://main'
}, {
name: RENDERER_JS_NAME,
download_url: 'https://renderer'
}, {
name: INDEX_HTML_NAME,
download_url: 'https://html'
}, {
name: 'other_stuff',
download_url: 'https://google.com'
}
]
})
};
describe('AddressBar component', () => {
let store: any;
(window as any).ElectronFiddle = new ElectronFiddleMock();
class MockStore {
@observable public gistId: string | null = null;
@observable public isWarningDialogShowing: boolean = false;
@observable public isConfirmationPromptShowing: boolean = false;
public setWarningDialogTexts = jest.fn();
public toogleWarningDialog = jest.fn();
public setConfirmationDialogTexts = jest.fn();
public setConfirmationPromptTexts = jest.fn();
public toggleWarningDialog = jest.fn();
}
beforeEach(() => {
@@ -91,8 +56,6 @@ describe('AddressBar component', () => {
});
it('handles submit', () => {
const oldLoadFiddle = AddressBar.prototype.fetchGistAndLoad;
AddressBar.prototype.fetchGistAndLoad = jest.fn();
const preventDefault = jest.fn();
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
@@ -102,8 +65,6 @@ describe('AddressBar component', () => {
expect(wrapper.state('value')).toBe('abcdtestid');
expect(preventDefault).toHaveBeenCalled();
AddressBar.prototype.fetchGistAndLoad = oldLoadFiddle;
});
it('disables during gist publishing', async () => {
@@ -117,202 +78,4 @@ describe('AddressBar component', () => {
expect(wrapper.find('fieldset').prop('disabled')).toBe(false);
});
});
describe('fetchGistAndLoad()', () => {
it('loads a fiddle', async () => {
(getOctokit as jest.Mock).mockReturnValue({ gists: mockGists });
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
store.gistId = 'abcdtestid';
instance.handleChange({ target: { value: 'abcdtestid' } } as any);
await (wrapper.instance() as AddressBar).fetchGistAndLoad('abcdtestid');
expect(wrapper.state('value')).toBe('abcdtestid');
expect(document.title).toBe('Electron Fiddle - gist.github.com/abcdtestid');
});
it('handles an error', async () => {
(getOctokit as jest.Mock).mockReturnValue({
gists: {
get: async () => {
throw new Error('Bwap bwap');
}
}
});
const wrapper = shallow(<AddressBar appState={store} />);
const result = await (wrapper.instance() as AddressBar).fetchGistAndLoad('abcdtestid');
expect(result).toBe(false);
});
});
describe('fetchExampleAndLoad()', () => {
it('loads an Electron exmaple', async () => {
(getOctokit as jest.Mock).mockReturnValue({ repos: mockRepos });
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
// Setup the mock
(fetch as any).mockResponses(
[ 'main' ],
[ 'renderer' ],
[ 'index' ]
);
await instance.fetchExampleAndLoad('4.0.0', 'test/path');
expect(document.title).toBe('Electron Fiddle - Unsaved');
const { calls } = (window.ElectronFiddle.app.setValues as any).mock;
expect(calls).toHaveLength(1);
expect(calls[0]).toEqual([{
html: 'index',
main: 'main',
renderer: 'renderer'
}]);
});
it('handles an error', async () => {
(getOctokit as jest.Mock).mockReturnValue({
repos: {
getContents: async () => {
throw new Error('Bwap bwap');
}
}
});
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
// Setup the mock
(fetch as any).mockResponses(
[ 'main' ],
[ 'renderer' ],
[ 'index' ]
);
const result = await instance.fetchExampleAndLoad('4.0.0', 'test/path');
expect(result).toBe(false);
});
it('handles incorrect results', async () => {
(getOctokit as jest.Mock).mockReturnValue({
repos: {
getContents: async () => ({
not_an_array: true
})
}
});
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
// Setup the mock
(fetch as any).mockResponses(
[ 'main' ],
[ 'renderer' ],
[ 'index' ]
);
const result = await instance.fetchExampleAndLoad('4.0.0', 'test/path');
expect(result).toBe(false);
expect(store.setWarningDialogTexts.mock.calls[0][0].label).toEqual(
'Loading the fiddle failed: Error: The example Fiddle tried to launch is not a valid Electron example'
);
});
});
describe('verifyRemoteLoad()', () => {
it('asks the user if they want to load remote content', (done) => {
const mockStore: any = new MockStore();
const wrapper = shallow(<AddressBar appState={mockStore} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad('test').then(done);
expect(mockStore.isWarningDialogShowing).toBe(true);
mockStore.isWarningDialogShowing = false;
});
});
describe('verifyRemoteLoad()', () => {
it('asks the user if they want to load remote content', (done) => {
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad('test').then(done);
expect(store.isWarningDialogShowing).toBe(true);
store.isWarningDialogShowing = false;
});
});
describe('loadFiddleFromElectronExample()', () => {
it('loads the example with confirmation', async () => {
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad = jest.fn().mockReturnValue(true);
instance.fetchExampleAndLoad = jest.fn();
await instance.loadFiddleFromElectronExample(
{},
{ path: 'test/path', ref: '4.0.0' }
);
expect(instance.verifyRemoteLoad).toHaveBeenCalled();
expect(instance.fetchExampleAndLoad).toHaveBeenCalledWith('4.0.0', 'test/path');
});
it('does not load the example without confirmation', async () => {
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad = jest.fn().mockReturnValue(false);
instance.fetchExampleAndLoad = jest.fn();
await instance.loadFiddleFromElectronExample(
{},
{ path: 'test/path', ref: '4.0.0' }
);
expect(instance.verifyRemoteLoad).toHaveBeenCalled();
expect(instance.fetchExampleAndLoad).toHaveBeenCalledTimes(0);
});
});
describe('loadFiddleFromGist()', () => {
it('loads the example with confirmation', async () => {
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad = jest.fn().mockReturnValue(true);
instance.fetchGistAndLoad = jest.fn();
await instance.loadFiddleFromGist(
{},
{ id: 'gist' }
);
expect(instance.verifyRemoteLoad).toHaveBeenCalled();
expect(instance.fetchGistAndLoad).toHaveBeenCalledWith('gist');
});
it('does not load the example without confirmation', async () => {
const wrapper = shallow(<AddressBar appState={store} />);
const instance: AddressBar = wrapper.instance() as any;
instance.verifyRemoteLoad = jest.fn().mockReturnValue(false);
instance.fetchGistAndLoad = jest.fn();
await instance.loadFiddleFromGist(
{},
{ id: 'gist' }
);
expect(instance.verifyRemoteLoad).toHaveBeenCalled();
expect(instance.fetchGistAndLoad).toHaveBeenCalledTimes(0);
});
});
});
@@ -0,0 +1,86 @@
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import { BisectHandler } from '../../../src/renderer/components/commands-bisect';
describe('Bisect commands component', () => {
let store: any;
beforeEach(() => {
store = {
Bisector: {
continue: jest.fn(),
getCurrentVersion: jest.fn()
},
setVersion: jest.fn(),
version: '1.0.0',
pushOutput: jest.fn()
};
});
it('renders helper buttons if bisect instance is active', () => {
const wrapper = shallow(<BisectHandler appState={store} />);
expect(wrapper).toMatchSnapshot();
});
it('renders bisect dialog button if no bisect instance', () => {
delete store.Bisector;
const wrapper = shallow(<BisectHandler appState={store} />);
expect(wrapper).toMatchSnapshot();
});
describe('buttons', () => {
let wrapper: ShallowWrapper;
let instance: BisectHandler;
beforeEach(() => {
wrapper = shallow(<BisectHandler appState={store} />);
instance = wrapper.instance() as any;
instance.continueBisect = jest.fn();
});
it('passes in Version=Good when thumbs up button is pressed', () => {
wrapper.find('[icon="thumbs-up"]').simulate('click');
expect(instance.continueBisect).toHaveBeenCalledWith(true);
});
it('passes in Version=Bad when thumbs down button is pressed', () => {
wrapper.find('[icon="thumbs-down"]').simulate('click');
expect(instance.continueBisect).toHaveBeenCalledWith(false);
});
});
describe('continueBisect()', () => {
it('sets version assigned by bisect algorithm', () => {
const wrapper = shallow(<BisectHandler appState={store} />);
const instance: BisectHandler = wrapper.instance() as any;
store.Bisector.continue.mockReturnValue({
version: '2.0.0'
});
instance.continueBisect(true);
expect(store.setVersion).toHaveBeenCalledWith('2.0.0');
});
it('terminates bisect if algorithm returns array', () => {
const wrapper = shallow(<BisectHandler appState={store} />);
const instance: BisectHandler = wrapper.instance() as any;
instance.terminateBisect = jest.fn();
// same value is only returned when there is only 1 version left
store.Bisector.continue.mockReturnValue(['minRev', 'maxRev']);
instance.continueBisect(true);
expect(store.setVersion).not.toHaveBeenCalled();
expect(instance.terminateBisect).toHaveBeenCalled();
expect(store.pushOutput).toHaveBeenCalled();
});
});
describe('terminateBisect()', () => {
it('removes the bisect instance from the app state', () => {
const wrapper = shallow(<BisectHandler appState={store} />);
const instance: BisectHandler = wrapper.instance() as any;
instance.terminateBisect();
expect(store.Bisector).toBeUndefined();
});
});
});
@@ -1,20 +1,20 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { shallow } from "enzyme";
import * as React from "react";
import { PublishButton } from '../../../src/renderer/components/commands-publish-button';
import { getOctokit } from '../../../src/utils/octokit';
import { PublishButton } from "../../../src/renderer/components/commands-publish-button";
import { getOctokit } from "../../../src/utils/octokit";
jest.mock('../../../src/utils/octokit');
jest.mock("../../../src/utils/octokit");
describe('Publish button component', () => {
describe("Publish button component", () => {
let store: any;
const expectedGistCreateOpts = {
description: 'Electron Fiddle Gist',
description: "Electron Fiddle Gist",
files: {
'index.html': { content: 'html-content' },
'renderer.js': { content: 'renderer-content' },
'main.js': { content: 'main-content' },
"index.html": { content: "html-content" },
"renderer.js": { content: "renderer-content" },
"main.js": { content: "main-content" }
},
public: true
};
@@ -25,12 +25,12 @@ describe('Publish button component', () => {
};
});
it('renders', () => {
it("renders", () => {
const wrapper = shallow(<PublishButton appState={store} />);
expect(wrapper).toMatchSnapshot();
});
it('toggles the auth dialog on click if not authed', async () => {
it("toggles the auth dialog on click if not authed", async () => {
store.toggleAuthDialog = jest.fn();
const wrapper = shallow(<PublishButton appState={store} />);
@@ -40,8 +40,8 @@ describe('Publish button component', () => {
expect(store.toggleAuthDialog).toHaveBeenCalled();
});
it('toggles the publish method on click if authed', async () => {
store.gitHubToken = 'github-token';
it("toggles the publish method on click if authed", async () => {
store.gitHubToken = "github-token";
const wrapper = shallow(<PublishButton appState={store} />);
const instance: PublishButton = wrapper.instance() as any;
@@ -51,11 +51,11 @@ describe('Publish button component', () => {
expect(instance.publishFiddle).toHaveBeenCalled();
});
it('attempts to publish to Gist', async () => {
it("attempts to publish to Gist", async () => {
const mockOctokit = {
authenticate: jest.fn(),
gists: {
create: jest.fn(async () => ({ data: { id: '123' } }))
create: jest.fn(async () => ({ data: { id: "123" } }))
}
};
@@ -67,21 +67,21 @@ describe('Publish button component', () => {
await instance.publishFiddle();
expect(mockOctokit.gists.create).toHaveBeenCalledWith({
description: 'Electron Fiddle Gist',
description: "Electron Fiddle Gist",
files: {
'index.html': { content: 'html-content' },
'renderer.js': { content: 'renderer-content' },
'main.js': { content: 'main-content' },
"index.html": { content: "html-content" },
"renderer.js": { content: "renderer-content" },
"main.js": { content: "main-content" }
},
public: true
});
});
it('handles missing content', async () => {
it("handles missing content", async () => {
const mockOctokit = {
authenticate: jest.fn(),
gists: {
create: jest.fn(async () => ({ data: { id: '123' } }))
create: jest.fn(async () => ({ data: { id: "123" } }))
}
};
@@ -90,27 +90,27 @@ describe('Publish button component', () => {
const wrapper = shallow(<PublishButton appState={store} />);
const instance: PublishButton = wrapper.instance() as any;
(window as any).ElectronFiddle.app.getValues.mockReturnValueOnce({});
(window as any).ElectronFiddle.app.getEditorValues.mockReturnValueOnce({});
await instance.publishFiddle();
expect(mockOctokit.gists.create).toHaveBeenCalledWith({
description: 'Electron Fiddle Gist',
description: "Electron Fiddle Gist",
files: {
'index.html': { content: '<!-- Empty -->' },
'renderer.js': { content: '// Empty' },
'main.js': { content: '// Empty' },
"index.html": { content: "<!-- Empty -->" },
"renderer.js": { content: "// Empty" },
"main.js": { content: "// Empty" }
},
public: true
});
});
it('handles an error in Gist publishing', async () => {
it("handles an error in Gist publishing", async () => {
const mockOctokit = {
authenticate: jest.fn(),
gists: {
create: jest.fn(() => {
throw new Error('bwap bwap');
throw new Error("bwap bwap");
})
}
};
@@ -125,12 +125,12 @@ describe('Publish button component', () => {
expect(store.isPublishing).toBe(false);
});
it('uses the privacy setting correctly', async () => {
it("uses the privacy setting correctly", async () => {
const mockOctokit = {
authenticate: jest.fn(),
gists: {
create: jest.fn(() => {
throw new Error('bwap bwap');
throw new Error("bwap bwap");
})
}
};
@@ -145,7 +145,7 @@ describe('Publish button component', () => {
expect(mockOctokit.gists.create).toHaveBeenCalledWith({
...expectedGistCreateOpts,
public: false,
public: false
});
instance.setPublic();
@@ -153,24 +153,24 @@ describe('Publish button component', () => {
expect(mockOctokit.gists.create).toHaveBeenCalledWith({
...expectedGistCreateOpts,
public: true,
public: true
});
});
it('disables during gist publishing', async () => {
it("disables during gist publishing", async () => {
store.isPublishing = false;
const wrapper = shallow(<PublishButton appState={store} />);
const instance: PublishButton = wrapper.instance() as any;
expect(wrapper.find('fieldset').prop('disabled')).toBe(false);
expect(wrapper.find("fieldset").prop("disabled")).toBe(false);
instance.publishFiddle = jest.fn().mockImplementationOnce(() => {
return new Promise((resolve) => {
wrapper.setProps({appState: {store, isPublishing: true}}, () => {
expect(wrapper.find('fieldset').prop('disabled')).toBe(true);
return new Promise(resolve => {
wrapper.setProps({ appState: { store, isPublishing: true } }, () => {
expect(wrapper.find("fieldset").prop("disabled")).toBe(true);
});
wrapper.setProps({appState: {store, isPublishing: false}}, () => {
expect(wrapper.find('fieldset').prop('disabled')).toBe(false);
wrapper.setProps({ appState: { store, isPublishing: false } }, () => {
expect(wrapper.find("fieldset").prop("disabled")).toBe(false);
});
resolve();
});
@@ -179,8 +179,8 @@ describe('Publish button component', () => {
await instance.publishFiddle();
});
describe('privacy menu', () => {
it('toggles the privacy setting', () => {
describe("privacy menu", () => {
it("toggles the privacy setting", () => {
const wrapper = shallow(<PublishButton appState={store} />);
const instance: PublishButton = wrapper.instance() as any;
@@ -1,15 +1,16 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { IpcEvents } from '../../../src/ipc-events';
import { AddVersionDialog } from '../../../src/renderer/components/dialog-add-version';
import { ipcRendererManager } from '../../../src/renderer/ipc';
import { overridePlatform, resetPlatform } from '../../utils';
jest.mock('../../../src/renderer/ipc');
describe('AddVersionDialog component', () => {
let store: any;
const mockFile = {
path: '/test/file'
};
const mockFile = '/test/file';
beforeAll(() => {
// We render the buttons different depending on the
@@ -37,7 +38,7 @@ describe('AddVersionDialog component', () => {
wrapper.setState({
isValidVersion: true,
isValidElectron: true,
file: mockFile
folderPath: mockFile
});
expect(wrapper).toMatchSnapshot();
@@ -45,35 +46,32 @@ describe('AddVersionDialog component', () => {
wrapper.setState({
isValidVersion: false,
isValidElectron: true,
file: mockFile
folderPath: mockFile
});
expect(wrapper).toMatchSnapshot();
});
describe('onChangeFile()', () => {
it('handles the change event', async () => {
it('overrides default input with Electron dialog', () => {
const preventDefault = jest.fn();
const wrapper = shallow(<AddVersionDialog appState={store} />);
const inp = wrapper.find('#custom-electron-version');
inp.dive().find('input[type="file"]').simulate('click', { preventDefault });
expect(ipcRendererManager.send as jest.Mock)
.toHaveBeenCalledWith(IpcEvents.SHOW_LOCAL_VERSION_FOLDER_DIALOG);
expect(preventDefault).toHaveBeenCalled();
});
describe('setFolderPath()', () => {
it('does something', async () => {
store.binaryManager.getIsDownloaded.mockResolvedValue(true);
const wrapper = shallow(<AddVersionDialog appState={store} />);
await (wrapper.instance() as any).setFolderPath('/test/');
await (wrapper.instance() as any).onChangeFile({ target: { files: [ mockFile ] } });
expect(wrapper.state('file')).toBe(mockFile);
});
it('handles invalid input', async () => {
const wrapper = shallow(<AddVersionDialog appState={store} />);
await (wrapper.instance() as any).onChangeFile({ target: { files: [] } });
expect(wrapper.state('file')).toBe(undefined);
});
it('handles the change event and checks for Electron', async () => {
const wrapper = shallow(<AddVersionDialog appState={store} />);
store.binaryManager.getIsDownloaded.mockReturnValueOnce(false);
await (wrapper.instance() as any).onChangeFile({ target: { files: [ mockFile ] } });
expect(wrapper.state('file')).toBe(mockFile);
expect(wrapper.state('isValidElectron')).toBe(false);
expect(wrapper.state('isValidElectron')).toBe(true);
expect(wrapper.state('folderPath')).toBe('/test/');
});
});
@@ -93,7 +91,7 @@ describe('AddVersionDialog component', () => {
expect(wrapper.state('isValidVersion')).toBe(false);
expect(wrapper.state('version')).toBe('foo');
(wrapper.instance() as any).onChangeVersion({ target: { } });
(wrapper.instance() as any).onChangeVersion({ target: {} });
expect(wrapper.state('isValidVersion')).toBe(false);
expect(wrapper.state('version')).toBe('');
});
@@ -113,9 +111,7 @@ describe('AddVersionDialog component', () => {
wrapper.setState({
version: '3.3.3',
file: {
path: '/test/path'
}
folderPath: '/test/path'
});
await (wrapper.instance() as any).onSubmit();
@@ -0,0 +1,124 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { ElectronVersionSource, ElectronVersionState } from '../../../src/interfaces';
import { Bisector } from '../../../src/renderer/bisect';
import { BisectDialog } from '../../../src/renderer/components/dialog-bisect';
import { ElectronReleaseChannel } from '../../../src/renderer/versions';
jest.mock('../../../src/renderer/bisect');
describe('BisectDialog component', () => {
let store: any;
const generateVersionRange = (rangeLength: number) =>
(new Array(rangeLength)).fill(0).map((_, i) => ({
state: ElectronVersionState.ready,
version: `${i + 1}.0.0`,
source: ElectronVersionSource.local
}));
beforeEach(() => {
store = {
versions: generateVersionRange(5),
versionsToShow: [ElectronReleaseChannel.stable],
statesToShow: [ElectronVersionState.ready],
setVersion: jest.fn()
};
});
it('renders', () => {
const wrapper = shallow(<BisectDialog appState={store} />);
// start and end selected
wrapper.setState({
startIndex: 3,
endIndex: 0,
allVersions: generateVersionRange(5)
});
expect(wrapper).toMatchSnapshot();
// no selection
wrapper.setState({
startIndex: undefined,
endIndex: undefined,
allVersions: generateVersionRange(5)
});
expect(wrapper).toMatchSnapshot();
// only start selected
wrapper.setState({
startIndex: 3,
endIndex: undefined,
allVersions: generateVersionRange(5)
});
expect(wrapper).toMatchSnapshot();
});
describe('onBeginSelect()', () => {
it('sets the begin version', () => {
const wrapper = shallow(<BisectDialog appState={store} />);
const instance: BisectDialog = wrapper.instance() as any;
expect(instance.state.startIndex).toBeUndefined();
instance.onBeginSelect(store.versions[2]);
expect(instance.state.startIndex).toBe(2);
});
});
describe('onEndSelect()', () => {
it('sets the end version', () => {
const wrapper = shallow(<BisectDialog appState={store} />);
const instance: BisectDialog = wrapper.instance() as any;
expect(instance.state.endIndex).toBeUndefined();
instance.onEndSelect(store.versions[2]);
expect(instance.state.endIndex).toBe(2);
});
});
describe('onSubmit()', () => {
it('initiates a bisect instance and sets a version', async () => {
const version = '1.0.0';
(Bisector as jest.Mock).mockImplementation(() => {
return {
getCurrentVersion: () => ({ version })
};
});
const versions = generateVersionRange(5);
const wrapper = shallow(<BisectDialog appState={store} />);
wrapper.setState({
startIndex: 4,
endIndex: 0,
allVersions: versions
});
const instance: BisectDialog = wrapper.instance() as any;
await instance.onSubmit();
expect(Bisector).toHaveBeenCalledWith(versions.slice(0, 5).reverse());
expect(store.Bisector).toBeDefined();
expect(store.setVersion).toHaveBeenCalledWith(version);
});
it('does nothing if endIndex or startIndex are falsy', async () => {
const wrapper = shallow(<BisectDialog appState={store} />);
wrapper.setState({
startIndex: undefined,
endIndex: 0
});
const instance1: BisectDialog = wrapper.instance() as any;
await instance1.onSubmit();
expect(Bisector).not.toHaveBeenCalled();
wrapper.setState({
startIndex: 4,
endIndex: undefined
});
const instance2: BisectDialog = wrapper.instance() as any;
await instance2.onSubmit();
expect(Bisector).not.toHaveBeenCalled();
});
});
});
@@ -17,7 +17,7 @@ describe('TokenDialog component', () => {
store = {
isWarningDialogShowing: false,
warningDialogTexts: { label: '', ok: '', cancel: '' },
toogleWarningDialog: jest.fn()
toggleWarningDialog: jest.fn()
};
});
@@ -37,6 +37,6 @@ describe('TokenDialog component', () => {
const instance: WarningDialog = wrapper.instance() as any;
instance.onClose(true);
expect(store.toogleWarningDialog).toHaveBeenCalledTimes(1);
expect(store.toggleWarningDialog).toHaveBeenCalledTimes(1);
});
});
+3 -9
View File
@@ -26,27 +26,21 @@ describe('Dialogs component', () => {
resetPlatform();
});
it('renders initially without visible dialogs', () => {
const wrapper = shallow(<Dialogs appState={store} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.dialogs').html()).toBe('<div class="dialogs"></div>');
});
it('renders the token dialog', () => {
store.isTokenDialogShowing = true;
const wrapper = shallow(<Dialogs appState={store} />);
expect(wrapper.text()).toBe('<TokenDialog /><WarningDialog />');
expect(wrapper.text()).toBe('<TokenDialog /><ConfirmDialog />');
});
it('renders the settings dialog', () => {
store.isSettingsShowing = true;
const wrapper = shallow(<Dialogs appState={store} />);
expect(wrapper.text()).toBe('<Settings /><WarningDialog />');
expect(wrapper.text()).toBe('<Settings /><ConfirmDialog />');
});
it('renders the settings dialog', () => {
store.isAddVersionDialogShowing = true;
const wrapper = shallow(<Dialogs appState={store} />);
expect(wrapper.text()).toBe('<AddVersionDialog /><WarningDialog />');
expect(wrapper.text()).toBe('<AddVersionDialog /><ConfirmDialog />');
});
});
+69 -43
View File
@@ -8,6 +8,7 @@ describe('Editor component', () => {
let store: any;
let monaco: any;
const editorDispose = jest.fn();
const updateOptions = jest.fn();
beforeEach(() => {
store = {
@@ -26,7 +27,9 @@ describe('Editor component', () => {
setModel: jest.fn(),
restoreViewState: jest.fn()
})),
createModel: jest.fn(),
createModel: jest.fn(() => ({
updateOptions
})),
setModel: jest.fn(),
dispose: jest.fn()
}
@@ -35,7 +38,7 @@ describe('Editor component', () => {
it('renders the editor container', () => {
const wrapper = shallow(
<Editor appState={store} monaco={monaco} monoacoOptions={{}} id={EditorId.main} />
<Editor appState={store} monaco={monaco} monacoOptions={{}} id={EditorId.main} />
);
expect(wrapper.html()).toBe('<div class="editorContainer"></div>');
@@ -43,13 +46,13 @@ describe('Editor component', () => {
it('correctly sets the language', () => {
let wrapper = shallow(
<Editor appState={store} monaco={monaco} monoacoOptions={{}} id={EditorId.main} />
<Editor appState={store} monaco={monaco} monacoOptions={{}} id={EditorId.main} />
);
expect((wrapper.instance() as any).language).toBe('javascript');
wrapper = shallow(
<Editor appState={store} monaco={monaco} monoacoOptions={{}} id={EditorId.html} />
<Editor appState={store} monaco={monaco} monacoOptions={{}} id={EditorId.html} />
);
expect((wrapper.instance() as any).language).toBe('html');
@@ -57,7 +60,7 @@ describe('Editor component', () => {
it('denies updates', () => {
const wrapper = shallow(
<Editor appState={store} monaco={monaco} monoacoOptions={{}} id={EditorId.main} />
<Editor appState={store} monaco={monaco} monacoOptions={{}} id={EditorId.main} />
);
expect((wrapper as any)
@@ -66,49 +69,72 @@ describe('Editor component', () => {
).toBe(false);
});
it('initMonaco() attempts to create an editor', async () => {
const didMount = jest.fn();
const wrapper = shallow(
<Editor
appState={store}
monaco={monaco}
monoacoOptions={{}}
id={EditorId.main}
editorDidMount={didMount}
/>
);
const instance: any = wrapper.instance();
describe('initMonaco()', async () => {
it('attempts to create an editor', async () => {
const didMount = jest.fn();
const wrapper = shallow(
<Editor
appState={store}
monaco={monaco}
monacoOptions={{}}
id={EditorId.main}
editorDidMount={didMount}
/>
);
const instance: any = wrapper.instance();
instance.containerRef.current = 'ref';
await instance.initMonaco();
instance.containerRef.current = 'ref';
await instance.initMonaco();
expect(didMount).toHaveBeenCalled();
expect(monaco.editor.create).toHaveBeenCalled();
expect(monaco.editor.createModel).toHaveBeenCalled();
});
it('initMonaco() attempts to create an editor and restores a backup', async () => {
store.getAndRemoveEditorValueBackup.mockReturnValueOnce({
model: true,
viewState: true
expect(didMount).toHaveBeenCalled();
expect(monaco.editor.create).toHaveBeenCalled();
expect(monaco.editor.createModel).toHaveBeenCalled();
});
const wrapper = shallow(
<Editor
appState={store}
monaco={monaco}
monoacoOptions={{}}
id={EditorId.main}
editorDidMount={() => undefined}
/>
);
const instance: any = wrapper.instance();
it('attempts to restore a backup if available', async () => {
store.getAndRemoveEditorValueBackup.mockReturnValueOnce({
model: true,
viewState: true
});
instance.containerRef.current = 'ref';
await instance.initMonaco();
const wrapper = shallow(
<Editor
appState={store}
monaco={monaco}
monacoOptions={{}}
id={EditorId.main}
editorDidMount={() => undefined}
/>
);
const instance: any = wrapper.instance();
expect(instance.editor.restoreViewState).toHaveBeenCalledTimes(1);
expect(instance.editor.setModel).toHaveBeenCalledTimes(1);
instance.containerRef.current = 'ref';
await instance.initMonaco();
expect(instance.editor.restoreViewState).toHaveBeenCalledTimes(1);
expect(instance.editor.setModel).toHaveBeenCalledTimes(1);
});
it('initializes with a fixed tab size', async () => {
const didMount = jest.fn();
const wrapper = shallow(
<Editor
appState={store}
monaco={monaco}
monacoOptions={{}}
id={EditorId.main}
editorDidMount={didMount}
/>
);
const instance: any = wrapper.instance();
instance.containerRef.current = 'ref';
await instance.initMonaco();
expect(updateOptions).toHaveBeenCalledWith(expect.objectContaining({
tabSize: 2
}));
});
});
it('componentWillUnmount() attempts to dispose the editor', async () => {
@@ -117,7 +143,7 @@ describe('Editor component', () => {
<Editor
appState={store}
monaco={monaco}
monoacoOptions={{}}
monacoOptions={{}}
id={EditorId.main}
editorDidMount={didMount}
/>
+30 -20
View File
@@ -10,9 +10,11 @@ import { updateEditorLayout } from '../../../src/utils/editor-layout';
import { createMosaicArrangement } from '../../../src/utils/editors-mosaic-arrangement';
import { getFocusedEditor } from '../../../src/utils/focused-editor';
jest.mock('monaco-loader', () => jest.fn(async () => {
return { monaco: true };
}));
jest.mock('monaco-loader', () =>
jest.fn(async () => {
return { monaco: true };
})
);
jest.mock('../../../src/renderer/components/editor', () => ({
Editor: () => 'Editor'
@@ -84,11 +86,10 @@ describe('Editors component', () => {
});
it('handles an error', () => {
(window.ElectronFiddle.editors.html!.updateOptions as jest.Mock).mockImplementationOnce(
() => {
throw new Error('Bwap bwap');
}
);
(window.ElectronFiddle.editors.html!
.updateOptions as jest.Mock).mockImplementationOnce(() => {
throw new Error('Bwap bwap');
});
const wrapper = shallow(<Editors appState={store} />);
const instance: Editors = wrapper.instance() as any;
@@ -101,19 +102,22 @@ describe('Editors component', () => {
const instance: Editors = wrapper.instance() as any;
expect(instance.toggleEditorOption('wordWrap')).toBe(true);
expect(window.ElectronFiddle.editors.html!.updateOptions).toHaveBeenCalledWith(
{
minimap: { enabled: false },
wordWrap: 'off'
}
);
expect(
window.ElectronFiddle.editors.html!.updateOptions
).toHaveBeenCalledWith({
minimap: { enabled: false },
wordWrap: 'off'
});
});
});
it('renders a toolbar', () => {
const wrapper = shallow(<Editors appState={store} />);
const instance: Editors = wrapper.instance() as any;
const toolbar = instance.renderToolbar({ title: TITLE_MAP[EditorId.main] } as any, EditorId.main);
const toolbar = instance.renderToolbar(
{ title: TITLE_MAP[EditorId.main] } as any,
EditorId.main
);
expect(toolbar).toMatchSnapshot();
});
@@ -162,7 +166,7 @@ describe('Editors component', () => {
ipcRendererManager.emit(IpcEvents.FS_NEW_FIDDLE, null);
process.nextTick(() => {
expect(window.ElectronFiddle.app.fileManager.setFiddle).toHaveBeenCalled();
expect(window.ElectronFiddle.app.replaceFiddle).toHaveBeenCalled();
done();
});
});
@@ -172,9 +176,15 @@ describe('Editors component', () => {
ipcRendererManager.emit(IpcEvents.MONACO_TOGGLE_OPTION, null, 'wordWrap');
expect(window.ElectronFiddle.editors.html!.updateOptions).toHaveBeenCalled();
expect(window.ElectronFiddle.editors.renderer!.updateOptions).toHaveBeenCalled();
expect(window.ElectronFiddle.editors.main!.updateOptions).toHaveBeenCalled();
expect(
window.ElectronFiddle.editors.html!.updateOptions
).toHaveBeenCalled();
expect(
window.ElectronFiddle.editors.renderer!.updateOptions
).toHaveBeenCalled();
expect(
window.ElectronFiddle.editors.main!.updateOptions
).toHaveBeenCalled();
});
});
@@ -185,7 +195,7 @@ describe('Editors component', () => {
shallow(<Editors appState={store} />);
process.nextTick(() => {
expect(window.ElectronFiddle.app.monaco).toEqual({ monaco: true});
expect(window.ElectronFiddle.app.monaco).toEqual({ monaco: true });
done();
});
});
+45 -11
View File
@@ -2,20 +2,43 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { Output } from '../../../src/renderer/components/output';
import { MockState } from '../../mocks/state';
let mockContext: any = {};
jest.mock('react-mosaic-component', () => {
const { MosaicContext, MosaicRootActions } = jest.requireActual('react-mosaic-component');
return {
MosaicContext,
MosaicRootActions
};
});
beforeAll(() => {
mockContext = {
mosaicActions: {
expand: jest.fn(),
remove: jest.fn(),
hide: jest.fn(),
replaceWith: jest.fn(),
updateTree: jest.fn(),
getRoot: jest.fn()
},
mosaicId: 'output'
};
});
describe('Output component', () => {
let store: any;
beforeEach(() => {
store = {
isConsoleShowing: true,
output: []
};
store = new MockState();
});
it('renders', () => {
const wrapper = shallow(<Output appState={store} />);
expect(wrapper.html()).toBe('<div class="output showing"></div>');
expect(wrapper.html()).toBe('<div class="output"></div>');
});
it('renders with output', () => {
@@ -54,14 +77,25 @@ describe('Output component', () => {
expect(result).toBeTruthy();
});
it('hides the console via class', () => {
it('hides the console with react-mosaic-component', () => {
// manually trigger lifecycle methods so that
// context can be set before mounting method
const wrapper = shallow(<Output appState={store} />, {
context: mockContext,
disableLifecycleMethods: true
});
wrapper.instance().context = mockContext;
wrapper.instance().componentDidMount!();
expect(mockContext.mosaicActions.expand).toHaveBeenCalledWith(['first'], 25);
store.isConsoleShowing = false;
const wrapper = shallow(
<Output appState={store} />
);
expect(wrapper.html().includes('showing')).toBe(false);
// Todo: There's a scary bug here in Jest / Enzyme. At this point in time,
// the context is {}. That's never the case in production.
// expect(mockContext.mosaicActions.expand).toHaveBeenCalledWith(['first'], 0);
expect(wrapper.html()).toBe(null);
});
it('handles componentDidUpdate', () => {

Some files were not shown because too many files have changed in this diff Show More