├── .babelrc
├── .circleci
└── config.yml
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── LICENSE.md
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── project.json
├── src
├── mocks
│ ├── cursor-mock.js
│ ├── desktop-mock.js
│ ├── editor-application-mock.js
│ ├── editor-line-mock.js
│ ├── player-mock.js
│ ├── styles-mock.js
│ ├── terminal-application-mock.js
│ ├── terminal-command-line-mock.js
│ └── terminal-response-line-mock.js
├── scripts
│ ├── components
│ │ ├── application
│ │ │ ├── application.html
│ │ │ ├── application.js
│ │ │ └── application.test.js
│ │ ├── cursor
│ │ │ ├── cursor.html
│ │ │ ├── cursor.js
│ │ │ └── cursor.test.js
│ │ ├── desktop
│ │ │ ├── desktop.html
│ │ │ ├── desktop.js
│ │ │ └── desktop.test.js
│ │ ├── editor-application
│ │ │ ├── editor-application.js
│ │ │ └── editor-application.test.js
│ │ ├── editor-line
│ │ │ ├── editor-line.html
│ │ │ ├── editor-line.js
│ │ │ └── editor-line.test.js
│ │ ├── player
│ │ │ ├── player.js
│ │ │ └── player.test.js
│ │ ├── terminal-application
│ │ │ ├── terminal-application.js
│ │ │ └── terminal-application.test.js
│ │ ├── terminal-command-line
│ │ │ ├── terminal-command-line.html
│ │ │ ├── terminal-command-line.js
│ │ │ └── terminal-command-line.test.js
│ │ ├── terminal-line
│ │ │ ├── terminal-line.html
│ │ │ ├── terminal-line.js
│ │ │ └── terminal-line.test.js
│ │ └── terminal-response-line
│ │ │ ├── terminal-response-line.html
│ │ │ ├── terminal-response-line.js
│ │ │ └── terminal-response-line.test.js
│ ├── constants
│ │ └── application.js
│ ├── index.js
│ ├── index.test.js
│ └── services
│ │ ├── dom
│ │ ├── dom.js
│ │ └── dom.test.js
│ │ ├── text
│ │ ├── text.js
│ │ └── text.test.js
│ │ ├── type-html-text
│ │ ├── type-html-text.js
│ │ └── type-html-text.test.js
│ │ ├── type-plain-text
│ │ ├── type-plain-text.js
│ │ └── type-plain-text.test.js
│ │ └── type
│ │ ├── type.js
│ │ └── type.test.js
└── styles
│ ├── _mixins.styl
│ ├── _variables.styl
│ ├── application.styl
│ ├── cursor.styl
│ ├── desktop.styl
│ ├── editor-application.styl
│ ├── editor-line.styl
│ ├── terminal-command-line.styl
│ ├── terminal-line.styl
│ └── terminal-response-line.styl
├── standalone
└── index.html
├── webpack.conf.base.js
├── webpack.conf.dev.js
├── webpack.conf.prod.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
3 | version: 2.1
4 | orbs:
5 | coveralls: coveralls/coveralls@1.0.6
6 | jobs:
7 | build:
8 | docker:
9 | - image: cimg/node:16.14.2-browsers
10 |
11 | steps:
12 | - checkout
13 | # Download and cache dependencies
14 | - restore_cache:
15 | keys:
16 | - v1-dependencies-{{ checksum "package.json" }}
17 | # fallback to using the latest cache if no exact match is found
18 | - v1-dependencies-
19 |
20 | - run:
21 | name: Dependencies
22 | command: npm install
23 |
24 | - save_cache:
25 | paths:
26 | - node_modules
27 | key: v1-dependencies-{{ checksum "package.json" }}
28 |
29 | - run:
30 | name: Format
31 | command: npm run format
32 |
33 | - run:
34 | name: Build
35 | command: npm run build -- --env=production
36 |
37 | - run:
38 | name: Test
39 | command: npm run test -- --maxWorkers=2 --coverage --coverageReporters=lcov
40 | - coveralls/upload:
41 | verbose: true
42 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | insert_final_newline = false
15 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "ecmaVersion": 2018,
11 | "sourceType": "module"
12 | },
13 | "globals": {
14 | "spyOn": true,
15 | "module": true
16 | },
17 | "rules": {
18 | "indent": ["error", 2],
19 | "linebreak-style": ["error", "unix"],
20 | "quotes": ["error", "single"],
21 | "semi": ["error", "always"],
22 | "complexity": ["error", { "max": 3 }],
23 | "max-lines": ["error", { "max": 100 }],
24 | "max-statements": ["error", { "max": 5 }]
25 | },
26 | "overrides": [
27 | {
28 | "files": [ "src/**/*.test.js" ],
29 | "rules": {
30 | "max-lines": ["error", { "max": 200 }],
31 | "max-statements": [
32 | "error", { "max": 10 },
33 | { "ignoreTopLevelFunctions": true }
34 | ]
35 | }
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/git,node,macos,linux,windows,sublimetext,visualstudiocode
3 |
4 | ### Git ###
5 | *.orig
6 |
7 | ### Linux ###
8 | *~
9 |
10 | # temporary files which can be created if a process still has a handle open of a deleted file
11 | .fuse_hidden*
12 |
13 | # KDE directory preferences
14 | .directory
15 |
16 | # Linux trash folder which might appear on any partition or disk
17 | .Trash-*
18 |
19 | # .nfs files are created when an open file is removed but is still being accessed
20 | .nfs*
21 |
22 | ### macOS ###
23 | *.DS_Store
24 | .AppleDouble
25 | .LSOverride
26 |
27 | # Icon must end with two \r
28 | Icon
29 |
30 | # Thumbnails
31 | ._*
32 |
33 | # Files that might appear in the root of a volume
34 | .DocumentRevisions-V100
35 | .fseventsd
36 | .Spotlight-V100
37 | .TemporaryItems
38 | .Trashes
39 | .VolumeIcon.icns
40 | .com.apple.timemachine.donotpresent
41 |
42 | # Directories potentially created on remote AFP share
43 | .AppleDB
44 | .AppleDesktop
45 | Network Trash Folder
46 | Temporary Items
47 | .apdisk
48 |
49 | ### Node ###
50 | # Logs
51 | logs
52 | *.log
53 | npm-debug.log*
54 | yarn-debug.log*
55 | yarn-error.log*
56 |
57 | # Runtime data
58 | pids
59 | *.pid
60 | *.seed
61 | *.pid.lock
62 |
63 | # Directory for instrumented libs generated by jscoverage/JSCover
64 | lib-cov
65 |
66 | # Coverage directory used by tools like istanbul
67 | coverage
68 |
69 | # nyc test coverage
70 | .nyc_output
71 |
72 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
73 | .grunt
74 |
75 | # Bower dependency directory (https://bower.io/)
76 | bower_components
77 |
78 | # node-waf configuration
79 | .lock-wscript
80 |
81 | # Compiled binary addons (http://nodejs.org/api/addons.html)
82 | build/Release
83 |
84 | # Dependency directories
85 | node_modules/
86 | jspm_packages/
87 |
88 | # Typescript v1 declaration files
89 | typings/
90 |
91 | # Optional npm cache directory
92 | .npm
93 |
94 | # Optional eslint cache
95 | .eslintcache
96 |
97 | # Optional REPL history
98 | .node_repl_history
99 |
100 | # Output of 'npm pack'
101 | *.tgz
102 |
103 | # Yarn Integrity file
104 | .yarn-integrity
105 |
106 | # dotenv environment variables file
107 | .env
108 |
109 |
110 | ### SublimeText ###
111 | # cache files for sublime text
112 | *.tmlanguage.cache
113 | *.tmPreferences.cache
114 | *.stTheme.cache
115 |
116 | # workspace files are user-specific
117 | *.sublime-workspace
118 |
119 | # project files should be checked into the repository, unless a significant
120 | # proportion of contributors will probably not be using SublimeText
121 | # *.sublime-project
122 |
123 | # sftp configuration file
124 | sftp-config.json
125 |
126 | # Package control specific files
127 | Package Control.last-run
128 | Package Control.ca-list
129 | Package Control.ca-bundle
130 | Package Control.system-ca-bundle
131 | Package Control.cache/
132 | Package Control.ca-certs/
133 | Package Control.merged-ca-bundle
134 | Package Control.user-ca-bundle
135 | oscrypto-ca-bundle.crt
136 | bh_unicode_properties.cache
137 |
138 | # Sublime-github package stores a github token in this file
139 | # https://packagecontrol.io/packages/sublime-github
140 | GitHub.sublime-settings
141 |
142 | ### VisualStudioCode ###
143 | .vscode/*
144 | !.vscode/settings.json
145 | !.vscode/tasks.json
146 | !.vscode/launch.json
147 | !.vscode/extensions.json
148 | .history
149 |
150 | ### Windows ###
151 | # Windows thumbnail cache files
152 | Thumbs.db
153 | ehthumbs.db
154 | ehthumbs_vista.db
155 |
156 | # Folder config file
157 | Desktop.ini
158 |
159 | # Recycle Bin used on file shares
160 | $RECYCLE.BIN/
161 |
162 | # Windows Installer files
163 | *.cab
164 | *.msi
165 | *.msm
166 | *.msp
167 |
168 | # Windows shortcuts
169 | *.lnk
170 |
171 |
172 | # End of https://www.gitignore.io/api/git,node,macos,linux,windows,sublimetext,visualstudiocode
173 |
174 | # Built files
175 | dist/
176 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Rafael Camargo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Glorious Demo
2 |
3 | > The easiest way to demonstrate your code in action.
4 |
5 | [](https://circleci.com/gh/glorious-codes/glorious-demo)
6 | [](https://coveralls.io/github/glorious-codes/glorious-demo?branch=master)
7 |
8 |
9 |
10 |
11 |
12 | ## Installation
13 |
14 | ```
15 | npm install @glorious/demo --save
16 | ```
17 |
18 | ## Basic Usage
19 |
20 | ``` html
21 |
22 |
23 | ```
24 |
25 | *Note: If you're not into package management, load it from a third-party [CDN provider](https://github.com/rafaelcamargo/glorious-demo/wiki/CDN-Providers).*
26 |
27 | ``` javascript
28 | // Constructor receives a selector that indicates
29 | // where to inject the demonstration in your page.
30 | const demo = new GDemo('#container');
31 |
32 | const code = `
33 | function greet(){
34 | console.log("Hello World!");
35 | }
36 |
37 | greet();
38 | `
39 |
40 | demo
41 | .openApp('editor', {minHeight: '350px', windowTitle: 'demo.js'})
42 | .write(code, {onCompleteDelay: 1500})
43 | .openApp('terminal', {minHeight: '350px', promptString: '$'})
44 | .command('node ./demo', {onCompleteDelay: 500})
45 | .respond('Hello World!')
46 | .command('')
47 | .end();
48 | ```
49 |
50 | *NOTE: Check [here](https://github.com/rafaelcamargo/glorious-demo/wiki/Syntax-highlight) to know how to use Prism to get your code highlighted.*
51 |
52 | ### API
53 |
54 | #### `openApp`
55 | Opens or maximizes an open application.
56 | ``` javascript
57 | /*
58 | ** @applicationType: String [required]
59 | ** @options: Object [optional]
60 | */
61 |
62 | // Possible values are 'editor' or 'terminal'
63 | const applicationType = 'terminal';
64 |
65 | const openAppOptions = {
66 | minHeight: '350px',
67 | windowTitle: 'bash',
68 | id: 'someId', // Identifies an application, in case of multiple instances
69 | inanimate: true, // Turns off application's window animation
70 | promptString: '~/my-project $', // For 'terminal' applications only
71 | initialContent: 'Some text', // For 'editor' applications only
72 | onCompleteDelay: 1000 // Delay before executing the next method
73 | }
74 |
75 | demo.openApp(applicationType, openAppOptions).end();
76 | ```
77 |
78 | #### `write`
79 | Writes some code in the open Editor application.
80 | ``` javascript
81 | /*
82 | ** @codeSample: String [required]
83 | ** @options: Object [optional]
84 | */
85 |
86 | // Tabs and line breaks will be preserved
87 | const codeSample = `
88 | function sum(a, b) {
89 | return a + b;
90 | }
91 |
92 | sum();
93 | `;
94 |
95 | const writeOptions = {
96 | id: 'someId', // Identifies an application, in case of multiple instances
97 | onCompleteDelay: 500 // Delay before executing the next method
98 | }
99 |
100 | demo.openApp('editor').write(codeSample, writeOptions).end();
101 | ```
102 |
103 | #### `command`
104 | Writes some command in the open Terminal application.
105 | ``` javascript
106 | /*
107 | ** @command: String [required]
108 | ** @options: Object [optional]
109 | */
110 |
111 | const command = 'npm install @glorious/demo --save';
112 |
113 | // Redefines prompt string for this and following commands
114 | const promptString = '$'
115 |
116 | // Can optionally be an HTML string:
117 | const promptString = '$'
118 |
119 | const commandOptions = {
120 | id: 'someId', // Identifies an application, in case of multiple instances
121 | promptString, // Sets a custom string. Default: ~/demo $
122 | onCompleteDelay: 500 // Delay before executing the next method
123 | }
124 |
125 | demo.openApp('terminal').command(command, commandOptions).end();
126 | ```
127 |
128 | #### `respond`
129 | Shows some response on the open Terminal application.
130 | ``` javascript
131 | /*
132 | ** @response: String [required]
133 | ** @options: Object [optional]
134 | */
135 |
136 | // Line breaks will be preserved
137 | const response = `
138 | + @glorious/demo successfully installed!
139 | + v0.1.0
140 | `;
141 |
142 | // Can optionally be an HTML string:
143 | const response = `
144 | + @glorious/demo successfully installed!
145 | + v0.6.0
146 | `;
147 |
148 | const respondOptions = {
149 | id: 'someId', // Identifies an application, in case of multiple instances
150 | onCompleteDelay: 500 // Delay before executing the next method
151 | }
152 |
153 | demo.openApp('terminal').respond(response, respondOptions).end();
154 | ```
155 |
156 | #### `end`
157 | Indicates the end of the demonstration. The method returns a promise in case you want to perform some action at the end of the demonstration.
158 |
159 | ``` javascript
160 | demo.openApp('terminal')
161 | .command('node demo')
162 | .respond('Hello World!')
163 | .end()
164 | .then(() => {
165 | // Custom code to be performed at the end of the demostration goes here.
166 | });
167 | ```
168 |
169 | **IMPORTANT:** Do not forget to invoke it at the end of your demo. Otherwise, the demo won't be played.
170 |
171 | ## Contributing
172 |
173 | 1. Install [Node](https://nodejs.org/en/). Download the "Recommend for Most Users" version.
174 |
175 | 2. Clone the repo:
176 | ``` bash
177 | git clone git@github.com:glorious-codes/glorious-demo.git
178 | ```
179 |
180 | 3. Go to the project directory:
181 | ``` bash
182 | cd glorious-demo
183 | ```
184 |
185 | 4. Install the project dependencies:
186 | ``` bash
187 | npm install
188 | ```
189 |
190 | 5. Build the project:
191 | ``` bash
192 | npm run build
193 | ```
194 |
195 | ## Tests
196 |
197 | Ensure that all code that you have added is covered with unit tests:
198 | ``` bash
199 | npm run test -- --coverage
200 | ```
201 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const project = require('./project.json');
2 |
3 | module.exports = {
4 | "clearMocks": true,
5 | "collectCoverageFrom": [project.scripts.source.files],
6 | "coverageReporters": ["html"],
7 | "coverageThreshold": {
8 | "global": {
9 | "statements": 100,
10 | "branches": 100,
11 | "functions": 100,
12 | "lines": 100
13 | }
14 | },
15 | "moduleNameMapper": {
16 | '@styles\/(.*)$': `/${project.styles.source.root}$1`,
17 | '@mocks\/(.*)$': `/${project.mocks.source.root}$1`
18 | },
19 | "transform": {
20 | "^.+\\.(css|styl)$": "/src/mocks/styles-mock.js",
21 | "^.+\\.js$": "babel-jest",
22 | "^.+\\.html$": "html-loader-jest"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glorious/demo",
3 | "version": "0.12.0",
4 | "description": "The easiest way to demonstrate your code in action",
5 | "main": "dist/gdemo.min.js",
6 | "files": [
7 | "dist/**/*.*"
8 | ],
9 | "scripts": {
10 | "format": "node ./node_modules/eslint/bin/eslint.js ./src/**/*.js",
11 | "build": "node ./node_modules/webpack/bin/webpack.js",
12 | "test": "node ./node_modules/jest/bin/jest.js",
13 | "prepublishOnly": "rm -rf ./dist && NODE_ENV=production npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/rafaelcamargo/glorious-demo.git"
18 | },
19 | "keywords": [
20 | "demo",
21 | "demonstration",
22 | "code",
23 | "snippet",
24 | "animation",
25 | "mac",
26 | "editor",
27 | "terminal"
28 | ],
29 | "author": "Rafael Camargo ",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/rafaelcamargo/glorious-demo/issues"
33 | },
34 | "homepage": "https://github.com/rafaelcamargo/glorious-demo#readme",
35 | "devDependencies": {
36 | "@babel/core": "^7.6.4",
37 | "@babel/preset-env": "^7.6.3",
38 | "babel-jest": "^24.9.0",
39 | "babel-loader": "^8.0.6",
40 | "css-loader": "^3.5.2",
41 | "eslint": "^5.6.0",
42 | "html-loader": "^0.5.5",
43 | "html-loader-jest": "^0.2.1",
44 | "jest": "^24.9.0",
45 | "mini-css-extract-plugin": "^0.8.0",
46 | "optimize-css-assets-webpack-plugin": "^5.0.3",
47 | "style-loader": "^0.20.3",
48 | "stylus": "^0.54.5",
49 | "stylus-loader": "^3.0.1",
50 | "terser-webpack-plugin": "^2.3.5",
51 | "webpack": "^4.42.1",
52 | "webpack-cli": "^3.3.11",
53 | "webpack-merge": "^4.2.2"
54 | },
55 | "dependencies": {
56 | "@glorious/fyzer": "^0.1.1"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "source": {
4 | "root": "src/scripts/",
5 | "files": "src/scripts/**/*.js",
6 | "entry": "src/scripts/index.js"
7 | },
8 | "dist": {
9 | "root": "dist",
10 | "filename": {
11 | "prod": "gdemo.min.js",
12 | "dev": "gdemo.js"
13 | }
14 | }
15 | },
16 | "styles": {
17 | "source": {
18 | "root": "src/styles/"
19 | },
20 | "dist": {
21 | "filename": {
22 | "prod": "gdemo.min.css",
23 | "dev": "gdemo.css"
24 | }
25 | }
26 | },
27 | "mocks": {
28 | "source": {
29 | "root": "src/mocks/"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/mocks/cursor-mock.js:
--------------------------------------------------------------------------------
1 | export const cursorInstanceMock = {
2 | element: document.createElement('span'),
3 | write: jest.fn()
4 | };
5 |
6 | export const CursorMock = jest.fn(() => {
7 | return cursorInstanceMock;
8 | });
9 |
--------------------------------------------------------------------------------
/src/mocks/desktop-mock.js:
--------------------------------------------------------------------------------
1 | export const desktopInstanceMock = {
2 | openApplication: jest.fn(),
3 | minimizeAllApplications: jest.fn(onComplete => {
4 | onComplete();
5 | }),
6 | maximizeApplication: jest.fn((application, onComplete) => {
7 | onComplete();
8 | })
9 | };
10 |
11 | export const TerminalApplicationMock = jest.fn(() => {
12 | return desktopInstanceMock;
13 | });
14 |
--------------------------------------------------------------------------------
/src/mocks/editor-application-mock.js:
--------------------------------------------------------------------------------
1 | export const editorApplicationInstanceMock = {
2 | minimize: jest.fn(),
3 | maximize: jest.fn(),
4 | element: 'Something
',
5 | type: 'editor'
6 | };
7 |
8 | export const EditorApplicationMock = jest.fn((containerEl, { inanimate, id } = {}) => {
9 | return {
10 | ...editorApplicationInstanceMock,
11 | inanimate,
12 | id
13 | };
14 | });
15 |
--------------------------------------------------------------------------------
/src/mocks/editor-line-mock.js:
--------------------------------------------------------------------------------
1 | export const editorLineInstanceMock = {
2 | setActive: jest.fn(),
3 | setInactive: jest.fn(),
4 | write: jest.fn((textLine, onComplete) => {
5 | onComplete();
6 | }),
7 | element: document.createElement('div')
8 | };
9 |
10 | export const EditorLineMock = jest.fn(() => {
11 | return editorLineInstanceMock;
12 | });
13 |
--------------------------------------------------------------------------------
/src/mocks/player-mock.js:
--------------------------------------------------------------------------------
1 | export const playerInstanceMock = {
2 | play: jest.fn(() => {
3 | return {
4 | then: resolve => resolve()
5 | };
6 | })
7 | };
8 |
9 | export const PlayerMock = jest.fn(() => {
10 | return playerInstanceMock;
11 | });
12 |
--------------------------------------------------------------------------------
/src/mocks/styles-mock.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | process() {
3 | return '';
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/src/mocks/terminal-application-mock.js:
--------------------------------------------------------------------------------
1 | export const terminalApplicationInstanceMock = {
2 | minimize: jest.fn(),
3 | maximize: jest.fn(),
4 | element: 'Something
',
5 | type: 'terminal'
6 | };
7 |
8 | export const TerminalApplicationMock = jest.fn((containerEl, { inanimate, id } = {}) => {
9 | return {
10 | ...terminalApplicationInstanceMock,
11 | inanimate,
12 | id
13 | };
14 | });
15 |
--------------------------------------------------------------------------------
/src/mocks/terminal-command-line-mock.js:
--------------------------------------------------------------------------------
1 | export const terminalCommandLineInstanceMock = {
2 | command: jest.fn((textLine, onComplete) => {
3 | onComplete();
4 | }),
5 | setActive: jest.fn(),
6 | setInactive: jest.fn(),
7 | element: 'Something
'
8 | };
9 |
10 | export const TerminalCommandLineMock = jest.fn(() => {
11 | return terminalCommandLineInstanceMock;
12 | });
13 |
--------------------------------------------------------------------------------
/src/mocks/terminal-response-line-mock.js:
--------------------------------------------------------------------------------
1 | export const terminalResponseLineInstanceMock = {
2 | setText: jest.fn(),
3 | element: 'Something
'
4 | };
5 |
6 | export const TerminalResponseLineMock = jest.fn(() => {
7 | return terminalResponseLineInstanceMock;
8 | });
9 |
--------------------------------------------------------------------------------
/src/scripts/components/application/application.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/scripts/components/application/application.js:
--------------------------------------------------------------------------------
1 | import '@styles/application.styl';
2 | import { DEFAULT_APPLICATION_ID } from '../../constants/application';
3 | import domService from '../../services/dom/dom';
4 | import textService from '../../services/text/text';
5 | import template from './application.html';
6 |
7 | export class Application {
8 | constructor(applicationType, { id = DEFAULT_APPLICATION_ID, ...options } = {}){
9 | this.type = applicationType;
10 | this.id = id;
11 | this.options = options;
12 | this.element = buildElement(applicationType);
13 | this.setOptions(this.options);
14 | }
15 | setOptions(options){
16 | Object.entries(options).forEach(([optionName, optionValue]) => {
17 | const handle = this.getOptionHandler(optionName)
18 | handle && handle(optionValue)
19 | });
20 | }
21 | getOptionHandler(optionName){
22 | return {
23 | 'minHeight': optionValue => this.setMinHeight(optionValue),
24 | 'windowTitle': optionValue => this.setWindowTitle(optionValue),
25 | 'inanimate': optionValue => this.configAnimation(optionValue),
26 | }[optionName];
27 | }
28 | setMinHeight(height){
29 | const applicationTopbarHeight = 26;
30 | const contentContainer = getContentContainerElement(this.element);
31 | const contentContainerMinHeight = parseInt(height) - applicationTopbarHeight;
32 | contentContainer.style.minHeight = `${contentContainerMinHeight}px`;
33 | }
34 | setWindowTitle(title){
35 | const titleContainerElement = getWindowTitleContainerElement(this.element);
36 | titleContainerElement.innerText = title;
37 | this.windowTitle = title;
38 | }
39 | configAnimation(inanimate){
40 | this.setInanimate(inanimate);
41 | inanimate && getBaseApplicationElement(this.element).classList.add('application-inanimate');
42 | }
43 | addContent(content){
44 | const container = getContentContainerElement(this.element);
45 | container.appendChild(content);
46 | }
47 | setInanimate(inanimate){
48 | this.inanimate = inanimate;
49 | }
50 | minimize(){
51 | this.setMaximized(false);
52 | handleResizingCssClass(this.element, 'remove', 'application-maximized');
53 | handleResizingCssClass(this.element, 'add', 'application-minimized');
54 | }
55 | maximize(){
56 | this.setMaximized(true);
57 | handleResizingCssClass(this.element, 'remove', 'application-minimized');
58 | handleResizingCssClass(this.element, 'add', 'application-maximized');
59 | }
60 | setMaximized(isMaximized){
61 | this.isMaximized = isMaximized;
62 | }
63 | }
64 |
65 | function buildElement(applicationType){
66 | let element = buildWrapper(applicationType);
67 | element.appendChild(domService.parseHtml(template));
68 | return element;
69 | }
70 |
71 | function buildWrapper(applicationType){
72 | const wrapper = document.createElement('div');
73 | const cssClass = `${textService.toKebabCase(applicationType)}-application`;
74 | wrapper.setAttribute('class', cssClass);
75 | return wrapper;
76 | }
77 |
78 | function getContentContainerElement(applicationElement){
79 | return applicationElement.querySelector('[data-content-container]');
80 | }
81 |
82 | function getWindowTitleContainerElement(applicationElement){
83 | return applicationElement.querySelector('[data-title-container]');
84 | }
85 |
86 | function handleResizingCssClass(element, classListMethod, cssClass){
87 | getBaseApplicationElement(element).classList[classListMethod](cssClass);
88 | }
89 |
90 | function getBaseApplicationElement(element){
91 | return element.querySelector('[data-application]');
92 | }
93 |
--------------------------------------------------------------------------------
/src/scripts/components/application/application.test.js:
--------------------------------------------------------------------------------
1 | import { Application } from './application';
2 |
3 | jest.useFakeTimers();
4 |
5 | describe('Application Component', () => {
6 |
7 | it('should have a default id', () => {
8 | const application = new Application('editor');
9 | expect(application.id).toEqual('_default');
10 | });
11 |
12 | it('should optionally have a custom id', () => {
13 | const id = 'editor3';
14 | const application = new Application('editor', { id });
15 | expect(application.id).toEqual(id);
16 | });
17 |
18 | it('should instantiate application from application type', () => {
19 | const application = new Application('editor');
20 | expect(application.element.classList.length).toEqual(1);
21 | expect(application.element.classList[0]).toEqual('editor-application');
22 | });
23 |
24 | it('should have a topbar', () => {
25 | const application = new Application('editor');
26 | const topbar = application.element.querySelector('application-topbar');
27 | expect(topbar).toBeDefined();
28 | });
29 |
30 | it('should have a title container', () => {
31 | const application = new Application('editor');
32 | const titleContainer = application.element.querySelector('[data-title-container]');
33 | expect(titleContainer).toBeDefined();
34 | });
35 |
36 | it('should have a content container', () => {
37 | const application = new Application('editor');
38 | const contentContainer = application.element.querySelector('[data-content-container]');
39 | expect(contentContainer).toBeDefined();
40 | });
41 |
42 | it('should minimum height option subtract application top bar height', () => {
43 | const minHeight = '300px';
44 | const application = new Application('editor', { minHeight });
45 | const contentContainer = application.element.querySelector('[data-content-container]');
46 | expect(contentContainer.style.minHeight).toEqual('274px');
47 | });
48 |
49 | it('should allow window title option', () => {
50 | const windowTitle = 'Atom';
51 | const application = new Application('editor', { windowTitle });
52 | const titleContainer = application.element.querySelector('[data-title-container]');
53 | expect(titleContainer.innerText).toEqual(windowTitle);
54 | });
55 |
56 | it('should add content', () => {
57 | const content = document.createElement('h1');
58 | const application = new Application('editor');
59 | content.innerText = 'Some content.';
60 | application.addContent(content);
61 | expect(application.element.querySelector('h1')).toEqual(content);
62 | });
63 |
64 | it('should maximize', () => {
65 | const application = new Application('editor');
66 | application.maximize();
67 | expect(application.isMaximized).toEqual(true);
68 | });
69 |
70 | it('should set maximized look on maximize', () => {
71 | const application = new Application('editor');
72 | const applicationElement = application.element.querySelector('[data-application]');
73 | application.maximize();
74 | expect(applicationElement.classList.contains('application-minimized')).toEqual(false);
75 | expect(applicationElement.classList.contains('application-maximized')).toEqual(true);
76 | });
77 |
78 | it('should minimize', () => {
79 | const application = new Application('editor');
80 | application.minimize();
81 | expect(application.isMaximized).toEqual(false);
82 | });
83 |
84 | it('should set minimized look on minimize', () => {
85 | const application = new Application('editor');
86 | const applicationElement = application.element.querySelector('[data-application]');
87 | application.minimize();
88 | expect(applicationElement.classList.contains('application-minimized')).toEqual(true);
89 | expect(applicationElement.classList.contains('application-maximized')).toEqual(false);
90 | });
91 |
92 | it('should optionally set application as inanimate', () => {
93 | const application = new Application('editor', { inanimate: true });
94 | const applicationElement = application.element.querySelector('[data-application]');
95 | expect(applicationElement.classList.contains('application-inanimate')).toEqual(true);
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/scripts/components/cursor/cursor.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/scripts/components/cursor/cursor.js:
--------------------------------------------------------------------------------
1 | import '@styles/cursor.styl';
2 | import domService from '../../services/dom/dom';
3 | import typeService from '../../services/type/type';
4 | import template from './cursor.html';
5 |
6 | const ACTIVE_CSS_CLASS = 'cursor-active';
7 |
8 | export class Cursor {
9 | constructor(){
10 | this.element = domService.parseHtml(template);
11 | }
12 | write(text, onComplete){
13 | typeService.type(this.element, text, onComplete);
14 | }
15 | setActive(){
16 | handleCssClass(this.element, 'add', ACTIVE_CSS_CLASS);
17 | }
18 | setInactive(){
19 | handleCssClass(this.element, 'remove', ACTIVE_CSS_CLASS);
20 | }
21 | }
22 |
23 | function handleCssClass(cursorElement, action, cssClass){
24 | cursorElement.classList[action](cssClass);
25 | }
26 |
--------------------------------------------------------------------------------
/src/scripts/components/cursor/cursor.test.js:
--------------------------------------------------------------------------------
1 | import typeService from '../../services/type/type';
2 | import { Cursor } from './cursor';
3 |
4 | describe('Cursor', () => {
5 | function mockContent(){
6 | return document.createElement('div');
7 | }
8 |
9 | it('should write some text', () => {
10 | typeService.type = jest.fn();
11 | const onComplete = jest.fn();
12 | const text = 'some text';
13 | const cursor = new Cursor(mockContent());
14 | cursor.write(text, onComplete);
15 | expect(typeService.type).toHaveBeenCalledWith(cursor.element, text, onComplete);
16 | });
17 |
18 | it('should set cursor as active', () => {
19 | const cursor = new Cursor(mockContent());
20 | cursor.setActive();
21 | expect(cursor.element.classList.contains('cursor-active')).toEqual(true);
22 | });
23 |
24 | it('should set cursor as inactive', () => {
25 | const cursor = new Cursor(mockContent());
26 | cursor.element.classList.add('cursor-active');
27 | cursor.setInactive();
28 | expect(cursor.element.classList.contains('cursor-active')).toEqual(false);
29 | });
30 |
31 | });
32 |
--------------------------------------------------------------------------------
/src/scripts/components/desktop/desktop.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/scripts/components/desktop/desktop.js:
--------------------------------------------------------------------------------
1 | import '@styles/desktop.styl';
2 | import { DEFAULT_APPLICATION_ID } from '../../constants/application';
3 | import { EditorApplication } from '../editor-application/editor-application';
4 | import { TerminalApplication } from '../terminal-application/terminal-application';
5 | import domService from '../../services/dom/dom';
6 | import template from './desktop.html';
7 |
8 | export class Desktop {
9 | constructor(container){
10 | this.element = domService.parseHtml(template);
11 | this.container = container;
12 | this.openApplications = [];
13 | this.container.appendChild(this.element);
14 | }
15 | openApplication(appType, appOptions){
16 | const app = getOpenApplication(this.openApplications, appType, appOptions);
17 | return app ? app : buildApplication(this, appType, appOptions);
18 | }
19 | minimizeAllApplications(onComplete){
20 | this.openApplications.forEach(openApplication => openApplication.minimize());
21 | if(onComplete) setTimeout(onComplete, getDefaultAnimationDuration());
22 | }
23 | maximizeApplication(application, onComplete){
24 | application.maximize();
25 | if(application.inanimate) return onComplete();
26 | return setTimeout(onComplete, getDefaultAnimationDuration());
27 | }
28 | }
29 |
30 | function buildApplication(desktop, appType, options){
31 | const Application = appType == 'editor' ? EditorApplication: TerminalApplication;
32 | const application = new Application(desktop.element, options);
33 | desktop.openApplications.push(application);
34 | desktop.element.appendChild(application.element);
35 | return application;
36 | }
37 |
38 | function getOpenApplication(openApplications, appType, appOptions = {}){
39 | const appId = appOptions.id || DEFAULT_APPLICATION_ID;
40 | return openApplications.filter(openApplication => {
41 | return openApplication.type === appType;
42 | }).find(openApplication => {
43 | return openApplication.id === appId;
44 | });
45 | }
46 |
47 | function getDefaultAnimationDuration(){
48 | return 750;
49 | }
50 |
--------------------------------------------------------------------------------
/src/scripts/components/desktop/desktop.test.js:
--------------------------------------------------------------------------------
1 | import { Desktop } from './desktop';
2 | import { EditorApplication } from '../editor-application/editor-application';
3 | import { TerminalApplication } from '../terminal-application/terminal-application';
4 | import { EditorApplicationMock } from '@mocks/editor-application-mock';
5 | import { TerminalApplicationMock } from '@mocks/terminal-application-mock';
6 |
7 | jest.mock('../editor-application/editor-application');
8 | EditorApplication.mockImplementation(EditorApplicationMock);
9 |
10 | jest.mock('../terminal-application/terminal-application');
11 | TerminalApplication.mockImplementation(TerminalApplicationMock);
12 |
13 | describe('Desktop Component', () => {
14 |
15 | jest.useFakeTimers();
16 |
17 | function instantiateDesktop(){
18 | const container = document.createElement('div');
19 | return new Desktop(container);
20 | }
21 |
22 | it('should build desktop on instantiate', () => {
23 | const desktop = instantiateDesktop();
24 | expect(desktop.element.classList[0]).toEqual('desktop');
25 | });
26 |
27 | it('should open an application', () => {
28 | const desktop = instantiateDesktop();
29 | spyOn(desktop.element, 'appendChild');
30 | const application = desktop.openApplication('editor');
31 | expect(desktop.openApplications[0]).toEqual(application);
32 | });
33 |
34 | it('should instantiate an editor application when first opening it', () => {
35 | const desktop = instantiateDesktop();
36 | spyOn(desktop.element, 'appendChild');
37 | const options = {some: 'option'};
38 | desktop.openApplication('editor', options);
39 | expect(EditorApplication).toHaveBeenCalledWith(desktop.element, options);
40 | });
41 |
42 | it('should instantiate a terminal application when first opening it', () => {
43 | const desktop = instantiateDesktop();
44 | spyOn(desktop.element, 'appendChild');
45 | const options = {some: 'option'};
46 | desktop.openApplication('terminal', options);
47 | expect(TerminalApplication).toHaveBeenCalledWith(desktop.element, options);
48 | });
49 |
50 | it('should not instantiate an application when that application was already open', () => {
51 | const desktop = instantiateDesktop();
52 | spyOn(desktop.element, 'appendChild');
53 | desktop.openApplication('editor');
54 | desktop.openApplication('terminal');
55 | desktop.openApplication('editor');
56 | expect(EditorApplication).not.toHaveBeenCalledTimes(1);
57 | });
58 |
59 | it('should optionally instantiate an application more than once', () => {
60 | const desktop = instantiateDesktop();
61 | spyOn(desktop.element, 'appendChild');
62 | desktop.openApplication('editor', { id: 'editor1' });
63 | desktop.openApplication('editor', { id: 'editor2' });
64 | desktop.openApplication('editor', { id: 'editor1' });
65 | desktop.openApplication('editor', { id: 'editor2' });
66 | expect(EditorApplication).toHaveBeenCalledTimes(2);
67 | });
68 |
69 | it('should append the application element to the desktop when opening it', () => {
70 | const desktop = instantiateDesktop();
71 | spyOn(desktop.element, 'appendChild');
72 | const options = {some: 'option'};
73 | const application = desktop.openApplication('terminal', options);
74 | expect(desktop.element.appendChild).toHaveBeenCalledWith(application.element);
75 | });
76 |
77 | it('should minimize all open applications', () => {
78 | const desktop = instantiateDesktop();
79 | spyOn(desktop.element, 'appendChild');
80 | const editor = desktop.openApplication('editor');
81 | const terminal = desktop.openApplication('terminal');
82 | desktop.minimizeAllApplications();
83 | expect(editor.minimize).toHaveBeenCalled();
84 | expect(terminal.minimize).toHaveBeenCalled();
85 | });
86 |
87 | it('should optionally execute complete callback when minimization ends', () => {
88 | const onComplete = jest.fn();
89 | const desktop = instantiateDesktop();
90 | desktop.minimizeAllApplications(onComplete);
91 | expect(setTimeout).toHaveBeenCalledWith(onComplete, 750);
92 | });
93 |
94 | it('should maximize some application', () => {
95 | const desktop = instantiateDesktop();
96 | spyOn(desktop.element, 'appendChild');
97 | const application = desktop.openApplication('editor');
98 | desktop.maximizeApplication(application);
99 | expect(application.maximize).toHaveBeenCalled();
100 | });
101 |
102 | it('should execute complete callback when maximization ends', () => {
103 | const onComplete = jest.fn();
104 | const desktop = instantiateDesktop();
105 | spyOn(desktop.element, 'appendChild');
106 | const application = desktop.openApplication('editor');
107 | desktop.maximizeApplication(application, onComplete);
108 | expect(setTimeout).toHaveBeenCalledWith(onComplete, 750);
109 | });
110 |
111 | it('should execute complete callback instantly when maximizing inanimate applications', () => {
112 | const onComplete = jest.fn();
113 | const desktop = instantiateDesktop();
114 | spyOn(desktop.element, 'appendChild');
115 | const application = desktop.openApplication('editor', { inanimate: true });
116 | desktop.maximizeApplication(application, onComplete);
117 | expect(onComplete).toHaveBeenCalled();
118 | expect(setTimeout).not.toHaveBeenCalled();
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/src/scripts/components/editor-application/editor-application.js:
--------------------------------------------------------------------------------
1 | import '@styles/editor-application.styl';
2 | import { Application } from '../application/application';
3 | import { EditorLine } from '../editor-line/editor-line';
4 | import textService from '../../services/text/text';
5 |
6 | export class EditorApplication extends Application {
7 | constructor(container, options = {}){
8 | super('editor', options);
9 | this.container = container;
10 | this.container.appendChild(this.element);
11 | this.setupInitialContent(options.initialContent);
12 | this.setWindowTitle(buildWindowTitle(options));
13 | }
14 | setupInitialContent(initialContent = ''){
15 | const linesToAppend = parseContent(initialContent);
16 | this.setLines([]);
17 | this.appendLines(linesToAppend);
18 | }
19 | setLines(lines){
20 | this.lines = [];
21 | }
22 | appendLines(textLines){
23 | textLines.forEach(textLine => {
24 | const line = buildEditorLine(this.lines, textLine);
25 | appendContentToApplication(this, line);
26 | });
27 | }
28 | write({ codeSample }, onComplete){
29 | const textLines = parseContent(codeSample);
30 | writeMultipleLines(this, textLines, onComplete);
31 | }
32 | }
33 |
34 | function parseContent(content){
35 | return textService.removeBlankFirstLine(content);
36 | }
37 |
38 | function buildWindowTitle(options){
39 | return options.windowTitle || '~/demo/demo.js';
40 | }
41 |
42 | function writeMultipleLines(application, textLines, onComplete){
43 | const textLine = textLines.shift();
44 | if(textLine !== undefined){
45 | inactivateLastLineWritten(application.lines);
46 | writeSingleLine(application, textLine, () => {
47 | writeMultipleLines(application, textLines, onComplete);
48 | });
49 | } else {
50 | onComplete();
51 | }
52 | }
53 |
54 | function inactivateLastLineWritten(lines){
55 | if(lines.length) lines[lines.length-1].setInactive();
56 | }
57 |
58 | function writeSingleLine(application, text, onComplete){
59 | const line = buildEditorLine(application.lines);
60 | appendContentToApplication(application, line);
61 | line.setActive();
62 | line.write(text, onComplete);
63 | }
64 |
65 | function buildEditorLine(applicationLines, text){
66 | return new EditorLine(applicationLines.length + 1, text);
67 | }
68 |
69 | function appendContentToApplication(application, line){
70 | application.addContent(line.element);
71 | application.lines.push(line);
72 | }
73 |
--------------------------------------------------------------------------------
/src/scripts/components/editor-application/editor-application.test.js:
--------------------------------------------------------------------------------
1 | import textService from '../../services/text/text';
2 | import { EditorLine } from '../editor-line/editor-line';
3 | import { EditorApplication } from './editor-application';
4 | import { EditorLineMock, editorLineInstanceMock } from '@mocks/editor-line-mock';
5 |
6 | jest.mock('../editor-line/editor-line');
7 | EditorLine.mockImplementation(EditorLineMock);
8 |
9 | describe('Editor Application Component', () => {
10 |
11 | function stubRemoveBlankFirstLine(value){
12 | spyOn(textService, 'removeBlankFirstLine').and.returnValue(value);
13 | }
14 |
15 | function instantiateEditorApplication(options){
16 | const container = document.createElement('div');
17 | return new EditorApplication(container, options);
18 | }
19 |
20 | it('should build editor application wrapper on instantiate', () => {
21 | const application = instantiateEditorApplication();
22 | expect(application.element.classList[0]).toEqual('editor-application');
23 | });
24 |
25 | it('should append lines on instantiate if initial content has been given', () => {
26 | const initialContent = `Some text.
27 | Some more text.`;
28 | const application = instantiateEditorApplication({ initialContent });
29 | expect(EditorLineMock).toHaveBeenCalledWith(1, 'Some text.');
30 | expect(EditorLineMock).toHaveBeenCalledWith(2, 'Some more text.');
31 | expect(application.lines.length).toEqual(2);
32 | });
33 |
34 | it('should config window title with default window title if no window title option is given', () => {
35 | const application = instantiateEditorApplication();
36 | expect(application.windowTitle).toEqual('~/demo/demo.js');
37 | });
38 |
39 | it('should config window title with the window title given as option', () => {
40 | const windowTitle = 'Atom';
41 | const application = instantiateEditorApplication({ windowTitle });
42 | expect(application.windowTitle).toEqual(windowTitle);
43 | });
44 |
45 | it('should add written line to the application content', () => {
46 | const application = instantiateEditorApplication();
47 | const codeSample = 'Line 1';
48 | const onComplete = jest.fn();
49 | application.addContent = jest.fn();
50 | stubRemoveBlankFirstLine(codeSample.split('\n'));
51 | application.write({ codeSample }, onComplete);
52 | expect(application.addContent).toHaveBeenCalledWith(editorLineInstanceMock.element);
53 | });
54 |
55 | it('should instantiate an editor application line when writing some code', () => {
56 | const application = instantiateEditorApplication();
57 | const codeSample = 'Line 1';
58 | const onComplete = jest.fn();
59 | application.addContent = jest.fn();
60 | stubRemoveBlankFirstLine(codeSample.split('\n'));
61 | application.write({ codeSample }, onComplete);
62 | expect(EditorLineMock).toHaveBeenCalledWith(1, undefined);
63 | });
64 |
65 | it('should set line as active on write', () => {
66 | const application = instantiateEditorApplication();
67 | const codeSample = 'Line 1';
68 | const onComplete = jest.fn();
69 | application.addContent = jest.fn();
70 | stubRemoveBlankFirstLine(codeSample.split('\n'));
71 | application.write({ codeSample }, onComplete);
72 | expect(editorLineInstanceMock.setActive.mock.calls.length).toEqual(1);
73 | });
74 |
75 | it('should write a single-line code', () => {
76 | const application = instantiateEditorApplication();
77 | const codeSample = 'Line 1';
78 | const onComplete = jest.fn();
79 | application.addContent = jest.fn();
80 | stubRemoveBlankFirstLine(codeSample.split('\n'));
81 | application.write({ codeSample }, onComplete);
82 | expect(editorLineInstanceMock.write.mock.calls[0][0]).toEqual(codeSample);
83 | expect(typeof editorLineInstanceMock.write.mock.calls[0][1]).toEqual('function');
84 | });
85 |
86 | it('should write a multi-line code', () => {
87 | const application = instantiateEditorApplication();
88 | const codeSample = 'Line 1\nLine 2\nLine 3';
89 | const onComplete = jest.fn();
90 | application.addContent = jest.fn();
91 | stubRemoveBlankFirstLine(codeSample.split('\n'));
92 | application.write({ codeSample }, onComplete);
93 | expect(editorLineInstanceMock.write.mock.calls[0][0]).toEqual('Line 1');
94 | expect(editorLineInstanceMock.write.mock.calls[1][0]).toEqual('Line 2');
95 | expect(editorLineInstanceMock.write.mock.calls[2][0]).toEqual('Line 3');
96 | expect(editorLineInstanceMock.write.mock.calls.length).toEqual(3);
97 | });
98 |
99 | it('should set last line written as inactive when writing a new line', () => {
100 | const application = instantiateEditorApplication();
101 | const codeSample = 'Line 1\nLine 2';
102 | const onComplete = jest.fn();
103 | application.addContent = jest.fn();
104 | stubRemoveBlankFirstLine(codeSample.split('\n'));
105 | application.write({ codeSample }, onComplete);
106 | expect(editorLineInstanceMock.setInactive.mock.calls.length).toEqual(1);
107 | });
108 |
109 | it('should execute on complete callback after finish writing some code', () => {
110 | const application = instantiateEditorApplication();
111 | const codeSample = 'Line 1';
112 | const onComplete = jest.fn();
113 | application.addContent = jest.fn();
114 | stubRemoveBlankFirstLine(codeSample.split('\n'));
115 | application.write({ codeSample }, onComplete);
116 | expect(onComplete).toHaveBeenCalled();
117 | });
118 |
119 | });
120 |
--------------------------------------------------------------------------------
/src/scripts/components/editor-line/editor-line.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/scripts/components/editor-line/editor-line.js:
--------------------------------------------------------------------------------
1 | import '@styles/editor-line.styl';
2 | import { Cursor } from '../cursor/cursor';
3 | import domService from '../../services/dom/dom';
4 | import template from './editor-line.html';
5 |
6 | export class EditorLine {
7 | constructor(lineNumber, writtenText){
8 | this.cursor = new Cursor();
9 | this.element = domService.parseHtml(template);
10 | setNumber(this.element, lineNumber);
11 | setupWrittenText(this.element, writtenText);
12 | if(!writtenText)
13 | getTextElement(this.element).appendChild(this.cursor.element);
14 | }
15 | write(text, onComplete){
16 | this.cursor.write(text, onComplete);
17 | }
18 | setActive(){
19 | this.cursor.setActive();
20 | }
21 | setInactive(){
22 | this.cursor.setInactive();
23 | }
24 | }
25 |
26 | function setNumber(lineElement, number){
27 | getNumberElement(lineElement).innerText = number;
28 | }
29 |
30 | function getNumberElement(lineElement){
31 | return getInnerElement(lineElement, '[data-editor-line-number]');
32 | }
33 |
34 | function getTextElement(lineElement){
35 | return getInnerElement(lineElement, '[data-editor-line-text]');
36 | }
37 |
38 | function getInnerElement(lineElement, selector){
39 | return lineElement.querySelector(selector);
40 | }
41 |
42 | function setupWrittenText(lineElement, text = ''){
43 | getTextElement(lineElement).innerHTML = text;
44 | }
45 |
--------------------------------------------------------------------------------
/src/scripts/components/editor-line/editor-line.test.js:
--------------------------------------------------------------------------------
1 | import typeService from '../../services/type/type';
2 | import { Cursor } from '../cursor/cursor';
3 | import { EditorLine } from './editor-line';
4 | import { CursorMock } from '@mocks/cursor-mock';
5 |
6 | jest.mock('../cursor/cursor');
7 | Cursor.mockImplementation(CursorMock);
8 |
9 | describe('Editor Line Component', () => {
10 |
11 | beforeEach(() => {
12 | spyOn(typeService, 'type');
13 | });
14 |
15 | it('should have appropriate css class', () => {
16 | const line = new EditorLine();
17 | expect(line.element.classList[0]).toEqual('editor-line');
18 | });
19 |
20 | it('should set line number on instantiate', () => {
21 | const line = new EditorLine(1);
22 | const numberElement = line.element.querySelector('[data-editor-line-number]');
23 | expect(numberElement.innerText).toEqual(1);
24 | });
25 |
26 | it('should append a cursor element in editor line text element on instantiate', () => {
27 | const line = new EditorLine(1);
28 | const cursorElement = line.element.querySelectorAll('[data-editor-line-text] > span');
29 | expect(cursorElement.length).toEqual(1);
30 | });
31 |
32 | it('should not append a cursor element if line contains some written text on instantiate', () => {
33 | const line = new EditorLine(1, 'Some text');
34 | const cursorElement = line.element.querySelectorAll('[data-editor-line-text] > span');
35 | expect(cursorElement.length).toEqual(0);
36 | });
37 |
38 | it('should write some text using cursor', () => {
39 | const line = new EditorLine(1);
40 | const text = 'const number = 1;';
41 | const onComplete = jest.fn();
42 | line.write(text, onComplete);
43 | expect(line.cursor.write).toHaveBeenCalledWith(text, onComplete);
44 | });
45 |
46 | it('should set line as active', () => {
47 | const line = new EditorLine(1);
48 | line.cursor.setActive = jest.fn();
49 | line.setActive();
50 | expect(line.cursor.setActive).toHaveBeenCalled();
51 | });
52 |
53 | it('should set line as inactive', () => {
54 | const line = new EditorLine(1);
55 | line.cursor.setInactive = jest.fn();
56 | line.setInactive();
57 | expect(line.cursor.setInactive).toHaveBeenCalled();
58 | });
59 |
60 | });
61 |
--------------------------------------------------------------------------------
/src/scripts/components/player/player.js:
--------------------------------------------------------------------------------
1 | import { Desktop } from '../desktop/desktop';
2 |
3 | export class Player {
4 | constructor(container, steps){
5 | this.container = container;
6 | this.steps = steps;
7 | this.desktop = new Desktop(container);
8 | this.setCurrentStepNumber(0);
9 | }
10 | play(){
11 | const { desktop, steps } = this;
12 | return new Promise(resolve => playSteps(this, desktop, steps, resolve));
13 | }
14 | getCurrentStepNumber(){
15 | return this.currentStepNumber;
16 | }
17 | setCurrentStepNumber(stepNumber){
18 | this.currentStepNumber = stepNumber;
19 | }
20 | }
21 |
22 | function playSteps(player, desktop, steps, onComplete){
23 | let currentStepNumber = player.getCurrentStepNumber();
24 | if(currentStepNumber < steps.length){
25 | const step = steps[currentStepNumber];
26 | playStep(desktop, step, () => {
27 | player.setCurrentStepNumber(currentStepNumber + 1);
28 | playSteps(player, desktop, steps, onComplete);
29 | }, step.onCompleteDelay);
30 | } else {
31 | onComplete();
32 | }
33 | }
34 |
35 | function playStep(desktop, step, onComplete, onCompleteDelay = 0){
36 | getApplication(desktop, step.app, step.options, application => {
37 | const callback = () => setTimeout(onComplete, onCompleteDelay);
38 | if(step.action) return application[step.action](step.params, callback);
39 | return callback();
40 | });
41 | }
42 |
43 | function getApplication(desktop, app, options, onGetApplication){
44 | const application = desktop.openApplication(app, options);
45 | const onMaximizeApplication = () => onGetApplication(application);
46 | if(application.isMaximized) {
47 | onGetApplication(application);
48 | } else if(application.inanimate) {
49 | desktop.minimizeAllApplications();
50 | desktop.maximizeApplication(application, onMaximizeApplication)
51 | } else {
52 | desktop.minimizeAllApplications(() => {
53 | desktop.maximizeApplication(application, onMaximizeApplication);
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/scripts/components/player/player.test.js:
--------------------------------------------------------------------------------
1 | import { Player } from './player';
2 | import { Desktop } from '../desktop/desktop';
3 | import { DesktopMock } from '@mocks/desktop-mock';
4 |
5 | jest.mock('../desktop/desktop');
6 | Desktop.mockImplementation(DesktopMock);
7 |
8 | describe('Player Component', () => {
9 | let container;
10 |
11 | jest.useFakeTimers();
12 |
13 | function instantiatePlayer(steps = []){
14 | container = document.createElement('div');
15 | return new Player(container, steps);
16 | }
17 |
18 | function mockMaximizedTerminalApplication(){
19 | return {
20 | isMaximized: true,
21 | command: jest.fn((params, onComplete) => { onComplete(); }),
22 | respond: jest.fn((params, onComplete) => { onComplete(); })
23 | };
24 | }
25 |
26 | it('should set steps on instantiate', () => {
27 | const options = {windowTitle: 'Atom'};
28 | const steps = [{app: 'editor', options}];
29 | const player = instantiatePlayer(steps);
30 | expect(player.steps).toEqual(steps);
31 | });
32 |
33 | it('should build desktop passing player container on instantiate', () => {
34 | const player = instantiatePlayer();
35 | expect(Desktop).toHaveBeenCalledWith(container);
36 | expect(player.desktop).toBeDefined();
37 | });
38 |
39 | it('should set current step number number as zero on instantiate', () => {
40 | const player = instantiatePlayer();
41 | expect(player.currentStepNumber).toEqual(0);
42 | });
43 |
44 | it('should open step application when playing some step', () => {
45 | const options = {windowTitle: 'Atom'};
46 | const steps = [{app: 'editor', options}];
47 | const player = instantiatePlayer(steps);
48 | player.desktop.openApplication.mockReturnValue({a:1});
49 | player.play();
50 | expect(player.desktop.openApplication).toHaveBeenCalledWith('editor', options);
51 | });
52 |
53 | it('should minimize all applications and maximize step application if step application is not maximized when playing some step', () => {
54 | const steps = [{app: 'editor'}];
55 | const player = instantiatePlayer(steps);
56 | const application = {};
57 | player.desktop.openApplication.mockReturnValue(application);
58 | player.desktop.minimizeAllApplications.mockImplementation(onComplete => { onComplete(); });
59 | player.desktop.maximizeApplication.mockImplementation((application, onComplete) => { onComplete(); });
60 | player.play();
61 | expect(typeof player.desktop.minimizeAllApplications.mock.calls[0][0]).toEqual('function');
62 | expect(player.desktop.maximizeApplication.mock.calls[0][0]).toEqual(application);
63 | expect(typeof player.desktop.maximizeApplication.mock.calls[0][1]).toEqual('function');
64 | });
65 |
66 | it('should minimize all applications and maximize step application synchronously if step application to be open is inanimate', () => {
67 | const steps = [{app: 'editor'}];
68 | const player = instantiatePlayer(steps);
69 | const application = { inanimate: true };
70 | player.desktop.openApplication.mockReturnValue(application);
71 | player.play();
72 | expect(player.desktop.minimizeAllApplications).toHaveBeenCalled();
73 | expect(player.desktop.maximizeApplication.mock.calls[0][0]).toEqual(application);
74 | expect(typeof player.desktop.maximizeApplication.mock.calls[0][1]).toEqual('function');
75 | });
76 |
77 | it('should neither minimize or maximize any application if step application is already maximized when playing some step', () => {
78 | const steps = [{app: 'editor'}];
79 | const player = instantiatePlayer(steps);
80 | const application = {isMaximized: true};
81 | player.desktop.openApplication.mockReturnValue(application);
82 | player.play();
83 | expect(player.desktop.minimizeAllApplications).not.toHaveBeenCalled();
84 | expect(player.desktop.maximizeApplication).not.toHaveBeenCalled();
85 | });
86 |
87 | it('should play some step action with its parameters', () => {
88 | const params = {codeSample: 'console.log("test!")'};
89 | const steps = [{app: 'editor', action: 'write', params}];
90 | const player = instantiatePlayer(steps);
91 | const application = {isMaximized: true, write: jest.fn()};
92 | player.desktop.openApplication.mockReturnValue(application);
93 | player.play();
94 | expect(application.write.mock.calls[0][0]).toEqual(params);
95 | expect(typeof application.write.mock.calls[0][1]).toEqual('function');
96 | });
97 |
98 | it('should increment current step number after play some step', () => {
99 | const steps = [
100 | {app: 'terminal', action: 'command'},
101 | {app: 'terminal', action: 'respond'}
102 | ];
103 | const player = instantiatePlayer(steps);
104 | const application = mockMaximizedTerminalApplication();
105 | player.desktop.openApplication.mockReturnValue(application);
106 | player.play();
107 | jest.runOnlyPendingTimers();
108 | expect(player.currentStepNumber).toEqual(1);
109 | jest.runOnlyPendingTimers();
110 | expect(player.currentStepNumber).toEqual(2);
111 | });
112 |
113 | it('should optionally delay to play the next step', () => {
114 | const steps = [
115 | {app: 'terminal', action: 'command', params: {}, onCompleteDelay: 500},
116 | {app: 'terminal', action: 'respond', params: {}}
117 | ];
118 | const player = instantiatePlayer(steps);
119 | const application = mockMaximizedTerminalApplication();
120 | player.desktop.openApplication.mockReturnValue(application);
121 | player.play();
122 | expect(application.command).toHaveBeenCalled();
123 | jest.advanceTimersByTime(499);
124 | expect(application.respond).not.toHaveBeenCalled();
125 | jest.advanceTimersByTime(1);
126 | expect(application.respond).toHaveBeenCalled();
127 | });
128 |
129 | it('should return a promise when playing steps', () => {
130 | const steps = [];
131 | const player = instantiatePlayer(steps);
132 | const promise = player.play();
133 | expect(promise.then).toBeDefined();
134 | });
135 |
136 | it('should resolve promise when finish to play steps', done => {
137 | let resolved;
138 | const steps = [
139 | {app: 'terminal', action: 'command', params: {}},
140 | {app: 'terminal', action: 'respond', params: {}}
141 | ];
142 | const player = instantiatePlayer(steps);
143 | const application = mockMaximizedTerminalApplication();
144 | player.desktop.openApplication.mockReturnValue(application);
145 | player.play().then(() => {
146 | resolved = true;
147 | expect(resolved).toEqual(true);
148 | done()
149 | });
150 | jest.runAllTimers();
151 | });
152 |
153 | });
154 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-application/terminal-application.js:
--------------------------------------------------------------------------------
1 | import textService from '../../services/text/text';
2 | import { Application } from '../application/application';
3 | import { TerminalCommandLine } from '../terminal-command-line/terminal-command-line';
4 | import { TerminalResponseLine } from '../terminal-response-line/terminal-response-line';
5 |
6 | export class TerminalApplication extends Application {
7 | constructor(container, options = {}){
8 | super('terminal', options);
9 | this.container = container;
10 | this.container.appendChild(this.element);
11 | this.setCommandLines([]);
12 | this.configOptions(options);
13 | }
14 | configOptions(options){
15 | this.setWindowTitle(buildWindowTitle(options));
16 | this.setPromptString(buildPromptString(options));
17 | }
18 | setCommandLines(lines){
19 | this.commandLines = lines;
20 | }
21 | setPromptString(string){
22 | this.promptString = string;
23 | }
24 | command({ command, promptString }, onComplete){
25 | if(promptString)
26 | this.setPromptString(promptString);
27 | setLastCommandLineWrittenAsInactive(this.commandLines);
28 | writeCommandLine(this, command, onComplete);
29 | }
30 | respond({ response }, onComplete){
31 | const responseLines = textService.removeBlankFirstLine(response);
32 | for(let i = 0; i < responseLines.length; i++)
33 | this.addContent(buildResponseLineElement(responseLines[i]));
34 | onComplete();
35 | }
36 | }
37 |
38 | function buildWindowTitle(options){
39 | return options.windowTitle || 'bash';
40 | }
41 |
42 | function buildPromptString(options){
43 | return options.promptString || '~/demo $';
44 | }
45 |
46 | function setLastCommandLineWrittenAsInactive(commandLines){
47 | if(commandLines.length)
48 | commandLines[commandLines.length - 1].setInactive();
49 | }
50 |
51 | function writeCommandLine(terminalApplication, command, onComplete){
52 | const line = new TerminalCommandLine(terminalApplication.promptString);
53 | terminalApplication.commandLines.push(line);
54 | terminalApplication.addContent(line.element);
55 | line.setActive();
56 | line.command(command, onComplete);
57 | }
58 |
59 | function buildResponseLineElement(lineText){
60 | const line = new TerminalResponseLine();
61 | line.setText(lineText);
62 | return line.element;
63 | }
64 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-application/terminal-application.test.js:
--------------------------------------------------------------------------------
1 | import { TerminalApplication } from './terminal-application';
2 | import { TerminalCommandLine } from '../terminal-command-line/terminal-command-line';
3 | import { TerminalResponseLine } from '../terminal-response-line/terminal-response-line';
4 | import { TerminalCommandLineMock, terminalCommandLineInstanceMock } from '@mocks/terminal-command-line-mock';
5 | import { TerminalResponseLineMock, terminalResponseLineInstanceMock } from '@mocks/terminal-response-line-mock';
6 | import textService from '../../services/text/text';
7 |
8 | jest.mock('../terminal-command-line/terminal-command-line');
9 | TerminalCommandLine.mockImplementation(TerminalCommandLineMock);
10 |
11 | jest.mock('../terminal-response-line/terminal-response-line');
12 | TerminalResponseLine.mockImplementation(TerminalResponseLineMock);
13 |
14 | describe('Terminal Application Component', () => {
15 |
16 | function instantiateTerminalApplication(options){
17 | const container = document.createElement('div');
18 | return new TerminalApplication(container, options);
19 | }
20 |
21 | beforeEach(() => {
22 | spyOn(textService, 'removeBlankFirstLine').and.callFake(value => [value]);
23 | });
24 |
25 | it('should build terminal application wrapper on instantiate', () => {
26 | const application = instantiateTerminalApplication();
27 | expect(application.element.classList[0]).toEqual('terminal-application');
28 | });
29 |
30 | it('should build lines wrapper on instantiate', () => {
31 | const application = instantiateTerminalApplication();
32 | const linesWrapper = application.element.querySelector('ul');
33 | expect(linesWrapper).toBeDefined();
34 | });
35 |
36 | it('should config window title with default window title if no window title option is given', () => {
37 | const application = instantiateTerminalApplication();
38 | expect(application.windowTitle).toEqual('bash');
39 | });
40 |
41 | it('should config window title with the window title given as option', () => {
42 | const windowTitle = 'zsh';
43 | const application = instantiateTerminalApplication({ windowTitle });
44 | expect(application.windowTitle).toEqual(windowTitle);
45 | });
46 |
47 | it('should config prompt string with default prompt string if no prompt string option is given', () => {
48 | const application = instantiateTerminalApplication();
49 | expect(application.promptString).toEqual('~/demo $');
50 | });
51 |
52 | it('should config prompt string with the prompt string given as option', () => {
53 | const promptString = '> $';
54 | const application = instantiateTerminalApplication({ promptString });
55 | expect(application.promptString).toEqual(promptString);
56 | });
57 |
58 | it('should build a command line with current defined prompt string on command', () => {
59 | const application = instantiateTerminalApplication();
60 | const command = 'npm install';
61 | const onComplete = jest.fn();
62 | spyOn(application, 'addContent');
63 | application.command({ command }, onComplete);
64 | expect(TerminalCommandLine).toHaveBeenCalledWith(application.promptString);
65 | });
66 |
67 | it('should add a command line to terminal application on command', () => {
68 | const application = instantiateTerminalApplication();
69 | const command = 'npm install';
70 | const onComplete = jest.fn();
71 | spyOn(application, 'addContent');
72 | application.command({ command }, onComplete);
73 | expect(application.addContent).toHaveBeenCalledWith(terminalCommandLineInstanceMock.element);
74 | });
75 |
76 | it('should write some command', () => {
77 | const application = instantiateTerminalApplication();
78 | const command = 'npm install';
79 | const onComplete = jest.fn();
80 | spyOn(application, 'addContent');
81 | application.command({ command }, onComplete);
82 | expect(terminalCommandLineInstanceMock.command).toHaveBeenCalledWith(command, onComplete);
83 | });
84 |
85 | it('should allow customizing prompt string when writing some command', () => {
86 | const application = instantiateTerminalApplication({ promptString: '~/demo $' });
87 | const command = 'npm install';
88 | const onComplete = jest.fn();
89 | const customPromptString = '>';
90 | spyOn(application, 'addContent');
91 | application.command({ command, promptString: customPromptString }, onComplete);
92 | expect(application.promptString).toEqual(customPromptString);
93 | });
94 |
95 | it('should set line as active on command', () => {
96 | const application = instantiateTerminalApplication();
97 | spyOn(application, 'addContent');
98 | application.command({ command: 'npm install' }, jest.fn());
99 | expect(terminalCommandLineInstanceMock.setActive).toHaveBeenCalled();
100 | });
101 |
102 | it('should set last line commanded as inactive on command', () => {
103 | const application = instantiateTerminalApplication();
104 | spyOn(application, 'addContent');
105 | application.command({ command: 'first command' }, jest.fn());
106 | const lastLineCommanded = application.commandLines[application.commandLines.length - 1];
107 | lastLineCommanded.setInactive = jest.fn();
108 | application.command({ command: 'second command' }, jest.fn());
109 | expect(lastLineCommanded.setInactive).toHaveBeenCalled();
110 | });
111 |
112 | it('should build a response line on respond', () => {
113 | const application = instantiateTerminalApplication();
114 | const response = 'Successfully installed!';
115 | const onComplete = jest.fn();
116 | spyOn(application, 'addContent');
117 | application.respond({ response }, onComplete);
118 | expect(TerminalResponseLine).toHaveBeenCalled();
119 | expect(terminalResponseLineInstanceMock.setText).toHaveBeenCalledWith(response);
120 | });
121 |
122 | it('should add a response line to terminal application on respond', () => {
123 | const application = instantiateTerminalApplication();
124 | const response = 'Successfully installed!';
125 | const onComplete = jest.fn();
126 | spyOn(application, 'addContent');
127 | application.respond({ response }, onComplete);
128 | expect(application.addContent).toHaveBeenCalledWith(terminalResponseLineInstanceMock.element);
129 | });
130 |
131 | it('should execute complete callback after finish writing response', () => {
132 | const application = instantiateTerminalApplication();
133 | const response = 'Successfully installed!';
134 | const onComplete = jest.fn();
135 | spyOn(application, 'addContent');
136 | application.respond({ response }, onComplete);
137 | expect(onComplete).toHaveBeenCalled();
138 | });
139 |
140 | });
141 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-command-line/terminal-command-line.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-command-line/terminal-command-line.js:
--------------------------------------------------------------------------------
1 | import '@styles/terminal-command-line.styl';
2 | import { Cursor } from '../cursor/cursor';
3 | import { TerminalLine } from '../terminal-line/terminal-line';
4 | import domService from '../../services/dom/dom';
5 | import template from './terminal-command-line.html';
6 |
7 | export class TerminalCommandLine extends TerminalLine {
8 | constructor(promptString){
9 | super();
10 | this.cursor = new Cursor();
11 | this.setContent(domService.parseHtml(template));
12 | this.setPromptString(promptString);
13 | getTextElement(this.element).appendChild(this.cursor.element);
14 | }
15 | setPromptString(promptString){
16 | const container = this.element.querySelector('[data-terminal-command-line-prompt-string]');
17 | container.appendChild(domService.parseHtml(promptString));
18 | }
19 | command(text, onComplete){
20 | this.cursor.write(text, onComplete);
21 | }
22 | setActive(){
23 | this.cursor.setActive();
24 | }
25 | setInactive(){
26 | this.cursor.setInactive();
27 | }
28 | }
29 |
30 | function getTextElement(lineElement){
31 | return lineElement.querySelector('[data-terminal-command-line-text]');
32 | }
33 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-command-line/terminal-command-line.test.js:
--------------------------------------------------------------------------------
1 | import { Cursor } from '../cursor/cursor';
2 | import { TerminalCommandLine } from './terminal-command-line';
3 | import { CursorMock } from '@mocks/cursor-mock';
4 |
5 | jest.mock('../cursor/cursor');
6 | Cursor.mockImplementation(CursorMock);
7 |
8 | describe('Terminal Command Line Component', () => {
9 |
10 | it('should set line content in terminal line on instantiate', () => {
11 | const line = new TerminalCommandLine();
12 | const wrapper = line.element;
13 | const promptStringElement = line.element.querySelector('[data-terminal-command-line-prompt-string]');
14 | const textElement = line.element.querySelector('[data-terminal-command-line-text]');
15 | expect(wrapper.classList[0]).toEqual('terminal-line');
16 | expect(promptStringElement.classList[0]).toEqual('terminal-command-line-prompt-string');
17 | expect(textElement.classList[0]).toEqual('terminal-command-line-text');
18 | });
19 |
20 | it('should set prompt string as plain text', () => {
21 | const line = new TerminalCommandLine('>');
22 | const promptStringElement = line.element.querySelector('[data-terminal-command-line-prompt-string]');
23 | expect(promptStringElement.innerHTML.trim()).toEqual('>');
24 | });
25 |
26 | it('should set prompt string as html', () => {
27 | const line = new TerminalCommandLine('!');
28 | const promptStringElement = line.element.querySelector('[data-terminal-command-line-prompt-string]');
29 | expect(promptStringElement.querySelector('span').innerHTML).toEqual('!');
30 | });
31 |
32 | it('should append a cursor element in terminal line text element on instantiate', () => {
33 | const line = new TerminalCommandLine();
34 | const cursorElement = line.element.querySelectorAll('[data-terminal-command-line-text] > span');
35 | expect(cursorElement.length).toEqual(1);
36 | });
37 |
38 | it('should write some command using cursor', () => {
39 | const line = new TerminalCommandLine();
40 | const command = 'npm install';
41 | const onComplete = jest.fn();
42 | line.command(command, onComplete);
43 | expect(line.cursor.write).toHaveBeenCalledWith(command, onComplete);
44 | });
45 |
46 | it('should set line as active', () => {
47 | const line = new TerminalCommandLine();
48 | line.cursor.setActive = jest.fn();
49 | line.setActive();
50 | expect(line.cursor.setActive).toHaveBeenCalled();
51 | });
52 |
53 | it('should set line as inactive', () => {
54 | const line = new TerminalCommandLine();
55 | line.cursor.setInactive = jest.fn();
56 | line.setInactive();
57 | expect(line.cursor.setInactive).toHaveBeenCalled();
58 | });
59 |
60 | });
61 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-line/terminal-line.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-line/terminal-line.js:
--------------------------------------------------------------------------------
1 | import '@styles/terminal-line.styl';
2 | import domService from '../../services/dom/dom';
3 | import template from './terminal-line.html';
4 |
5 | export class TerminalLine {
6 | constructor(){
7 | this.element = domService.parseHtml(template);
8 | }
9 | setContent(html){
10 | this.element.append(html);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-line/terminal-line.test.js:
--------------------------------------------------------------------------------
1 | import { TerminalLine } from './terminal-line';
2 |
3 | describe('Terminal Line Component', () => {
4 |
5 | it('should build terminal line on instantiate', () => {
6 | const line = new TerminalLine();
7 | expect(line.element.classList[0]).toEqual('terminal-line');
8 | });
9 |
10 | it('should set content', () => {
11 | const line = new TerminalLine();
12 | const paragraph = document.createElement('p');
13 | paragraph.innerText = 'content';
14 | line.setContent(paragraph);
15 | expect(line.element.querySelector('p').innerText).toEqual('content');
16 | });
17 |
18 | });
19 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-response-line/terminal-response-line.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-response-line/terminal-response-line.js:
--------------------------------------------------------------------------------
1 | import '@styles/terminal-response-line.styl';
2 | import { TerminalLine } from '../terminal-line/terminal-line';
3 | import domService from '../../services/dom/dom';
4 | import template from './terminal-response-line.html';
5 |
6 | export class TerminalResponseLine extends TerminalLine {
7 | constructor(){
8 | super();
9 | this.setContent(domService.parseHtml(template));
10 | }
11 | setText(text){
12 | const textContainer = this.element.querySelector('[data-terminal-response-line-text]');
13 | return domService.containsClosingHtmlTag(text) ?
14 | appendHtml(textContainer, domService.parseHtml(text)) :
15 | appendText(textContainer, text);
16 | }
17 | }
18 |
19 | function appendHtml(container, html){
20 | addElementCssClass(container, 'terminal-response-line-html-text');
21 | container.appendChild(html);
22 | }
23 |
24 | function appendText(container, text){
25 | addElementCssClass(container, 'terminal-response-line-plain-text');
26 | container.innerText = text;
27 | }
28 |
29 | function addElementCssClass(element, cssClass){
30 | element.classList.add(cssClass);
31 | }
32 |
--------------------------------------------------------------------------------
/src/scripts/components/terminal-response-line/terminal-response-line.test.js:
--------------------------------------------------------------------------------
1 | import { TerminalResponseLine } from './terminal-response-line';
2 |
3 | describe('Terminal Response Line Component', () => {
4 |
5 | it('should build line element on instantiate', () => {
6 | const line = new TerminalResponseLine();
7 | const wrapper = line.element;
8 | const textElement = line.element.querySelector('[data-terminal-response-line-text]');
9 | expect(wrapper.classList[0]).toEqual('terminal-line');
10 | expect(textElement.classList[0]).toEqual('terminal-response-line-text');
11 | });
12 |
13 | it('should set line as plain text', () => {
14 | const text = 'hello';
15 | const line = new TerminalResponseLine();
16 | line.setText(text);
17 | const textElement = line.element.querySelector('[data-terminal-response-line-text]');
18 | expect(textElement.innerText).toEqual(text);
19 | });
20 |
21 | it('should set plain text css class on set line as plain text', () => {
22 | const line = new TerminalResponseLine();
23 | line.setText('');
24 | const textElement = line.element.querySelector('[data-terminal-response-line-text]');
25 | expect(textElement.classList.contains('terminal-response-line-plain-text')).toEqual(true);
26 | });
27 |
28 | it('should set line as html', () => {
29 | const text = 'hello';
30 | const line = new TerminalResponseLine();
31 | line.setText(text);
32 | const textElement = line.element.querySelector('[data-terminal-response-line-text]');
33 | expect(textElement.querySelector('span').innerHTML).toEqual('hello');
34 | });
35 |
36 | it('should set plain text css class on set line as plain text', () => {
37 | const line = new TerminalResponseLine();
38 | line.setText('hello');
39 | const textElement = line.element.querySelector('[data-terminal-response-line-text]');
40 | expect(textElement.classList.contains('terminal-response-line-html-text')).toEqual(true);
41 | });
42 |
43 | });
44 |
--------------------------------------------------------------------------------
/src/scripts/constants/application.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_APPLICATION_ID = '_default';
2 |
--------------------------------------------------------------------------------
/src/scripts/index.js:
--------------------------------------------------------------------------------
1 | import '@styles/_variables.styl';
2 | import '@styles/_mixins.styl';
3 | import fyzer from '@glorious/fyzer';
4 | import { Player } from './components/player/player';
5 |
6 | export default class {
7 | constructor(selector){
8 | this.container = document.querySelector(selector);
9 | this.steps = [];
10 | }
11 | openApp(app, options = {}){
12 | this.steps.push({
13 | app,
14 | options,
15 | onCompleteDelay: options.onCompleteDelay
16 | });
17 | return this;
18 | }
19 | write(codeSample, { onCompleteDelay, ...options } = {}){
20 | this.steps.push({
21 | app: 'editor',
22 | action: 'write',
23 | params: { codeSample },
24 | onCompleteDelay,
25 | options
26 | });
27 | return this;
28 | }
29 | command(command, { onCompleteDelay, promptString, ...options } = {}){
30 | this.steps.push({
31 | app: 'terminal',
32 | action: 'command',
33 | params: {
34 | command,
35 | promptString
36 | },
37 | onCompleteDelay,
38 | options
39 | });
40 | return this;
41 | }
42 | respond(response, { onCompleteDelay, ...options } = {}){
43 | this.steps.push({
44 | app: 'terminal',
45 | action: 'respond',
46 | params: { response },
47 | onCompleteDelay,
48 | options
49 | });
50 | return this;
51 | }
52 | end(){
53 | return new Promise(resolve => {
54 | const { container, steps } = this;
55 | awaitContainerAppearsAboveTheFoldToPlay(container, steps, resolve);
56 | });
57 | }
58 | }
59 |
60 | function awaitContainerAppearsAboveTheFoldToPlay(container, steps, resolve){
61 | const subscriptionId = fyzer.subscribe(container, () => {
62 | const player = new Player(container, steps);
63 | fyzer.unsubscribe(subscriptionId);
64 | player.play().then(resolve);
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/scripts/index.test.js:
--------------------------------------------------------------------------------
1 | import fyzer from '@glorious/fyzer';
2 | import GDemo from './index';
3 | import { Player } from './components/player/player';
4 | import { PlayerMock, playerInstanceMock } from '@mocks/player-mock';
5 |
6 | jest.useFakeTimers();
7 | jest.mock('./components/player/player');
8 | Player.mockImplementation(PlayerMock);
9 |
10 | describe('Glorious Demo class', () => {
11 | function instantiateGDemo(){
12 | return new GDemo('[data-container]');
13 | }
14 |
15 | function simulateElementAboveTheFold(){
16 | fyzer.subscribe = jest.fn((element, callback) => {
17 | setTimeout(callback);
18 | return '123';
19 | });
20 | }
21 |
22 | beforeEach(() => {
23 | spyOn(document, 'querySelector').and.returnValue({});
24 | fyzer.unsubscribe = jest.fn();
25 | });
26 |
27 | it('should build its container on instantiate', () => {
28 | const gDemo = instantiateGDemo();
29 | expect(document.querySelector).toHaveBeenCalledWith('[data-container]');
30 | expect(gDemo.container).toEqual({});
31 | });
32 |
33 | it('should initialize steps as an empty array on instantiate', () => {
34 | const gDemo = instantiateGDemo();
35 | expect(gDemo.steps).toEqual([]);
36 | });
37 |
38 | it('should not play animation if container element is below the page fold', () => {
39 | const gDemo = instantiateGDemo();
40 | gDemo.openApp('editor').end();
41 | expect(playerInstanceMock.play).not.toHaveBeenCalled();
42 | });
43 |
44 | it('should play animation if container element is above the page fold', () => {
45 | simulateElementAboveTheFold();
46 | const gDemo = instantiateGDemo();
47 | gDemo.openApp('editor').end();
48 | jest.runOnlyPendingTimers();
49 | expect(playerInstanceMock.play).toHaveBeenCalled();
50 | expect(fyzer.unsubscribe).toHaveBeenCalledWith('123');
51 | });
52 |
53 | it('should play steps', () => {
54 | simulateElementAboveTheFold();
55 | const gDemo = instantiateGDemo();
56 | gDemo
57 | .openApp('editor')
58 | .write('console.log("hello!");')
59 | .openApp('terminal')
60 | .command('node demo.js')
61 | .respond('hello!')
62 | .end();
63 | jest.runOnlyPendingTimers();
64 | expect(Player).toHaveBeenCalledWith(gDemo.container, [
65 | {app: 'editor', options: {}, onCompleteDelay: undefined},
66 | {app: 'editor', action: 'write', params: {codeSample: 'console.log("hello!");'}, options: {}, onCompleteDelay: undefined},
67 | {app: 'terminal', options: {}, onCompleteDelay: undefined},
68 | {app: 'terminal', action: 'command', params: {command: 'node demo.js', promptString: undefined}, options: {}, onCompleteDelay: undefined},
69 | {app: 'terminal', action: 'respond', params: {response: 'hello!'}, options: {}, onCompleteDelay: undefined}
70 | ]);
71 | expect(playerInstanceMock.play).toHaveBeenCalled();
72 | });
73 |
74 | it('should play steps with options', () => {
75 | simulateElementAboveTheFold();
76 | const gDemo = instantiateGDemo();
77 | gDemo
78 | .openApp('editor', {id: 'editor1', windowTitle: 'atom', onCompleteDelay: 200})
79 | .write('console.log("hello!");', {id: 'editor1', onCompleteDelay: 300})
80 | .openApp('terminal', {id: 'terminal1', windowTitle: 'bash'})
81 | .command('node demo.js', {id: 'terminal1', promptString: '>', onCompleteDelay: 400})
82 | .respond('hello!', {id: 'terminal1', onCompleteDelay: 500})
83 | .end();
84 | jest.runOnlyPendingTimers();
85 | expect(Player).toHaveBeenCalledWith(gDemo.container, [
86 | {app: 'editor', options: {id: 'editor1', windowTitle: 'atom', onCompleteDelay: 200}, onCompleteDelay: 200},
87 | {app: 'editor', action: 'write', params: {codeSample: 'console.log("hello!");'}, options: {id: 'editor1'}, onCompleteDelay: 300},
88 | {app: 'terminal', options: {id: 'terminal1', windowTitle: 'bash'}, onCompleteDelay: undefined },
89 | {app: 'terminal', action: 'command', params: {command: 'node demo.js', promptString: '>'}, options: {id: 'terminal1'}, onCompleteDelay: 400},
90 | {app: 'terminal', action: 'respond', params: {response: 'hello!'}, options: {id: 'terminal1'}, onCompleteDelay: 500}
91 | ]);
92 | expect(playerInstanceMock.play).toHaveBeenCalled();
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/scripts/services/dom/dom.js:
--------------------------------------------------------------------------------
1 | const _public = {};
2 |
3 | _public.parseHtml = htmlString => {
4 | const parser = new DOMParser();
5 | const doc = parser.parseFromString(htmlString, 'text/html');
6 | return doc.querySelector('body').firstChild;
7 | };
8 |
9 | _public.wrapHtmlStringInHtmlTag = (htmlString, tagName) => {
10 | return `<${tagName}>${htmlString}${tagName}>`;
11 | };
12 |
13 | _public.clearNodeContent = node => {
14 | node.innerHTML = '';
15 | return node;
16 | };
17 |
18 | _public.containsClosingHtmlTag = string => {
19 | const regex = new RegExp('', 'gm');
20 | return regex.test(string);
21 | };
22 |
23 | _public.isHtmlNodeTypeText = node => {
24 | return node && node.nodeName.toLowerCase() == '#text';
25 | };
26 |
27 | export default _public;
28 |
--------------------------------------------------------------------------------
/src/scripts/services/dom/dom.test.js:
--------------------------------------------------------------------------------
1 | import domService from './dom';
2 |
3 | describe('DOM Service', () => {
4 |
5 | function createElement(tagName){
6 | return document.createElement(tagName);
7 | }
8 |
9 | it('should parse an html from string', () => {
10 | const element = domService.parseHtml('Hello World!
');
11 | const span = element.querySelector('span');
12 | expect(span.innerHTML).toEqual('World');
13 | });
14 |
15 | it('should wrap html string in html tag', () => {
16 | const htmlString = 'Hello
';
17 | const wrappedHtmlString = domService.wrapHtmlStringInHtmlTag(htmlString, 'div');
18 | expect(wrappedHtmlString).toEqual(`${htmlString}
`);
19 | });
20 |
21 | it('should clear some node content', () => {
22 | const node = createElement('div');
23 | node.innertText = 'Rafael';
24 | const clearedNode = domService.clearNodeContent(node);
25 | expect(clearedNode.innerHTML).toEqual('');
26 | });
27 |
28 | it('should detect some closing html tag in a string', () => {
29 | const string = 'Text';
30 | expect(domService.containsClosingHtmlTag(string)).toEqual(true);
31 | });
32 |
33 | it('should detect some closing html tag in a multi line string', () => {
34 | const string = `
35 |
36 | Text
37 |
38 | `;
39 | expect(domService.containsClosingHtmlTag(string)).toEqual(true);
40 | });
41 |
42 | it('should not detect some closing html tag when string does not contain it', () => {
43 | const string = 'No Closing Tags';
44 | expect(domService.containsClosingHtmlTag(string)).toEqual(false);
45 | });
46 |
47 | it('should identify text html nodes', () => {
48 | const notTextNode = createElement('div');
49 | notTextNode.append('Some text');
50 | const textNode = Array.from(notTextNode.childNodes)[0];
51 | expect(domService.isHtmlNodeTypeText(notTextNode)).toEqual(false);
52 | expect(domService.isHtmlNodeTypeText(textNode)).toEqual(true);
53 | });
54 |
55 | });
56 |
--------------------------------------------------------------------------------
/src/scripts/services/text/text.js:
--------------------------------------------------------------------------------
1 | const _public = {};
2 |
3 | _public.toKebabCase = text => {
4 | return text.toLowerCase().replace(' ', '-');
5 | };
6 |
7 | _public.isEmptyString = string => {
8 | return string.trim() === '';
9 | };
10 |
11 | _public.removeBlankFirstLine = text => {
12 | const lines = text.split('\n');
13 | if(lines[0].trim() === '')
14 | lines.splice(0,1);
15 | return lines;
16 | };
17 |
18 | export default _public;
19 |
--------------------------------------------------------------------------------
/src/scripts/services/text/text.test.js:
--------------------------------------------------------------------------------
1 | import textService from './text';
2 |
3 | describe('Text Service', () => {
4 |
5 | it('should transform some text in kebab case', () => {
6 | const text = textService.toKebabCase('Editor Application');
7 | expect(text).toEqual('editor-application');
8 | });
9 |
10 | it('should identify an empty string', () => {
11 | expect(textService.isEmptyString('')).toEqual(true);
12 | });
13 |
14 | it('should identify an non empty string', () => {
15 | expect(textService.isEmptyString('application')).toEqual(false);
16 | });
17 |
18 | it('should remove first line when it\'s empty', () => {
19 | const text = `
20 | second line
21 | third line`;
22 | expect(textService.removeBlankFirstLine(text)).toEqual([
23 | 'second line',
24 | 'third line'
25 | ]);
26 | });
27 |
28 | it('should not remove first line when it\'s not empty', () => {
29 | const text = 'first line';
30 | expect(textService.removeBlankFirstLine(text)).toEqual(['first line']);
31 | });
32 |
33 | });
34 |
--------------------------------------------------------------------------------
/src/scripts/services/type-html-text/type-html-text.js:
--------------------------------------------------------------------------------
1 | import domService from '../dom/dom';
2 | import typePlainTextService from '../type-plain-text/type-plain-text';
3 |
4 | const _public = {};
5 |
6 | _public.type = (container, htmlString, onComplete) => {
7 | const nodes = buildHtmlNodes(htmlString);
8 | typeHtmlNodes(container, nodes, onComplete);
9 | };
10 |
11 | function buildHtmlNodes(htmlString){
12 | const wrappedHtmlString = domService.wrapHtmlStringInHtmlTag(htmlString, 'span');
13 | const html = domService.parseHtml(wrappedHtmlString);
14 | return Array.from(html.childNodes);
15 | }
16 |
17 | function typeHtmlNodes(container, nodes, onComplete){
18 | if(!nodes.length)
19 | return onComplete();
20 | typeSingleHtmlNode(container, nodes.shift(), () => {
21 | typeHtmlNodes(container, nodes, onComplete);
22 | });
23 | }
24 |
25 | function typeSingleHtmlNode(container, node, onComplete){
26 | if(domService.isHtmlNodeTypeText(node))
27 | typePlainText(node.textContent, container, onComplete);
28 | else
29 | typePlainText(node.textContent, buildSubContainer(container, node), onComplete);
30 | }
31 |
32 | function buildSubContainer(container, node){
33 | const subContainer = domService.clearNodeContent(node);
34 | container.appendChild(subContainer);
35 | return subContainer;
36 | }
37 |
38 | function typePlainText(text, container, onComplete){
39 | typePlainTextService.type(container, text, onComplete);
40 | }
41 |
42 | export default _public;
43 |
--------------------------------------------------------------------------------
/src/scripts/services/type-html-text/type-html-text.test.js:
--------------------------------------------------------------------------------
1 | import typePlainTextService from '../type-plain-text/type-plain-text';
2 | import typeHtmlTextService from './type-html-text';
3 |
4 | describe('Type Html Text Service', () => {
5 | function createElement(tagName){
6 | return document.createElement(tagName);
7 | }
8 |
9 | beforeEach(() => {
10 | spyOn(typePlainTextService, 'type').and.callFake((container, text, onComplete) => {
11 | onComplete();
12 | });
13 | });
14 |
15 | it('should type some html text', () => {
16 | const container = createElement('div');
17 | const htmlString = 'some text';
18 | typeHtmlTextService.type(container, htmlString, jest.fn());
19 | expect(typePlainTextService.type.calls.allArgs()[0][0]).toEqual(container);
20 | expect(typePlainTextService.type.calls.allArgs()[0][1]).toEqual('some ');
21 | expect(typeof typePlainTextService.type.calls.allArgs()[0][2]).toEqual('function');
22 | expect(typePlainTextService.type.calls.allArgs()[1][0]).toEqual(createElement('span'));
23 | expect(typePlainTextService.type.calls.allArgs()[1][1]).toEqual('text');
24 | expect(typeof typePlainTextService.type.calls.allArgs()[1][2]).toEqual('function');
25 | });
26 |
27 | it('should type some html text containing special character', () => {
28 | const container = createElement('div');
29 | const htmlString = 'some <';
30 | typeHtmlTextService.type(container, htmlString, jest.fn());
31 | expect(typePlainTextService.type.calls.allArgs()[1][1]).toEqual('<');
32 | });
33 |
34 | it('should type some html text containing blank lines', () => {
35 | const container = createElement('div');
36 | const htmlString = `first
37 |
38 | second`;
39 | typeHtmlTextService.type(container, htmlString, jest.fn());
40 | expect(typePlainTextService.type.calls.allArgs()[0][1]).toEqual('first');
41 | expect(typePlainTextService.type.calls.allArgs()[2][1]).toEqual('second');
42 | });
43 |
44 | it('should execute on complete callback on complete', () => {
45 | const container = createElement('div');
46 | const htmlString = 'some text';
47 | const onComplete = jest.fn();
48 | typeHtmlTextService.type(container, htmlString, onComplete);
49 | expect(onComplete).toHaveBeenCalled();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/scripts/services/type-plain-text/type-plain-text.js:
--------------------------------------------------------------------------------
1 | const _public = {};
2 |
3 | _public.type = (container, text, onComplete) => {
4 | const letters = text.split('');
5 | const letter = letters.shift();
6 | if(letter)
7 | appendLetter(container, letter, letters, onComplete);
8 | else
9 | onComplete();
10 | };
11 |
12 | function appendLetter(container, letter, letters, onComplete){
13 | container.append(letter);
14 | setTimeout(() => {
15 | _public.type(container, letters.join(''), onComplete);
16 | }, 75);
17 | }
18 |
19 | export default _public;
20 |
--------------------------------------------------------------------------------
/src/scripts/services/type-plain-text/type-plain-text.test.js:
--------------------------------------------------------------------------------
1 | import typePlainTextService from './type-plain-text';
2 |
3 | describe('Type Plain Text Service', () => {
4 |
5 | let onCompleteCallbackMock;
6 |
7 | jest.useFakeTimers();
8 |
9 | function mockOnCompleteCallback(){
10 | onCompleteCallbackMock = jest.fn();
11 | }
12 |
13 | beforeEach(() => {
14 | mockOnCompleteCallback();
15 | });
16 |
17 | it('should type some plain text', () => {
18 | const containerElement = document.createElement('div');
19 | const textToBeTyped = 'Typed!';
20 | typePlainTextService.type(containerElement, textToBeTyped, onCompleteCallbackMock);
21 | jest.runAllTimers();
22 | expect(containerElement.innerHTML).toEqual(textToBeTyped);
23 | });
24 |
25 | it('should callback after type some plain text', () => {
26 | const containerElement = document.createElement('div');
27 | const textToBeTyped = 'Typed!';
28 | typePlainTextService.type(containerElement, textToBeTyped, onCompleteCallbackMock);
29 | jest.runAllTimers();
30 | expect(onCompleteCallbackMock).toHaveBeenCalledTimes(1);
31 | });
32 |
33 | });
34 |
--------------------------------------------------------------------------------
/src/scripts/services/type/type.js:
--------------------------------------------------------------------------------
1 | import domService from '../dom/dom';
2 | import typePlainTextService from '../type-plain-text/type-plain-text';
3 | import typeHtmlTextService from '../type-html-text/type-html-text';
4 |
5 | const _public = {};
6 |
7 | const TYPING_CSS_CLASS = 'is-typing';
8 |
9 | _public.type = (container, text, onComplete) => {
10 | handleIsTypingCssClass(container, 'add', TYPING_CSS_CLASS);
11 | getSpecificTypeService(text).type(container, text, () => {
12 | handleIsTypingCssClass(container, 'remove', TYPING_CSS_CLASS);
13 | onComplete();
14 | });
15 | };
16 |
17 | function handleIsTypingCssClass(element, action, cssClass){
18 | element.classList[action](cssClass);
19 | }
20 |
21 | function getSpecificTypeService(text){
22 | if(domService.containsClosingHtmlTag(text))
23 | return typeHtmlTextService;
24 | return typePlainTextService;
25 | }
26 |
27 | export default _public;
28 |
--------------------------------------------------------------------------------
/src/scripts/services/type/type.test.js:
--------------------------------------------------------------------------------
1 | import typeService from './type';
2 | import domService from '../text/text';
3 | import typeHtmlTextService from '../type-html-text/type-html-text';
4 | import typePlainTextService from '../type-plain-text/type-plain-text';
5 |
6 | describe('Type Service', () => {
7 | function mockContainer(){
8 | return document.createElement('div');
9 | }
10 |
11 | function stubServiceType(shouldAbortCompleteCallback){
12 | typePlainTextService.type = jest.fn((container, text, onComplete) => {
13 | if(!shouldAbortCompleteCallback)
14 | onComplete();
15 | });
16 | }
17 |
18 | beforeEach(() => {
19 | typeHtmlTextService.type = jest.fn();
20 | });
21 |
22 | it('should type some plain text', () => {
23 | const container = mockContainer();
24 | const onComplete = jest.fn();
25 | const text = 'just plain text';
26 | stubServiceType();
27 | domService.containsClosingHtmlTag = jest.fn(() => false);
28 | typeService.type(container, text, onComplete);
29 | expect(typePlainTextService.type.mock.calls[0][0]).toEqual(container);
30 | expect(typePlainTextService.type.mock.calls[0][1]).toEqual(text);
31 | expect(typeof typePlainTextService.type.mock.calls[0][2]).toEqual('function');
32 | });
33 |
34 | it('should type some html text', () => {
35 | const container = mockContainer();
36 | const onComplete = jest.fn();
37 | const text = 'this is bold';
38 | stubServiceType();
39 | domService.containsClosingHtmlTag = jest.fn(() => true);
40 | typeService.type(container, text, onComplete);
41 | expect(typeHtmlTextService.type.mock.calls[0][0]).toEqual(container);
42 | expect(typeHtmlTextService.type.mock.calls[0][1]).toEqual(text);
43 | expect(typeof typeHtmlTextService.type.mock.calls[0][2]).toEqual('function');
44 | });
45 |
46 | it('should add typing css class on type', () => {
47 | const container = mockContainer();
48 | const onComplete = jest.fn();
49 | const text = 'just plain text';
50 | stubServiceType(true);
51 | domService.containsClosingHtmlTag = jest.fn(() => false);
52 | typeService.type(container, text, onComplete);
53 | expect(container.classList.contains('is-typing')).toEqual(true);
54 | });
55 |
56 | it('should remove typing css class on type complete', () => {
57 | const container = mockContainer();
58 | const onComplete = jest.fn();
59 | const text = 'just plain text';
60 | stubServiceType();
61 | domService.containsClosingHtmlTag = jest.fn(() => false);
62 | typeService.type(container, text, onComplete);
63 | expect(container.classList.contains('is-typing')).toEqual(false);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/styles/_mixins.styl:
--------------------------------------------------------------------------------
1 | @require '_variables'
2 |
3 | border-radius($radius)
4 | -webkit-border-radius $radius
5 | -moz-border-radius $radius
6 | border-radius $radius
7 |
8 | box-sizing($size)
9 | -webkit-box-sizing $size
10 | -moz-box-sizing $size
11 | box-sizing $size
12 |
13 | clearfix()
14 | zoom 1
15 | &:after
16 | content " "
17 | display table
18 | clear both
19 |
20 | transition($property, $time)
21 | -webkit-transition $property $time $animation-ease-default
22 | -moz-transition $property $time $animation-ease-default
23 | -o-transition $property $time $animation-ease-default
24 | transition $property $time $animation-ease-default
25 |
26 | transform($property)
27 | -webkit-transform unquote($property)
28 | -moz-transform unquote($property)
29 | -ms-transform unquote($property)
30 | -o-transform unquote($property)
31 | transform unquote($property)
32 |
33 | unselectable()
34 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
35 | -moz-user-select: -moz-none;
36 | -khtml-user-select: none;
37 | -webkit-user-select: none;
38 | -o-user-select: none;
39 | user-select: none;
40 |
--------------------------------------------------------------------------------
/src/styles/_variables.styl:
--------------------------------------------------------------------------------
1 | // COLORS
2 | $color-red = #FF4040
3 | $color-yellow = #FFB700
4 | $color-green = #00AE55
5 | $color-blue = #2898DD
6 | $color-grey-dark = #282C34
7 | $color-lightest = #FFFFFF
8 | $color-darkest = #0E141A
9 |
10 | // FONT
11 | $font-size-xxxs = 10px
12 | $font-size-xxs = 12px
13 | $font-size-xs = 14px
14 | $font-size-sm = 16px
15 | $font-size-md = 18px
16 | $font-size-lg = 20px
17 | $font-size-xl = 24px
18 | $font-size-xxl = 36px
19 | $font-size-xxxl = 48px
20 |
21 | // ANIMATION
22 | $animation-duration = 750ms
23 | $animation-ease-default = cubic-bezier(0.080, 0.690, 0.485, 0.990)
24 |
--------------------------------------------------------------------------------
/src/styles/application.styl:
--------------------------------------------------------------------------------
1 | @import '_variables'
2 | @import '_mixins'
3 |
4 | $application-action-size = 8px
5 | $application-box-shadow-intensity = 12px 18px 48px 0
6 | $application-box-shadow-color = alpha($color-darkest, .6)
7 |
8 | .application
9 | background-color $color-darkest
10 | color alpha($color-lightest, .5)
11 | font-family 'Helvetica', 'Arial', sans-serif
12 | box-shadow $application-box-shadow-intensity $application-box-shadow-color
13 | overflow hidden
14 | border-radius(6px)
15 |
16 | .application-topbar
17 | position relative
18 | padding 3px 12px
19 | clearfix()
20 | box-sizing(border-box)
21 |
22 | .application-actions-container
23 | position absolute
24 | top 50%
25 | transform('translateY(-50%)')
26 |
27 | .application-actions-container,
28 | .application-action
29 | float left
30 |
31 | .application-action
32 | width $application-action-size
33 | height $application-action-size
34 | border-radius(50%)
35 | &.application-action-close
36 | background-color $color-red
37 | &.application-action-minimize
38 | background-color $color-yellow
39 | &.application-action-maximize
40 | background-color $color-green
41 | & + .application-action
42 | margin-left 6px
43 |
44 | .application-title-container
45 | padding-left 45px
46 | font-size $font-size-xxxs
47 | text-align center
48 | line-height $font-size-lg
49 | white-space nowrap
50 | letter-spacing .5px
51 |
52 | .application-content-container
53 | overflow auto
54 |
55 | .application-line
56 | position relative
57 | padding 0 6px
58 | min-height $font-size-xl
59 | font-size $font-size-xs
60 | line-height $font-size-xl
61 | font-family monospace
62 | clearfix()
63 |
--------------------------------------------------------------------------------
/src/styles/cursor.styl:
--------------------------------------------------------------------------------
1 | @import '_variables'
2 | @import '_mixins'
3 |
4 | @keyframes blink
5 | 0%
6 | background-color $color-lightest
7 | 100%
8 | background-color transparent
9 |
10 | $cursor-height = $font-size-xl
11 |
12 | .cursor
13 | display inline-block
14 | height $cursor-height
15 | &.cursor-active
16 | position relative
17 | &:after
18 | position absolute
19 | content ' '
20 | top 50%
21 | right -3px
22 | width 1px
23 | height 17px
24 | animation .7s blink ease-in-out infinite
25 | transform('translateY(-50%)')
26 | &.is-typing
27 | &:after
28 | right 0
29 | background-color alpha($color-lightest, .5)
30 | animation none
31 |
--------------------------------------------------------------------------------
/src/styles/desktop.styl:
--------------------------------------------------------------------------------
1 | @require '_variables'
2 | @require '_mixins'
3 | @require 'application'
4 |
5 | .desktop
6 | position relative
7 | .application
8 | position absolute
9 | top 0
10 | left 0
11 | right 0
12 | opacity 0
13 | &:not(.application-inanimate)
14 | transform('scale(0)')
15 | transition(all, $animation-duration)
16 | box-shadow 2px 3px 8px 0 $application-box-shadow-color
17 | &.application-maximized
18 | opacity 1
19 | box-shadow $application-box-shadow-intensity $application-box-shadow-color
20 | transform('scale(1)')
21 |
--------------------------------------------------------------------------------
/src/styles/editor-application.styl:
--------------------------------------------------------------------------------
1 | @import '_variables'
2 | @import '_mixins'
3 |
4 | $editor-application-line-number-width = 36px
5 |
6 | .editor-application
7 | .application-content-container
8 | background-color $color-grey-dark
9 | .application-line-text
10 | padding-left 36px
11 |
12 | .editor-line-number
13 | unselectable()
14 |
--------------------------------------------------------------------------------
/src/styles/editor-line.styl:
--------------------------------------------------------------------------------
1 | @require 'application'
2 | @require 'cursor'
3 |
4 | $editor-line-number-width = 25px
5 |
6 | .editor-line
7 | @extend .application-line
8 |
9 | .editor-line-number,
10 | .editor-line-text
11 | margin 0
12 | height $cursor-height
13 |
14 | .editor-line-number
15 | position absolute
16 | top 0
17 | left 0
18 | padding-left 6px
19 | width $editor-line-number-width
20 | text-align right
21 | opacity .5
22 |
23 | .editor-line-text
24 | padding-left $editor-line-number-width * 2
25 | overflow hidden
26 |
--------------------------------------------------------------------------------
/src/styles/terminal-command-line.styl:
--------------------------------------------------------------------------------
1 | @require '_variables'
2 | @require 'cursor'
3 |
4 | .terminal-command-line-prompt-string
5 | float left
6 | margin-right 10px
7 | color $color-blue
8 |
9 | .terminal-command-line-text
10 | height $cursor-height
11 | color $color-lightest
12 |
--------------------------------------------------------------------------------
/src/styles/terminal-line.styl:
--------------------------------------------------------------------------------
1 | @require 'application'
2 |
3 | .terminal-line
4 | @extend .application-line
5 | & > div
6 | white-space nowrap
7 |
--------------------------------------------------------------------------------
/src/styles/terminal-response-line.styl:
--------------------------------------------------------------------------------
1 | .terminal-response-line-text
2 | margin 0
3 | &.terminal-response-line-plain-text
4 | white-space pre
5 |
--------------------------------------------------------------------------------
/standalone/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
28 | Standalone: Glorious Demo
29 |
30 |
31 |
32 | Examples
33 |
37 |
38 |
Multiple instances
39 |
40 |
41 |
42 |
43 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/webpack.conf.base.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const project = require('./project.json');
4 |
5 | module.exports = {
6 | entry: `${__dirname}/${project.scripts.source.entry}`,
7 | output: {
8 | library: 'GDemo',
9 | libraryTarget: 'umd',
10 | libraryExport: 'default',
11 | path: `${__dirname}/${project.scripts.dist.root}`
12 | },
13 | module: {
14 | rules: [{
15 | test: /\.(styl|css)$/,
16 | use: [ MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader' ]
17 | }, {
18 | test: /\.html$/,
19 | include: [path.resolve(__dirname,project.scripts.source.root)],
20 | use: 'html-loader'
21 | }, {
22 | test: /\.js$/,
23 | exclude: /node_modules/,
24 | use: 'babel-loader'
25 | }]
26 | },
27 | resolve: {
28 | alias: {
29 | '@styles': `${__dirname}/${project.styles.source.root}`
30 | }
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/webpack.conf.dev.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const project = require('./project.json');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | devtool: 'inline-source-map',
8 | output: {
9 | filename: project.scripts.dist.filename.dev
10 | },
11 | plugins: [
12 | new webpack.SourceMapDevToolPlugin(),
13 | new webpack.HotModuleReplacementPlugin(),
14 | new MiniCssExtractPlugin({
15 | filename: project.styles.dist.filename.dev
16 | })
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.conf.prod.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
4 | const TerserPlugin = require('terser-webpack-plugin');
5 | const project = require('./project.json');
6 |
7 | module.exports = {
8 | mode: 'production',
9 | output: {
10 | filename: project.scripts.dist.filename.prod
11 | },
12 | optimization: {
13 | minimizer: [
14 | new TerserPlugin({
15 | terserOptions: {
16 | mangle: true,
17 | compress: {
18 | warnings: false
19 | }
20 | }
21 | }),
22 | new OptimizeCSSAssetsPlugin()
23 | ]
24 | },
25 | plugins: [
26 | new MiniCssExtractPlugin({
27 | filename: project.styles.dist.filename.prod
28 | })
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const baseConfig = require('./webpack.conf.base');
3 | const devConfig = require('./webpack.conf.dev');
4 | const prodConfig = require('./webpack.conf.prod');
5 | const specificConfig = process.env.NODE_ENV == 'production' ? prodConfig : devConfig;
6 |
7 | module.exports = merge(baseConfig, specificConfig);
8 |
--------------------------------------------------------------------------------