├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .github └── stale.yml ├── .gitignore ├── .travis.yml ├── .watchmanconfig ├── ATTRIBUTION.md ├── LICENSE ├── README.md ├── app ├── app.js ├── application │ ├── adapter.js │ ├── route.js │ └── template.hbs ├── components │ ├── .gitkeep │ ├── app-version │ │ ├── component.js │ │ └── template.hbs │ ├── config-form │ │ ├── component.js │ │ └── template.hbs │ ├── context-menu │ │ ├── component.js │ │ └── template.hbs │ ├── download-form │ │ ├── component.js │ │ └── template.hbs │ ├── fetch-summary │ │ ├── component.js │ │ └── template.hbs │ ├── file-selector │ │ ├── component.js │ │ └── template.hbs │ ├── get-started-link │ │ ├── component.js │ │ └── template.hbs │ ├── hold-button │ │ ├── component.js │ │ └── template.hbs │ ├── local-problem-selector │ │ ├── component.js │ │ └── template.hbs │ ├── new-release │ │ ├── component.js │ │ └── template.hbs │ ├── restore-summary │ │ ├── component.js │ │ └── template.hbs │ ├── submit-status │ │ ├── component.js │ │ └── template.hbs │ ├── track-action-menu │ │ ├── component.js │ │ └── template.hbs │ ├── track-selector │ │ ├── component.js │ │ └── template.hbs │ ├── track-status │ │ ├── component.js │ │ └── template.hbs │ └── window-menu │ │ ├── component.js │ │ └── template.hbs ├── configuration │ ├── route.js │ ├── service.js │ └── template.hbs ├── debug │ ├── controller.js │ ├── route.js │ ├── service.js │ └── template.hbs ├── download │ ├── loading │ │ └── template.hbs │ ├── route.js │ ├── status │ │ ├── route.js │ │ └── template.hbs │ └── template.hbs ├── error │ └── template.hbs ├── exercism │ └── service.js ├── help │ └── template.hbs ├── helpers │ └── .gitkeep ├── index.html ├── index │ ├── route.js │ └── template.hbs ├── initializers │ └── selections.js ├── loading │ └── template.hbs ├── models │ └── .gitkeep ├── notifier │ └── service.js ├── problem │ └── model.js ├── resolver.js ├── router.js ├── selections │ └── service.js ├── status │ └── model.js ├── styles │ └── app.css ├── submission │ └── model.js ├── track │ └── model.js ├── tracks │ ├── loading │ │ └── template.hbs │ ├── route.js │ ├── template.hbs │ └── track │ │ ├── fetch-all │ │ ├── route.js │ │ └── template.hbs │ │ ├── fetch │ │ ├── route.js │ │ └── template.hbs │ │ ├── loading │ │ └── template.hbs │ │ ├── local-problems │ │ ├── route.js │ │ └── template.hbs │ │ ├── problems │ │ └── problem │ │ │ ├── route.js │ │ │ └── skip │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── restore │ │ ├── route.js │ │ └── template.hbs │ │ ├── route.js │ │ ├── status │ │ ├── route.js │ │ ├── submission │ │ │ ├── route.js │ │ │ └── template.hbs │ │ └── template.hbs │ │ ├── submit-status │ │ ├── route.js │ │ └── template.hbs │ │ └── template.hbs └── updates │ ├── route.js │ └── template.hbs ├── appveyor.yml ├── bower.json ├── config └── environment.js ├── ember-cli-build.js ├── ember-electron ├── .compilerc ├── electron-forge-config.js ├── main.js ├── resources-darwin │ └── .gitkeep ├── resources-linux │ ├── .gitkeep │ └── icons │ │ └── linux.png ├── resources-win32 │ └── .gitkeep └── resources │ └── .gitkeep ├── mirage ├── config.js ├── factories │ ├── languages.js │ ├── problem.js │ ├── status.js │ └── track.js ├── models │ ├── problem.js │ ├── status.js │ └── track.js ├── scenarios │ └── default.js └── serializers │ └── application.js ├── package.json ├── public ├── assets │ ├── icons │ │ ├── darwin.icns │ │ └── win32.ico │ └── images │ │ ├── e_red_small.png │ │ └── logo.png ├── crossdomain.xml └── robots.txt ├── resources └── anim.gif ├── testem-electron.js ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ ├── download-test.js │ ├── problems-test.js │ └── tracks-test.js ├── electron.js ├── ember-electron │ └── main.js ├── helpers │ ├── destroy-app.js │ ├── flash-message.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ ├── config-form │ │ └── component-test.js │ │ ├── download-form │ │ └── component-test.js │ │ ├── fetch-summary │ │ └── component-test.js │ │ ├── file-selector │ │ └── component-test.js │ │ ├── local-problem-selector │ │ └── component-test.js │ │ ├── new-release │ │ └── component-test.js │ │ ├── submit-status │ │ └── component-test.js │ │ ├── track-action-menu │ │ └── component-test.js │ │ ├── track-selector │ │ └── component-test.js │ │ └── track-status │ │ └── component-test.js ├── package.json ├── test-helper.js └── unit │ ├── .gitkeep │ ├── application │ └── adapter-test.js │ ├── configuration │ ├── route-test.js │ └── service-test.js │ ├── download │ ├── route-test.js │ └── status │ │ └── route-test.js │ ├── exercism │ └── service-test.js │ ├── notifier │ └── service-test.js │ ├── track │ └── model-test.js │ ├── tracks │ └── track │ │ └── restore │ │ └── route-test.js │ └── updates │ └── route-test.js ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false, 9 | "usePods": true 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 6, 5 | sourceType: 'module' 6 | }, 7 | extends: 'eslint:recommended', 8 | env: { 9 | browser: true 10 | }, 11 | rules: { 12 | }, 13 | globals: { 14 | process: true, 15 | requireNode: true, 16 | server: true 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/.github/stale.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | electron-out/ 7 | 8 | # dependencies 9 | /node_modules 10 | /bower_components 11 | 12 | # misc 13 | /.sass-cache 14 | /connect.lock 15 | /coverage/* 16 | /libpeerconnection.log 17 | npm-debug.log* 18 | testem.log 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | matrix: 4 | include: 5 | - os: osx 6 | node_js: '6' 7 | osx_image: xcode8.2 8 | env: ELECTRON_PLATFORM=darwin 9 | - os: linux 10 | node_js: '6' 11 | sudo: required 12 | dist: trusty 13 | env: ELECTRON_PLATFORM=linux CC=clang CXX=clang++ npm_config_clang=1 14 | 15 | cache: 16 | yarn: true 17 | directories: 18 | - node_modules 19 | - bower_components 20 | - "$HOME/.npm" 21 | - "$HOME/.cache" 22 | 23 | addons: 24 | apt: 25 | packages: 26 | - clang 27 | - xvfb 28 | 29 | before_install: 30 | # - npm config set spin false 31 | - npm install -g bower yarn@0.23.2 32 | 33 | install: 34 | - yarn install 35 | - bower install 36 | - export DISPLAY=':99.0' 37 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 38 | 39 | script: 40 | - '[ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || export PATH="$PATH:node_modules/ember-cli/bin"' 41 | - ember --version 42 | - ember electron:test 43 | 44 | after_success: 45 | - if [ -z "$TRAVIS_TAG" ]; then echo "Done!"; exit 0; fi 46 | - export ARTIFACT_NAME=exercism-gui-$ELECTRON_PLATFORM-x64 47 | # Icon is set as a BrowserWindow option for Linux, so the icon option is only valid for Mac and disregarded for Linux 48 | - ember electron:package --platform $ELECTRON_PLATFORM --arch x64 --icon public/assets/icons/darwin.icns --environment production 49 | - tar -C electron-out -czvf electron-out/$ARTIFACT_NAME-$TRAVIS_TAG.tar.gz $ARTIFACT_NAME 50 | - ls -lah electron-out 51 | 52 | deploy: 53 | provider: releases 54 | skip_cleanup: true 55 | api_key: 56 | secure: iay/2iEavXrbHvw+S3KgMxv7zjiBlS81pxFgFjbxVQa4SMXlkpkwgn3DyBai1erlTs9PBJBVtQ9q8ByYmq9IMqeiNSaBRGrhy1iYxLfJYyp717NTRZQ74X5hVYv8AvZNt6ny+cSd1loF6L9tiHFkm1BqsIs0r/Q05MQLWIvyvNMLvy8s5flttO6KWnsNIYJ0sKN6nYgLgZEGyF6xyeux0CaDC0qoww1Okl4uSP/7w0MZDTBSKhQWl6NzRbvZyyCI+DpGnx413lMEJ+Pe2a8nCEzSgZALecxMWS+TK9AVJ/hm4Tr1dDuzbBFkVIyrKpGDzKNCXrczSvS012tbjQJT/cry0usEsAe6Ncvxeh1jKI8VxWX70gap4IkNbc7rlpu1f+8ao77mE72ing1E4N6rBQxoGDQ8hJH4gtyT9fUPAwnVmVnlavdATdr4kfd5cOvP0VRM0hzLNMW6V8KmK7MRQp3IfEaPFbtUgqWp6HgWsuhB5H8TurbknhgAOC1H9RK9Gtzx1HavCliE0yhAtRmEVAy58oJABMgbzaafK3QKUgyCUpbBE8SYIU+R9D/Z8o9fTk7vpFTrBV9q3edK7s9hvFU9Rky6K7Tqk8LI28PmSbq/RRS3y0or2TbcGEzbEElkv4EwVnXzByeFE4Vc4urwBgJGEaVpQtd07oG1WIRVB/Q= 57 | file_glob: true 58 | file: electron-out/*.tar.gz 59 | on: 60 | tags: true 61 | repo: exercism/gui 62 | all_branches: true 63 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /ATTRIBUTION.md: -------------------------------------------------------------------------------- 1 | **e_red_small.png** https://github.com/exercism/exercism.io/blob/master/public/img/e_red_small.png 2 | Using the official exercism logo icon 3 | 4 | **logo.png** https://github.com/exercism/exercism.io/blob/master/public/img/logo.png 5 | Using the official exercism logo 6 | 7 | **All the languages logos** All images are downloaded from exercism.io 8 | refer to https://github.com/exercism/exercism.io/blob/master/ATTRIBUTION.md 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Pablo Klijnjan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/exercism/gui.svg?label=ready&title=Ready)](https://waffle.io/exercism/gui) 2 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/m7djinnk5hcyivab?svg=true)](https://ci.appveyor.com/project/holandes22/exercism-gui) 3 | [![Linux/Mac Build Status](https://travis-ci.org/exercism/gui.svg?branch=master)](https://travis-ci.org/exercism/gui) 4 | [![Dependency Status](https://david-dm.org/exercism/gui.svg)](https://david-dm.org/exercism/gui) 5 | [![devDependency Status](https://david-dm.org/exercism/gui/dev-status.svg)](https://david-dm.org/exercism/gui#info=devDependencies) 6 | [![Code Climate](https://codeclimate.com/github/exercism/gui/badges/gpa.svg)](https://codeclimate.com/github/exercism/gui) 7 | 8 | 9 | # Overview 10 | 11 | The purpose of excercism GUI is to provide an alternative to the command line interface (CLI) for [exercism.io](http://exercism.io/). 12 | 13 | It aims to be a cross-platform desktop app that lowers the barrier of entry for people which feel 14 | more comfortable with a graphical interface than the command line. 15 | 16 | ![](https://github.com/exercism/gui/blob/master/resources/anim.gif) 17 | 18 | # Installing and running the app 19 | 20 | **Note:** 21 | 22 | > Currently there is no installer available, this is a planned feature. Check ticket #6 for more details 23 | 24 | The application is distributed using a compressed package (`.tar.gz` for Linux/MacOS and `.zip` for Windows) 25 | To start using it, simply download the appropiate package for your platform from [here](https://github.com/exercism/gui/releases/latest) 26 | and extract it anywhere you like in the file system. 27 | 28 | All the needed files for the app to run are contained within the extracted folder, nothing is installed outside of it. 29 | 30 | To start the app, go to the extracted folder and double click on the **exercism-gui** executable. 31 | 32 | 33 | ## Upgrade 34 | 35 | Until an installer is available, upgrading is a manual process. To "upgrade" simply download the new package and extract over the old files. 36 | Or extract to a new folder and remove the old version. 37 | Removing the old folder is completely safe as no user information is stored in it (unless you explicitly configure the `exercism` folder to be 38 | under it, which is *not* recommended) 39 | 40 | ## Remove 41 | 42 | Until an installer is available, removing is simply a matter of deleting the extracted folder. 43 | 44 | # Supported platforms 45 | 46 | Platform | Architecture 47 | ------------ | ------------- 48 | GNU/Linux (any distro)| 64 Bits (x64) 49 | MacOS X | 64 Bits (x64) 50 | Windows 7,8,10 | 64 Bits (x64) 51 | Windows 7,8,10 | 32 Bits (x86, ia32) 52 | 53 | 54 | # Contributing guide 55 | 56 | Contributions are more than welcome! 57 | 58 | To help with the code a basic knowledge of Javascript is required. 59 | 60 | If Javascript or programming is not your thing, there are many ways to help: 61 | 62 | - Testing the app on the different supported platforms (nothing fancy, just using it normally is enough) 63 | - Design: if you have some thoughts on how to improve the UI or UX, let's hear them! 64 | - Writing docs to help newcomers getting started 65 | - Giving feedback: things that are not clear or hard to understand 66 | 67 | ## Project overview 68 | 69 | This is an application written with the help of the following technologies: 70 | 71 | - [ember.js](http://emberjs.com/) An awesome javascript web framework 72 | - [electron](http://electron.atom.io/) To build the cross-platform desktop app 73 | - [ember-electron](https://github.com/felixrieseberg/ember-electron) an ember.js addon that facilitates 74 | ember and electron integration (dev, packaging, running tests, etc.) 75 | - [node.js](https://nodejs.org) and node packages to communicate with the desktop (notifications, file system, etc.) 76 | 77 | ## Project structure 78 | 79 | This is a standard ember.js app (wrapped with electron) so if you are familiar with ember and [ember-cli](https://ember-cli.com/) you can jump right into. 80 | 81 | If you are not familiar with the framework, getting to know ember is needed, but it is very easy to get started with. 82 | Some resources: 83 | 84 | - [The ember.js guides](https://guides.emberjs.com) 85 | - Great free ebook (but you can pay for it if you find it useful): https://leanpub.com/ember-cli-101 86 | - Great free learning videos https://www.emberscreencasts.com/ 87 | 88 | The project uses the "pod" folder structure (you don't need to worry about this if using ember-cli generators 89 | as this is handled automatically, just be aware if creating files manually) 90 | 91 | ## Coding conventions 92 | 93 | At build time, files are inspected with jshint so make sure there are not warnings. 94 | 95 | ## Writing tests 96 | 97 | - Follow ember conventions as close as possible https://guides.emberjs.com/v2.6.0/testing/ 98 | - Use data-test-XYZ selectors to find HTML components during tests, which helps decoupling 99 | the HTML layout from finding stuff (thus we avoid breaking tests if we decide to do some 100 | re-designing). We use `ember-test-selectors` addon to help manage this. 101 | 102 | ## Setting up the dev env 103 | 104 | ### Prerequisites: 105 | 106 | - Node.js 6 or later (check install instructions at https://nodejs.org/en/) 107 | - Bower: `npm install -g bower` 108 | - Yarn (check install instructions at https://yarnpkg.com/en/docs/install) 109 | - Watchman: optional but highly recommended (to listen on file changes for automatic rebuilds during dev: https://facebook.github.io/watchman/) 110 | - Python 2.7: A dependency of this project, [node-gyp](https://github.com/nodejs/node-gyp#installation), requires Python 2.7 (check install instructions at https://www.python.org/downloads/). Python 3 is *not* supported. If you have Python 3 as your primary Python environment you can use the `--python` switch when running `npm install`: `npm install --python=python2.7` 111 | 112 | 113 | ### Clone and get started 114 | 115 | After forking and cloning the exercism/gui repo, install the dependencies: 116 | 117 | $ cd /path/to/repo 118 | $ yarn install 119 | $ bower install 120 | 121 | Start the dev app 122 | 123 | $ ember electron 124 | 125 | _Note:_ The dev server by default uses [ember-cli-mirage](http://www.ember-cli-mirage.com/) to 126 | intercept outgoing requests and mock API responses. This has the purpose of avoiding 127 | extra load on the API and be able to develop even if the API servers are down. 128 | If you want to disable this, set an envar DISABLE_EMBER_CLI_MIRAGE with a value of true: 129 | 130 | $ DISABLE_EMBER_CLI_MIRAGE=true ember electron 131 | 132 | ### Running tests 133 | 134 | To run the tests, do 135 | 136 | $ ember electron:test 137 | 138 | If you want to leave the test running on each file save, TDD style: 139 | 140 | $ ember electron:test --server 141 | 142 | ### Debugging 143 | 144 | #### Dev 145 | You have the regular debugging tools at your disposal: chromium dev tools, devtron and ember inspector. 146 | 147 | You can access the chromium dev tools via the menu in the dev server window (View -> Toggle Developer Tools) or by shortcut (Ctrl+Shift+I on Linux) 148 | 149 | TODO: specify dev tools shortcuts for Mac/Windows 150 | 151 | You can access the ember inspector and devtron from their respective tabs within the dev tools. 152 | 153 | #### Production 154 | 155 | In production, chrome dev tools can be accessed by right clicking on at app and selecting `View -> Toggle Developer Tools` 156 | 157 | ## Packaging 158 | 159 | In order to package the app, run the following 160 | 161 | ember electron:package --platform --arch 162 | 163 | This will output a package under the `./electron-builds` folder 164 | 165 | _Note_: If you are on OSX or Linux and Have [Wine](https://www.winehq.org/) configured, you can also cross-compile for 166 | Windows 167 | 168 | ## Submitting a PR 169 | 170 | - If there is a ticket connected to the PR, add it as prefix in the subject. e.g. `gh-3 some comment closes #3` 171 | - If the commit closes one or several tickets, add comment like so `closes #` 172 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /app/application/adapter.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ActiveModelAdapter from 'active-model-adapter'; 3 | 4 | const urlJoin = requireNode('url-join'), 5 | lodash = requireNode('lodash'), 6 | url = requireNode('url'); 7 | 8 | export default ActiveModelAdapter.extend({ 9 | configuration: Ember.inject.service(), 10 | 11 | host: Ember.computed(function() { 12 | return this.get('configuration.xapi'); 13 | }), 14 | 15 | addKeyParam(fullUrl) { 16 | let params = url.parse(fullUrl, true).query, 17 | apiKey = this.get('configuration.apiKey'), 18 | suffix = ''; 19 | if (lodash.isEmpty(params)) { 20 | suffix = `?key=${apiKey}`; 21 | } else if (!params.key) { 22 | suffix = `&key=${apiKey}`; 23 | } 24 | return `${fullUrl}${suffix}`; 25 | }, 26 | 27 | ajax() { 28 | let [fullUrl, ...rest] = arguments; 29 | return this._super(this.addKeyParam(fullUrl), ...rest); 30 | }, 31 | 32 | handleResponse: function(status, headers, payload, requestData) { 33 | if (!this.isSuccess() && payload && payload.error && !payload.errors) { 34 | payload.errors = [ 35 | { 36 | status: `${status}`, 37 | title: "The backend responded with an error", 38 | detail: payload.error 39 | } 40 | ]; 41 | } 42 | return this._super(status, headers, payload, requestData); 43 | }, 44 | 45 | buildURL(modelName, id, snapshot, requestType, query) { 46 | if (requestType === 'GET' && modelName === 'problem') { 47 | // URL for restore problems 48 | return urlJoin(this.get('configuration.xapi'), `/v2/exercises/`); 49 | } 50 | if (requestType === 'POST' && modelName === 'problem') { 51 | // URL for skip problem 52 | let track = snapshot.attr('trackId'), 53 | slug = snapshot.attr('slug'); 54 | return urlJoin(this.get('configuration.api'), `/api/v1/iterations/${track}/${slug}`); 55 | } 56 | if (requestType === 'POST' && modelName === 'submission') { 57 | return urlJoin(this.get('configuration.api'), '/api/v1/user'); 58 | } 59 | return this._super(modelName, id, snapshot, requestType, query); 60 | }, 61 | 62 | urlForQuery(query, modelName) { 63 | if (modelName === 'problem') { 64 | let url = urlJoin(this.get('host'), `/v2/exercises/${query.track_id}`); 65 | if (query.slug) { 66 | return urlJoin(url, `${query.slug}`); 67 | } 68 | return url; 69 | } 70 | return this._super(...arguments); 71 | }, 72 | 73 | urlForQueryRecord(query, modelName) { 74 | if (modelName === 'submission') { 75 | return urlJoin(this.get('configuration.api'), `/api/v1/submissions/${query.track_id}/${query.slug}`); 76 | } 77 | return this._super(...arguments); 78 | }, 79 | 80 | urlForFindRecord(id, modelName) { 81 | if (modelName === 'status') { 82 | return urlJoin(this.get('configuration.api'), `/api/v1/tracks/${id}/status`); 83 | } 84 | if (modelName === 'submission') { 85 | return urlJoin(this.get('configuration.api'), `/api/v1/submissions/${id}`); 86 | } 87 | return this._super(...arguments); 88 | }, 89 | 90 | findRecord (store, type, id, snapshot) { 91 | return this._super(...arguments).then((response) => { 92 | if (snapshot.modelName === 'status') { 93 | response.id = response.track_id; 94 | response = { status: response }; 95 | } 96 | if (snapshot.modelName === 'submission') { 97 | response.id = response.uuid; 98 | response = { submission: response }; 99 | } 100 | return response; 101 | }); 102 | }, 103 | 104 | queryRecord(store, type) { 105 | return this._super(...arguments).then((response) => { 106 | if (type.modelName === 'submission') { 107 | response.id = url.parse(response.url).path.split('/').slice(-1)[0]; 108 | response = { submission: response }; 109 | } 110 | return response; 111 | }); 112 | } 113 | }); 114 | -------------------------------------------------------------------------------- /app/application/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | refreshModel() { 6 | this.refresh(); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /app/application/template.hbs: -------------------------------------------------------------------------------- 1 | {{context-menu}} 2 | 33 | 34 |
35 | {{#each flashMessages.queue as |flash|}} 36 | {{#flash-message flash=flash as |component flash|}} 37 | {{#if flash.componentName}} 38 | {{component flash.componentName content=flash.content}} 39 | {{else}} 40 |
41 |
42 | {{flash.message}} 43 |
44 |
45 | {{/if}} 46 | {{/flash-message}} 47 | {{/each}} 48 |
49 | {{outlet}} 50 |
51 | 52 | 59 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/app-version/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'exercism-gui/config/environment'; 3 | 4 | const { releaseTag } = ENV.APP; 5 | const semver = requireNode('semver'); 6 | 7 | export default Ember.Component.extend({ 8 | tagName: '', 9 | releaseTag: Ember.computed(function() { 10 | return semver.clean(releaseTag); 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /app/components/app-version/template.hbs: -------------------------------------------------------------------------------- 1 | {{releaseTag}} 2 | -------------------------------------------------------------------------------- /app/components/config-form/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | let presenceOptions = { 5 | presence: true, 6 | dependentKeys: ['model.validationsDisabled'], 7 | disabled: Ember.computed.not('model.validationsEnabled') 8 | }; 9 | 10 | let Validations = buildValidations({ 11 | dir: validator('presence', presenceOptions), 12 | api: validator('presence', presenceOptions), 13 | xapi: validator('presence', presenceOptions), 14 | apiKey: validator('presence', presenceOptions) 15 | }); 16 | 17 | 18 | export default Ember.Component.extend(Validations, { 19 | showingAdvanced: false, 20 | validationsEnabled: false, 21 | 22 | init() { 23 | this._super(...arguments); 24 | this.resetForm(); 25 | }, 26 | 27 | resetForm() { 28 | this.setProperties(this.get('config')); 29 | this.set('validationsDisabled', true); 30 | }, 31 | 32 | actions: { 33 | saveConfig() { 34 | this.set('validationsEnabled', true); 35 | if (this.get('validations.isValid')) { 36 | let dir = this.get('dir'), 37 | apiKey = this.get('apiKey'), 38 | api = this.get('api'), 39 | xapi = this.get('xapi'); 40 | this.get('attrs').saveConfig({ dir, apiKey, api, xapi }); 41 | } 42 | } 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /app/components/config-form/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
Using configuration path {{configFilePath}}
5 |
6 |
7 | 8 |
9 |
10 | 11 | 17 | {{#if validations.attrs.dir.message}} 18 |
19 |

{{validations.attrs.dir.messages}}

20 |
21 | {{/if}} 22 |
23 |
24 | 29 | 34 | {{#if validations.attrs.apiKey.message}} 35 |
36 |

{{validations.attrs.apiKey.messages}}

37 |
38 | {{/if}} 39 |
40 | 41 |
42 | 46 |
47 | 48 | {{#if showingAdvanced}} 49 |
50 | 51 | 56 | {{#if validations.attrs.api.message}} 57 |
58 |

{{validations.attrs.api.messages}}

59 |
60 | {{/if}} 61 |
62 |
63 | 64 | 69 | {{#if validations.attrs.xapi.message}} 70 |
71 |

{{validations.attrs.xapi.messages}}

72 |
73 | {{/if}} 74 |
75 | {{/if}} 76 |
77 |
78 | 79 |
80 |
81 | -------------------------------------------------------------------------------- /app/components/context-menu/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const {remote} = requireNode('electron'); 4 | const {Menu} = remote; 5 | 6 | let template = [ 7 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:', role: 'cut' }, 8 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:', role: 'copy' }, 9 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:', role: 'paste' }, 10 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:', role: 'selectall' }, 11 | { type: 'separator' }, 12 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:', role: 'undo'}, 13 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:', role: 'redo' } 14 | ]; 15 | 16 | const menu = Menu.buildFromTemplate(template); 17 | 18 | /*TODO: this is a super lame hack 19 | Ideally, this code would live under electron.js 20 | and we would use the webContents event emitter 21 | see tickets #26 and #28 22 | */ 23 | 24 | export default Ember.Component.extend({ 25 | tagName: '', 26 | 27 | init() { 28 | this._super(...arguments); 29 | window.addEventListener('contextmenu', (e) => { 30 | e.preventDefault(); 31 | menu.popup(remote.getCurrentWindow()); 32 | }, false); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /app/components/context-menu/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /app/components/download-form/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { validator, buildValidations } from 'ember-cp-validations'; 3 | 4 | let Validations = buildValidations({ 5 | uuid: validator('presence', { 6 | presence: true, 7 | dependentKeys: ['model.validationsDisabled'], 8 | disabled: Ember.computed.not('model.validationsEnabled') 9 | }), 10 | }); 11 | 12 | export default Ember.Component.extend(Validations, { 13 | validationsEnabled: false, 14 | actions: { 15 | download() { 16 | this.set('validationsEnabled', true); 17 | if (this.get('validations.isValid')) { 18 | this.get('download')(this.get('uuid')); 19 | } 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/download-form/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 10 | {{#if validations.attrs.uuid.message}} 11 |
12 |

{{validations.attrs.uuid.messages}}

13 |
14 | {{/if}} 15 |
16 |
Download
17 |
18 | -------------------------------------------------------------------------------- /app/components/fetch-summary/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/fetch-summary/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#each summary as |info|}} 4 |
5 |
6 | 7 |
8 |
{{info.problem}}
9 |
10 | {{#if info.new}} 11 |
12 | 13 |
14 |
New files
15 |
16 | {{#each info.new as |new|}} 17 |
18 | 19 |
20 |
{{new}}
21 |
22 |
23 | {{/each}} 24 |
25 |
26 |
27 | {{/if}} 28 | {{#if info.unchanged}} 29 |
30 | 31 |
32 |
Unchanged files
33 |
34 | {{#each info.unchanged as |unchanged|}} 35 |
36 | 37 |
38 |
{{unchanged}}
39 |
40 |
41 | {{/each}} 42 |
43 |
44 |
45 | {{/if}} 46 |
47 |
48 |
49 |
50 | {{#if (gt summary.length 1)}} 51 |
52 | {{/if}} 53 | {{/each}} 54 |
55 |
56 |
57 | 58 | Open track folder 59 |
60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /app/components/file-selector/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const fs = requireNode('fs'), 4 | path = requireNode('path'), 5 | electron = requireNode('electron'); 6 | 7 | export default Ember.Component.extend({ 8 | selections: Ember.inject.service(), 9 | selectedFile: null, 10 | errorMessage: null, 11 | comment: null, 12 | refreshing: false, 13 | 14 | change() { 15 | this.set('errorMessage', null); 16 | }, 17 | 18 | handleAction(callback) { 19 | let filePath = path.join(this.get('problem.dir'), this.get('selectedFile')); 20 | if (fs.existsSync(filePath)) { 21 | return callback(filePath); 22 | } else { 23 | this.set('errorMessage', `The file ${filePath} no longer exists`); 24 | } 25 | }, 26 | 27 | actions: { 28 | submit() { 29 | this.handleAction((filePath) => { 30 | let selections = this.get('selections'); 31 | selections.set('selectedFileToSubmit', filePath); 32 | selections.set('submitComment', this.get('comment')); 33 | this.get('attrs').submit(); 34 | }); 35 | }, 36 | 37 | open() { 38 | this.handleAction((filePath) => { 39 | electron.shell.openItem(filePath); 40 | }); 41 | }, 42 | 43 | refresh() { 44 | this.set('refreshing', true); 45 | this.get('attrs').refresh(); 46 | Ember.run.later(() => { 47 | // We wait a little since usually the response 48 | // is very fast and the visual feedback is lost 49 | this.set('refreshing', false); 50 | }, 300); 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /app/components/file-selector/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | Available files 3 |
note that README.md, test files and nested folders are skipped from this list
4 |
5 | {{#if problem.files}} 6 |
7 |
8 | {{#each problem.files as |file|}} 9 |
10 | {{ui-radio name="selectedFile" value=file label=file current=selectedFile onChange=(action (mut selectedFile))}} 11 |
12 | {{/each}} 13 |
14 | 15 |
16 | 17 | 21 |
22 | 23 | 29 | 35 | 40 | {{#if errorMessage}} 41 |
42 |

43 | {{errorMessage}} 44 |

45 |
46 | {{/if}} 47 |
48 | {{else}} 49 |

None

50 | {{/if}} 51 | -------------------------------------------------------------------------------- /app/components/get-started-link/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | configuration: Ember.inject.service(), 5 | 6 | link: Ember.computed('configuration.isConfigured', function() { 7 | return this.get('configuration.isConfigured') ? 'tracks' : 'configuration'; 8 | }) 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /app/components/get-started-link/template.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to link}} 2 |
Get started
3 | {{/link-to}} 4 | -------------------------------------------------------------------------------- /app/components/hold-button/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { task, timeout } from 'ember-concurrency'; 3 | 4 | export default Ember.Component.extend({ 5 | delay: 300, 6 | 7 | mouseUp() { 8 | this.set('countdown', null); 9 | this.get('task').cancelAll(); 10 | }, 11 | 12 | clickedTask: task(function * () { 13 | this.set('pulsate', true); 14 | yield timeout(100); 15 | this.set('pulsate', false); 16 | }).on('click'), 17 | 18 | task: task(function * () { 19 | let countdown = 4; 20 | 21 | while(countdown > 0) { 22 | yield timeout(this.get('delay')); 23 | countdown -= 1; 24 | this.set('countdown', countdown); 25 | } 26 | 27 | this.set('countdown', null); 28 | 29 | this.get('attrs').action(); 30 | }).on('mouseDown').restartable() 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /app/components/hold-button/template.hbs: -------------------------------------------------------------------------------- 1 | {{yield (hash countdown=countdown pulsate=pulsate)}} 2 | -------------------------------------------------------------------------------- /app/components/local-problem-selector/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const lodash = requireNode('lodash'); 4 | 5 | export default Ember.Component.extend({ 6 | didUpdateAttrs() { 7 | this._super(...arguments); 8 | // replace the old selected problem on model refresh 9 | let name = this.get('selectedProblem.name'), 10 | problem = lodash.find(this.get('problems'), { name }); 11 | this.set('selectedProblem', (problem) ? problem : null); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/local-problem-selector/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if problems}} 2 |
3 |
4 |
5 | {{#power-select 6 | options=(sort-by "name" problems) 7 | searchField="name" 8 | selected=selectedProblem 9 | placeholder="Please, select a problem" 10 | onchange=(action (mut selectedProblem)) as |problem|}} 11 | {{problem.name}} 12 | {{/power-select}} 13 |
14 |
15 | {{#if selectedProblem}} 16 |
17 | 18 |
19 | {{selectedProblem.dir}} 20 |
21 |
22 | {{/if}} 23 |
24 |
25 |
26 |
27 | {{#if selectedProblem}} 28 | {{file-selector 29 | problem=selectedProblem 30 | submit=(route-action "submit") 31 | refresh=(route-action "refreshModel")}} 32 | {{/if}} 33 |
34 |
35 |
36 | {{else}} 37 |
38 |

39 | No problem files found under this track 40 |

41 |
42 | {{/if}} 43 | -------------------------------------------------------------------------------- /app/components/new-release/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const semver = requireNode('semver'), 4 | lodash = requireNode('lodash'); 5 | 6 | export default Ember.Component.extend({ 7 | newerVersionAvailable: Ember.computed('release.tagName', 'info.tag', function() { 8 | if (!this.get('release')) { 9 | return false; 10 | } 11 | return semver.gt(this.get('release.tagName'), this.get('info.tag')); 12 | }), 13 | 14 | asset: Ember.computed('release.assets.[]', function() { 15 | let assets = this.get('release.assets'), 16 | arch = this.get('info.arch'), 17 | platform = this.get('info.platform'); 18 | 19 | if (arch === 'ia32') { 20 | arch = 'x86'; 21 | } 22 | 23 | return lodash.find(assets, (asset) => { 24 | // eslint-disable-next-line no-unused-vars 25 | let [_n0, _n1, pkgPlatform, pkgArch, ...rest] = asset.name.split('-'); // jshint ignore:line 26 | 27 | if (platform === pkgPlatform && pkgArch === arch) { 28 | return asset; 29 | } 30 | }); 31 | }), 32 | 33 | cleanTag: Ember.computed('release.tagName', function() { 34 | return semver.clean(this.get('release.tagName')); 35 | }), 36 | 37 | assetSize: Ember.computed('asset', function() { 38 | return (this.get('asset.size') / 1000000).toFixed(2); 39 | }) 40 | }); 41 | -------------------------------------------------------------------------------- /app/components/new-release/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if newerVersionAvailable}} 2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | Version {{cleanTag}} became available 11 |
12 | {{moment-from-now release.publishedAt}} 13 |
14 |
15 |
16 |
17 | {{#if asset}} 18 |
19 |
20 |
21 |
22 | Download link for your platform 23 |
24 |
25 | {{assetSize}} MiB 26 |
27 | 30 |
31 |
32 | {{/if}} 33 |
34 |
35 | {{else}} 36 | {{#if showMessage}} 37 | {{#if (not release)}} 38 |
39 |

Could not connect to Github API to determine the latest release information

40 |
41 | {{else}} 42 |
43 |

Your version is up to date

44 |
45 | {{/if}} 46 | {{/if}} 47 | {{/if}} 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/components/restore-summary/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/restore-summary/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if summary}} 2 |
3 |
4 |
5 |

6 | Please note that existing files are not overwritten 7 |

8 |
9 |
10 |
11 |
12 | {{fetch-summary summary=summary}} 13 | {{else}} 14 |
15 |

16 | No files were submitted under this track yet. Nothing to restore. 17 |

18 |
19 | {{/if}} 20 | -------------------------------------------------------------------------------- /app/components/submit-status/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const urlJoin = requireNode('url-join'); 4 | 5 | export default Ember.Component.extend({ 6 | submissionPath: Ember.computed('status.submissionPath', function() { 7 | return urlJoin('http://exercism.io', this.get('status.submissionPath')); 8 | }) 9 | }); 10 | -------------------------------------------------------------------------------- /app/components/submit-status/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {{#if status.error}} 6 |

{{status.error}}

7 | {{else}} 8 |

file submitted succesfully

9 | {{/if}} 10 |
11 | {{#if status.error}} 12 |

Submitted file was {{status.submittedFile}}

13 | {{else}} 14 |
    15 |
  • Link to submission: {{submissionPath}}
  • 16 |
  • File: {{status.submittedFile}}
  • 17 |
  • 18 | Iteration number: {{status.iteration}} 19 |
  • 20 |
21 | {{/if}} 22 |
23 |
24 | -------------------------------------------------------------------------------- /app/components/track-action-menu/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | btnColor: 'primary', 5 | configuration: Ember.inject.service(), 6 | apiKeyUnset: Ember.computed.empty('configuration.apiKey'), 7 | trackIsNull: Ember.computed.empty('track'), 8 | disableActions: Ember.computed.or('apiKeyUnset', 'trackIsNull'), 9 | buttonClass: Ember.computed('disableActions', function() { 10 | let status = this.get('disableActions') ? 'disabled' : ''; 11 | return `ui basic fluid button ${status}`; 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/track-action-menu/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if apiKeyUnset}} 2 | {{#link-to "configuration" class="item"}} 3 | Please configure your API key to enable the actions below 4 | {{/link-to}} 5 | {{/if}} 6 |
Links
7 | {{link-to 'Submitting' 'tracks.track.local-problems' track.id class="item" disabled=(or apiKeyUnset trackIsNull)}} 8 |
9 | Actions 10 | 75 |
76 | -------------------------------------------------------------------------------- /app/components/track-selector/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | selectedTrack: null, 5 | actions: { 6 | chooseTrack(track) { 7 | this.set('selectedTrack', track); 8 | this.get('attrs').action(track); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/track-selector/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#power-select 3 | options=tracks 4 | selected=selectedTrack 5 | placeholder="Please, select a track" 6 | onchange=(action "chooseTrack") as |track|}} 7 | {{track.language}} 8 | {{/power-select}} 9 |
10 | -------------------------------------------------------------------------------- /app/components/track-status/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | noSubmissions: Ember.computed.match('status.recent.problem', /^.*any solutions.*/) 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/track-status/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if noSubmissions}} 2 |
3 |
4 | {{status.recent.problem}} 5 |
6 |
7 | {{else}} 8 |
9 |
10 |
11 |
12 |

Most recent problem

13 |
14 | {{status.recent.problem}} submitted {{moment-from-now status.recent.submitted_at allowEmpty=true}} 15 |
16 |
17 |
18 |
19 |
20 |
21 |

Submitted problems

22 | {{#if (not status.submitted)}} 23 |

24 | None. Latest submission was probably deleted 25 |

26 | {{else}} 27 |
28 | {{#each status.submitted as |exercise|}} 29 | {{#link-to "tracks.track.status.submission" 30 | status.trackId exercise 31 | class="ui card"}} 32 |
33 | {{exercise}} 34 |
35 | {{/link-to}} 36 | {{/each}} 37 |
38 | {{/if}} 39 |
40 |
41 | {{#pinned-content top=70}} 42 | {{yield}} 43 | {{/pinned-content}} 44 |
45 |
46 |
47 |
48 |

Skipped problems

49 |
50 | {{#each status.skipped as |exercise|}} 51 |
52 | {{exercise}} 53 |
54 | {{else}} 55 |
56 | None 57 |
58 | {{/each}} 59 |
60 |
61 |
62 |
63 | {{/if}} 64 | -------------------------------------------------------------------------------- /app/components/window-menu/component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const {remote} = requireNode('electron'); 4 | 5 | export default Ember.Component.extend({ 6 | tagName: '', 7 | 8 | actions: { 9 | reload() { 10 | remote.getCurrentWindow().reload(); 11 | }, 12 | 13 | toggleDevTools() { 14 | remote.getCurrentWindow().webContents.toggleDevTools(); 15 | }, 16 | toggleFullScreen() { 17 | let win = remote.getCurrentWindow(); 18 | win.setFullScreen(!win.isFullScreen()); 19 | }, 20 | 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/components/window-menu/template.hbs: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /app/configuration/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | notifier: Ember.inject.service(), 5 | configuration: Ember.inject.service(), 6 | 7 | model() { 8 | let configService = this.get('configuration'), 9 | config = configService.readConfigFile(); 10 | config.configFilePath = configService.getConfigFilePath(); 11 | return config; 12 | }, 13 | 14 | actions: { 15 | saveConfig(config) { 16 | this.get('configuration').writeConfigFile(config); 17 | let message = `Configuration saved to ${this.get('currentModel.configFilePath')}`; 18 | this.get('notifier').notify(message); 19 | } 20 | } 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /app/configuration/service.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | let fs = requireNode('fs'), 4 | jsonfile = requireNode('jsonfile'), 5 | path = requireNode('path'), 6 | osHomedir = requireNode('os-homedir'); 7 | 8 | export default Ember.Service.extend({ 9 | init() { 10 | this._super(...arguments); 11 | let config = this.readConfigFile(); 12 | this.update(config); 13 | }, 14 | 15 | isConfigured: Ember.computed.notEmpty('apiKey'), 16 | 17 | getHomeConfigFilePath() { 18 | return path.join(osHomedir(), '.exercism.json'); 19 | }, 20 | 21 | getHomeExercisesDir() { 22 | return path.join(osHomedir(), 'exercism'); 23 | }, 24 | 25 | getDefaultHomeExercisesDir() { 26 | return path.join(osHomedir(), 'exercism') 27 | }, 28 | 29 | getDefaults() { 30 | return { 31 | api: 'http://exercism.io', 32 | xapi: 'http://x.exercism.io', 33 | apiKey: null, 34 | dir: this.getDefaultHomeExercisesDir() 35 | }; 36 | }, 37 | 38 | update(config) { 39 | let defaults = this.getDefaults(); 40 | this.set('api', config.api ? config.api : defaults.api); 41 | this.set('xapi', config.xapi ? config.xapi : defaults.xapi); 42 | this.set('dir', config.dir ? config.dir : defaults.dir); 43 | this.set('apiKey', config.apiKey ? config.apiKey : defaults.key); 44 | }, 45 | 46 | fileExists(filePath) { 47 | try { 48 | let stat = fs.statSync(filePath); 49 | return stat.isFile(); 50 | } catch(err) { 51 | return false; 52 | } 53 | }, 54 | 55 | getConfigFilePath() { 56 | let configFilePath = process.env.EXERCISM_CONFIG_FILE; 57 | if (configFilePath) { 58 | Ember.Logger.info(`Using config file ${configFilePath} set by envar EXERCISM_CONFIG_FILE`); 59 | } else { 60 | configFilePath = this.getHomeConfigFilePath(); 61 | } 62 | return configFilePath; 63 | }, 64 | 65 | writeConfigFile(config) { 66 | let configFilePath = this.getConfigFilePath(); 67 | jsonfile.writeFileSync(configFilePath, config); 68 | this.update(config); 69 | }, 70 | 71 | readConfigFile() { 72 | let configFilePath = this.getConfigFilePath(); 73 | if (!this.fileExists(configFilePath)) { 74 | return this.getDefaults(); 75 | } 76 | return jsonfile.readFileSync(configFilePath); 77 | } 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /app/configuration/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{config-form config=model saveConfig=(route-action "saveConfig")}} 3 |
4 | -------------------------------------------------------------------------------- /app/debug/controller.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | debug: Ember.inject.service() 5 | }); 6 | -------------------------------------------------------------------------------- /app/debug/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | debug: Ember.inject.service(), 5 | 6 | model() { 7 | let debug = this.get('debug'), 8 | servicesStatus = debug.getServicesStatus(); 9 | 10 | return debug.getLatestRelease().then((release) => { 11 | let latestTag = (release && release.tagName)? release.tagName : 'N/A'; 12 | return { 13 | latestTag: latestTag, 14 | servicesStatus, 15 | }; 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/debug/service.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from 'exercism-gui/config/environment'; 3 | 4 | const os = requireNode('os'), 5 | url = requireNode("url"), 6 | ping = requireNode("ping"), 7 | osHomedir = requireNode('os-homedir'); 8 | 9 | const { releaseTag } = ENV.APP; 10 | 11 | const OK = 'OK', 12 | NOT_OK = 'NOT_OK'; 13 | 14 | const ServiceStatus = Ember.Object.extend({ 15 | name: null, 16 | host: null, 17 | status: 'VERIFYING' 18 | }); 19 | 20 | export default Ember.Service.extend({ 21 | ajax: Ember.inject.service(), 22 | 23 | configuration: Ember.inject.service(), 24 | 25 | init() { 26 | this._super(...arguments); 27 | 28 | this.set('currentTag', releaseTag); 29 | this.set('arch', os.arch()); 30 | this.set('platform', os.platform()); 31 | this.set('homeDir', osHomedir()); 32 | this.set('configFilePath', this.get('configuration').getHomeConfigFilePath()); 33 | }, 34 | 35 | homeExercisesDir: Ember.computed('configuration.dir', function() { 36 | return this.get('configuration.dir'); 37 | }), 38 | 39 | getLatestRelease() { 40 | let url = 'https://api.github.com/repos/exercism/gui/releases/latest'; 41 | 42 | return this.get('ajax').request(url).then((release) => { 43 | let assets = release.assets.map((asset) => { 44 | return { 45 | browserDownloadUrl: asset.browser_download_url, 46 | name: asset.name, 47 | size: asset.size 48 | }; 49 | }); 50 | 51 | return { 52 | htmlUrl: release.html_url, 53 | publishedAt: release.published_at, 54 | tagName: release.tag_name, 55 | assets 56 | }; 57 | 58 | }).catch((error) => { 59 | Ember.Logger.warn('Cannot verify new release. Skipped due to error', error); 60 | return null; 61 | }); 62 | }, 63 | 64 | getServicesStatus() { 65 | let configuration = this.get('configuration'), 66 | servicesStatus = []; 67 | 68 | // x.exercism.io is not responding to ping at port 80 so we just 69 | // do a simple GET request 70 | let serviceStatus = ServiceStatus.create({ 71 | name: 'XAPI', 72 | host: url.parse(configuration.xapi, true).host 73 | }); 74 | 75 | servicesStatus.push(serviceStatus); 76 | 77 | this.get('ajax').request(configuration.xapi).then((response) => { 78 | if (response.build_id || response.repository) { 79 | serviceStatus.set('status', OK); 80 | } else { 81 | serviceStatus.set('status', NOT_OK); 82 | } 83 | }).catch(() => { 84 | serviceStatus.set('status', NOT_OK); 85 | }); 86 | 87 | let targets = [ 88 | { host: url.parse(configuration.api, true).host, name: 'API' }, 89 | { host: 'api.github.com', name: 'Github API' } 90 | ]; 91 | 92 | targets.forEach((target) => { 93 | let serviceStatus = ServiceStatus.create({ name: target.name, host: target.host }); 94 | servicesStatus.push(serviceStatus); 95 | 96 | ping.sys.probe(target.host, (isAlive) => { 97 | 98 | serviceStatus.set('status', isAlive ? OK : NOT_OK); 99 | 100 | }, { timeout: 2000 }); 101 | 102 | }); 103 | 104 | return servicesStatus; 105 | } 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /app/debug/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
Platform: {{debug.platform}} - {{model.arch}}
6 |
Version: {{debug.currentTag}}
7 |
Latest version: {{model.latestTag}}
8 |
Home dir: {{debug.homeDir}}
9 |
Exercises dir: {{debug.homeExercisesDir}}
10 |
Config file: {{debug.configFilePath}}
11 |
12 |
13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | {{#each model.servicesStatus as |serviceStatus|}} 24 | 25 | 26 | 32 | 33 | 34 | {{/each}} 35 | 36 |
18 | Services status 19 |
{{serviceStatus.name}} 27 | {{#if (not-eq serviceStatus.status 'VERIFYING')}} 28 | 29 | {{/if}} 30 | {{serviceStatus.status}} 31 | {{serviceStatus.host}}
37 | 38 |
39 | {{outlet}} 40 | -------------------------------------------------------------------------------- /app/download/loading/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /app/download/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | 5 | actions: { 6 | download(uuid) { 7 | this.transitionTo('download.status', uuid); 8 | } 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /app/download/status/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const electron = requireNode('electron'); 4 | 5 | export default Ember.Route.extend({ 6 | exercism: Ember.inject.service(), 7 | 8 | model(params) { 9 | return this.store.findRecord('submission', params.submission_id).then((submission) => { 10 | let path = this.get('exercism').saveSubmittedFiles(submission); 11 | return { path, error: null }; 12 | }).catch((error) => { 13 | Ember.Logger.error(error); 14 | let message = error.errors[0].detail; 15 | return { path: null, error: message }; 16 | }); 17 | }, 18 | 19 | actions: { 20 | openSolutionFolder(path) { 21 | electron.shell.openItem(path); 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /app/download/status/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if model.error}} 2 |
3 |
4 | Something went wrong while retrieving the submission 5 |
6 |

7 | {{model.error}} 8 |

9 |
10 | {{else}} 11 | 12 |

13 | Successfully downloaded submission 14 |

15 |
16 |
17 |
18 |
19 |

It can be viewed at {{model.path}}

20 |
21 |
22 |
23 |
24 |
25 | 26 | Open folder 27 |
28 |
29 |
30 |
31 |
32 | {{/if}} 33 | 34 | -------------------------------------------------------------------------------- /app/download/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{download-form download=(route-action "download")}} 3 |
4 | {{outlet}} 5 |
6 | 7 | -------------------------------------------------------------------------------- /app/error/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
An error occurred
4 |

{{model.message}}

5 | {{#each model.errors as |error|}} 6 | {{#if error.detail.error}} 7 |

{{error.status}} {{error.detail.error}}

8 | {{else}} 9 |

{{model.errors}}

10 | {{/if}} 11 | {{/each}} 12 |
13 |
14 | -------------------------------------------------------------------------------- /app/exercism/service.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { get } = Ember; 4 | 5 | const fs = requireNode('fs'), 6 | mkdirp = requireNode('mkdirp'), 7 | path = requireNode('path'), 8 | lodash = requireNode('lodash'); 9 | 10 | export default Ember.Service.extend({ 11 | ajax: Ember.inject.service(), 12 | configuration: Ember.inject.service(), 13 | 14 | saveProblems(problems) { 15 | let problemsSaved = [], 16 | dir = this.get('configuration.dir'); 17 | 18 | problems.forEach((problem) => { 19 | let slug = get(problem, 'slug'), 20 | language = get(problem, 'language'), 21 | dirPath = path.join(dir, language, slug), 22 | summary = { problem: slug, new: [], unchanged: [] }; 23 | 24 | lodash.forEach(get(problem, 'files'), (content, fileName) => { 25 | let filePath = path.join(dirPath, fileName); 26 | 27 | // Make sure the dirs exists 28 | mkdirp.sync(path.dirname(filePath)); 29 | 30 | if (!fs.existsSync(filePath)) { 31 | fs.writeFileSync(filePath, content); 32 | summary.new.push(fileName); 33 | } else { 34 | summary.unchanged.push(fileName); 35 | } 36 | }); 37 | 38 | problemsSaved.push(summary); 39 | }); 40 | 41 | return problemsSaved; 42 | }, 43 | 44 | _getValidLocalDirs(root, validSlugs) { 45 | let dirs = []; 46 | 47 | if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { 48 | return dirs; 49 | } 50 | 51 | lodash.forEach(fs.readdirSync(root), (file) => { 52 | let fpath = path.join(root, file); 53 | 54 | if (fs.statSync(fpath).isDirectory() && validSlugs.contains(file)) { 55 | dirs.push(fpath); 56 | } 57 | }); 58 | 59 | return dirs; 60 | }, 61 | 62 | _getProblemFiles(dir) { 63 | return fs.readdirSync(dir).filter((file) => { 64 | let fpath = path.join(dir, file); 65 | 66 | return file !== 'README.md' && 67 | fs.statSync(fpath).isFile() && 68 | !file.toLowerCase().includes('test'); 69 | }); 70 | }, 71 | 72 | getLocalProblems(trackId, validSlugs) { 73 | let exercismDir = this.get('configuration.dir'), 74 | trackDir = path.join(exercismDir, trackId), 75 | problems = [], 76 | dirs = this._getValidLocalDirs(trackDir, validSlugs); 77 | 78 | lodash.forEach(dirs, (dir) => { 79 | let files = this._getProblemFiles(dir), 80 | name = path.basename(dir); 81 | 82 | problems.push({ name, files, dir }); 83 | }); 84 | 85 | return problems; 86 | }, 87 | 88 | _extractInfoFromFilePath(filePath, dir, sep=path.sep) { 89 | let bits = filePath.replace(dir, '').split(sep), 90 | problem = bits[2], 91 | language = bits[1], 92 | fileName = bits.slice(-1)[0]; 93 | 94 | return { fileName, problem, language }; 95 | }, 96 | 97 | getSubmitPayload(filePath, comment) { 98 | let solution = {}, code = '', 99 | key = this.get('configuration.apiKey'), 100 | dir = this.get('configuration.dir'); 101 | 102 | let { fileName, problem, language } = this._extractInfoFromFilePath(filePath, dir); 103 | 104 | solution[fileName] = fs.readFileSync(filePath, { encoding: 'utf-8' }); 105 | 106 | return { key, dir, language, problem, solution, code, comment }; 107 | }, 108 | 109 | saveSubmittedFiles(submission) { 110 | let dir = this.get('configuration.dir'), 111 | username = get(submission, 'username'), 112 | trackId = get(submission, 'trackId'), 113 | slug = get(submission, 'slug'), 114 | uuid = get(submission, 'uuid'), 115 | folderPath = path.join(dir, 'solutions', username, trackId, slug, uuid); 116 | 117 | mkdirp.sync(folderPath); 118 | 119 | lodash.forEach(get(submission, 'solutionFiles'), (content, name) => { 120 | fs.writeFileSync(path.join(folderPath, name), content); 121 | }); 122 | 123 | return folderPath; 124 | } 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /app/help/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 |
6 | Need some help? 7 |
8 |
9 | Here are a few links to exercism.io docs that can help you get started or troubleshoot an issue 10 |
11 |
12 |

13 | 14 | 19 | 20 | 21 |
22 |

23 | Got an issue or feedback to report for exercism-gui? 24 | Please open a ticket in the project issue tracker 25 |

26 |
27 |
28 | {{outlet}} 29 | -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exercism GUI 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/index/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | debug: Ember.inject.service(), 5 | 6 | model() { 7 | let debug = this.get('debug'); 8 | let response = Ember.Object.create({ 9 | info: { 10 | arch: debug.arch, 11 | tag: debug.currentTag, 12 | platform: debug.platform, 13 | } 14 | }); 15 | 16 | debug.getLatestRelease().then((release) => { 17 | response.set('release', release); 18 | }); 19 | return response; 20 | } 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /app/index/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Welcome to Exercism GUI 6 |

7 |

8 | Version {{app-version}} 9 |

10 | {{get-started-link}} 11 |
12 |
13 |
14 | 15 |
16 |
17 | {{new-release release=model.release info=model.info}} 18 |
19 | -------------------------------------------------------------------------------- /app/initializers/selections.js: -------------------------------------------------------------------------------- 1 | export function initialize(application) { 2 | application.inject('controller', 'selections', 'service:selections'); 3 | } 4 | 5 | export default { 6 | name: 'selections', 7 | initialize 8 | }; 9 | -------------------------------------------------------------------------------- /app/loading/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/app/models/.gitkeep -------------------------------------------------------------------------------- /app/notifier/service.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const notifier = requireNode('node-notifier'); 3 | 4 | export default Ember.Service.extend({ 5 | 6 | notifier, 7 | notify(message) { 8 | let notification = message; 9 | if (typeof message === 'string') { 10 | notification = { title: 'Excersism GUI', message }; 11 | } 12 | this.get('notifier').notify(notification); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/problem/model.js: -------------------------------------------------------------------------------- 1 | import Model from 'ember-data/model'; 2 | import attr from 'ember-data/attr'; 3 | import { memberAction, classOp } from 'ember-api-actions'; 4 | 5 | export default Model.extend({ 6 | 7 | trackId: attr('string'), 8 | language: attr('string'), 9 | slug: attr('string'), 10 | name: attr('string'), 11 | files: attr(), 12 | fresh: attr('boolean'), 13 | skip: memberAction({ path: 'skip', type: 'post' }), 14 | restoreAll: classOp({ path: 'restore', type: 'get' }) 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('tracks', function() { 11 | this.route('track', { path: ':track_id' }, function() { 12 | this.route('status', function() { 13 | this.route('submission', { path: 'submission/:slug' }); 14 | }); 15 | 16 | this.route('problems', function() { 17 | this.route('problem', { path: ':problem_id' }, function() { 18 | this.route('skip'); 19 | }); 20 | }); 21 | 22 | this.route('fetch'); 23 | this.route('restore'); 24 | this.route('fetch-all'); 25 | this.route('local-problems'); //Submitting 26 | this.route('submit-status'); 27 | }); 28 | }); 29 | this.route('configuration'); 30 | this.route('help'); 31 | this.route('debug'); 32 | this.route('updates'); 33 | this.route('download', function() { 34 | this.route('status', { path: ':submission_id/status' }); 35 | }); 36 | }); 37 | 38 | export default Router; 39 | -------------------------------------------------------------------------------- /app/selections/service.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | selectedTrack: null, 5 | selectedFileToSubmit: null, 6 | submitComment: null 7 | }); 8 | -------------------------------------------------------------------------------- /app/status/model.js: -------------------------------------------------------------------------------- 1 | import Model from 'ember-data/model'; 2 | import attr from 'ember-data/attr'; 3 | 4 | export default Model.extend({ 5 | fetched: attr(), 6 | recent: attr(), 7 | skipped: attr(), 8 | submitted: attr(), 9 | trackId: attr('string') 10 | }); 11 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | .ui.main { 2 | padding-top: 4rem; 3 | padding-bottom: 4rem; 4 | } 5 | -------------------------------------------------------------------------------- /app/submission/model.js: -------------------------------------------------------------------------------- 1 | import Model from 'ember-data/model'; 2 | import attr from 'ember-data/attr'; 3 | import { memberAction } from 'ember-api-actions'; 4 | 5 | export default Model.extend({ 6 | uuid: attr('string'), 7 | url: attr('string'), 8 | slug: attr('string'), 9 | trackId: attr('string'), 10 | username: attr('string'), 11 | submissionPath: attr('string'), 12 | solutionFiles: attr(), 13 | submit: memberAction({ path: 'assignments', type: 'post' }) 14 | }); 15 | -------------------------------------------------------------------------------- /app/track/model.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Model from 'ember-data/model'; 3 | import attr from 'ember-data/attr'; 4 | 5 | export default Model.extend({ 6 | language: attr('string'), 7 | active: attr('boolean'), 8 | repository: attr('string'), 9 | slug: attr('string'), 10 | todo: attr(), 11 | problems: attr(), 12 | 13 | languageUrl: Ember.computed('slug', function() { 14 | return `http://exercism.io/languages/${this.get('slug')}`; 15 | }), 16 | imageSrc: Ember.computed('slug', function() { 17 | return `http://exercism.io/tracks/${this.get('slug')}/icon`; 18 | }) 19 | }); 20 | -------------------------------------------------------------------------------- /app/tracks/loading/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /app/tracks/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | selections: Ember.inject.service(), 5 | model() { 6 | return this.store.findAll('track'); 7 | }, 8 | 9 | redirect() { 10 | let track = this.get('selections.selectedTrack'); 11 | if (track) { 12 | this.transitionTo('tracks.track', track); 13 | } 14 | }, 15 | 16 | actions: { 17 | showTrack(track) { 18 | this.transitionTo('tracks.track', track); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /app/tracks/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 17 |
18 |
19 | {{#if (not selections.selectedTrack)}} 20 |
21 | Select a track from the menu to start 22 |
23 | {{/if}} 24 | {{outlet}} 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /app/tracks/track/fetch-all/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | 6 | model() { 7 | //TODO: this is terribly inefficient 8 | let track = this.modelFor('tracks.track'), 9 | trackId = track.get('id'), 10 | problems = track.get('problems'); 11 | 12 | let promises = problems.map((slug) => { 13 | return this.store.query('problem', { track_id: trackId, slug }); 14 | }); 15 | 16 | return Ember.RSVP.all(promises).then((response) => { 17 | let problems = response.map((result) => { 18 | return result.get('firstObject'); 19 | }); 20 | return this.get('exercism').saveProblems(problems); 21 | }); 22 | }, 23 | 24 | actions: { 25 | refreshModel() { 26 | this.refresh(); 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/tracks/track/fetch-all/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Fetch summary 3 |

4 | 5 | {{fetch-summary summary=model}} 6 | -------------------------------------------------------------------------------- /app/tracks/track/fetch/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | 6 | model() { 7 | let trackId = this.paramsFor('tracks.track').track_id; 8 | return this.store.query('problem', { track_id: trackId }).then((problems) => { 9 | return this.get('exercism').saveProblems(problems); 10 | }); 11 | }, 12 | 13 | actions: { 14 | refreshModel() { 15 | this.refresh(); 16 | } 17 | } 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /app/tracks/track/fetch/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Fetch summary 3 |

4 | 5 | {{fetch-summary summary=model}} 6 | -------------------------------------------------------------------------------- /app/tracks/track/loading/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /app/tracks/track/local-problems/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | model() { 6 | let track = this.modelFor('tracks.track'), 7 | trackId = track.get('slug'), 8 | validSlugs = track.get('problems'); 9 | return this.get('exercism').getLocalProblems(trackId, validSlugs); 10 | }, 11 | 12 | actions: { 13 | submit() { 14 | this.transitionTo('tracks.track.submit-status'); 15 | }, 16 | 17 | refreshModel() { 18 | this.refresh(); 19 | } 20 | } 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /app/tracks/track/local-problems/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Submitting 3 |

4 | {{local-problem-selector problems=model}} 5 | {{outlet}} 6 | 7 | -------------------------------------------------------------------------------- /app/tracks/track/problems/problem/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params) { 5 | return { id: params.problem_id }; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/tracks/track/problems/problem/skip/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | 6 | model() { 7 | let trackId = this.paramsFor('tracks.track').track_id, 8 | slug = this.paramsFor('tracks.track.problems.problem').problem_id, 9 | problem = this.store.createRecord('problem', { trackId, slug }); 10 | 11 | return problem.skip().then(() => { 12 | this.store.deleteRecord(problem); 13 | return { slug, track: trackId }; 14 | }); 15 | } 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /app/tracks/track/problems/problem/skip/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Skip summary 3 |

4 | 5 |
6 |
7 |
8 | Skipped {{model.slug}} in track {{model.track}} 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /app/tracks/track/restore/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const lodash = requireNode('lodash'); 4 | 5 | export default Ember.Route.extend({ 6 | exercism: Ember.inject.service(), 7 | 8 | model() { 9 | let problem = this.store.createRecord('problem'); 10 | 11 | return problem.restoreAll().then((response) => { 12 | this.store.deleteRecord(problem); 13 | 14 | let track_id = this.paramsFor('tracks.track').track_id, 15 | filteredProblems = lodash.filter(response.problems, { track_id }); 16 | 17 | return this.get('exercism').saveProblems(filteredProblems); 18 | }); 19 | }, 20 | 21 | actions: { 22 | refreshModel() { 23 | this.refresh(); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/tracks/track/restore/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Restore summary 3 |

4 | 5 | {{restore-summary summary=model}} 6 | -------------------------------------------------------------------------------- /app/tracks/track/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const {getOwner} = Ember; 4 | const electron = requireNode('electron'); 5 | const path = requireNode('path'); 6 | 7 | export default Ember.Route.extend({ 8 | configuration: Ember.inject.service(), 9 | selections: Ember.inject.service(), 10 | 11 | model(params) { 12 | return this.store.peekRecord('track', params.track_id); 13 | }, 14 | 15 | afterModel(model) { 16 | // set track in track selector when we go directly to this route 17 | this.set('selections.selectedTrack', model); 18 | }, 19 | 20 | refreshOrTransition(){ 21 | let [route, ...args] = arguments; 22 | let currentRoute = getOwner(this).lookup('controller:application').currentRouteName; 23 | if (currentRoute === route || currentRoute === `${route}.index`) { 24 | Ember.Logger.info('Refreshing route', currentRoute); 25 | this.send('refreshModel'); 26 | } else { 27 | this.transitionTo(route, ...args); 28 | } 29 | }, 30 | 31 | actions: { 32 | fetch() { 33 | this.refreshOrTransition('tracks.track.fetch'); 34 | }, 35 | 36 | fetchAll() { 37 | this.refreshOrTransition('tracks.track.fetch-all'); 38 | }, 39 | 40 | status(trackId) { 41 | this.refreshOrTransition('tracks.track.status', trackId); 42 | }, 43 | 44 | restore() { 45 | this.refreshOrTransition('tracks.track.restore'); 46 | }, 47 | 48 | openTrackFolder() { 49 | let dir = this.get('configuration.dir'), 50 | selectedTrack = this.get('selections.selectedTrack.slug'), 51 | fullPath = path.join(dir, selectedTrack); 52 | 53 | electron.shell.openItem(fullPath); 54 | } 55 | 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /app/tracks/track/status/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | 6 | model() { 7 | let trackId = this.paramsFor('tracks.track').track_id; 8 | return this.store.findRecord('status', trackId); 9 | }, 10 | 11 | actions: { 12 | refreshModel() { 13 | this.refresh(); 14 | } 15 | } 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /app/tracks/track/status/submission/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | 6 | model(params) { 7 | let trackId = this.paramsFor('tracks.track').track_id; 8 | return this.store.queryRecord('submission', { track_id: trackId, slug: params.slug }); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /app/tracks/track/status/submission/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{if model.url "Link to latest submission" "No solutions found"}} for "{{model.slug}}" 6 |
7 |
8 | {{model.url}} 9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /app/tracks/track/status/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Status 3 |

4 | {{#track-status status=model}} 5 | {{outlet}} 6 | {{/track-status}} 7 | -------------------------------------------------------------------------------- /app/tracks/track/submit-status/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | exercism: Ember.inject.service(), 5 | selections: Ember.inject.service(), 6 | 7 | beforeModel() { 8 | let selections = this.get('selections'); 9 | if (!selections.get('selectedFileToSubmit')) { 10 | this.transitionTo('tracks.track.local-problems', this.get('selections.selectedTrack')); 11 | } 12 | }, 13 | 14 | model() { 15 | let filePath = this.get('selections.selectedFileToSubmit'), 16 | comment = this.get('selections.submitComment'), 17 | props = this.get('exercism').getSubmitPayload(filePath, comment), 18 | submission = this.store.createRecord('submission'); 19 | return submission.submit(props).then((response) => { 20 | return { 21 | submissionPath: response.submission_path, 22 | submittedFile: filePath, 23 | iteration: response.iteration 24 | }; 25 | }, (error) => { 26 | if (error.errors[0].status === '400') { 27 | return { 28 | error: error.errors[0].detail, 29 | submittedFile: filePath 30 | }; 31 | } 32 | throw error; 33 | }).finally(() => { 34 | let selections = this.get('selections'); 35 | selections.set('selectedFileToSubmit', null); 36 | selections.set('submitComment', null); 37 | }); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /app/tracks/track/submit-status/template.hbs: -------------------------------------------------------------------------------- 1 |

2 | Submit status 3 |

4 | {{submit-status status=model}} 5 | 6 | -------------------------------------------------------------------------------- /app/tracks/track/template.hbs: -------------------------------------------------------------------------------- 1 | {{#with model as |track|}} 2 |
3 |
4 | 5 | 6 | 7 |
8 | {{track.language}} track 9 |
10 |
11 | Track details {{track.languageUrl}} 12 |
13 |
14 | Repository {{track.repository}} 15 |
16 |
17 | 18 | {{track.problems.length}} Problems 19 |
20 |
21 |
22 |
23 |
24 | {{/with}} 25 |
26 | {{outlet}} 27 | -------------------------------------------------------------------------------- /app/updates/route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | debug: Ember.inject.service(), 5 | 6 | model() { 7 | let debug = this.get('debug'); 8 | 9 | return debug.getLatestRelease().then((release) => { 10 | return { 11 | info: { 12 | arch: debug.arch, 13 | tag: debug.currentTag, 14 | platform: debug.platform, 15 | }, 16 | release 17 | }; 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/updates/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{new-release 3 | showMessage=true 4 | release=model.release 5 | info=model.info}} 6 |
7 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.0.{build} 2 | 3 | skip_non_tags: true 4 | 5 | build: 6 | verbosity: minimal 7 | 8 | platform: 9 | - x86 10 | - x64 11 | 12 | cache: 13 | - node_modules -> package.json 14 | - bower_components -> bower.json 15 | 16 | install: 17 | - ps: Install-Product node 6 $env:PLATFORM 18 | - npm install bower -g yarn@0.23.2 19 | - yarn install 20 | - bower install 21 | 22 | build_script: 23 | - node --version 24 | - IF %PLATFORM% EQU x64 (SET package_arch=x64) ELSE (SET package_arch=ia32) 25 | - node node_modules\ember-cli\bin\ember electron:package --platform win32 --arch %package_arch% --icon public/assets/icons/win32.ico --environment production 26 | 27 | after_build: 28 | - cd electron-out 29 | - 7z a exercism-gui-win32-%PLATFORM%-%APPVEYOR_REPO_TAG_NAME%.zip exercism-gui-win32-%package_arch% 30 | - cd .. 31 | - ps: ls electron-out 32 | 33 | artifacts: 34 | - path: 'electron-out\*.zip' 35 | name: electron-package 36 | 37 | test: off 38 | 39 | deploy: 40 | provider: GitHub 41 | auth_token: 42 | secure: 4Lc+r0RBFpZLKT+JZ5tlHQsEttScYJBtrTQ9BmtYQqcZyXlMP8v/+wjrBRdtY4XO 43 | artifact: electron-package 44 | on: 45 | appveyor_repo_tag: true 46 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercism-gui", 3 | "dependencies": { 4 | "semantic-ui": "^2.2.9", 5 | "pretender": "~1.1.0", 6 | "Faker": "~3.1.0", 7 | "testdouble": "testdouble/testdouble.js#^1.4.2", 8 | "animation-frame": "~0.2.4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node: true */ 2 | module.exports = function(environment) { 3 | var ENV = { 4 | modulePrefix: 'exercism-gui', 5 | environment: environment, 6 | rootURL: null, 7 | locationType: 'hash', 8 | EmberENV: { 9 | FEATURES: { 10 | // Here you can enable experimental features on an ember canary build 11 | // e.g. 'with-controller': true 12 | }, 13 | EXTEND_PROTOTYPES: { 14 | // Prevent Ember Data from overriding Date.parse. 15 | Date: false 16 | } 17 | }, 18 | 19 | APP: { 20 | // Here you can pass flags/options to your application instance 21 | // when it is created 22 | releaseTag: process.env.TRAVIS_TAG ? process.env.TRAVIS_TAG : process.env.APPVEYOR_REPO_TAG_NAME ? process.env.APPVEYOR_REPO_TAG_NAME : 'v0.0.0-dev' 23 | }, 24 | flashMessageDefaults: { 25 | timeout: 3000, 26 | types: ['positive', 'negative', 'info'] 27 | }, 28 | }; 29 | 30 | 31 | ENV['ember-cli-mirage'] = { 32 | enabled: (process.env.DISABLE_EMBER_CLI_MIRAGE === 'true')? false : true 33 | }; 34 | 35 | if (environment === 'development') { 36 | // ENV.APP.LOG_RESOLVER = true; 37 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 38 | // ENV.APP.LOG_TRANSITIONS = true; 39 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 40 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 41 | } 42 | 43 | if (environment === 'test') { 44 | // Testem prefers this... 45 | ENV.locationType = 'none'; 46 | 47 | // keep test console output quieter 48 | ENV.APP.LOG_ACTIVE_GENERATION = false; 49 | ENV.APP.LOG_VIEW_LOOKUPS = false; 50 | 51 | ENV.APP.rootElement = '#ember-testing'; 52 | ENV['ember-cli-mirage'].enabled = true; 53 | } 54 | 55 | if (environment === 'production') { 56 | ENV['ember-cli-mirage'].enabled = false; 57 | } 58 | 59 | return ENV; 60 | }; 61 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberApp(defaults, { 7 | fingerprint: { 8 | exclude: ['icons/linux.png'] 9 | } 10 | }); 11 | 12 | // Use `app.import` to add additional libraries to the generated 13 | // output files. 14 | // 15 | // If you need to use different assets in different 16 | // environments, specify an object as the first parameter. That 17 | // object's keys should be the environment name and the values 18 | // should be the asset to use in that environment. 19 | // 20 | // If the library that you are including contains AMD or ES6 21 | // modules that you would like to import into your application 22 | // please specify an object with the list of modules as keys 23 | // along with the exports of each module as its value. 24 | 25 | return app.toTree(); 26 | }; 27 | -------------------------------------------------------------------------------- /ember-electron/.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "passthrough": true 4 | } 5 | } -------------------------------------------------------------------------------- /ember-electron/electron-forge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "make_targets": { 3 | "win32": [ 4 | "squirrel" 5 | ], 6 | "darwin": [ 7 | "zip" 8 | ], 9 | "linux": [ 10 | "deb", 11 | "rpm" 12 | ] 13 | }, 14 | "electronPackagerConfig": {}, 15 | "electronWinstallerConfig": { 16 | "name": "" 17 | }, 18 | "electronInstallerDebian": {}, 19 | "electronInstallerRedhat": {}, 20 | "github_repository": { 21 | "owner": "", 22 | "name": "" 23 | }, 24 | "windowsStoreConfig": { 25 | "packageName": "" 26 | } 27 | }; -------------------------------------------------------------------------------- /ember-electron/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const { app, BrowserWindow, protocol, shell } = require('electron'); 3 | const { dirname, join, resolve } = require('path'); 4 | const protocolServe = require('electron-protocol-serve'); 5 | const localshortcut = require('electron-localshortcut'); 6 | 7 | let mainWindow = null; 8 | 9 | protocol.registerStandardSchemes(['serve'], { secure: true }); 10 | protocolServe({ 11 | cwd: join(__dirname || resolve(dirname('')), '..', 'ember'), 12 | app, 13 | protocol, 14 | }); 15 | 16 | // Uncomment the lines below to enable Electron's crash reporter 17 | // For more information, see http://electron.atom.io/docs/api/crash-reporter/ 18 | 19 | // electron.crashReporter.start({ 20 | // productName: 'YourName', 21 | // companyName: 'YourCompany', 22 | // submitURL: 'https://your-domain.com/url-to-submit', 23 | // autoSubmit: true 24 | // }); 25 | 26 | app.on('window-all-closed', () => { 27 | if (process.platform !== 'darwin') { 28 | app.quit(); 29 | } 30 | }); 31 | 32 | app.on('ready', () => { 33 | mainWindow = new BrowserWindow({ 34 | minWidth: 1024, 35 | minHeight: 768, 36 | width: 1024, 37 | height: 768, 38 | icon: join(__dirname, 'resources', 'icons', 'linux.png') 39 | }); 40 | 41 | 42 | mainWindow.setMenu(null); 43 | 44 | mainWindow.maximize(); 45 | 46 | // delete mainWindow.module; 47 | 48 | const emberAppLocation = `file://${__dirname}/../ember/index.html`; 49 | 50 | // If you want to open up dev tools programmatically, call 51 | // mainWindow.openDevTools(); 52 | 53 | mainWindow.loadURL(emberAppLocation); 54 | 55 | // If a loading operation goes wrong, we'll send Electron back to 56 | // Ember App entry point 57 | mainWindow.webContents.on('did-fail-load', () => { 58 | mainWindow.loadURL(emberAppLocation); 59 | }); 60 | 61 | mainWindow.webContents.on('crashed', () => { 62 | console.log('Your Ember app (or other code) in the main window has crashed.'); 63 | console.log('This is a serious issue that needs to be handled and/or debugged.'); 64 | }); 65 | 66 | mainWindow.on('unresponsive', () => { 67 | console.log('Your Ember app (or other code) has made the window unresponsive.'); 68 | }); 69 | 70 | mainWindow.on('responsive', () => { 71 | console.log('The main window has become responsive again.'); 72 | }); 73 | 74 | mainWindow.on('closed', () => { 75 | mainWindow = null; 76 | }); 77 | 78 | // Open external links in default browser 79 | let handleRedirect = (e, url) => { 80 | if(url !== mainWindow.webContents.getURL()) { 81 | e.preventDefault(); 82 | shell.openExternal(url); 83 | } 84 | }; 85 | 86 | mainWindow.webContents.on('will-navigate', handleRedirect); 87 | mainWindow.webContents.on('new-window', handleRedirect); 88 | 89 | // Shortcuts 90 | localshortcut.register(mainWindow, 'F11', () => { 91 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 92 | }); 93 | 94 | localshortcut.register(mainWindow, 'Shift+CmdOrCtrl+I', () => { 95 | mainWindow.webContents.toggleDevTools(); 96 | }); 97 | 98 | localshortcut.register(mainWindow, 'CmdOrCtrl+R', () => { 99 | mainWindow.reload(); 100 | }); 101 | }); 102 | 103 | 104 | process.on('uncaughtException', (err) => { 105 | console.log('An exception in the main thread was not handled.'); 106 | console.log('This is a serious issue that needs to be handled and/or debugged.'); 107 | console.log(`Exception: ${err}`); 108 | }); 109 | -------------------------------------------------------------------------------- /ember-electron/resources-darwin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/ember-electron/resources-darwin/.gitkeep -------------------------------------------------------------------------------- /ember-electron/resources-linux/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/ember-electron/resources-linux/.gitkeep -------------------------------------------------------------------------------- /ember-electron/resources-linux/icons/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/ember-electron/resources-linux/icons/linux.png -------------------------------------------------------------------------------- /ember-electron/resources-win32/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/ember-electron/resources-win32/.gitkeep -------------------------------------------------------------------------------- /ember-electron/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/ember-electron/resources/.gitkeep -------------------------------------------------------------------------------- /mirage/config.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { faker, Response } from 'ember-cli-mirage'; 3 | 4 | export default function() { 5 | 6 | this.passthrough('https://api.github.com/**'); 7 | 8 | this.timing = 300; // delay for each request, automatically set to 0 during testing 9 | 10 | // Exercises API host 11 | // ------------------- 12 | this.urlPrefix = 'http://x.exercism.io'; 13 | 14 | this.get('', () => { // PING request done by debug service 15 | if (faker.random.boolean()) { 16 | return new Response(200, {}, { repository: 'https://github.com/exercism/x-api' }); 17 | } 18 | return new Response(500, {}, {}); 19 | }); 20 | 21 | this.get('/tracks'); 22 | 23 | this.get('/tracks/:id'); 24 | 25 | this.get('/v2/exercises/:track', (schema, request) => { 26 | let problems = [schema.problems.find(request.params.track)]; 27 | return { problems }; 28 | }); 29 | 30 | this.get('/v2/exercises/:track/:slug', (schema, request) => { 31 | let problems = [schema.problems.find(request.params.track)]; 32 | return { problems }; 33 | }); 34 | 35 | this.get('/v2/exercises/restore', (schema) => { 36 | let ids = ['elixir', 'elixir2', 'python', 'rust', 'erlang'], 37 | problems = schema.problems.find(ids).models; 38 | return { problems }; 39 | }); 40 | 41 | // Exercism API host 42 | // ----------------- 43 | this.urlPrefix = 'http://exercism.io'; 44 | this.namespace = 'api/v1'; 45 | 46 | this.get('tracks/:id/status', (schema, request) => { 47 | return schema.statuses.find(request.params.id); 48 | }); 49 | 50 | this.get('submissions/:id', (schema, request) => { 51 | let uuid = request.params.id; 52 | if (uuid.includes('bad')) { 53 | return new Response(404, {}, { error: `unknown submission ${uuid}` }); 54 | } 55 | 56 | return { 57 | uuid, 58 | url: `http://exercism.io/submissions/${uuid})}`, 59 | slug: 'exercism-gui-fake-problem', 60 | track_id: 'elixir', 61 | username: 'frodo', 62 | solution_files: { 'file1.ex': 'some content' } 63 | }; 64 | }); 65 | 66 | this.get('submissions/:track/:slug', (schema, request) => { 67 | let slug = request.params.slug, 68 | track_id = request.params.track; 69 | return { 70 | url: `http://exercism.io/submissions/${faker.random.uuid()}`, 71 | slug, 72 | track_id 73 | }; 74 | }); 75 | 76 | this.post('iterations/:track/:slug/skip', () => { 77 | return new Response(204, {}, {}); 78 | }); 79 | 80 | this.post('user/assignments', (schema, request) => { 81 | const attrs = JSON.parse(request.requestBody); 82 | if (attrs.language === 'bash') { 83 | return new Response(400, {}, { error: 'duplicate of previous iteration' }); 84 | } 85 | if (attrs.language === 'perl6') { 86 | return new Response(500, {}, { errors: ['some error'] }); 87 | } 88 | let id = faker.random.uuid(), 89 | submissionPath = `submissions/${id}`; 90 | let data = { 91 | id, 92 | iteration: faker.random.number({ min: 1, max: 25 }), 93 | status: 'saved', 94 | slug: attrs.problem, 95 | track: attrs.language, 96 | exercise: attrs.problem, 97 | track_id: attrs.language, 98 | submission_path: `/${submissionPath}`, 99 | url: `http://exercism.io/${submissionPath}`, 100 | language: Ember.String.capitalize(attrs.language), 101 | name: Ember.String.capitalize(attrs.problem), 102 | }; 103 | return new Response(201, {}, data); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /mirage/factories/languages.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'C', 'C++', 'Elixir', 'Rust', 'Python', 3 | 'Ruby', 'Bash', 'Elm', 'JavaScript', 'Go', 4 | 'Perl6', 'Scala', 'Clojure', 'OCaml','PHP', 5 | 'Lisp', 'Lua', 'Erlang' 6 | ]; 7 | -------------------------------------------------------------------------------- /mirage/factories/problem.js: -------------------------------------------------------------------------------- 1 | import { Factory } from 'ember-cli-mirage'; 2 | import languages from 'exercism-gui/mirage/factories/languages'; 3 | 4 | export default Factory.extend({ 5 | id(i) { 6 | return languages[i].toLowerCase(); 7 | }, 8 | track_id(i) { 9 | return languages[i].toLowerCase(); 10 | }, 11 | language(i) { 12 | return languages[i].toLowerCase(); 13 | }, 14 | slug: 'exercism-gui-fake-problem', 15 | name: 'Fake problem', 16 | files: { 17 | 'problem': 'problem', 18 | 'problem_tests': 'tests', 19 | 'nested/dir/file1': 'aaaa', 20 | 'README.md': 'readme' 21 | }, 22 | fresh: false 23 | }); 24 | -------------------------------------------------------------------------------- /mirage/factories/status.js: -------------------------------------------------------------------------------- 1 | import { Factory, faker } from 'ember-cli-mirage'; 2 | import languages from 'exercism-gui/mirage/factories/languages'; 3 | 4 | export default Factory.extend({ 5 | id(i) { 6 | return languages[i].toLowerCase(); 7 | }, 8 | 9 | track_id(i) { 10 | return languages[i].toLowerCase(); 11 | }, 12 | 13 | submitted() { 14 | let num = faker.random.number({ max: 60 }); 15 | let problems = faker 16 | .random.words(num) 17 | .toLowerCase() 18 | .split(' '); 19 | 20 | problems.push('exercism-gui-fake-problem'); 21 | 22 | return problems; 23 | }, 24 | 25 | skipped() { 26 | let num = faker.random.number({ max: 20 }); 27 | return faker 28 | .random.words(num) 29 | .toLowerCase() 30 | .split(' '); 31 | }, 32 | 33 | recent(i) { 34 | let lang = languages[i].toLowerCase(); 35 | let problem = (lang.indexOf('c')) ? 'exercism-gui-fake-problem' : 'You haven\'t submitted any solutions yet'; 36 | return { 37 | problem, 38 | submitted_at: faker.date.recent() 39 | }; 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /mirage/factories/track.js: -------------------------------------------------------------------------------- 1 | import { Factory, faker } from 'ember-cli-mirage'; 2 | import languages from 'exercism-gui/mirage/factories/languages'; 3 | 4 | 5 | export default Factory.extend({ 6 | id(i) { 7 | return languages[i].toLowerCase(); 8 | }, 9 | language(i) { 10 | return languages[i]; 11 | }, 12 | slug(i) { 13 | return languages[i].toLowerCase(); 14 | }, 15 | active(i) { 16 | let language = languages[i]; 17 | if (language.startsWith('L')) { 18 | return false; 19 | } 20 | return true; 21 | }, 22 | repository() { 23 | return faker.internet.url(); 24 | }, 25 | todo() { 26 | return []; 27 | }, 28 | problems() { 29 | const length = faker.random.number({ min: 0, max: 5 }); 30 | let problems = []; 31 | for(let i=0; i <= length; i++) { 32 | problems.push(faker.lorem.word()); 33 | } 34 | problems.push('exercism-gui-fake-problem'); 35 | return problems; 36 | } 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /mirage/models/problem.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /mirage/models/status.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /mirage/models/track.js: -------------------------------------------------------------------------------- 1 | import { Model } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | import languages from 'exercism-gui/mirage/factories/languages'; 2 | 3 | export default function(server) { 4 | server.createList('track', languages.length); 5 | server.createList('problem', languages.length); 6 | server.createList('status', languages.length); 7 | server.create('problem', { 8 | id: 'elixir2', 9 | track_id: 'elixir', 10 | language: 'elixir', 11 | slug: 'exercism-gui-fake-problem-2' 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { ActiveModelSerializer } from 'ember-cli-mirage'; 2 | 3 | export default ActiveModelSerializer.extend({ 4 | serialize(object) { 5 | let json = ActiveModelSerializer.prototype.serialize.apply(this, arguments); 6 | if (object.modelName === 'status') { 7 | return json.status; 8 | } 9 | return json; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercism-gui", 3 | "version": "0.4.0", 4 | "description": "A desktop app to fetch and submit exercism.io problems", 5 | "private": true, 6 | "directories": { 7 | "doc": "doc", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "build": "ember build", 12 | "start": "ember server", 13 | "test": "ember test" 14 | }, 15 | "repository": "", 16 | "engines": { 17 | "node": ">= 4" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "active-model-adapter": "2.1.1", 23 | "babel-plugin-transform-async-to-generator": "^6.24.1", 24 | "babel-preset-env": "^1.4.0", 25 | "babel-preset-react": "^6.24.1", 26 | "broccoli-asset-rev": "^2.4.5", 27 | "devtron": "^1.4.0", 28 | "electron-prebuilt-compile": "1.6.2", 29 | "electron-rebuild": "^1.5.7", 30 | "ember-ajax": "^2.4.1", 31 | "ember-api-actions": "0.1.6", 32 | "ember-cli": "2.12.1", 33 | "ember-cli-app-version": "^2.0.0", 34 | "ember-cli-babel": "^5.1.7", 35 | "ember-cli-dependency-checker": "^1.3.0", 36 | "ember-cli-eslint": "^3.0.0", 37 | "ember-cli-flash": "1.4.2", 38 | "ember-cli-htmlbars": "^1.1.1", 39 | "ember-cli-htmlbars-inline-precompile": "^0.3.6", 40 | "ember-cli-inject-live-reload": "^1.4.1", 41 | "ember-cli-mirage": "0.2.6", 42 | "ember-cli-moment-shim": "^3.0.1", 43 | "ember-cli-qunit": "^3.1.0", 44 | "ember-cli-release": "0.2.9", 45 | "ember-cli-shims": "^1.0.2", 46 | "ember-cli-sri": "^2.1.0", 47 | "ember-cli-test-loader": "^1.1.0", 48 | "ember-cli-testdouble": "0.1.0", 49 | "ember-cli-uglify": "^1.2.0", 50 | "ember-composable-helpers": "2.0.0", 51 | "ember-concurrency": "0.7.19", 52 | "ember-cp-validations": "3.2.3", 53 | "ember-data": "^2.12.0", 54 | "ember-electron": "^2.1.1", 55 | "ember-export-application-global": "^1.0.5", 56 | "ember-inspector": "^2.0.4", 57 | "ember-load-initializers": "^0.6.0", 58 | "ember-moment": "^7.3.0", 59 | "ember-pin": "0.1.7", 60 | "ember-power-select": "1.4.4", 61 | "ember-resolver": "^2.0.3", 62 | "ember-route-action-helper": "1.0.0", 63 | "ember-source": "~2.12.0", 64 | "ember-test-selectors": "0.2.1", 65 | "ember-transition-helper": "0.0.6", 66 | "ember-truth-helpers": "1.3.0", 67 | "ember-windoc": "^0.1.8", 68 | "loader.js": "^4.2.3", 69 | "mock-fs": "^4.1.0", 70 | "semantic-ui-ember": "2.0.1" 71 | }, 72 | "dependencies": { 73 | "electron-compile": "^6.4.0", 74 | "electron-localshortcut": "^1.0.0", 75 | "electron-protocol-serve": "^1.3.0", 76 | "jsonfile": "^2.4.0", 77 | "lodash": "^4.17.4", 78 | "mkdirp": "^0.5.1", 79 | "node-notifier": "^5.0.2", 80 | "os-homedir": "^1.0.2", 81 | "ping": "^0.2.2", 82 | "request-promise": "^4.1.1", 83 | "semver": "^5.3.0", 84 | "url-join": "^1.1.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/assets/icons/darwin.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/public/assets/icons/darwin.icns -------------------------------------------------------------------------------- /public/assets/icons/win32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/public/assets/icons/win32.ico -------------------------------------------------------------------------------- /public/assets/images/e_red_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/public/assets/images/e_red_small.png -------------------------------------------------------------------------------- /public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/public/assets/images/logo.png -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /resources/anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/resources/anim.gif -------------------------------------------------------------------------------- /testem-electron.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'framework': 'qunit', 3 | 'test_page': 'tests/index.html?hidepassed', 4 | 'disable_watching': true, 5 | 'launchers': { 6 | 'Electron': require('ember-electron/test-runner'), 7 | }, 8 | 'launch_in_ci': [ 9 | 'Electron', 10 | ], 11 | 'launch_in_dev': [ 12 | 'Electron', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "PhantomJS" 7 | ], 8 | "launch_in_dev": [ 9 | "PhantomJS", 10 | "Chrome" 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | }, 5 | globals: { 6 | process: true, 7 | requireNode: true, 8 | server: true, 9 | selectChoose: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /tests/acceptance/download-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from 'exercism-gui/tests/helpers/module-for-acceptance'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForAcceptance('Acceptance | download'); 6 | 7 | test('download shows success status', function(assert) { 8 | visit('/download'); 9 | 10 | fillIn(testSelector('input'), 'good'); 11 | click(testSelector('btn')); 12 | 13 | andThen(function() { 14 | assert.equal(currentURL(), '/download/good/status'); 15 | assert.ok(find(testSelector('success')).text().toLowerCase().includes('success')); 16 | assert.equal(find(testSelector('error')).text(), ''); 17 | }); 18 | }); 19 | 20 | test('unsuccesfull download shows error message', function(assert) { 21 | visit('/download/bad/status'); 22 | 23 | andThen(function() { 24 | assert.equal(currentURL(), '/download/bad/status'); 25 | assert.ok(find(testSelector('error')).text().includes('wrong')); 26 | assert.equal(find(testSelector('success')).text(), ''); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/acceptance/problems-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from 'exercism-gui/tests/helpers/module-for-acceptance'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForAcceptance('Acceptance | problems'); 6 | 7 | test('it skips a problem', function(assert) { 8 | visit('/tracks/elixir/problems/bob/skip'); 9 | 10 | andThen(function() { 11 | assert.equal(currentURL(), '/tracks/elixir/problems/bob/skip'); 12 | let expected = 'Skipped bob in track elixir'; 13 | assert.equal(find(testSelector('skip-success')).text().trim(), expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/acceptance/tracks-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { test } from 'qunit'; 3 | import moduleForAcceptance from 'exercism-gui/tests/helpers/module-for-acceptance'; 4 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 5 | import td from 'testdouble'; 6 | 7 | moduleForAcceptance('Acceptance | tracks'); 8 | 9 | test('selecting a track at /tracks redirects to track details', function(assert) { 10 | const lang = 'elixir'; 11 | server.create('track', { id: lang, language: lang }); 12 | 13 | visit('/tracks'); 14 | 15 | andThen(function() { 16 | selectChoose('.track-selector', lang); 17 | andThen(function() { 18 | assert.equal(find('.track-selector .ember-power-select-trigger').text().trim(), lang); 19 | assert.equal(currentURL(), `/tracks/${lang}`); 20 | }); 21 | }); 22 | }); 23 | 24 | test('it shows error page if api responds with error when listing tracks', function(assert) { 25 | server.get('http://x.exercism.io/tracks', { errors: ['There was an error'] }, 500); 26 | 27 | // we need to stub this to avoid ember catching the error 28 | // taken from this question at stackoverflow 29 | // http://stackoverflow.com/questions/34074386/how-to-test-ember-error-substate-with-ember-cli-test-runner 30 | const emberTestAdapterException = Ember.Test.adapter.exception; 31 | Ember.Test.adapter.exception = td.function(); 32 | 33 | visit('/tracks'); 34 | 35 | andThen(function() { 36 | assert.equal(currentURL(), '/tracks'); 37 | assert.equal(find(testSelector('errors')).text().trim(), 'There was an error'); 38 | 39 | // Restore the adapter 40 | Ember.Test.adapter.exception = emberTestAdapterException; 41 | }); 42 | }); 43 | 44 | test('it shows submission link at submission route', function(assert) { 45 | let slug = 'bob', 46 | trackId = 'elixir', 47 | url = 'http://exercism.io/submissions/fake'; 48 | 49 | server.create('track', { id: trackId }); 50 | let recent = { problem: slug, submitted_at: null }; 51 | server.create('status', { id: trackId, recent }); 52 | 53 | server.get( 54 | `http://exercism.io/api/v1/submissions/${trackId}/${slug}`, 55 | { slug, url, track_id: trackId }); 56 | visit(`/tracks/${trackId}/status/submission/${slug}`); 57 | 58 | andThen(function() { 59 | let expected = `Link to latest submission for "${slug}"`; 60 | assert.equal(currentURL(), `/tracks/${trackId}/status/submission/${slug}`); 61 | assert.equal(find(testSelector('submission-header')).text().trim(), expected); 62 | assert.equal(find(testSelector('submission-link')).text().trim(), url); 63 | }); 64 | }); 65 | 66 | test('it redirect to status when clicking on status button', function(assert) { 67 | let lang = 'elixir'; 68 | server.create('track', { id: lang }); 69 | server.create('status', { id: lang }); 70 | 71 | visit(`/tracks/${lang}`); 72 | andThen(function() { 73 | click(find(testSelector('status-btn'))); 74 | andThen(function() { 75 | assert.equal(currentURL(), `/tracks/${lang}/status`); 76 | }); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /tests/electron.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const BrowserWindow = require('electron').BrowserWindow; 4 | const app = require('electron').app; 5 | 6 | let mainWindow = null; 7 | 8 | app.on('window-all-closed', function onWindowAllClosed() { 9 | if (process.platform !== 'darwin') { 10 | app.quit(); 11 | } 12 | }); 13 | 14 | app.on('ready', function onReady() { 15 | mainWindow = new BrowserWindow({ 16 | width: 800, 17 | height: 600, 18 | backgroundThrottling: false 19 | }); 20 | 21 | delete mainWindow.module; 22 | 23 | if (process.env.EMBER_ENV === 'test') { 24 | mainWindow.loadURL('file://' + __dirname + '/index.html'); 25 | } else { 26 | mainWindow.loadURL('file://' + __dirname + '/dist/index.html'); 27 | } 28 | 29 | mainWindow.on('closed', function onClosed() { 30 | mainWindow = null; 31 | }); 32 | }); 33 | 34 | /* eslint-enable */ 35 | -------------------------------------------------------------------------------- /tests/ember-electron/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const { app, BrowserWindow, protocol } = require('electron'); 3 | const { dirname, resolve } = require('path'); 4 | const url = require('url'); 5 | const protocolServe = require('electron-protocol-serve'); 6 | 7 | let mainWindow = null; 8 | 9 | // The testUrl is a file: url pointing to our index.html, with some query 10 | // params we need to preserve for testem. So we need to register our ember 11 | // protocol accordingly. 12 | const [, , indexUrl] = process.argv; 13 | const { 14 | pathname: indexPath, 15 | search: indexQuery, 16 | } = url.parse(indexUrl); 17 | const emberAppLocation = `serve://dist${indexQuery}`; 18 | 19 | protocol.registerStandardSchemes(['serve'], { secure: true }); 20 | // The index.html is in the tests/ directory, so we want all other assets to 21 | // load from its parent directory 22 | protocolServe({ 23 | cwd: resolve(dirname(indexPath), '..'), 24 | app, 25 | protocol, 26 | indexPath, 27 | }); 28 | 29 | app.on('window-all-closed', function onWindowAllClosed() { 30 | if (process.platform !== 'darwin') { 31 | app.quit(); 32 | } 33 | }); 34 | 35 | app.on('ready', function onReady() { 36 | mainWindow = new BrowserWindow({ 37 | width: 800, 38 | height: 600, 39 | backgroundThrottling: false, 40 | }); 41 | 42 | delete mainWindow.module; 43 | 44 | mainWindow.loadURL(emberAppLocation); 45 | 46 | mainWindow.on('closed', function onClosed() { 47 | mainWindow = null; 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | server.shutdown(); 6 | } 7 | -------------------------------------------------------------------------------- /tests/helpers/flash-message.js: -------------------------------------------------------------------------------- 1 | import FlashObject from 'ember-cli-flash/flash/object'; 2 | 3 | FlashObject.reopen({ init() {} }); 4 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import registerPowerSelectHelpers from '../../tests/helpers/ember-power-select'; 4 | 5 | registerPowerSelectHelpers(); 6 | 7 | import config from '../../config/environment'; 8 | 9 | export default function startApp(attrs) { 10 | let attributes = Ember.merge({}, config.APP); 11 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 12 | 13 | return Ember.run(() => { 14 | let application = Application.create(attributes); 15 | application.setupForTesting(); 16 | application.injectTestHelpers(); 17 | return application; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ExercismGui Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/config-form/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | 6 | moduleForComponent('config-form', 'Integration | Component | config form', { 7 | integration: true 8 | }); 9 | 10 | test('it hides validation errors on first render', function(assert) { 11 | this.set('config', {}); 12 | this.render(hbs`{{config-form config=config}}`); 13 | assert.equal(this.$(testSelector('dir-errmsg')).text(), ''); 14 | assert.notOk(this.$(testSelector('dir-field')).hasClass('error')); 15 | }); 16 | 17 | test('it validates input', function(assert) { 18 | this.set('config', {}); 19 | this.render(hbs`{{config-form config=config}}`); 20 | assert.equal(this.$(testSelector('dir-errmsg')).text(), ''); 21 | this.$(testSelector('save-btn')).click(); 22 | assert.equal(this.$(testSelector('dir-errmsg')).text().trim(), 'This field can\'t be blank'); 23 | assert.ok(this.$(testSelector('dir-field')).hasClass('error')); 24 | }); 25 | 26 | test('it sends action up on save will all the correct values', function(assert) { 27 | let config = { 28 | api: 'http://a.com', 29 | xapi: 'http://x.a.com', 30 | apiKey: 'aabbcc', 31 | dir: '/a/fake/path' 32 | }; 33 | let newPath = '/some/new/path'; 34 | this.set('save', (configToSave) => { 35 | assert.equal(this.$(testSelector('dir-errmsg')).text(), ''); 36 | assert.notOk(this.$(testSelector('dir-field')).hasClass('error')); 37 | 38 | assert.equal(configToSave.api, config.api); 39 | assert.equal(configToSave.xapi, config.xapi); 40 | assert.equal(configToSave.apiKey, config.apiKey); 41 | assert.equal(configToSave.dir, newPath); 42 | }); 43 | 44 | this.set('config', config); 45 | 46 | this.render(hbs`{{config-form config=config saveConfig=(action save)}}`); 47 | this.$(testSelector('dir-input')).val(newPath).trigger('input'); 48 | this.$(testSelector('save-btn')).click(); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/integration/components/download-form/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('download-form', 'Integration | Component | download form', { 6 | integration: true 7 | }); 8 | 9 | test('it renders', function(assert) { 10 | this.render(hbs`{{download-form}}`); 11 | 12 | let input = this.$(testSelector('input')); 13 | assert.equal(input.attr('placeholder'), 'ID of submitted solution to dowload'); 14 | assert.equal(input.val(), ''); 15 | 16 | input.val('someuuid'); 17 | input.trigger('input'); 18 | 19 | assert.equal(input.val(), 'someuuid'); 20 | }); 21 | 22 | test('it shows validation error and disabled button if empty input', function(assert) { 23 | this.render(hbs`{{download-form}}`); 24 | 25 | this.$(testSelector('input')).val('').trigger('input'); 26 | this.$(testSelector('btn')).click(); 27 | 28 | assert.ok(this.$(testSelector('btn')).hasClass('disabled')); 29 | assert.equal(this.$(testSelector('errmsg')).text().trim(), 'This field can\'t be blank'); 30 | }); 31 | 32 | test('it sends uuid when clicking download button', function(assert) { 33 | this.set('download', (uuid) => { 34 | assert.equal(this.$(testSelector('errmsg')).text(), ''); 35 | 36 | assert.equal(uuid, 'fakeuuid'); 37 | }); 38 | 39 | this.render(hbs`{{download-form download=(action download)}}`); 40 | 41 | this.$(testSelector('input')).val('fakeuuid').trigger('input'); 42 | this.$(testSelector('btn')).click(); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/integration/components/fetch-summary/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('fetch-summary', 'Integration | Component | fetch summary', { 6 | integration: true 7 | }); 8 | 9 | test('it renders several problems', function(assert) { 10 | let summary = [ 11 | { problem: 'a', new: ['f1.a', 'f2.a'] }, 12 | { problem: 'b', new: ['f1.b'], unchanged: ['f2.b'] }, 13 | { problem: 'c', unchanged: ['f1.c', 'f2.c', 'f3.c'] } 14 | ]; 15 | 16 | this.set('summary', summary); 17 | this.render(hbs`{{fetch-summary summary=summary}}`); 18 | 19 | assert.equal(this.$(testSelector('new-files', 'a')).children().length, 2); 20 | assert.equal(this.$(testSelector('unchanged-files', 'a')).children().length, 0); 21 | 22 | assert.equal(this.$(testSelector('new-files', 'b')).children().length, 1); 23 | assert.equal(this.$(testSelector('unchanged-files', 'b')).children().length, 1); 24 | 25 | assert.equal(this.$(testSelector('new-files', 'c')).children().length, 0); 26 | assert.equal(this.$(testSelector('unchanged-files', 'c')).children().length, 3); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/integration/components/file-selector/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | const mockFs = requireNode('mock-fs'); 6 | 7 | moduleForComponent('file-selector', 'Integration | Component | file selector', { 8 | integration: true, 9 | afterEach() { 10 | mockFs.restore(); 11 | } 12 | }); 13 | 14 | test('it renders one radio button per file', function(assert) { 15 | let problem = { files: ['f1', 'f2', 'f3', 'f4'] }; 16 | this.set('problem', problem); 17 | 18 | this.render(hbs`{{file-selector problem=problem}}`); 19 | assert.equal(this.$(testSelector('files')).length, problem.files.length); 20 | }); 21 | 22 | test('it shows message when no files available', function(assert) { 23 | this.set('problem', { files: [] }); 24 | 25 | this.render(hbs`{{file-selector problem=problem}}`); 26 | assert.equal(this.$(testSelector('no-files-msg')).text().trim(), 'None'); 27 | }); 28 | 29 | test('it disables submit and open buttons if no selection', function(assert) { 30 | let problem = { files: ['f1'] }; 31 | this.set('problem', problem); 32 | 33 | this.render(hbs`{{file-selector problem=problem}}`); 34 | assert.equal(this.$(testSelector('files')).length, 1); 35 | assert.ok(this.$(testSelector('submit-btn')).hasClass('disabled')); 36 | assert.ok(this.$(testSelector('open-btn')).hasClass('disabled')); 37 | }); 38 | 39 | test('it enables submit and open buttons if selection', function(assert) { 40 | let problem = { files: ['f1'] }; 41 | this.set('problem', problem); 42 | this.set('selectedFile', 'f1'); 43 | 44 | this.render(hbs`{{file-selector problem=problem selectedFile=selectedFile}}`); 45 | assert.notOk(this.$(testSelector('submit-btn')).hasClass('disabled')); 46 | assert.notOk(this.$(testSelector('open-btn')).hasClass('disabled')); 47 | }); 48 | 49 | test('it shows error message if file no longer exists', function(assert) { 50 | let problem = { files: ['f1', 'f2'], dir: '/some/dir/bob', name: 'bob' }; 51 | this.set('problem', problem); 52 | this.set('selectedFile', 'f2'); 53 | let submit = () => {}; 54 | this.set('actions', { submit }); 55 | 56 | this.render(hbs`{{file-selector problem=problem selectedFile=selectedFile submit=(action "submit")}}`); 57 | assert.equal(this.$(testSelector('error-msg')).text(), ''); 58 | this.$(testSelector('submit-btn')).click(); 59 | assert.equal(this.$(testSelector('error-msg')).text().trim(), 'The file /some/dir/bob/f2 no longer exists'); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /tests/integration/components/local-problem-selector/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('local-problem-selector', 'Integration | Component | local problem selector', { 6 | integration: true 7 | }); 8 | 9 | test('it shows a message if problems is an empty list', function(assert) { 10 | this.set('problems', []); 11 | this.render(hbs`{{local-problem-selector problems=problems}}`); 12 | 13 | assert.equal(this.$(testSelector('no-problems-msg')).text().trim(), 'No problem files found under this track'); 14 | }); 15 | 16 | test('it shows no files or dir if problem was not selected', function(assert) { 17 | this.set('problems', [ { name: 'bob', files: [], dir: '/aaa' } ]); 18 | this.render(hbs`{{local-problem-selector problems=problems}}`); 19 | 20 | assert.equal(this.$(testSelector('dir')).text().trim(), ''); 21 | assert.equal(this.$(testSelector('file-selector')).text().trim(), ''); 22 | }); 23 | 24 | test('it shows dir and file selector if problem was selected', function(assert) { 25 | let problem = { name: 'bob', files: [], dir: '/aaa' }; 26 | this.set('problems', [problem]); 27 | this.set('problem', problem); 28 | this.render(hbs`{{local-problem-selector problems=problems selectedProblem=problem}}`); 29 | 30 | assert.equal(this.$(testSelector('dir')).text().trim(), '/aaa'); 31 | assert.equal(this.$(testSelector('no-files-msg')).text().trim(), 'None'); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/integration/components/new-release/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('new-release', 'Integration | Component | new release', { 6 | integration: true 7 | }); 8 | 9 | 10 | let getAssets = function(tagName, platforms=null) { 11 | platforms = platforms? platforms : ['linux-x64', 'darwin-x64', 'win32-x64', 'win32-x86']; 12 | return platforms.map((platform) => { 13 | let name = `exercism-gui-${platform}-${tagName}.tar.gz`; 14 | 15 | return { 16 | browserDownloadUrl: `https://gh.com/download/${tagName}/${name}`, 17 | size: 43604687, 18 | name 19 | }; 20 | }); 21 | }; 22 | 23 | 24 | let getRelease = function(tagName='v0.0.5') { 25 | return { 26 | htmlUrl: `https://github.com/exercism/gui/releases/tag/${tagName}`, 27 | publishedAt: '2016-07-11T19:00:00Z', 28 | tagName, 29 | assets: getAssets(tagName), 30 | }; 31 | }; 32 | 33 | test('it shows new release details', function(assert) { 34 | let release = getRelease('v0.1.0'); 35 | this.set('release', release); 36 | this.set('info', { tag: 'v0.0.1', platform: 'linux', arch: 'x64' }); 37 | this.render(hbs`{{new-release release=release info=info}}`); 38 | assert.ok(this.$(testSelector('published-at')).text().includes('ago')); 39 | assert.equal(this.$(testSelector('tag-name')).text().trim(), '0.1.0'); 40 | assert.equal(this.$(testSelector('tag-name')).attr('href'), release.htmlUrl); 41 | }); 42 | 43 | test('it shows nothing if release is null', function(assert) { 44 | this.set('release', null); 45 | 46 | this.render(hbs`{{new-release release=release info=info}}`); 47 | assert.equal(this.$().text().trim(), ''); 48 | }); 49 | 50 | test('it shows nothing if release is the same', function(assert) { 51 | this.set('release', getRelease('v0.0.1')); 52 | this.set('info', { tag: 'v0.0.1' }); 53 | 54 | this.render(hbs`{{new-release release=release info=info}}`); 55 | assert.equal(this.$().text().trim(), ''); 56 | }); 57 | 58 | test('it shows nothing if release is older', function(assert) { 59 | this.set('release', getRelease('v0.0.1')); 60 | this.set('info', { tag: 'v0.0.2' }); 61 | 62 | this.render(hbs`{{new-release release=release info=info}}`); 63 | assert.equal(this.$().text().trim(), ''); 64 | }); 65 | 66 | test('it provides a download link for the proper platform - linux', function(assert) { 67 | let release = getRelease(); 68 | this.set('release', release); 69 | this.set('info', { tag: 'v0.0.1', platform: 'linux', arch: 'x64' }); 70 | this.render(hbs`{{new-release release=release info=info}}`); 71 | let pkg = release.assets[0]; 72 | assert.equal(this.$(testSelector('pkg-download-link')).text().trim(), pkg.browserDownloadUrl); 73 | assert.equal(this.$(testSelector('pkg-download-link')).attr('href'), pkg.browserDownloadUrl); 74 | assert.equal(this.$(testSelector('pkg-size')).text().trim(), `${(pkg.size / 1000000).toFixed(2)} MiB`); 75 | }); 76 | 77 | test('it provides a download link for the proper platform - darwin', function(assert) { 78 | let release = getRelease(); 79 | this.set('release', release); 80 | this.set('info', { tag: 'v0.0.1', platform: 'darwin', arch: 'x64' }); 81 | this.render(hbs`{{new-release release=release info=info}}`); 82 | let pkg = release.assets[1]; 83 | assert.equal(this.$(testSelector('pkg-download-link')).text().trim(), pkg.browserDownloadUrl); 84 | }); 85 | 86 | test('it provides a download link for the proper platform - win32 x64', function(assert) { 87 | let release = getRelease(); 88 | this.set('release', release); 89 | this.set('info', { tag: 'v0.0.1', platform: 'win32', arch: 'x64' }); 90 | this.render(hbs`{{new-release release=release info=info}}`); 91 | let pkg = release.assets[2]; 92 | assert.equal(this.$(testSelector('pkg-download-link')).text().trim(), pkg.browserDownloadUrl); 93 | }); 94 | 95 | test('it provides a download link for the proper platform - win32 x86', function(assert) { 96 | let release = getRelease(); 97 | this.set('release', release); 98 | this.set('info', { tag: 'v0.0.1', platform: 'win32', arch: 'x86' }); 99 | this.render(hbs`{{new-release release=release info=info}}`); 100 | let pkg = release.assets[3]; 101 | assert.equal(this.$(testSelector('pkg-download-link')).text().trim(), pkg.browserDownloadUrl); 102 | }); 103 | 104 | test('it provides a download link for the proper platform - win32 ia32', function(assert) { 105 | let release = getRelease(); 106 | this.set('release', release); 107 | this.set('info', { tag: 'v0.0.1', platform: 'win32', arch: 'ia32' }); 108 | this.render(hbs`{{new-release release=release info=info}}`); 109 | let pkg = release.assets[3]; 110 | assert.equal(this.$(testSelector('pkg-download-link')).text().trim(), pkg.browserDownloadUrl); 111 | }); 112 | 113 | test('it hides message if showMessage is false', function(assert) { 114 | let tag = 'v0.0.1', 115 | release = getRelease('v0.0.1'); 116 | this.setProperties({ release, info: { tag } }); 117 | this.render(hbs`{{new-release release=release info=info showMessage=false}}`); 118 | assert.equal(this.$(testSelector('message')).text(), ''); 119 | }); 120 | 121 | test('it shows a message if up-to-date', function(assert) { 122 | let tag = 'v0.0.1', 123 | release = getRelease('v0.0.1'); 124 | this.setProperties({ release, info: { tag } }); 125 | this.render(hbs`{{new-release release=release info=info showMessage=true}}`); 126 | assert.ok(this.$(testSelector('message')).text().includes('up to date')); 127 | }); 128 | 129 | test('it shows a message if no release info available', function(assert) { 130 | this.render(hbs`{{new-release release=null info=null showMessage=true}}`); 131 | assert.ok(this.$(testSelector('message')).text().toLowerCase().includes('could not connect')); 132 | }); 133 | 134 | test('it hides package link if no assets', function(assert) { 135 | let release = getRelease('v0.1.0'); 136 | release.assets = []; 137 | this.set('release', release); 138 | this.set('info', { tag: 'v0.0.1' }); 139 | this.render(hbs`{{new-release release=release info=info}}`); 140 | assert.equal(this.$(testSelector('download-link')).text(), ''); 141 | }); 142 | 143 | test('it hides package link if no asset for the platform', function(assert) { 144 | let release = getRelease('v0.1.0'); 145 | release.assets = getAssets('v0.1.0', ['linux-x64']); 146 | this.set('release', release); 147 | this.set('info', { tag: 'v0.0.1', platform: 'win32', arch: 'ia32' }); 148 | this.render(hbs`{{new-release release=release info=info}}`); 149 | assert.equal(this.$(testSelector('download-link')).text(), ''); 150 | }); 151 | -------------------------------------------------------------------------------- /tests/integration/components/submit-status/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('submit-status', 'Integration | Component | submit status', { 6 | integration: true 7 | }); 8 | 9 | test('it shows a success message if submission succeeded', function(assert) { 10 | let status = { 11 | error: null, 12 | submittedFile: 'somefile', 13 | iteration: 3, 14 | submissionPath: 'submissions/someid', 15 | }; 16 | this.set('status', status); 17 | this.render(hbs`{{submit-status status=status}}`); 18 | assert.equal(this.$(testSelector('success-msg')).text().trim(), 'file submitted succesfully'); 19 | assert.equal(this.$(testSelector('iteration')).text().trim(), 'Iteration number: 3'); 20 | assert.equal(this.$(testSelector('submitted-file')).text(), 'File: somefile'); 21 | assert.equal(this.$(testSelector('submission-path')).text().trim(), `Link to submission: http://exercism.io/${status.submissionPath}`); 22 | }); 23 | 24 | test('it shows an error message if submission duplicate', function(assert) { 25 | let status = { 26 | error: 'some error message', 27 | submittedFile: 'somefile', 28 | iteration: null, 29 | }; 30 | this.set('status', status); 31 | this.render(hbs`{{submit-status status=status}}`); 32 | assert.equal(this.$(testSelector('error-msg')).text().trim(), 'some error message'); 33 | assert.equal(this.$(testSelector('iteration')).text(), ''); 34 | assert.equal(this.$(testSelector('submitted-file')).text().trim(), 'Submitted file was somefile'); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/integration/components/track-action-menu/component-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleForComponent, test } from 'ember-qunit'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 5 | 6 | const configurationStub = Ember.Service.extend({ 7 | apiKey: null 8 | }); 9 | 10 | moduleForComponent('track-action-menu', 'Integration | Component | track action menu', { 11 | integration: true, 12 | beforeEach() { 13 | this.register('service:configuration', configurationStub); 14 | this.inject.service('configuration', { as: 'configuration' }); 15 | 16 | } 17 | }); 18 | 19 | test('it disables actions if api track is null', function(assert) { 20 | this.set('configuration.apiKey', 'aabbcc'); 21 | this.set('track', null); 22 | this.render(hbs`{{track-action-menu track=track}}`); 23 | 24 | assert.ok(this.$(testSelector('fetch-btn')).hasClass('disabled')); 25 | assert.ok(this.$(testSelector('status-btn')).hasClass('disabled')); 26 | }); 27 | 28 | test('it disables actions if api key is unset', function(assert) { 29 | this.set('track', {}); 30 | this.render(hbs`{{track-action-menu track=track}}`); 31 | 32 | assert.ok(this.$(testSelector('fetch-btn')).hasClass('disabled')); 33 | assert.ok(this.$(testSelector('status-btn')).hasClass('disabled')); 34 | }); 35 | 36 | test('it enables actions if api key is set', function(assert) { 37 | this.set('configuration.apiKey', 'aabbcc'); 38 | this.set('track', {}); 39 | this.render(hbs`{{track-action-menu track=track}}`); 40 | 41 | assert.notOk(this.$(testSelector('fetch-btn')).hasClass('disabled')); 42 | assert.notOk(this.$(testSelector('status-btn')).hasClass('disabled')); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/integration/components/track-selector/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('track-selector', 'Integration | Component | track selector', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | this.render(hbs`{{track-selector}}`); 10 | assert.equal(this.$().text().trim(), 'Please, select a track'); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /tests/integration/components/track-status/component-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import testSelector from 'exercism-gui/tests/helpers/ember-test-selectors'; 4 | 5 | moduleForComponent('track-status', 'Integration | Component | track status', { 6 | integration: true 7 | }); 8 | 9 | test('it shows no submission message if no problems submitted', function(assert) { 10 | this.set('status', { recent: { problem: '...any submissions yet...' } }); 11 | this.render(hbs`{{track-status status=status}}`); 12 | assert.equal(this.$(testSelector('message-empty')).text(), ''); 13 | }); 14 | 15 | test('it shows empty skipped list if no skipped problems', function(assert) { 16 | this.set('status', { 17 | recent: { 18 | problem: 'bob', 19 | submitted_at: null 20 | }, 21 | submitted: ['foo'], 22 | skipped: [] 23 | }); 24 | this.render(hbs`{{track-status status=status}}`); 25 | assert.equal(this.$(testSelector('skipped-empty-msg')).text().trim(), 'None'); 26 | }); 27 | 28 | test('it shows info of latest problem', function(assert) { 29 | this.set('status', { 30 | recent: { 31 | problem: 'bob', 32 | submitted_at: null 33 | }, 34 | submitted: ['foo', 'bar', 'baz'] 35 | }); 36 | this.render(hbs`{{track-status status=status}}`); 37 | assert.equal(this.$(testSelector('recent-problem')).text(), 'bob'); 38 | assert.equal(this.$(testSelector('submitted')).children().length, 3); 39 | }); 40 | 41 | test('it shows skipped problems list', function(assert) { 42 | this.set('status', { 43 | recent: { 44 | problem: 'bob', 45 | submitted_at: null 46 | }, 47 | submitted: ['fake'], 48 | skipped: ['foo', 'bar', 'baz'] 49 | }); 50 | this.render(hbs`{{track-status status=status}}`); 51 | assert.equal(this.$(testSelector('skipped')).children().length, 3); 52 | }); 53 | 54 | test('it shows message if last submission was deleted', function(assert) { 55 | this.set('status', { 56 | recent: { 57 | problem: 'bob', 58 | submitted_at: null 59 | }, 60 | skipped: ['foo'] 61 | }); 62 | this.render(hbs`{{track-status status=status}}`); 63 | assert.equal(this.$(testSelector('recent-problem')).text(), 'bob'); 64 | assert.equal(this.$(testSelector('submitted-empty-msg')).text().trim(), 'None. Latest submission was probably deleted'); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-electron-test", 3 | "main": "electron.js" 4 | } 5 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import './helpers/flash-message'; 3 | 4 | import { 5 | setResolver 6 | } from 'ember-qunit'; 7 | 8 | setResolver(resolver); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/application/adapter-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('adapter:application', 'Unit | Adapter | application', { 4 | needs: ['service:exercism', 'service:configuration'] 5 | }); 6 | 7 | test('it adds key params if no params', function(assert) { 8 | let adapter = this.subject(); 9 | adapter.configuration = { apiKey: 'fake' }; 10 | assert.equal(adapter.addKeyParam('http://a.com/api/'), 'http://a.com/api/?key=fake'); 11 | }); 12 | 13 | test('it adds key param when url has params', function(assert) { 14 | let adapter = this.subject(); 15 | adapter.configuration = { apiKey: 'fake' }; 16 | assert.equal(adapter.addKeyParam('http://a.com/api?a=1&b=2'), 'http://a.com/api?a=1&b=2&key=fake'); 17 | }); 18 | 19 | test('it avoids adding the key param if already there', function(assert) { 20 | let adapter = this.subject(); 21 | adapter.configuration = { apiKey: 'fake' }; 22 | assert.equal(adapter.addKeyParam('http://a.com/api?a=1&key=k'), 'http://a.com/api?a=1&key=k'); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/unit/configuration/route-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | import td from 'testdouble'; 3 | 4 | moduleFor('route:configuration', 'Unit | Route | configuration', { 5 | needs: ['service:notifier', 'service:exercism', 'service:configuration'] 6 | }); 7 | 8 | test('it bubbles if error when reading file', function(assert) { 9 | let route = this.subject(); 10 | route.configuration = td.function(); 11 | route.configuration.readConfigFile = td.function(); 12 | td 13 | .when(route.configuration.readConfigFile()) 14 | .thenThrow(new Error('Error: ENOENT, no such file or directory "/fake"')); 15 | 16 | assert.throws(function() { 17 | route.model(); 18 | }, 19 | new Error('Error: ENOENT, no such file or directory "/fake"'), 20 | '' 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/configuration/service-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | import td from 'testdouble'; 3 | 4 | let fs = requireNode('fs'), 5 | mockFs = requireNode('mock-fs'), 6 | fakePath = '/home/fake/e.json'; 7 | 8 | process.env.EXERCISM_CONFIG_FILE = fakePath; 9 | 10 | moduleFor('service:configuration', 'Unit | Service | configuration', { 11 | afterEach() { 12 | mockFs.restore(); 13 | } 14 | }); 15 | 16 | test('it uses config path set in envar', function(assert) { 17 | let service = this.subject(); 18 | assert.equal('/home/fake/e.json', service.getConfigFilePath()); 19 | }); 20 | 21 | test('it returns defaults if config file does not exists', function(assert) { 22 | let service = this.subject(), 23 | expected = { 24 | api: 'http://exercism.io', 25 | xapi: 'http://x.exercism.io', 26 | apiKey: null, 27 | dir: '/home/fake/exercises', 28 | }; 29 | service.getDefaultHomeExercisesDir = td.function(); 30 | td.when(service.getDefaultHomeExercisesDir()).thenReturn(expected.dir); 31 | assert.deepEqual(service.readConfigFile(), expected); 32 | }); 33 | 34 | test('it returns file contents on read', function(assert) { 35 | let expected = { 36 | api: 'http://exercism.io', 37 | xapi: 'http://x.exercism.io', 38 | apiKey: 'aaabbbccc', 39 | dir: '/home/fake/exercises', 40 | }; 41 | 42 | mockFs({ '/home/fake/e.json': JSON.stringify(expected) }); 43 | let service = this.subject(); 44 | assert.deepEqual(service.readConfigFile(), expected); 45 | }); 46 | 47 | test('it writes config file', function(assert) { 48 | let service = this.subject(), 49 | config = { 50 | api: 'http://exercism', 51 | xapi: 'http://x.exercism', 52 | apiKey: 'aaabbbccc', 53 | dir: '/home/fake/exercises', 54 | }; 55 | 56 | mockFs({ '/home/fake/e.json': '' }); 57 | assert.equal(service.api, 'http://exercism.io'); 58 | service.writeConfigFile(config); 59 | assert.deepEqual(JSON.parse(fs.readFileSync(fakePath)), config); 60 | assert.equal(service.api, config.api); 61 | }); 62 | 63 | test('isConfigured returns false only if API key is empty', function(assert) { 64 | let config = { 65 | api: 'http://exercism', 66 | xapi: 'http://x.exercism', 67 | dir: '/home/fake/exercises', 68 | }; 69 | 70 | mockFs({ '/home/fake/e.json': JSON.stringify(config) }); 71 | let service = this.subject(); 72 | 73 | assert.notOk(service.get('isConfigured')); 74 | 75 | config.apiKey = null; 76 | service.update(config); 77 | assert.notOk(service.get('isConfigured')); 78 | 79 | config.apiKey = ''; 80 | service.update(config); 81 | assert.notOk(service.get('isConfigured')); 82 | 83 | config.apiKey = 'somekey'; 84 | service.update(config); 85 | assert.ok(service.get('isConfigured')); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/unit/download/route-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:download', 'Unit | Route | download', { 4 | //needs: ['service:exercism'] 5 | }); 6 | 7 | test('it exists', function(assert) { 8 | let route = this.subject(); 9 | assert.ok(route); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/download/status/route-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:download/status', 'Unit | Route | download/status', { 4 | needs: ['service:exercism'] 5 | }); 6 | 7 | test('it exists', function(assert) { 8 | let route = this.subject(); 9 | assert.ok(route); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/unit/exercism/service-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleFor, test } from 'ember-qunit'; 3 | 4 | let fs = requireNode('fs'), 5 | mockFs = requireNode('mock-fs'); 6 | 7 | moduleFor('service:exercism', 'Unit | Service | exercism', { 8 | needs: ['service:ajax', 'service:configuration'], 9 | afterEach() { 10 | mockFs.restore(); 11 | } 12 | }); 13 | 14 | test('saveProblems makes track and problem dirs if dont exists', function(assert) { 15 | let service = this.subject(); 16 | service.configuration = { dir: '/t' }; 17 | mockFs({ '/t': {} }); 18 | let Problem = Ember.Object.extend({ 19 | slug: 'bob', 20 | language: 'elixir', 21 | files: { f1: 'aa' } 22 | }), 23 | problems = [Problem.create()], 24 | summary = service.saveProblems(problems), 25 | info = summary[0]; 26 | assert.ok(fs.existsSync('/t/elixir/bob')); 27 | assert.equal(summary.length, 1); 28 | assert.equal(info.problem, 'bob'); 29 | assert.deepEqual(info.new, ['f1']); 30 | assert.deepEqual(info.unchanged, []); 31 | }); 32 | 33 | test('saveProblems creates exercism folder if it does not exists', function(assert) { 34 | let service = this.subject(); 35 | service.configuration = { dir: '/home/t' }; 36 | mockFs({ '/home': {} }); 37 | let problems = [{ 38 | slug: 'leap', 39 | language: 'c', 40 | files: { 'ex.c': 'content0' } 41 | }]; 42 | service.saveProblems(problems, '/t'); 43 | assert.equal(fs.readFileSync('/home/t/c/leap/ex.c').toString(), 'content0'); 44 | }); 45 | 46 | test('saveProblems make dirs recursively', function(assert) { 47 | let service = this.subject(); 48 | service.configuration = { dir: '/t' }; 49 | mockFs({ '/t': {} }); 50 | let Problem = Ember.Object.extend({ 51 | slug: 'leap', 52 | language: 'c', 53 | files: { 54 | 'ex.c': 'content0', 55 | 'test/vendor/aaa.c': 'content1', 56 | 'nested-folder/bbb.c': 'content2' 57 | } 58 | }), 59 | problems = [Problem.create()]; 60 | service.saveProblems(problems, '/t'); 61 | assert.ok(fs.existsSync('/t/c/leap/ex.c')); 62 | assert.ok(fs.existsSync('/t/c/leap/test/vendor/aaa.c')); 63 | assert.ok(fs.existsSync('/t/c/leap/nested-folder/bbb.c')); 64 | }); 65 | 66 | test('saveProblems skips files that already exist and creates missing', function(assert) { 67 | let service = this.subject(); 68 | service.configuration = { dir: '/t' }; 69 | mockFs({ '/t/elixir/bob': { 'problem.ex': 'old_content' } }); 70 | let Problem = Ember.Object.extend({ 71 | slug: 'bob', 72 | language: 'elixir', 73 | files: {'problem.ex': 'new_content', 'problem_test.ex': 'b'} 74 | }), 75 | problems = [Problem.create()], 76 | summary = service.saveProblems(problems, '/t'), 77 | info = summary[0]; 78 | assert.equal(summary.length, 1); 79 | assert.ok(fs.existsSync('/t/elixir/bob/problem_test.ex')); 80 | assert.equal(fs.readFileSync('/t/elixir/bob/problem_test.ex').toString(), 'b'); 81 | assert.equal(fs.readFileSync('/t/elixir/bob/problem.ex').toString(), 'old_content'); 82 | assert.deepEqual(info.new, ['problem_test.ex']); 83 | assert.deepEqual(info.unchanged, ['problem.ex']); 84 | }); 85 | 86 | test('getLocalProblems filters out invalid dirs', function(assert) { 87 | let service = this.subject(); 88 | service.configuration = { dir: '/t' }; 89 | mockFs({ '/t/elixir/bob': {}, '/t/elixir/bad-one': {} }); 90 | let problems = service.getLocalProblems('elixir', ['bob']); 91 | assert.equal(problems.length, 1); 92 | assert.equal(problems[0].name, 'bob'); 93 | }); 94 | 95 | test('getLocalProblems returns empty list if no track dir', function(assert) { 96 | let service = this.subject(); 97 | service.configuration = { dir: '/t' }; 98 | mockFs({ '/t/': {} }); 99 | let problems = service.getLocalProblems('elixir', ['bob']); 100 | assert.equal(problems.length, 0); 101 | }); 102 | 103 | test('getLocalProblems returns empty list if track dir is not a dir', function(assert) { 104 | let service = this.subject(); 105 | service.configuration = { dir: '/t' }; 106 | mockFs({ '/t/elixir': 'file content' }); 107 | let problems = service.getLocalProblems('elixir', ['bob']); 108 | assert.equal(problems.length, 0); 109 | }); 110 | 111 | test('getLocalProblems returns empty list if problem dir is not a dir', function(assert) { 112 | let service = this.subject(); 113 | service.configuration = { dir: '/t' }; 114 | mockFs({ '/t/elixir/bob': 'file content' }); 115 | let problems = service.getLocalProblems('elixir', ['bob']); 116 | assert.equal(problems.length, 0); 117 | }); 118 | 119 | test('getLocalProblems returns empty list if no problem dir', function(assert) { 120 | let service = this.subject(); 121 | service.configuration = { dir: '/t' }; 122 | mockFs({ '/t/elixir': {} }); 123 | let problems = service.getLocalProblems('elixir', ['bob']); 124 | assert.equal(problems.length, 0); 125 | }); 126 | 127 | test('getLocalProblems lists all files in a dir and excludes tests and readme', function(assert) { 128 | let service = this.subject(); 129 | service.configuration = { dir: '/t' }; 130 | mockFs({ 131 | '/t/elixir': { 132 | 'bob': { 133 | 'bob.ex': 'a', 134 | 'some_file': 'b', 135 | 'test_bob.ex': 'c', 136 | 'bobTest.ex': 'd' 137 | }, 138 | 'hello-world': { 139 | 'hello_world.ex': 'a', 140 | 'hello_world_test.ex': 'b', 141 | 'README.md': 'c' 142 | } 143 | } 144 | }); 145 | let problems = service.getLocalProblems('elixir', ['bob', 'hello-world']), 146 | expected = [ 147 | { name: 'bob', files: ['bob.ex', 'some_file'], dir: '/t/elixir/bob' }, 148 | { name: 'hello-world', files: ['hello_world.ex'], dir: '/t/elixir/hello-world' } 149 | ]; 150 | assert.deepEqual(problems, expected); 151 | }); 152 | 153 | test('extracts details from path', function(assert) { 154 | let service = this.subject(), 155 | filePath = 'C:\\t\\elixir\\bob\\file.ex'; 156 | let { fileName, problem, language } = service._extractInfoFromFilePath(filePath, 'C:\\t', '\\'); 157 | assert.equal(fileName, 'file.ex'); 158 | assert.equal(problem, 'bob'); 159 | assert.equal(language, 'elixir'); 160 | }); 161 | 162 | test('extracts details from path with nested folders', function(assert) { 163 | let service = this.subject(), 164 | filePath = '/t/exercism/elixir/bob/nested/folder/file.ex'; 165 | let { fileName, problem, language } = service._extractInfoFromFilePath(filePath, '/t/exercism', '/'); 166 | assert.equal(fileName, 'file.ex'); 167 | assert.equal(problem, 'bob'); 168 | assert.equal(language, 'elixir'); 169 | }); 170 | 171 | // /home//exercism/solutions//// 172 | 173 | test('downloaded submitted solutions are saved in the correspondent dir', function(assert) { 174 | let service = this.subject(); 175 | service.configuration = { dir: '/t' }; 176 | mockFs({ '/t': {} }); 177 | 178 | let submission = { 179 | uuid: 'fakeuuid', 180 | slug: 'hello-world', 181 | trackId: 'elixir', 182 | username: 'frodo', 183 | solutionFiles: { 184 | f1: 'content 1', 185 | f2: 'content 2' 186 | } 187 | }; 188 | 189 | service.saveSubmittedFiles(submission); 190 | assert.equal(fs.readFileSync('/t/solutions/frodo/elixir/hello-world/fakeuuid/f1').toString(), 'content 1'); 191 | assert.equal(fs.readFileSync('/t/solutions/frodo/elixir/hello-world/fakeuuid/f2').toString(), 'content 2'); 192 | }); 193 | 194 | test('saveSubmittedFiles overwrites existing files', function(assert) { 195 | let service = this.subject(); 196 | service.configuration = { dir: '/t' }; 197 | mockFs({ '/t/solutions/frodo/elixir/hello-world/fakeuuid/f1': 'old content' }); 198 | 199 | let submission = { 200 | uuid: 'fakeuuid', 201 | slug: 'hello-world', 202 | trackId: 'elixir', 203 | username: 'frodo', 204 | solutionFiles: { 205 | f1: 'content 1', 206 | f2: 'content 2' 207 | } 208 | }; 209 | 210 | service.saveSubmittedFiles(submission); 211 | assert.equal(fs.readFileSync('/t/solutions/frodo/elixir/hello-world/fakeuuid/f1').toString(), 'content 1'); 212 | assert.equal(fs.readFileSync('/t/solutions/frodo/elixir/hello-world/fakeuuid/f2').toString(), 'content 2'); 213 | }); 214 | -------------------------------------------------------------------------------- /tests/unit/notifier/service-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | import td from 'testdouble'; 3 | 4 | moduleFor('service:notifier', 'Unit | Service | notifier', { 5 | // Specify the other units that are required for this test. 6 | // needs: ['service:foo'] 7 | }); 8 | 9 | test('it adds a title when plain string', function(assert) { 10 | let service = this.subject(), 11 | message = 'my fake message', 12 | expected = { title: 'Excersism GUI', message }; 13 | 14 | service.notifier.notify = td.function(); 15 | service.notify(message); 16 | td.verify(service.notifier.notify(expected)); 17 | assert.ok(service); 18 | }); 19 | 20 | test('it adds not title when passing a hash', function(assert) { 21 | let service = this.subject(), 22 | message = { title: 'title', message: 'my fake message' }; 23 | 24 | service.notifier.notify = td.function(); 25 | service.notify(message); 26 | td.verify(service.notifier.notify(message)); 27 | assert.ok(service); 28 | }); 29 | 30 | //TODO: test bad hash passed to notify: no message or no title 31 | -------------------------------------------------------------------------------- /tests/unit/track/model-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('track', 'Unit | Model | track', { 4 | needs: [] 5 | }); 6 | 7 | test('it returns site url formed with slug', function(assert) { 8 | let model = this.subject({ slug: 'fake' }); 9 | assert.equal('http://exercism.io/languages/fake', model.get('languageUrl')); 10 | }); 11 | 12 | test('it returns logo url formed with slug', function(assert) { 13 | let model = this.subject({ slug: 'fake' }); 14 | assert.equal('http://exercism.io/tracks/fake/icon', model.get('imageSrc')); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/tracks/track/restore/route-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:tracks/track/restore', 'Unit | Route | tracks/track/restore', { 4 | // Specify the other units that are required for this test. 5 | needs: ['service:exercism'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/updates/route-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:updates', 'Unit | Route | updates', { 4 | // Specify the other units that are required for this test. 5 | needs: ['service:debug'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exercism/gui/3a9a92aeaf0649bc7d51e0d279a638bc8ac6e461/vendor/.gitkeep --------------------------------------------------------------------------------