4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/weewikipaint.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE.TXT:
--------------------------------------------------------------------------------
1 | License
2 | -------
3 | Copyright (c) 2012-2016 Titanium I.T. LLC except for Third-Party Material
4 | stated below.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a
7 | copy of this software and associated documentation files (the "Software"),
8 | to deal in the Software without restriction, including without limitation
9 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 | and/or sell copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following conditions:
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
21 |
22 | Third-Party Material
23 | --------------------
24 | Copyrights for some files belong to third parties. They have been included
25 | in their redistributable form and any existing copyright and license notices
26 | remain intact. These files may be found in the following locations:
27 |
28 | * `node_modules`
29 | * various `vendor` directories
30 |
31 | In addition, the file `src/client/content/images/cursor.png` is based on
32 | "near me" icon included in Google's Material Icons set, which is licensed
33 | under the Apache License Version 2.0. For more information, see:
34 | https://design.google.com/icons/
35 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node generated/dist/server/run.js $PORT
2 |
--------------------------------------------------------------------------------
/autojake.js:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/node
2 |
3 | // Automatically runs Jake when files change.
4 | //
5 | // Thanks to Davide Alberto Molin for contributing this code.
6 | // See http://www.letscodejavascript.com/v3/comments/live/7 for details.
7 | //
8 | // NOTE: The "COMMAND" variable must be changed for this to work on Windows.
9 |
10 | (function() {
11 | "use strict";
12 |
13 | var gaze;
14 | try { gaze = require("gaze"); }
15 | catch (err) { console.log("To use this script, run 'npm install gaze'."); process.exit(1); }
16 |
17 | var spawn = require("child_process").spawn;
18 |
19 | var WATCH = "src/**/*.js";
20 |
21 | var COMMAND = "./jake.sh"; // Mac/Unix
22 | // var COMMAND = "jake.bat"; // Windows
23 | var COMMAND_ARGS = ["loose=true"];
24 |
25 | var buildRunning = false;
26 |
27 | gaze(WATCH, function(err, watcher) {
28 | console.log("Will run " + COMMAND + " when " + WATCH + " changes.");
29 | watcher.on("all", function(evt, filepath) {
30 | if (buildRunning) return;
31 | buildRunning = true;
32 |
33 | console.log("\n> " + COMMAND + " " + COMMAND_ARGS.join(" "));
34 | var jake = spawn(COMMAND, COMMAND_ARGS, { stdio: "inherit" });
35 |
36 | jake.on("exit", function(code) {
37 | buildRunning = false;
38 | });
39 | });
40 | });
41 |
42 | }());
43 |
--------------------------------------------------------------------------------
/design/WeeWiki-404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-404.jpg
--------------------------------------------------------------------------------
/design/WeeWiki-buttons-blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-buttons-blue.jpg
--------------------------------------------------------------------------------
/design/WeeWiki-buttons.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-buttons.jpg
--------------------------------------------------------------------------------
/design/WeeWiki-main.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-main.jpg
--------------------------------------------------------------------------------
/design/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/arrow.png
--------------------------------------------------------------------------------
/design/arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
--------------------------------------------------------------------------------
/design/design.txt:
--------------------------------------------------------------------------------
1 | Design guidelines from Espen Brunborg of primate.co.uk:
2 |
3 | Colours:
4 | Blue (main background): #41a9cc
5 | Medium blue (button background): #00799c
6 | Darkened medium blue (mainly used for hover states on buttons): #006f8f
7 | Dark blue (for button dropshadows and text): #0d576d
8 | Gray (for button): #e5e5e5
9 | Darkened gray (for button hover state): #d9d9d9
10 | medium gray (for button dropshadow): #a7a9ab
11 | dark gray (for button text): #595959
12 |
13 | // If any of the colours are inconsistent with the JS site, please feel free to use what you already have.
14 |
15 | Typography:
16 | Intro text: Alwyn new rounded, regular, 14px (or em equivalent), dark blue
17 | Call-out text at the bottom: Alwyn new rounded, regular, 14px,white
18 |
19 | Clear button:
20 | Text: Alwyn new rounded, bold, 12px, dark gray, uppercase
21 | Background: gray
22 | Dimensions: try to keep it 30px tall, with a generous padding left/right
23 | Dropshadow: medium gray, 2px vertical offset, no blur
24 |
25 | Call-out button:
26 | Text: Alwyn new rounded, bold, 14px, white, uppercase
27 | Background: medium blue
28 | Dimensions, try to keep it 35px tall, generous padding left/right (maybe even have a min-width of 175px)
29 |
30 | 404 page:
31 | 404 Text: Alwyn new rounded, bold, 220px, darkened medium blue
32 | Strapline text: Same as intro text
33 | Button: same as call-out button
34 |
--------------------------------------------------------------------------------
/design/weewiki-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/weewiki-logo.png
--------------------------------------------------------------------------------
/design/weewiki-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
95 |
--------------------------------------------------------------------------------
/jake.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | if not exist node_modules\.bin\jake.cmd call npm rebuild
3 | node node_modules\jake\bin\cli.js %*
4 |
--------------------------------------------------------------------------------
/jake.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | [ ! -f node_modules/.bin/jake ] && npm rebuild
4 | node_modules/.bin/jake $*
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "weewikipaint",
3 | "version": "0.0.1",
4 | "engines": {
5 | "node": "9.11.1"
6 | },
7 | "devDependencies": {
8 | "async": "^2.6.0",
9 | "browserify": "^16.1.0",
10 | "eslint": "^4.18.2",
11 | "glob": "^7.1.2",
12 | "hashcat": "^0.3.1",
13 | "jake": "^8.0.15",
14 | "karma": "^2.0.0",
15 | "karma-commonjs": "^1.0.0",
16 | "karma-mocha": "^1.3.0",
17 | "mocha": "^5.0.1",
18 | "procfile": "^0.1.1",
19 | "selenium-webdriver": "^3.6.0",
20 | "semver": "^5.5.0",
21 | "shelljs": "^0.8.1",
22 | "simplebuild-karma": "^1.0.0",
23 | "socket.io-client": "^2.0.4"
24 | },
25 | "dependencies": {
26 | "lolex": "^2.3.2",
27 | "send": "^0.16.2",
28 | "socket.io": "^2.0.4"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Let's Code: Test-Driven Javascript
2 | ==================================
3 |
4 | "Let's Code: Test-Driven Javascript" is a screencast series focusing on
5 | rigorous, professional web development. For more information, visit
6 | http://letscodejavascript.com .
7 |
8 | This repository contains the source code for WeeWikiPaint, the application
9 | being developed in the series.
10 |
11 | (Wondering why we check in things like `node_modules` or IDE settings? See "[The Reliable Build](http://www.letscodejavascript.com/v3/blog/2014/12/the_reliable_build)".)
12 |
13 |
14 | Before building or running for the first time:
15 | -----------------------------------
16 |
17 | 1. Install [Node.js](http://nodejs.org/download/)
18 | 2. Install [Git](http://git-scm.com/downloads)
19 | 3. Install [Firefox](http://getfirefox.com) (for smoke tests)
20 | 4. Install geckodriver into path (for smoke tests). On Mac, this is most easily done with [Homebrew](http://brew.sh/): `brew install geckodriver`. On Windows, [download geckodriver](https://github.com/mozilla/geckodriver/releases/) and manually add it to your path. On Linux, either download geckodriver manually or use your favorite package manager.
21 | 5. Clone source repository: `git clone https://github.com/jamesshore/lets_code_javascript.git`
22 | 6. All commands must run from root of repository: `cd lets_code_javascript`
23 |
24 | *Note:* If you update the repository (with `git pull` or similar), be sure to erase generated files with `git clean -fdx` first. (Note that this will erase any files you've added, so be sure to check in what you want to keep first.)
25 |
26 |
27 | Running old episodes:
28 | ---------------------
29 |
30 | Every episode's source code has an associated `episodeXX` tag. You can check out and run those episodes, but some episodes' code won't work on the current version of Node. You'll need to install the exact version of Node the code was written for.
31 |
32 | ### To check out an old episode:
33 |
34 | 1. If you made any changes, check them in.
35 | 2. Erase generated files: `git clean -fdx`
36 | 3. Reset any changes: `git reset --hard`
37 | 4. Check out old version: `git checkout episodeXX` (For example, `git checkout episode200`.)
38 |
39 | Compatibility notes:
40 |
41 | * Episodes 1-9 don't work on case-sensitive file systems. To fix the problem, rename `jakefile.js` to `Jakefile.js` (with a capital 'J').
42 | * Episodes 37-39 don't work on Windows. A workaround is included in the episode 37 video.
43 | * Episodes 269-441 and 469+ may fail when running smoke tests. They use Selenium for smoke testing and download the appropriate Firefox driver as needed. Those drivers may be missing or incompatible with your current version of Firefox. Starting with episode 469, Selenium uses geckodriver, which is installed separately from the rest of Selenium, which may make the smoke tests more reliable.
44 |
45 | ### To change Node versions and run the code:
46 |
47 | 1. Look at the `engines.node` property of `package.json` to see which version of Node the code runs on. Prior to episode 31, the Node version was documented in `readme.md`. Prior to episode 10, the version wasn't documented; those episodes used v0.6.17.
48 |
49 | 2. Install the correct version of Node. On Unix and Mac, [n](https://github.com/visionmedia/n) is a convenient tool for switching Node versions. On Windows, you can use [nvmw](https://github.com/hakobera/nvmw).
50 |
51 | 3. To see how to run the code, look at the episode's `readme.md` or watch the episode in question. Note that some episodes end with non-working code.
52 |
53 |
54 | ### Known version issues:
55 |
56 | Node has introduced breaking changes with newer versions. Here are the issues we're aware of. I've included some workarounds, but the best way to run old code is to install the exact version of Node that the code was written for.
57 |
58 | * Some episodes include a version of Jake that doesn't run on Node 0.10+. You might be able to resolve this problem by running `npm install jake`.
59 |
60 | * Some episodes include a version of NodeUnit that relies on an internal 'evals' module that was removed in Node 0.12. (See Node.js [issue #291](https://github.com/caolan/nodeunit/issues/291).) You might be able to resolve this problem by running `npm install nodeunit`.
61 |
62 | * Some episodes include a version of Testacular (now named "Karma") that crashes when you capture a browser in Node 0.10+. There's no easy workaround for this problem, so just install Node 0.8 if you want to run the code in those episodes.
63 |
64 | * A few episodes rely on a feature of Node.js streams that was removed in Node 0.10. A workaround is included in the video for the episodes in question.
65 |
66 | * Most episodes have a test that checks how [server.close()](http://nodejs.org/api/net.html#net_server_close_callback) handles errors. This behavior was changed in Node 0.12, so the test will fail. (In previous versions, it threw an exception, but now it passes an `err` object to the server.close callback.) You can just delete the test in question, or see [episode 14](http://www.letscodejavascript.com/v3/comments/live/14#comment-1870243150) for a workaround.
67 |
68 |
69 | To build and test this episode:
70 | -------------------------------
71 |
72 | 1. Run `./jake.sh karma` (Unix/Mac) or `jake karma` (Windows)
73 | 2. Navigate at least one browser to http://localhost:9876
74 | 3. Run `./jake.sh loose=true` (Unix/Mac) or `jake loose=true` (Windows)
75 |
76 | You can also run `./jake.sh quick loose=true` for a faster but less thorough set of tests.
77 |
78 | *Note:* The master branch is not guaranteed to build successfully. For a known-good build (tested on Mac and Windows, and assumed to work on Linux), use the integration branch. To change branches, follow the steps under "Running old episodes" (above), but replace `episodeXX` with `integration` (for the known-good integration branch) or `master` (for the latest code).
79 |
80 |
81 | To run this episode locally:
82 | ----------------------------
83 |
84 | 1. Run `./jake.sh run` (Unix/Mac) or `jake run` (Windows)
85 | 2. Navigate a browser to http://localhost:5000
86 |
87 | *Note:* The master branch is not guaranteed to run successfully. For a known-good build, use the integration branch as described above.
88 |
89 |
90 | To deploy:
91 | ----------
92 |
93 | Before deploying for the first time:
94 |
95 | 1. Make sure code is in Git repository (clone GitHub repo, or 'git init' yours)
96 | 2. Install [Heroku Toolbelt](https://toolbelt.heroku.com/)
97 | 3. Sign up for a [Heroku account](https://id.heroku.com/signup)
98 | 4. Run `heroku create ` (requires git repository and Heroku account)
99 | 5. Search codebase for `weewikipaint.herokuapp.com` URLs and change them to refer to ``
100 | 6. Push known-good deploy to Heroku: `git push heroku episode321:master`
101 |
102 | Then, to deploy:
103 |
104 | 1. Run `./jake.sh deploy` (Unix/Mac) or `jake deploy` (Windows) for instructions
105 |
106 | *Note:* The master and integration branches are not guaranteed to deploy successfully. The last known-good deploy was commit 0eabb0cd7b9f16a9375cb8b16a1d449570d23162. We'll establish better deployment practices in a future chapter of the screencast.
107 |
108 |
109 | Finding your way around:
110 | ------------------------
111 |
112 | * `build`: Scripts and utilities used to build and test
113 | * `design`: Initial design concept
114 | * `generated`: Files generated by the build
115 | * `generated/dist`: Distribution files--what actually runs in production
116 | * `node_modules`: npm modules
117 | * `spikes`: One-off experiments
118 | * `src`: All the source code
119 | * `src/client`: Browser-side code
120 | * `src/client/content`: HTML, CSS, images, etc., and CSS tests
121 | * `src/client/network`: Code used to communicate with the server
122 | * `src/client/ui`: Code used to render the UI
123 | * `src/node_modules`: Commonly-used utility modules. Note that these are *not* npm modules; they are part of the application source code.
124 | * `src/server`: Server-side code
125 | * `src/shared`: Code shared between client and server
126 |
127 | Files that start with an underscore are test-related and not used in production.
--------------------------------------------------------------------------------
/spikes/node_http_get/http_get.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
2 |
3 | // This spike shows how to get a URL using Node's HTTP module.
4 | "use strict";
5 |
6 | var http = require("http");
7 |
8 | http.get("http://www.google.com/index.html", function(res) {
9 | console.log("Got response: " + res.statusCode);
10 | }).on('error', function(e) {
11 | console.log("Got error: " + e.message);
12 | });
13 |
--------------------------------------------------------------------------------
/spikes/node_http_get/run.sh:
--------------------------------------------------------------------------------
1 | node http_get.js
2 |
--------------------------------------------------------------------------------
/spikes/node_http_servefile/file.html:
--------------------------------------------------------------------------------
1 |
2 |
This is an HTML file that's been changed.
3 |
4 |
--------------------------------------------------------------------------------
/spikes/node_http_servefile/http_server.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
2 |
3 | // This spike demonstrates how to serve a static file.
4 | //
5 | // It's not robust and it reflects a very basic understanding of node; use it
6 | // as a starting point, not a production-quality example.
7 | "use strict";
8 |
9 | var http = require("http");
10 | var fs = require("fs");
11 |
12 | var server = http.createServer();
13 |
14 | server.on("request", function(request, response) {
15 | console.log("Received request");
16 |
17 | fs.readFile("file.html", function (err, data) {
18 | if (err) throw err;
19 | response.end(data);
20 | });
21 | });
22 |
23 | server.listen(8080);
24 |
25 | console.log("Server started");
--------------------------------------------------------------------------------
/spikes/node_http_servefile/run.sh:
--------------------------------------------------------------------------------
1 | node http_server.js
2 |
--------------------------------------------------------------------------------
/spikes/node_http_server/http_server.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
2 |
3 | // This is a simple spike of Node's HTTP module. The goal was to show
4 | // how to serve a very simple HTML page using Node.
5 | // It's not robust and it reflects a very basic understanding of node; use it
6 | // as a starting point, not a production-quality example.
7 | "use strict";
8 |
9 | var http = require("http");
10 |
11 | var server = http.createServer();
12 |
13 | server.on("request", function(request, response) {
14 | console.log("Received request");
15 |
16 | var body = "Node HTTP Spike" +
17 | "
15 |
16 |
17 |
18 |
19 |
20 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/spikes/socket.io/readme.md:
--------------------------------------------------------------------------------
1 | Example of using socket.io for multi-user real-time web
2 |
3 | To try it:
4 |
5 | 1. Start server: `node spikes/socket.io/app.js`
6 | 2. Open multiple client tabs in a browser: `http://localhost:8080`
7 | 3. Look at server and client consoles
8 |
9 |
10 | Also:
11 |
12 | * client-race.js demonstrates a race condition in the socket.io client. It was exposed by our attempt to test socket.io. The issue was fixed in v1.4.5. It was reported here: https://github.com/socketio/socket.io-client/issues/935
13 |
14 | * server-hang.js and server-hang.html demonstrates a bug in the socket.io server that prevents the server process from exiting. The issue was reported here: https://github.com/socketio/socket.io/issues/1602
--------------------------------------------------------------------------------
/spikes/socket.io/server-hang.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Server won't exit
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Look at the console!
14 |
15 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/spikes/socket.io/server-hang.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
2 | (function() {
3 | "use strict";
4 |
5 | var PORT = 5020;
6 | var TIMEOUT = 5000;
7 |
8 | var io = require('socket.io')(PORT);
9 |
10 | console.log("Waiting " + (TIMEOUT / 1000) + " seconds...");
11 | setTimeout(function() {
12 | io.close();
13 | console.log("PROCESS SHOULD NOW EXIT");
14 | }, TIMEOUT);
15 |
16 | }());
--------------------------------------------------------------------------------
/spikes/socket_io_client_close_race/readme.md:
--------------------------------------------------------------------------------
1 | Code to reproduce client-side race condition in Socket.IO.
2 |
3 | The issue:
4 |
5 | 1. Open a Socket.IO connection to the server
6 | 2. After the server connects, but before the client-side "connect" event has fired, close the client connection.
7 | 3. Socket.IO never shuts down the connection.
8 |
9 |
10 | To try it:
11 |
12 | 1. Run `node spikes/socket_io_client_close_race/run.js`
13 |
14 |
15 | Reported on socketio/socket.io-client GitHub as issue #1133:
16 | https://github.com/socketio/socket.io-client/issues/1133
--------------------------------------------------------------------------------
/spikes/socket_io_client_close_race/run.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | "use strict";
3 |
4 | var clientIo = require("socket.io-client");
5 | var http = require("http");
6 | var io = require("socket.io");
7 |
8 | var httpServer;
9 | var ioServer;
10 |
11 | var PORT = 5020;
12 |
13 | startServer(function() {
14 | console.log("SERVER STARTED");
15 |
16 | var connections = {};
17 | logAndTrackServerConnections(connections);
18 |
19 | console.log("** connecting client");
20 | var client = clientIo("http://localhost:" + PORT);
21 | logClientConnections(client);
22 |
23 | ioServer.once("connect", function() {
24 | console.log("** disconnecting client");
25 | client.disconnect();
26 | console.log("** waiting for server to disconnect");
27 | setTimeout(function() {
28 | console.log("#### Does the client think it's connected? (Expect 'false'):", client.connected);
29 | console.log("#### How many connections does the server have? (Expect 0):", numberOfServerConnections(connections));
30 | stopServer(function() {
31 | console.log("** end of test")
32 | });
33 | }, 500);
34 | });
35 | });
36 |
37 | function logAndTrackServerConnections(connections) {
38 | ioServer.on("connect", function(socket) {
39 | var key = socket.id;
40 | console.log("SERVER CONNECTED", key);
41 | connections[key] = socket;
42 | socket.on("disconnect", function() {
43 | console.log("SERVER DISCONNECTED", key);
44 | delete connections[key];
45 | });
46 | });
47 | }
48 |
49 | function logClientConnections(socket) {
50 | var id;
51 | socket.on("connect", function() {
52 | id = socket.id;
53 | console.log("CLIENT CONNECTED", id);
54 | });
55 | socket.on("disconnect", function() {
56 | console.log("CLIENT DISCONNECTED", id);
57 | });
58 | }
59 |
60 | function numberOfServerConnections(connections) {
61 | return Object.keys(connections).length;
62 | }
63 |
64 | function startServer(callback) {
65 | console.log("** starting server");
66 | httpServer = http.createServer();
67 | ioServer = io(httpServer);
68 | httpServer.listen(PORT, callback);
69 | };
70 |
71 | function stopServer(callback) {
72 | console.log("** stopping server");
73 |
74 | httpServer.on("close", function() {
75 | console.log("SERVER CLOSED");
76 | callback();
77 | });
78 |
79 | ioServer.close();
80 | };
81 |
82 | }());
--------------------------------------------------------------------------------
/spikes/socket_io_disconnection/app.js:
--------------------------------------------------------------------------------
1 | var app = require('http').createServer(handler)
2 | var io = require('socket.io')(app);
3 | var fs = require('fs');
4 |
5 | var port = process.argv[2] || 8080;
6 |
7 | app.listen(port);
8 |
9 | console.log("Server started on port " + port);
10 | function handler (req, res) {
11 | console.log("Request received");
12 |
13 | fs.readFile(__dirname + '/index.html',
14 | function (err, data) {
15 | if (err) {
16 | res.writeHead(500);
17 | return res.end('Error loading index.html');
18 | }
19 |
20 | res.writeHead(200);
21 | res.end(data);
22 | console.log("index.html served");
23 | });
24 | }
25 |
26 | io.on('connection', function (socket) {
27 | console.log("\nConnection created");
28 |
29 | socket.on("message", function(message) {
30 | console.log("Message posted: " + message);
31 | io.emit("serverMessage", message);
32 | });
33 |
34 | socket.on("disconnect", function(reason) {
35 | console.log("Disconnect: ", reason);
36 | });
37 |
38 | });
--------------------------------------------------------------------------------
/spikes/socket_io_disconnection/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | socket.io spike
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/spikes/socket_io_disconnection/readme.md:
--------------------------------------------------------------------------------
1 | How does Socket.IO behave when the browser disconnects? Use this spike to manually check the following on multiple browsers. Does the server get a disconnect event when...
2 | * ...when the 'disconnect' button is pushed?
3 | * ...the page is reloaded?
4 | * ...the user navigates away?
5 | * ...the tab is closed?
6 | * ...the browser is closed?
7 |
8 | To try it:
9 |
10 | 1. Start server: `node spikes/socket_io_disconnection/app.js`
11 | 2. Open client tabs in a browser: `http://localhost:8080`
12 | 3. Look at server and client consoles
13 |
14 |
15 | Results: (is the disconnect event received by the server?)
16 | * Firefox 54: yes in all cases
17 | * Chrome 59: yes in all cases
18 | * Safari 10.1.2 (desktop): yes in all cases
19 | * IE11: yes in all cases
20 | * MS Edge 14.14393.0: yes in all cases
21 | * Mobile Safari 10.0.0: yes in all cases
22 | * Chrome Mobile 44.0.2403: yes in all cases
--------------------------------------------------------------------------------
/spikes/socket_io_emit_on_disconnect_hang/readme.md:
--------------------------------------------------------------------------------
1 | An attempt to reproduce a client-side hang with Socket.IO.
2 |
3 | The issue was that our tests were failing to exit. This code was an attempt to reproduce the issue. Although we *did* find a "didn't exit" problem, it isn't the same as the one we were seeing in our tests. In fact, I think it may be the same as the `socket_io_client_close_race` spike, reported as https://github.com/socketio/socket.io-client/issues/1133.
4 |
5 | Ultimately, we fixed our tests by having it open sockets sequentially rather than in parallel.
--------------------------------------------------------------------------------
/spikes/socket_io_emit_on_disconnect_hang/run.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | "use strict";
3 |
4 | const socketIoClient = require("socket.io-client");
5 | const socketIoServer = require("socket.io");
6 | const http = require("http");
7 | const util = require("util");
8 |
9 | let httpServer;
10 | let ioServer;
11 |
12 | const PORT = 5020;
13 |
14 | startServer(async function() {
15 | console.log("SERVER STARTED");
16 |
17 | const connections = {};
18 | logAndTrackServerConnections(connections);
19 | logHttpConnections();
20 |
21 | console.log("** setting up server event handlers");
22 | ioServer.on("connect", function(serverSocket) {
23 | serverSocket.on("client_event", () => {
24 | console.log("CLIENT EVENT RECEIVED BY SERVER");
25 | console.log("** emitting server event");
26 | ioServer.emit("server_event", "server event sent in response to client event");
27 | });
28 | serverSocket.on("disconnect", (reason) => {
29 | // comment out following lines to prevent issue
30 | console.log("** emitting server event in disconnect handler");
31 | ioServer.emit("arbitrary_server_event", "server event sent during disconnect");
32 | });
33 | });
34 |
35 | console.log("** creating client sockets");
36 | const clientSocket1 = await createClientSocket();
37 | const clientSocket2 = await createClientSocket();
38 |
39 | console.log("** emitting client event");
40 | clientSocket1.emit("client_event", "client event");
41 |
42 | console.log("** waiting for server event");
43 | await new Promise((resolve) => {
44 | clientSocket1.on("server_event", () => {
45 | console.log("SERVER EVENT RECEIVED BY CLIENT");
46 | // use setTimeout, instead of calling resolve() directly, to prevent issue
47 | setTimeout(resolve, 200);
48 | resolve();
49 | });
50 | });
51 |
52 | console.log("** closing client sockets");
53 | await closeClientSocket(clientSocket1);
54 | await closeClientSocket(clientSocket2);
55 |
56 | // uncomment this block of code to prevent issue
57 | // console.log("** waiting for server sockets to disconnect");
58 | // const serverSockets = Object.values(connections);
59 | // const promises = serverSockets.map((serverSocket) => {
60 | // return new Promise((resolve) => serverSocket.on("disconnect", resolve));
61 | // });
62 | // await Promise.all(promises);
63 |
64 | await stopServer(ioServer);
65 | console.log("** end of test, Node.js should now exit")
66 | });
67 |
68 | function parallelCreateSockets(numSockets) {
69 | let createPromises = [];
70 | for (let i = 0; i < numSockets; i++) {
71 | createPromises.push(createSocket());
72 | }
73 | return Promise.all(createPromises);
74 | }
75 |
76 | function logAndTrackServerConnections(connections) {
77 | ioServer.on("connect", function(socket) {
78 | const key = socket.id;
79 | console.log("SERVER CONNECTED", key);
80 | connections[key] = socket;
81 | socket.on("disconnect", function(reason) {
82 | console.log(`SERVER DISCONNECTED; reason ${reason}; id ${socket.id}`);
83 | delete connections[key];
84 | });
85 | });
86 | }
87 |
88 | function logHttpConnections() {
89 | httpServer.on('connection', function(socket) {
90 | const id = socket.remoteAddress + ':' + socket.remotePort;
91 | console.log("HTTP CONNECT", id);
92 | socket.on("close", function() {
93 | console.log("HTTP DISCONNECT", id);
94 | });
95 | });
96 | }
97 |
98 | function logClientConnections(socket) {
99 | let id;
100 | socket.on("connect", function() {
101 | id = socket.id;
102 | console.log("CLIENT CONNECTED", id);
103 | });
104 | socket.on("disconnect", function() {
105 | console.log("CLIENT DISCONNECTED", id);
106 | });
107 | }
108 |
109 | function startServer(callback) {
110 | console.log("** starting server");
111 | httpServer = http.createServer();
112 | ioServer = socketIoServer(httpServer);
113 | httpServer.listen(PORT, callback);
114 | };
115 |
116 | async function stopServer(ioServer) {
117 | console.log("** stopping server");
118 |
119 | const close = util.promisify(ioServer.close.bind(ioServer));
120 | await close();
121 | console.log("SERVER STOPPED");
122 | }
123 |
124 | function createClientSocket() {
125 | const socket = socketIoClient("http://localhost:" + PORT);
126 | logClientConnections(socket);
127 | return new Promise(function(resolve) {
128 | socket.on("connect", function() {
129 | return resolve(socket);
130 | });
131 | });
132 | }
133 |
134 | function closeClientSocket(clientSocket) {
135 | const closePromise = new Promise(function(resolve) {
136 | clientSocket.on("disconnect", function() {
137 | return resolve();
138 | });
139 | });
140 | clientSocket.disconnect();
141 |
142 | return closePromise;
143 | }
144 |
145 | }());
--------------------------------------------------------------------------------
/spikes/socket_io_http_close_race/readme.md:
--------------------------------------------------------------------------------
1 | Code to reproduce client-side race condition in Socket.IO.
2 |
3 | The issue:
4 |
5 | 1. Open a Socket.IO connection to the server
6 | 2. Immediately after the client-side "connect" event has fired, close the connection
7 | 3. Immediately after the server-side "disconnect" event has fired, shutdown the server using httpServer.close()
8 | 4. Socket.IO opens several HTTP connections, but doesn't close them all, causing server to hang
9 |
10 | To try it:
11 |
12 | 1. Run `node spikes/socket_io_http_close_race/run.js`
13 |
14 | Reported to Socket.IO here: https://github.com/socketio/socket.io/issues/2975
--------------------------------------------------------------------------------
/spikes/socket_io_http_close_race/run.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | "use strict";
3 |
4 | var clientIo = require("socket.io-client");
5 | var http = require("http");
6 | var io = require("socket.io");
7 |
8 | var httpServer;
9 | var ioServer;
10 |
11 | var PORT = 5020;
12 |
13 | startServer(function() {
14 | console.log("SERVER STARTED");
15 |
16 | var connections = {};
17 | logAndTrackServerConnections(connections);
18 | logHttpConnections();
19 |
20 | var serverSocket;
21 | ioServer.once("connect", function(socket) {
22 | console.log("** stored server socket", socket.id);
23 | serverSocket = socket;
24 | });
25 |
26 | console.log("** connecting client");
27 | var client = clientIo("http://localhost:" + PORT);
28 | logClientConnections(client);
29 |
30 | client.once("connect", function() {
31 | console.log("** disconnecting client");
32 | client.once("disconnect", function() {
33 | console.log("** waiting for server to disconnect");
34 | serverSocket.once("disconnect", function() {
35 |
36 | // console.log("** waiting to stop server"); // uncommenting this timeout prevents hang
37 | // setTimeout(function() {
38 | stopServer(function() {
39 | console.log("** end of test, Node.js should now exit")
40 | });
41 | // }, 500);
42 | });
43 | });
44 | client.disconnect();
45 | });
46 | });
47 |
48 | function logAndTrackServerConnections(connections) {
49 | ioServer.on("connect", function(socket) {
50 | var key = socket.id;
51 | console.log("SERVER CONNECTED", key);
52 | connections[key] = socket;
53 | socket.on("disconnect", function() {
54 | console.log("SERVER DISCONNECTED", key);
55 | delete connections[key];
56 | });
57 | });
58 | }
59 |
60 | function logHttpConnections() {
61 | httpServer.on('connection', function(socket) {
62 | var id = socket.remoteAddress + ':' + socket.remotePort;
63 | console.log("HTTP CONNECT", id);
64 | socket.on("close", function() {
65 | console.log("HTTP DISCONNECT", id);
66 | });
67 | });
68 | }
69 |
70 | function logClientConnections(socket) {
71 | var id;
72 | socket.on("connect", function() {
73 | id = socket.id;
74 | console.log("CLIENT CONNECTED", id);
75 | });
76 | socket.on("disconnect", function() {
77 | console.log("CLIENT DISCONNECTED", id);
78 | });
79 | }
80 |
81 | function startServer(callback) {
82 | console.log("** starting server");
83 | httpServer = http.createServer();
84 | ioServer = io(httpServer);
85 | httpServer.listen(PORT, callback);
86 | };
87 |
88 | function stopServer(callback) {
89 | console.log("** stopping server");
90 |
91 | httpServer.close(function() { // using ioServer.close() instead of httpServer.close() prevents hang
92 | console.log("SERVER CLOSED");
93 | callback();
94 | });
95 | };
96 |
97 | }());
--------------------------------------------------------------------------------
/src/_release_test.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
2 | /*jshint regexp:false*/
3 |
4 | (function() {
5 | "use strict";
6 |
7 | var jake = require("jake");
8 | var child_process = require("child_process");
9 | var http = require("http");
10 | var fs = require("fs");
11 | var procfile = require("procfile");
12 | var assert = require("_assert");
13 |
14 | describe("Release", function() {
15 | /*eslint no-invalid-this:off */
16 | this.timeout(10 * 1000);
17 |
18 | it("is on web", function(done) {
19 | httpGet("http://weewikipaint.herokuapp.com", function(response, receivedData) {
20 | var foundHomePage = receivedData.indexOf("WeeWikiPaint home page") !== -1;
21 | assert.equal(foundHomePage, true, "home page should have contained test marker");
22 | done();
23 | });
24 | });
25 |
26 | });
27 |
28 | function httpGet(url, callback) {
29 | var request = http.get(url);
30 | request.on("response", function(response) {
31 | var receivedData = "";
32 | response.setEncoding("utf8");
33 |
34 | response.on("data", function(chunk) {
35 | receivedData += chunk;
36 | });
37 | response.on("end", function() {
38 | callback(response, receivedData);
39 | });
40 | });
41 | }
42 |
43 | }());
--------------------------------------------------------------------------------
/src/_run_server.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details.
2 | (function() {
3 | "use strict";
4 |
5 | var child_process = require("child_process");
6 | var fs = require("fs");
7 | var procfile = require("procfile");
8 |
9 | exports.runInteractively = function() {
10 | return run("inherit");
11 | };
12 |
13 | exports.runProgrammatically = function(callback) {
14 | var serverProcess = run(["pipe", "pipe", process.stderr]);
15 |
16 | serverProcess.stdout.setEncoding("utf8");
17 | serverProcess.stdout.on("data", function(chunk) {
18 | if (chunk.trim().indexOf("Server started") !== -1) callback(serverProcess);
19 | });
20 | };
21 |
22 | function run(stdioOptions) {
23 | var commandLine = parseProcFile();
24 | return child_process.spawn(commandLine.command, commandLine.options, {stdio: stdioOptions });
25 | }
26 |
27 | function parseProcFile() {
28 | var fileData = fs.readFileSync("Procfile", "utf8");
29 | var webCommand = procfile.parse(fileData).web;
30 | webCommand.options = webCommand.options.map(function(element) {
31 | if (element === "$PORT") return "5000";
32 | else return element;
33 | });
34 | return webCommand;
35 | }
36 |
37 | }());
--------------------------------------------------------------------------------
/src/client/content/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404: Not Found
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
404
67 |
68 |
There’s only one page on this site...
69 |
70 | Go Draw Something
71 |
72 |
--------------------------------------------------------------------------------
/src/client/content/_404_test.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
2 | (function() {
3 | "use strict";
4 |
5 | var assert = require("_assert");
6 | var cssHelper = require("./_css_test_helper.js");
7 | var quixote = require("./vendor/quixote-0.9.0.js");
8 |
9 | describe("CSS: 404 page", function() {
10 |
11 | var frame;
12 | var page;
13 | var viewport;
14 |
15 | var logo;
16 | var header;
17 | var tagline;
18 | var drawSomething;
19 |
20 | before(function(done) {
21 | /*eslint no-invalid-this:off */
22 | this.timeout(10 * 1000);
23 | var options = {
24 | src: "/base/src/client/content/404.html",
25 | width: cssHelper.IOS_BROWSER_WIDTH,
26 | height: cssHelper.IPAD_LANDSCAPE_HEIGHT_WITH_BROWSER_TABS
27 | };
28 | frame = quixote.createFrame(options, done);
29 | });
30 |
31 | after(function() {
32 | frame.remove();
33 | });
34 |
35 | beforeEach(function() {
36 | frame.reset();
37 |
38 | page = frame.page();
39 | viewport = frame.viewport();
40 |
41 | logo = frame.get("#logo");
42 | header = frame.get("#header");
43 | tagline = frame.get("#tagline");
44 | drawSomething = frame.get("#draw-something");
45 | });
46 |
47 | it("fits perfectly within viewport", function() {
48 | page.assert({
49 | width: viewport.width,
50 | height: viewport.height
51 | }, "page should not be larger than viewport");
52 | });
53 |
54 | it("has a nice margin when viewport is smaller than the page", function() {
55 | frame.resize(50, 50);
56 |
57 | drawSomething.assert({
58 | bottom: page.bottom.minus(13)
59 | }, "bottom element should have a nice margin before the bottom of the page");
60 | });
61 |
62 | it("has an overall layout", function() {
63 | logo.assert({
64 | top: logo.height.times(2),
65 | center: page.center,
66 | height: 30
67 | }, "logo should be centered at top of page");
68 | assert.equal(cssHelper.fontSize(logo), "30px", "logo font size");
69 | assert.equal(cssHelper.textAlign(logo), "center", "logo text should be centered");
70 | header.assert({
71 | top: logo.bottom,
72 | center: viewport.center,
73 | height: 200
74 | }, "404 header should be centered under logo");
75 | assert.equal(cssHelper.fontSize(header), "200px", "header font size");
76 | assert.equal(cssHelper.textAlign(header), "center", "header text should be centered");
77 | tagline.assert({
78 | top: header.bottom.plus(tagline.height),
79 | center: viewport.center,
80 | height: 18
81 | }, "tagline should be centered under 404 header");
82 | assert.equal(cssHelper.fontSize(tagline), "15px", "tagline font size");
83 | assert.equal(cssHelper.textAlign(tagline), "center", "tagline text should be centered");
84 | drawSomething.assert({
85 | top: tagline.bottom.plus(tagline.height),
86 | center: page.center,
87 | height: 35,
88 | width: 225
89 | }, "button should be centered below tagline");
90 | assert.equal(cssHelper.textAlign(drawSomething), "center", "button text should be centered");
91 | });
92 |
93 | it("has a color scheme", function() {
94 | assert.equal(cssHelper.backgroundColor(frame.body()), cssHelper.BACKGROUND_BLUE, "page background should be light blue");
95 | assert.equal(cssHelper.textColor(logo), cssHelper.WHITE, "logo text should be white");
96 | assert.equal(cssHelper.textColor(header), cssHelper.DARK_BLUE, "header should be dark blue");
97 | assert.equal(cssHelper.textColor(tagline), cssHelper.DARK_BLUE, "tagline should be dark blue");
98 | assert.equal(cssHelper.backgroundColor(drawSomething), cssHelper.MEDIUM_BLUE, "button background should be medium blue");
99 | assert.equal(cssHelper.textColor(drawSomething), cssHelper.WHITE, "button text should be white");
100 | });
101 |
102 | it("has a typographic scheme", function() {
103 | assert.equal(cssHelper.fontFamily(logo), cssHelper.STANDARD_FONT, "logo font");
104 | assert.equal(cssHelper.fontWeight(logo), cssHelper.HEADLINE_WEIGHT, "logo weight");
105 | assert.equal(cssHelper.fontFamily(header), cssHelper.STANDARD_FONT, "header font");
106 | assert.equal(cssHelper.fontWeight(header), cssHelper.HEADLINE_WEIGHT, "header weight");
107 | assert.equal(cssHelper.fontFamily(tagline), cssHelper.STANDARD_FONT, "tagline font");
108 | assert.equal(cssHelper.fontWeight(tagline), cssHelper.BODY_TEXT_WEIGHT, "tagline weight");
109 | assert.equal(cssHelper.fontFamily(drawSomething), cssHelper.STANDARD_FONT, "draw something button family");
110 | assert.equal(cssHelper.fontWeight(drawSomething), cssHelper.LINK_BUTTON_WEIGHT, "draw something button weight");
111 | });
112 |
113 |
114 | describe("button", function() {
115 |
116 | it("has common styling", function() {
117 | assertStandardButtonStyling(drawSomething, "draw something button");
118 | });
119 |
120 | it("has rounded corners", function() {
121 | assert.equal(cssHelper.roundedCorners(drawSomething), cssHelper.CORNER_ROUNDING, "draw something button");
122 | });
123 |
124 | it("has a drop shadow", function() {
125 | assert.equal(cssHelper.dropShadow(drawSomething), cssHelper.DARK_BLUE + cssHelper.BUTTON_DROP_SHADOW, "draw something button drop shadow");
126 | });
127 |
128 | it("darkens when user hovers over them", function() {
129 | cssHelper.assertHoverStyle(drawSomething, cssHelper.DARKENED_MEDIUM_BLUE, "draw something button");
130 | });
131 |
132 | it("appears to depress when user activates them", function() {
133 | cssHelper.assertActivateDepresses(drawSomething, tagline.bottom.plus(19), "draw something button");
134 | });
135 |
136 | });
137 | });
138 |
139 |
140 | function assertStandardButtonStyling(button, description) {
141 | assert.equal(cssHelper.textAlign(button), "center", description + "text horizontal centering");
142 | assert.equal(cssHelper.isTextVerticallyCentered(button), true, description + " text vertical centering");
143 | assert.equal(cssHelper.textIsUnderlined(button), false, description + " text underline");
144 | assert.equal(cssHelper.textIsUppercase(button), true, description + " text uppercase");
145 | assert.equal(cssHelper.hasBorder(button), false, description + " border");
146 | }
147 |
148 | }());
149 |
--------------------------------------------------------------------------------
/src/client/content/_button_css_test.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
2 | (function() {
3 | "use strict";
4 |
5 | var assert = require("_assert");
6 | var cssHelper = require("./_css_test_helper.js");
7 |
8 | describe("CSS: Button block", function() {
9 |
10 | cssHelper.setupUnitTests();
11 |
12 | var INHERITED_FONT = "inherit-this-font";
13 |
14 | var linkTag;
15 | var buttonTag;
16 |
17 | beforeEach(function() {
18 | cssHelper.frame.add(
19 | "