HW7: done task 1 and 2

This commit is contained in:
Claudio Maggioni 2019-11-11 16:58:06 +01:00
parent a73e47f83a
commit 243b1f5f6a
42 changed files with 39727 additions and 0 deletions

View File

@ -0,0 +1,51 @@
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const path = require('path');
const logger = require('morgan');
const bodyParser = require('body-parser');
const kleiDust = require('klei-dust');
const methodOverride = require('method-override');
const fs = require('fs');
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/SA3_hw6', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
require('./models/Favorites');
const app = express();
//configure app
app.use(logger('dev'));
app.set('views', __dirname + '/views');
app.engine('dust', kleiDust.dust);
app.set('view engine', 'dust');
app.set('view options', { layout: false });
app.use(methodOverride('_method'));
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());
// Needed to make fetch_tests passs
app.use(bodyParser.text({ type: '*/*' }));
app.use(express.static('public'));
// Initialize routers here
const routers = require('./routes/routers');
app.use('/', routers.root);
// app.use('/favorites', routers.favourites_db);
// app.use('/favorites', routers.favourites_db_promises);
app.use('/favorites', routers.favourites_db_asaw);
app.use('/bookmarked', routers.bookmarked);
app.use('/test/fetch', routers.fetch_tests);
module.exports = app;

9
hw7/Claudio_Maggioni/bin/www Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env node
var debug = require('debug')('canvas-server');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});

View File

@ -0,0 +1,11 @@
module.exports = {
//Server URL
url: "http://127.0.0.1:3000",
//Form structure
form: {
_id: "_id",
name: "name",
dataURL: "dataURL",
bookmarked: "bookmarked" // Optional
}
}

View File

@ -0,0 +1,20 @@
// vim: set ts=2 sw=2 et tw=80:
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
_id: {},
dataURL: { type: String, required: true },
name: { type: String, required: true, default: '' },
bookmarked: {
type: Boolean,
required: false,
default: false,
},
dateCreated: { type: Date, required: true, default: Date.now }
});
const Favorite = mongoose.model('Favorite', schema);

View File

@ -0,0 +1,36 @@
{
"name": "wa-exercise-7-2017-2018",
"version": "0.0.0",
"description": "Exercise 7 of Web Atelier. Express.js and Mongoose",
"main": "app.js",
"scripts": {
"start": "DEBUG='canvas-server' nodemon ./bin/www",
"test": "npm run test-mocha",
"test-mocha": "./node_modules/mocha/bin/mocha -R spec ./test/routes ./test/hypermedia"
},
"keywords": [
"Node.js",
"Express.js",
"MongoDB",
"Mongoose",
"REST"
],
"author": "Vincenzo Ferme, Andrea Gallidabino, Vasileios Triglianos, Ilya Yanok",
"license": "MIT",
"dependencies": {
"body-parser": "^1.19.0",
"debug": "^3.1.0",
"dustjs-linkedin": "^2.7.5",
"express": "^4.16.2",
"klei-dust": "^1.0.0",
"method-override": "^3.0.0",
"mongoose": "^5.7.7",
"morgan": "^1.9.0",
"request": "^2.88.0",
"supertest": "^3.0.0"
},
"devDependencies": {
"mocha": "^4.0.1",
"should": "^13.1.3"
}
}

View File

@ -0,0 +1,86 @@
/* Fetch */
// vim: set ts=2 sw=2 et tw=80:
const BODY_METHODS = ['PUT', 'POST', 'PATCH'];
const NON_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD', 'DELETE'];
const METHODS = BODY_METHODS + NON_BODY_METHODS;
/**
* @function doFetchRequest
* @param {String} method The method of the Fetch request. One of: 'GET',
* 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'
* @param {String} url The url of the API to call, optionally with parameters.
* @param {Object} headers The Associative Array containing the Request Headers.
* It must be undefined if there are no headers.
* @param {String} body The body String to be sent to the server. It must be
* undefined if there is no body.
* @returns {Promise} which receives the HTTP response.
*/
function doFetchRequest(method, url, headers, body) {
if (METHODS.indexOf(method) == -1) {
throw new Error(`${method} is not a method`);
return;
}
if (BODY_METHODS.indexOf(method) != -1 && typeof body != 'string') {
throw new Error(`body must be a string with ${BODY_METHODS}`);
return;
} else if (NON_BODY_METHODS.indexOf(method) != -1 && body !== undefined) {
throw new Error(`body must be undefined with ${NON_BODY_METHODS}`);
return;
}
return fetch(url, {
method: method,
headers: headers,
body: body
});
}
/** @function doJSONRequest
* @param {String} method The method of the Fetch request. One of: 'GET',
* 'POST', 'PUT', 'DELETE'.
* @param {String} url The url of the API to call, optionally with parameters.
* @param {Object} headers The Associative Array containing the Request Headers.
* It must be undefined if there are no headers.
* @param {Object} data The object to be sent as JSON body to the server.
* It must be undefined if there is no body.
* @returns {Promise} which receives directly the object parsed from the
* response JSON.
*/
function doJSONRequest(method, url, headers, data){
const JSON_MIME = 'application/json';
headers = Object.assign({}, headers);
if (('Content-Type' in headers && headers['Content-Type'] !== JSON_MIME) ||
('Accept' in headers && headers['Accept'] !== JSON_MIME)) {
throw new Error(`headers object contains Content-Type or Accept`);
return;
}
headers['Accept'] = JSON_MIME;
if (NON_BODY_METHODS.indexOf(method) != -1 && data !== undefined) {
throw new Error(`data must be undefined with ${NON_BODY_METHODS}`);
return;
} else if (BODY_METHODS.indexOf(method) != -1) {
if (typeof data != 'object' || data === null) {
throw new Error(`data must be a non-null object with ${BODY_METHODS}`);
return;
}
try {
data = JSON.stringify(data);
} catch (e) {
throw new Error('data cannot be converted into JSON: ' + e);
return;
}
headers['Content-Type'] = JSON_MIME;
}
return doFetchRequest(method, url, headers, data).then(res => res.json());
}

View File

@ -0,0 +1,15 @@
// vim: set ts=2 sw=2 tw=80 et:
// Enter your initialization code here
function init() {
// Create canvas app
const app = new App({
canvas: 'canvas',
buttons: {
clear: 'clear-btn',
camera: 'camera-btn',
undo: 'undo-btn'
},
brushToolbar: 'brush-toolbar'
});
}

View File

@ -0,0 +1,5 @@
let test = QUnit.test;
let equal = QUnit.assert.equal.bind(QUnit.assert);
let notEqual = QUnit.assert.notEqual.bind(QUnit.assert);
let deepEqual = QUnit.assert.deepEqual.bind(QUnit.assert);
let notDeepEqual = QUnit.assert.notDeepEqual.bind(QUnit.assert);

View File

@ -0,0 +1,436 @@
/*!
* QUnit 2.9.2
* https://qunitjs.com/
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2019-02-21T22:49Z
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header (excluding toolbar) */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699A4;
background-color: #0D3349;
font-size: 1.5em;
line-height: 1em;
font-weight: 400;
border-radius: 5px 5px 0 0;
}
#qunit-header a {
text-decoration: none;
color: #C2CCD1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #FFF;
}
#qunit-banner {
height: 5px;
}
#qunit-filteredTest {
padding: 0.5em 1em 0.5em 1em;
color: #366097;
background-color: #F4FF77;
}
#qunit-userAgent {
padding: 0.5em 1em 0.5em 1em;
color: #FFF;
background-color: #2B81AF;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
/** Toolbar */
#qunit-testrunner-toolbar {
padding: 0.5em 1em 0.5em 1em;
color: #5E740B;
background-color: #EEE;
}
#qunit-testrunner-toolbar .clearfix {
height: 0;
clear: both;
}
#qunit-testrunner-toolbar label {
display: inline-block;
}
#qunit-testrunner-toolbar input[type=checkbox],
#qunit-testrunner-toolbar input[type=radio] {
margin: 3px;
vertical-align: -2px;
}
#qunit-testrunner-toolbar input[type=text] {
box-sizing: border-box;
height: 1.6em;
}
.qunit-url-config,
.qunit-filter,
#qunit-modulefilter {
display: inline-block;
line-height: 2.1em;
}
.qunit-filter,
#qunit-modulefilter {
float: right;
position: relative;
margin-left: 1em;
}
.qunit-url-config label {
margin-right: 0.5em;
}
#qunit-modulefilter-search {
box-sizing: border-box;
width: 400px;
}
#qunit-modulefilter-search-container:after {
position: absolute;
right: 0.3em;
content: "\25bc";
color: black;
}
#qunit-modulefilter-dropdown {
/* align with #qunit-modulefilter-search */
box-sizing: border-box;
width: 400px;
position: absolute;
right: 0;
top: 50%;
margin-top: 0.8em;
border: 1px solid #D3D3D3;
border-top: none;
border-radius: 0 0 .25em .25em;
color: #000;
background-color: #F5F5F5;
z-index: 99;
}
#qunit-modulefilter-dropdown a {
color: inherit;
text-decoration: none;
}
#qunit-modulefilter-dropdown .clickable.checked {
font-weight: bold;
color: #000;
background-color: #D2E0E6;
}
#qunit-modulefilter-dropdown .clickable:hover {
color: #FFF;
background-color: #0D3349;
}
#qunit-modulefilter-actions {
display: block;
overflow: auto;
/* align with #qunit-modulefilter-dropdown-list */
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * {
box-sizing: border-box;
max-height: 2.8em;
display: block;
padding: 0.4em;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button {
float: right;
font: inherit;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child {
/* insert padding to align with checkbox margins */
padding-left: 3px;
}
#qunit-modulefilter-dropdown-list {
max-height: 200px;
overflow-y: auto;
margin: 0;
border-top: 2px groove threedhighlight;
padding: 0.4em 0 0;
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown-list li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#qunit-modulefilter-dropdown-list .clickable {
display: block;
padding-left: 0.15em;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 1em 0.4em 1em;
border-bottom: 1px solid #FFF;
list-style-position: inside;
}
#qunit-tests > li {
display: none;
}
#qunit-tests li.running,
#qunit-tests li.pass,
#qunit-tests li.fail,
#qunit-tests li.skipped,
#qunit-tests li.aborted {
display: list-item;
}
#qunit-tests.hidepass {
position: relative;
}
#qunit-tests.hidepass li.running,
#qunit-tests.hidepass li.pass:not(.todo) {
visibility: hidden;
position: absolute;
width: 0;
height: 0;
padding: 0;
border: 0;
margin: 0;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li.skipped strong {
cursor: default;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
text-decoration: none;
}
#qunit-tests li p a {
padding: 0.25em;
color: #6B6464;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #FFF;
border-radius: 5px;
}
.qunit-source {
margin: 0.6em 0 0.3em;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: 0.2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 0.5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
color: #374E0C;
background-color: #E0F2BE;
text-decoration: none;
}
#qunit-tests ins {
color: #500;
background-color: #FFCACA;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: #000; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #FFF;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3C510C;
background-color: #FFF;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #FFF;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
}
#qunit-tests .fail { color: #000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: #008000; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/*** Aborted tests */
#qunit-tests .aborted { color: #000; background-color: orange; }
/*** Skipped tests */
#qunit-tests .skipped {
background-color: #EBECE9;
}
#qunit-tests .qunit-todo-label,
#qunit-tests .qunit-skipped-label {
background-color: #F4FF77;
display: inline-block;
font-style: normal;
color: #366097;
line-height: 1.8em;
padding: 0 0.5em;
margin: -0.4em 0.4em -0.4em 0;
}
#qunit-tests .qunit-todo-label {
background-color: #EEE;
}
/** Result */
#qunit-testresult {
color: #2B81AF;
background-color: #D2E0E6;
border-bottom: 1px solid #FFF;
}
#qunit-testresult .clearfix {
height: 0;
clear: both;
}
#qunit-testresult .module-name {
font-weight: 700;
}
#qunit-testresult-display {
padding: 0.5em 1em 0.5em 1em;
width: 85%;
float:left;
}
#qunit-testresult-controls {
padding: 0.5em 1em 0.5em 1em;
width: 10%;
float:left;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,235 @@
/**
* QUnit v1.10.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699a4;
background-color: #0d3349;
font-size: 1.5em;
line-height: 1em;
font-weight: normal;
border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
-webkit-border-top-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
}
#qunit-header a {
text-decoration: none;
color: #c2ccd1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #fff;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 .5em 0 .1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #eee;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #c2ccd1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests ol {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 .5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #e0f2be;
color: #374e0c;
text-decoration: none;
}
#qunit-tests ins {
background-color: #ffcaca;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: black; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3c510c;
background-color: #fff;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
}
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: green; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-bottom: 1px solid white;
}
#qunit-testresult .module-name {
font-weight: bold;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

View File

@ -0,0 +1,259 @@
// vim: set ts=2 sw=2 et tw=80:
class App {
static get BRUSHES() {
return {
"PenBrush": new PenBrush(),
"DiscBrush": new DiscBrush(),
"StarBrush": new StarBrush(),
};
}
constructor(conf) {
if (!(typeof conf === 'object' && conf)) {
throw new Error('Argument conf different from specification');
}
this.canvas = document.getElementById(conf.canvas);
if (!this.canvas || this.canvas.tagName !== 'CANVAS') {
throw new Error(`canvas is not a canvas`);
}
this.ctx = this.canvas.getContext('2d');
this.favourites = document.querySelector('div#favourites');
if (typeof conf.buttons === 'object' && conf.buttons) {
this.buttons = {}
for (const b of ['clear', 'undo', 'camera'])
this.buttons[b] = document.getElementById(conf.buttons[b]);
if (this.buttons.clear) {
this.buttons.clear.addEventListener('click', () => {
this.erase();
this.background = null;
history.clear();
});
}
if (this.buttons.undo) {
this.buttons.undo.addEventListener('click', () => {
history.pop();
this.redrawAll();
});
}
if (this.buttons.camera) {
this.buttons.camera.addEventListener('click', () => {
const base64 = this.canvas.toDataURL();
const img = document.createElement('img');
img.src = base64;
const form = document.createElement('form');
form.method = 'POST';
form.action = '/favorites';
const image = document.createElement('input');
image.type = 'hidden';
image.name = 'dataURL';
image.value = base64;
form.appendChild(image);
const lblName = document.createElement('label');
lblName.setAttribute('for', 'name');
lblName.innerText = 'Name:';
form.appendChild(lblName);
const name = document.createElement('input');
name.type = 'text';
name.name = 'name';
name.placeholder = 'Name';
name.value = 'New Name';
form.appendChild(name);
const submit = document.createElement('button');
submit.innerText = 'Save';
form.appendChild(submit);
this.favourites.appendChild(img);
this.favourites.appendChild(form);
});
}
const player = document.createElement('video');
player.addEventListener('loadeddata', () => {
player.play();
setTimeout(() => {
const imgCanvas = document.createElement('canvas');
imgCanvas.width = this.canvas.width;
imgCanvas.height = this.canvas.height;
const imgCtx = imgCanvas.getContext('2d');
imgCtx.drawImage(player, 0, 0, canvas.width, canvas.height);
const imgData = imgCtx.getImageData(0, 0, imgCanvas.width,
imgCanvas.height);
for (let i = 0; i < data.length; i += 4) {
const pixel = (data[i] + 2 * data[i+1] + data[i+2]) / 4;
data[i] = data[i+1] = data[i+2] = pixel;
}
imgCtx.putImageData(imgData, 0, 0);
const img = document.createElement('img');
img.src = imgCanvas.toDataURL();
img.addEventListener('load', () => {
this.background = img;
this.redrawAll();
});
player.srcObject.getVideoTracks().forEach(track => track.stop());
}, 100);
});
const button = document.createElement('button');
button.type = 'button';
button.innerHTML = 'Photo';
button.addEventListener('click', () => {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
player.srcObject = stream;
});
});
document.getElementById('left-toolbar').appendChild(button);
}
this.ctx.lineWidth = 1;
this.strokeStyle = this.constructor.defaultStrokeStyle;
this.brush = "PenBrush";
const brushToolbar = document.querySelector('#brush-toolbar');
if (brushToolbar) {
for (const name in App.BRUSHES) {
const b = document.createElement('button');
b.innerText = name;
b.addEventListener('click', () => this.brush = name);
brushToolbar.appendChild(b);
}
const label = document.createElement('label');
label.setAttribute('for', 'color');
const color = document.createElement('input');
color.type = 'color';
color.name = 'color';
color.value = this.constructor.defaultStrokeStyle;
color.addEventListener('change', () => this.strokeStyle = color.value);
brushToolbar.appendChild(label);
brushToolbar.appendChild(color);
}
const toMouse = (e, func) => {
if (e && e.touches && e.touches[0]) {
return func.bind(this)({
offsetX: e.touches[0].pageX - this.canvas.offsetLeft,
offsetY: e.touches[0].pageY - this.canvas.offsetTop
});
}
};
this.canvas.addEventListener('touchstart', e => toMouse(e, this.startPath));
this.canvas.addEventListener('mousedown', this.startPath.bind(this));
this.canvas.addEventListener('touchmove', e => toMouse(e, this.draw));
this.canvas.addEventListener('mousemove', this.draw.bind(this));
this.canvas.addEventListener('touchcancel', e => toMouse(e, this.endPath));
this.canvas.addEventListener('mouseup', this.endPath.bind(this));
this.canvas.addEventListener('mouseout', this.endPath.bind(this));
}
static get defaultStrokeStyle() {
return 'black';
}
get strokeStyle() {
if (this.ctx.strokeStyle == '#000000') {
return 'black';
}
return this.ctx.strokeStyle;
}
set strokeStyle(style) {
if (typeof style !== 'string') {
throw new Error('style is not a string');
}
this.ctx.strokeStyle = style;
}
get brush() {
return this._brush.name;
}
set brush(brushName) {
this._brush = App.BRUSHES[brushName];
}
erase() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
startPath(e, record = true) {
this.ctx.beginPath();
this.ctx.moveTo(e.offsetX, e.offsetY);
if (record) {
history.initializeNewPath();
history.push(new Stroke(this.brush, this.strokeStyle,
e.offsetX, e.offsetY));
}
if (e instanceof MouseEvent) {
this.mousedown = true;
}
}
draw(e, beginNew = true, record = true) {
if (this.mousedown || !(e instanceof MouseEvent)) {
this._brush.draw(this.ctx, this.strokeStyle, e.offsetX, e.offsetY);
if (record) {
history.push(new Stroke(this.brush, this.strokeStyle,
e.offsetX, e.offsetY));
}
if (beginNew) {
this.ctx.beginPath();
this.ctx.moveTo(e.offsetX, e.offsetY);
} else if (e instanceof MouseEvent) {
this.mousedown = false;
}
}
}
endPath(e, record = true) {
this.draw(e, false, record);
}
drawPath(path) {
const last = path.length - 1;
const lastBrush = this.brush;
const lastStyle = this.strokeStyle;
for (let i = 0; i <= last; i++) {
this.brush = path[i].brushName;
this.strokeStyle = path[i].strokeStyle;
switch(i) {
case 0: this.startPath(path[i], false); break;
case last: this.endPath(path[i], false); break;
default: this.draw(path[i], true, false);
}
}
this.brush = lastBrush;
this.strokeStyle = lastStyle;
}
redrawAll() {
this.erase();
if (this.background) {
this.ctx.drawImage(this.background, 0, 0, this.canvas.width,
this.canvas.height);
}
for (const path of history.paths) {
this.drawPath(path);
}
}
}

View File

@ -0,0 +1,53 @@
// vim: set ts=2 sw=2 et tw=80:
class PenBrush {
constructor() {
this.opacity = 1;
this.name = "PenBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = strokeStyle;
ctx.lineTo(x, y);
ctx.stroke();
}
}
class DiscBrush extends PenBrush {
static get RADIUS() { return 10; }
constructor() {
super();
this.name = "DiscBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.beginPath(); // clear previous path starting
ctx.ellipse(x, y, 10, 10, 0, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
}
}
class StarBrush extends PenBrush {
constructor() {
super();
this.name = "StarBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.moveTo(x - 8, y - 3);
ctx.lineTo(x - 3, y - 3);
ctx.lineTo(x, y - 8);
ctx.lineTo(x + 3, y - 3);
ctx.lineTo(x + 8, y - 3);
ctx.lineTo(x + 4, y + 1);
ctx.lineTo(x + 4, y + 6);
ctx.lineTo(x, y + 3);
ctx.lineTo(x - 4, y + 6);
ctx.lineTo(x - 4, y + 1);
ctx.lineTo(x - 8, y - 3);
ctx.stroke();
}
}

View File

@ -0,0 +1,37 @@
// vim: set ts=2 sw=2 et tw=80:
const history = {
paths: []
}
history.pop = () => {
if (history.paths.length == 0) return;
return history.paths.pop();
};
history.initializeNewPath = () => {
history.paths.push([]);
};
history.push = (stroke) => {
if (!stroke || !stroke instanceof Stroke) {
throw new Error(JSON.stringify(stroke) + ' is not a Stroke instance');
}
history.paths[history.paths.length - 1].push(stroke);
return history.paths[history.paths.length - 1];
}
history.clear = () => {
history.paths = [];
};
class Stroke {
constructor(brushName, strokeStyle, x, y) {
this.brushName = brushName;
this.strokeStyle = strokeStyle;
this.offsetX = x;
this.offsetY = y;
}
}

View File

@ -0,0 +1,133 @@
/* vim: set ts=2 sw=2 et tw=80: */
body {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
#app {
display: flex;
}
#canvas {
border: 1px solid #bbb;
}
#left-toolbar, #brush-toolbar {
width: 6rem;
margin: 0 .5rem;
}
.toolbar button, .toolbar input {
width: 100%;
padding: .5rem 0;
margin-bottom: 10px;
min-height: 3rem;
cursor: pointer;
}
#brush-toolbar {
margin: 0 .5rem;
}
#palette {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
width: 53px;
height: 204px;
background-color: buttonface;
cursor: pointer;
}
.toolbar #camera-btn {
font-size: 1.5em;
line-height: 1.5em;
color: #444;
background: #eee;
border: none;
border-radius: 3px;
height: 40px;
outline: none;
}
.toolbar #camera-btn:hover {
background: #70a0e8;
}
.toolbar #camera-btn:active {
background: #0e57c3;
}
.p-color {
width: 25px;
height: 25px;
}
.black {background-color:rgb(0, 0, 0);}
.dark-gray {background-color:rgb(87, 87, 87);}
.red {background-color:rgb(173, 35, 35);}
.blue {background-color:rgb(42, 75, 215);}
.green {background-color:rgb(29, 105, 20);}
.brown {background-color:rgb(129, 74, 25);}
.purple {background-color:rgb(129, 38, 192);}
.light-gray {background-color:rgb(160, 160, 160);}
.light-green {background-color:rgb(129, 197, 122);}
.light-blue {background-color:rgb(157, 175, 255);}
.cyan {background-color:rgb(41, 208, 208);}
.orange {background-color:rgb(255, 146, 51);}
.yellow {background-color:rgb(255, 238, 51);}
.tan {background-color:rgb(233, 222, 187);}
.pink {background-color:rgb(255, 205, 243);}
.white {background-color:rgb(255, 255, 255);}
#clock {
background-color: #ddd;
width: 360px;
height: 48px;
text-align: center;
position: relative;
font-size: 2.5rem;
margin-top: -10px;
font-weight: bold;
margin-bottom: 20px;
}
#clock-time,
#progress-bar {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#clock-time {
z-index: 10;
line-height: 48px; /* same height as #clock so that the time is vertically centered */
}
#progress-bar {
width: 0%;
height: 48px;
background-color: #8DFF80;
text-align: center;
line-height: 30px;
color: white;
}
#favourites {
margin: 5px;
float: left;
width: 200px;
}
#favourites img {
width: 90%;
height: 100px;
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.42);
margin-left: 10px;
}
#favourites div.desc {
padding: 15px;
text-align: center;
}

View File

@ -0,0 +1,304 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0">
<title>Assignment 7 - Tests for Tasks 1 and 2</title>
<link rel="stylesheet" href="qunit/qunit-2.9.2.css">
</head>
<body>
<script src="js/fetch.js"></script>
<script src="qunit/qunit-2.9.2.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script>
//some test payloads
const headers = {};
const baseURL = "/test/fetch";
const jsonURL = baseURL + "/json/ok";
const body = "test body as a string"
const jsonBody_req = {
json: body,
rnd: Math.random()
};
const jsonBody_resp = (text) => {
return {
text, body: jsonBody_req
}
}
var notValidJSON = () => {};
/*
* Make sure the fetch_test/router.js is installed under /test/fetch and the server is running for these tests to work
*
*/
function testOk(f,method,url,headers,body) {
let msg = f.name+"('"+method+"','"+url+"','"+JSON.stringify(headers)+","+JSON.stringify(body)+") should accept the input";
return async function(assert) {
try {
await f(method,url,headers,body);
result = true;
} catch (error) {
console.log(msg, error);
msg += "\n but \n" + error;
result = false;
}
assert.ok(result, msg);
}
}
function testFail(f,method,url,headers,body) {
return async function(assert) {
try{
await f(method,url,headers,body);
result = false;
}
catch(error) {
result = true;
}
assert.ok(result, f.name+"("+JSON.stringify(method)+","+JSON.stringify(url)+","+JSON.stringify(headers)+","+JSON.stringify(body)+") should reject the input");
}
}
function testStatusBodyHeaders(f,method,url,headers,body,status,text,resp_headers) {
return async function(assert) {
return f(method,url,headers,body)
.then((res)=>{
assert.equal(res.status, status, method+" "+url+" should return "+status+" status code");
Object.keys(resp_headers).forEach((k)=>{
assert.ok(res.headers.has(k), method+" "+url+" response includes header "+k);
assert.equal(res.headers.get(k), resp_headers[k], method+" "+url+" response header "+k+" has expected value "+JSON.stringify(resp_headers[k]));
});
return res.text();
}).then((resp)=>{
assert.equal(resp, text, method+" "+url+" returns expected response body "+JSON.stringify(text));
});
}
}
function testStatusBody(f,method,url,headers,body,status,text) {
return async function(assert) {
return f(method,url,headers,body)
.then((res)=>{
assert.equal(res.status, status, method+" "+url+" should return "+status+" status code");
return res.text();
}).then((resp)=>{
assert.equal(resp, text, method+" "+url+" returns expected response body "+JSON.stringify(text));
});
}
}
function testStatusJSONBody(f,method,url,headers,body,status,json) {
return async function(assert) {
return f(method,url,headers,body)
.then((res)=>{
//assert.equal(res.status, status, method+" "+url+" should return "+status+" status code");
assert.deepEqual(res, json, method+" "+url+" returns expected response body "+JSON.stringify(json));
});
}
}
function testStatus(f,method,url,headers,body,status,text) {
return async function(assert) {
return f(method,url,headers,body)
.then((res)=>{
assert.equal(res.status, status, method+" "+url+" should return "+status+" status code");
});
}
}
function testRedirect(f,method,url,headers,body,text) {
return async function(assert) {
return f(method,url,headers,body)
.then((res)=>{
assert.ok(res.redirected, method+" "+url+" should have been redirected");
return res.text();
}).then((resp)=>{
assert.equal(resp, text, method+" "+url+" has expected response body after redirection "+JSON.stringify(text));
});
}
}
function testAll(a){
return function(assert) {
let b = [];
a.forEach((f)=>{b.push(f(assert))});
return Promise.all(b);
}
}
QUnit.module("Task 1: doFetchRequest Request Validation", {});
QUnit.test("reject incorrect methods", testAll(
[
testFail(doFetchRequest,"WRONG", baseURL,headers),
testFail(doFetchRequest,"RANDOM", baseURL,headers),
testFail(doFetchRequest,"",baseURL,headers),
testFail(doFetchRequest,undefined,baseURL,headers),
testFail(doFetchRequest,404,baseURL,headers)
]));
QUnit.test("GET, HEAD, OPTIONS, DELETE should not have a body", testAll(
[
testFail(doFetchRequest,"GET", baseURL,headers,body),
testFail(doFetchRequest,"HEAD", baseURL,headers,body),
testFail(doFetchRequest,"OPTIONS",baseURL,headers,body),
testFail(doFetchRequest,"DELETE",baseURL,headers,body),
testOk(doFetchRequest,"GET", baseURL,headers),
testOk(doFetchRequest,"HEAD", baseURL,headers),
testOk(doFetchRequest,"OPTIONS",baseURL,headers),
testOk(doFetchRequest,"DELETE",baseURL,headers)
]));
QUnit.test("POST, PUT, PATCH require the body", testAll(
[
testFail(doFetchRequest,"POST", baseURL,headers),
testFail(doFetchRequest,"PUT", baseURL,headers),
testFail(doFetchRequest,"PATCH",baseURL,headers),
testOk(doFetchRequest,"POST", baseURL,headers,body),
testOk(doFetchRequest,"PUT", baseURL,headers,body),
testOk(doFetchRequest,"PATCH",baseURL,headers,body)
]));
QUnit.test("POST, PUT, PATCH require a String body", testAll(
[
testFail(doFetchRequest,"POST", baseURL,headers,{}),
testFail(doFetchRequest,"PUT", baseURL,headers,{}),
testFail(doFetchRequest,"PATCH",baseURL,headers,{}),
testFail(doFetchRequest,"POST", baseURL,headers,[]),
testFail(doFetchRequest,"PUT", baseURL,headers,[]),
testFail(doFetchRequest,"PATCH",baseURL,headers,[]),
testFail(doFetchRequest,"POST", baseURL,headers,999),
testFail(doFetchRequest,"PUT", baseURL,headers,999),
testFail(doFetchRequest,"PATCH",baseURL,headers,999)
]));
QUnit.module("Task 1: doFetchRequest Response Validation", {});
QUnit.test("GET/DELETE return expected status and body", testAll(
[
testStatusBody(doFetchRequest,"GET",baseURL,{"Accept": 'text/html'},undefined,200,"GET Working"),
testStatus(doFetchRequest,"GET",baseURL+"/not/found",{"Accept": 'text/html'},undefined,404),
testStatusBody(doFetchRequest,"GET",baseURL,{"Accept": 'application/json'},undefined,200,"{\"text\":\"GET Working\"}"),
testStatusBody(doFetchRequest,"DELETE",baseURL,{"Accept": 'text/html'},undefined,204,"")
]));
QUnit.test("GET follows redirects", testAll(
[
testRedirect(doFetchRequest,"GET",baseURL+"/redirect",{"Accept": 'text/html'},undefined,"Redirect Works")
]));
QUnit.test("POST returns expected status, body and headers", testAll(
[
testStatusBodyHeaders(doFetchRequest,"POST",baseURL+"/new",{"Accept": 'text/html'},"X",201,"POST Working\nX",{'Location':'42'}),
testStatusBodyHeaders(doFetchRequest,"POST",baseURL+"/new",{"Accept": 'application/json'},"JSON",201,"{\"text\":\"POST Working\",\"body\":\"JSON\"}",{'Location':'24', 'Content-Type': 'application/json; charset=utf-8'})
]));
QUnit.test("PUT returns expected status, body and headers", testAll(
[
testStatusBody(doFetchRequest,"PUT",baseURL+"/echo",{"Accept": 'text/html'},"X",200,"X"),
testStatusBodyHeaders(doFetchRequest,"PUT",baseURL+"/echo",{"Accept": 'application/json'},"JSON",200,"{\"text\":\"PUT Working\",\"body\":\"JSON\"}",{'Content-Type': 'application/json; charset=utf-8'})
]));
QUnit.module("Task 2: doJSONRequest Request Validation", {});
QUnit.test("reject incorrect methods", testAll(
[
testFail(doJSONRequest,"WRONG", jsonURL,headers),
testFail(doJSONRequest,"RANDOM", jsonURL,headers),
testFail(doJSONRequest,"", jsonURL,headers),
testFail(doJSONRequest,undefined, jsonURL,headers),
testFail(doJSONRequest,404, jsonURL,headers)
]));
QUnit.test("GET, HEAD, OPTIONS, DELETE should not have a body", testAll(
[
testFail(doJSONRequest,"GET", jsonURL,headers,body),
testFail(doJSONRequest,"HEAD", jsonURL,headers,body),
testFail(doJSONRequest,"OPTIONS",jsonURL,headers,body),
testFail(doJSONRequest,"DELETE", jsonURL,headers,body),
testOk(doJSONRequest,"GET", jsonURL,headers),
//HEAD will also have an empty response body
//testOk(doJSONRequest,"HEAD", jsonURL,headers),
testOk(doJSONRequest,"OPTIONS",jsonURL,headers),
testOk(doJSONRequest,"DELETE", jsonURL,headers)
]));
QUnit.test("POST, PUT, PATCH should not have a String body", testAll(
[
testFail(doJSONRequest,"POST", jsonURL,headers,body),
testFail(doJSONRequest,"PUT", jsonURL,headers,body),
testFail(doJSONRequest,"PATCH",jsonURL,headers,body),
testFail(doJSONRequest,"POST", jsonURL,headers,999),
testFail(doJSONRequest,"PUT", jsonURL,headers,999),
testFail(doJSONRequest,"PATCH",jsonURL,headers,999),
testFail(doJSONRequest,"POST", jsonURL,headers,notValidJSON),
testFail(doJSONRequest,"PUT", jsonURL,headers,notValidJSON),
testFail(doJSONRequest,"PATCH",jsonURL,headers,notValidJSON)
]));
QUnit.test("POST, PUT, PATCH require an Object/Array body", testAll(
[
testOk(doJSONRequest,"POST", jsonURL,headers,{}),
testOk(doJSONRequest,"PUT", jsonURL,headers,{}),
testOk(doJSONRequest,"PATCH",jsonURL,headers,{}),
testOk(doJSONRequest,"POST", jsonURL,headers,[]),
testOk(doJSONRequest,"PUT", jsonURL,headers,[]),
testOk(doJSONRequest,"PATCH",jsonURL,headers,[])
]));
QUnit.test("ignore correct accept/content-type headers", testAll(
[
testOk(doJSONRequest,"POST", jsonURL,{'Accept': 'application/json'},{}),
testOk(doJSONRequest,"PUT", jsonURL,{'Accept': 'application/json'},{}),
testOk(doJSONRequest,"PATCH",jsonURL,{'Accept': 'application/json'},{}),
testOk(doJSONRequest,"POST", jsonURL,{'Accept': 'application/json', 'Content-Type': 'application/json'},{}),
testOk(doJSONRequest,"PUT", jsonURL,{'Accept': 'application/json', 'Content-Type': 'application/json'},{}),
testOk(doJSONRequest,"PATCH", jsonURL,{'Accept': 'application/json', 'Content-Type': 'application/json'},{}),
testOk(doJSONRequest,"PUT", jsonURL,{'Accept': 'application/json'},{}),
testOk(doJSONRequest,"PATCH",jsonURL,{'Accept': 'application/json'},{}),
testOk(doJSONRequest,"GET", jsonURL,{'Accept': 'application/json'}),
testOk(doJSONRequest,"OPTIONS", jsonURL,{'Accept': 'application/json'}),
testOk(doJSONRequest,"DELETE",jsonURL,{'Accept': 'application/json'})
]));
QUnit.test("reject incorrect accept/content-type headers", testAll(
[
testFail(doJSONRequest,"POST", jsonURL,{'Accept': 'text/plain'},{}),
testFail(doJSONRequest,"PUT", jsonURL,{'Accept': 'text/plain'},{}),
testFail(doJSONRequest,"PATCH",jsonURL,{'Accept': 'text/plain'},{}),
testFail(doJSONRequest,"POST", jsonURL,{'Accept': 'text/plain', 'Content-Type': 'text/plain'},{}),
testFail(doJSONRequest,"PUT", jsonURL,{'Accept': 'text/plain', 'Content-Type': 'text/plain'},{}),
testFail(doJSONRequest,"PATCH", jsonURL,{'Accept': 'text/plain', 'Content-Type': 'text/plain'},{}),
testFail(doJSONRequest,"PUT", jsonURL,{'Accept': 'text/plain'},{}),
testFail(doJSONRequest,"PATCH",jsonURL,{'Accept': 'text/plain'},{}),
testFail(doJSONRequest,"GET", jsonURL,{'Accept': 'text/plain'}),
testFail(doJSONRequest,"OPTIONS", jsonURL,{'Accept': 'text/plain'}),
testFail(doJSONRequest,"DELETE",jsonURL,{'Accept': 'text/plain'})
]));
QUnit.module("Task 2: doJSONRequest Response Validation", {});
QUnit.test("returns expected JSON body", testAll(
[
testStatusJSONBody(doJSONRequest,"PUT",baseURL+"/echo",headers,jsonBody_req,200,jsonBody_resp("PUT Working")),
testStatusJSONBody(doJSONRequest,"POST",baseURL+"/new",headers,jsonBody_req,201,jsonBody_resp("POST Working")),
testStatusJSONBody(doJSONRequest,"DELETE",baseURL,headers,undefined,204,{status: 204, text: 'DELETE Working'}),
testStatusJSONBody(doJSONRequest,"GET",jsonURL,headers,undefined,200,{text: 'ok'}),
testStatusJSONBody(doJSONRequest,"PATCH",jsonURL,headers,{},200,{text: 'ok'}),
testStatusJSONBody(doJSONRequest,"GET",baseURL,headers,undefined,200,{text: 'GET Working'})
]));
</script>
</body>
</html>

View File

@ -0,0 +1,103 @@
function tests() {
test("App class constructors and methods", function(assert) {
// The App class must be defined
equal(typeof App === 'function', true, "The App class must be defined");
equal(/^\s*class\s+/.test(App.toString()), true, "App is a function but it is not defined using the class keyword")
// The App class constructor should throw an error if its argument is undefined
assert.throws(function() {
new App(undefined)
}, "The App class constructor should throw an error if its argument is undefined")
// The App class constructor should throw an error if its argument is not a canvas
assert.throws(function() {
new App("");
}, "The App class constructor should throw an error if its argument is not an object")
assert.throws(function() {
new App(1);
}, "The App class constructor should throw an error if its argument is a number")
assert.throws(function() {
new App([]);
}, "The App class constructor should throw an error if its argument is an array")
assert.throws(function() {
new App(true);
}, "The App class constructor should throw an error if its argument is a boolean")
// The default Stroke Style should be accessible in a static way, and should be equal to "black"
equal(App.defaultStrokeStyle === 'black', true, 'The default Stroke Style should be accessible in a static way, and should be equal to "black"')
assert.throws(function() {
new App({});
}, "The App class constructor should throw an error if its argument options object is not pointing to a canvas element under the 'canvas' property")
const app = new App({canvas: 'test-canvas'})
equal(app.strokeStyle, "black", "Getter for strokeStyle is not defined")
// The draw method must be defined
equal(typeof app.draw === 'function', true, "The draw method must be defined")
});
test("History and Stroke object literals fields and methods", function(assert) {
// The Stroke class must be defined
equal(typeof Stroke === 'function', true, "The Stroke class must be defined");
equal(/^\s*class\s+/.test(Stroke.toString()), true, "Stroke is a function but it is not defined using the class keyword")
equal(function(){
try {
const stroke = new Stroke('square')
} catch (err) {
return false
}
return true
}(), true, "Stroke can be instantiated")
const stroke = new Stroke('square');
const stroke1 = new Stroke('circle');
const stroke2 = new Stroke('triangle');
history.initializeNewPath()
assert.throws(function() {
history.push()
}, "Must pass a Stroke instance when you push in the history")
equal(function() {
try {
history.push(stroke);
} catch (err) {
return false;
}
return true;
}(), true, "History push accepts Stroke instances as a parameter");
equal(history.pop()[0] === stroke, true, "Pop returns an array containing the pushed Stroke instance")
history.initializeNewPath();
history.push(stroke);
history.push(stroke1);
history.push(stroke2);
equal(history.pop().length, 3, "Pop returns an array containing the pushed Stroke instances")
history.initializeNewPath();
equal(history.pop().length, 0, "Pop on an empty history should return an empty array")
history.initializeNewPath(); //simulate mouse down
history.push(stroke); //simulate mouse move
history.push(stroke1); //simulate mouse move
history.initializeNewPath(); //simulate mouse up and down again
history.push(stroke2); //simulate mouse move
equal(history.pop().length, 1, "Pop returns an array containing the most recent path (Expected path with 1 Stroke)")
equal(history.pop().length, 2, "Pop returns an array containing the most recent path (Expected path with 2 Strokes)")
});
}

View File

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({ bookmarked: true }, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
});
/** router for /root */
module.exports = router;

View File

@ -0,0 +1,162 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav, parseId, notFound } = require('../utils');
function findAndRender(filter, req, res) {
Favorite.find(filter, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
}
router.post('/', (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked
}
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
favourite.save((err, fav) => {
if (err) {
return error(err, res);
}
res.status = 201;
renderFav(req, res, fav, false);
});
});
router.get('/', (req, res) => {
findAndRender({}, req, res);
});
router.get('/search', (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
findAndRender(filter, req, res);
});
function findOne(id) {
return (req, res) => {
Favorite.findById(id(req), (err, fav) => {
if (err) {
return error(err, res);
}
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
});
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
passRawResult: true,
}, (err, fav) => {
if (err) {
return error(err, res);
}
if (fav == null) {
res.status = 201;
findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
});
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => {
Favorite.findByIdAndDelete(parseId(req), (err, fav) => {
if (err) {
return error(err, res);
}
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
});
});
router.put('/:id/bookmarked', (req, res) => {
Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true }, (err, fav) => {
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
});
});
/** router for /root */
module.exports = router;

View File

@ -0,0 +1,164 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, catchErrs, renderFav, parseId, notFound } = require('../utils');
async function findAndRender(filter, req, res) {
try {
const favs = await Favorite.find(filter);
renderFav(req, res, favs);
} catch(e) {
error(e, res);
}
}
router.post('/', async (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked,
};
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
try {
const fav = await favourite.save();
res.status = 201;
renderFav(req, res, fav, false);
} catch(e) {
error(e, res);
}
});
router.get('/', async (req, res) => {
await findAndRender({}, req, res);
});
router.get('/search', async (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
await findAndRender(filter, req, res);
});
function findOne(id) {
return async (req, res) => {
try {
const fav = await Favorite.findById(id(req));
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
} catch(e) {
error(e, res);
}
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return async (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
try {
const fav = await Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
});
if (fav == null) {
res.status = 201;
await findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
} catch (e) {
error(e, res);
}
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', async (req, res) => {
try {
const fav = await Favorite.findByIdAndDelete(parseId(req));
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
} catch (e) {
error(e, res);
}
});
router.put('/:id/bookmarked', (req, res) => {
try {
const fav = Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true });
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
} catch (e) {
error(e, res);
}
});
/** router for /root */
module.exports = router;

View File

@ -0,0 +1,142 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, catchErrs, renderFav, parseId, notFound } = require('../utils');
function findAndRender(filter, req, res) {
catchErrs(Favorite.find(filter), res).then(favs => {
renderFav(req, res, favs);
});
}
router.post('/', (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked,
};
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
catchErrs(favourite.save(), res).then(fav => {
res.status = 201;
renderFav(req, res, fav, false);
});
});
router.get('/', (req, res) => {
findAndRender({}, req, res);
});
router.get('/search', (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
findAndRender(filter, req, res);
});
function findOne(id) {
return (req, res) => {
catchErrs(Favorite.findById(id(req)), res).then(fav => {
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
});
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
catchErrs(Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
}), res).then(fav => {
if (fav == null) {
res.status = 201;
findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
});
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => {
catchErrs(Favorite.findByIdAndDelete(parseId(req)), res).then(fav => {
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
});
});
router.put('/:id/bookmarked', (req, res) => {
catchErrs(Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true }), res).then(fav => {
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
});
});
/** router for /root */
module.exports = router;

View File

@ -0,0 +1,72 @@
'use strict';
const express = require('express');
const router = express.Router();
//These routes should be activated under /test/fetch so that they can be called by the test.html
//when testing your fetch wrappers doFetchRequest, doJSONRequest found in the public/js/fetch.js
//Add the following to your app.js:
//app.use('/test/fetch', routers.fetch_tests);
router.get('/', function(req, res, next) {
if(req.accepts('json')) {
res.json({
text: 'GET Working'
})
} else {
res.status(200).end('GET Working')
}
});
router.all('/json/ok', function(req, res, next) {
res.json({
text: 'ok'
});
});
router.delete('/', function(req, res, next) {
if(req.accepts('json')) {
res.json({
status: 204,
text: 'DELETE Working'
})
} else {
res.status(204).end()
}
});
router.get('/redirect', function(req, res, next) {
res.redirect('redirected')
});
router.get('/redirected', function(req, res, next) {
res.status(200).end('Redirect Works')
});
router.post('/new', function(req, res, next) {
if(req.accepts('json')) {
res.type('json');
res.set('Location', '24');
res.status(201).json({
text: 'POST Working',
body: req.body
})
} else {
res.set('Location', '42');
res.status(201).end('POST Working\n'+req.body)
}
});
router.put('/echo', function(req, res, next) {
if(req.accepts('json')) {
res.json({
text: 'PUT Working',
body: req.body
})
} else {
res.end(req.body)
}
});
module.exports = router;

View File

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({}, (err, favs) => {
if (err) {
return error(err, res);
}
res.render('index.dust', { favs });
});
});
/** router for /root */
module.exports = router;

View File

@ -0,0 +1,35 @@
/** @module routes/routers
* Exposes all routers
*/
'use strict';
const fs = require('fs');
const dirEntries = fs.readdirSync(__dirname);
const base = __dirname + '/';
const routers = {};
try{
dirEntries.forEach(function(dirEntry){
const stats = fs.statSync(base + dirEntry);
//try to load router of dir
if(stats.isDirectory()){
try{
const router = require(base + dirEntry + '/router');
//add router to our list of routers;
routers[dirEntry] = router;
}catch(err){
console.log('Could not get router for ' + dirEntry);
console.log(err.toString() + err.stack);
}
}
});
}catch(err){
console.log('Error while loading routers');
console.log(err.stack);
//We don't know what happened, export empty object
routers = {}
}finally{
module.exports = routers;
}

View File

@ -0,0 +1,65 @@
// vim: set ts=2 sw=2 et tw=80:
const mongoose = require('mongoose');
function catchErrs(promise, res) {
return promise.catch(err => error(err, res));
}
function error(err, res) {
console.error(err);
res.status = err instanceof mongoose.CastError ||
err instanceof mongoose.TypeError ? 400 : 500;
res.format({
json: () => res.json({ error: err }),
html: () => res.render('500.dust', { err: JSON.stringify(err, null, 2) }),
});
res.end();
}
function renderFav(req, res, favs, list = true) {
const makeTestsPass = e => {
return {
_id: e._id,
name: e.name,
dataURL: e.dataURL,
bookmarked: '' + e.bookmarked
}
};
if (req.accepts('html')) {
res.render(list ? 'favourites.dust' : 'favourite.dust',
list ? { favs } : favs);
} else if (req.accepts('json')) {
if (list) {
favs = favs.map(makeTestsPass);
} else {
favs = makeTestsPass(favs);
}
res.json(favs);
} else {
res.writeHead(406);
res.end();
}
}
function parseId(req) {
if (typeof req.params.id === 'string' && req.params.id.length == 24) {
return mongoose.Types.ObjectId(req.params.id);
} else {
return req.params.id;
}
}
function notFound(e, res) {
if (e == null) {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('Not found');
return true;
} else return false;
}
module.exports = { error, renderFav, catchErrs, parseId, notFound };

View File

@ -0,0 +1,9 @@
/**
* Standalond db seed
*/
var seed = require('./test/seed').seed;
seed(function(seedData){
console.log("Seeding complete!")
process.exit();
})

View File

@ -0,0 +1,56 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
describe('Task 2: Testing Create /favorites routes', function(){
describe('POST /favorites', function(){
it('should create a new favorite if the request data is valid', function(done){
var newFavData = {}
newFavData[config.form._id] = "tt1",
newFavData[config.form.name] = "NicePicture",
newFavData[config.form.dataURL] = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
newFavData[config.form.bookmarked] = "true"
request(config.url)
.post('/favorites')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(newFavData)
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
.expect(201)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should.equal(resFav[config.form._id], newFavData[config.form._id]);
should.equal(resFav[config.form.dataURL], newFavData[config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], newFavData[config.form.bookmarked]);
should.equal(resFav[config.form.name], newFavData[config.form.name]);
done();
});
});
it('should get a 400 Bad Request if data is invalid #1', function(done){
var newFavData = {
"invalid": "this object does not have the correct structure",
};
request(config.url)
.post('/favorites')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(newFavData)
.expect(400, done);
});
it('should get a 400 Bad Request if data is not in json', function(done){
request(config.url)
.post('/favorites')
.set('Content-Type', 'text/plain')
.set('Accept', 'application/json')
.send("This is a plain text request, it should result in a 400 bad request")
.expect(400, done);
});
});
});

View File

@ -0,0 +1,127 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favsOriginal = require('../seedData');
var favs = []
for (let i = 0; i < favsOriginal.length; i++) {
let o = {}
o[config.form._id] = favsOriginal[i]._id
o[config.form.name] = favsOriginal[i].name
o[config.form.dataURL] = favsOriginal[i].dataURL
o[config.form.bookmarked] = favsOriginal[i].bookmarked
favs.push(o)
}
describe('Task 3: Testing Read for /favorites routes', function(){
before(seed)
describe('GET /favorites', function(){
it('should list all the favs with correct data', function(done){
request(config.url)
.get('/favorites')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
console.log(resFavs);
resFavs.forEach(function(fav) {
for (let i = 0; i < favs.length; i++) {
if(favs[i][config.form._id] == fav[config.form._id]) {
should.equal(fav[config.form.dataURL], favs[i][config.form.dataURL]);
should.equal(fav[config.form.bookmarked], favs[i][config.form.bookmarked]);
should.equal(fav[config.form.name], favs[i][config.form.name]);
}
}
})
done();
});
});
});
describe('GET /favorites/:favoriteid', function(){
it('should get the favorite with correct data', function(done){
request(config.url)
.get('/favorites/' + favs[1][config.form._id])
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should.equal(resFav[config.form._id], favs[1][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[1][config.form.name]);
done();
});
});
it('should respond with a 404 if the favorite does not exist', function(done){
request(config.url)
.get('/favorites/notValidId')
.set('Accept', 'application/json')
.expect(404, done);
});
});
describe(`GET /favorites/search`, function(){
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}`, function(done){
request(config.url)
.get(`/favorites/search?${config.form._id}=${favs[1][config.form._id]}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavArray = JSON.parse(res.text);
should.equal(resFavArray.length, 1)
var resFav = resFavArray[0]
should.equal(resFav[config.form._id], favs[1][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[1][config.form.name]);
done();
});
});
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}&${config.form.name}`, function(done){
request(config.url)
.get(`/favorites/search?${config.form._id}=${favs[5][config.form._id]}&${config.form.name}=${favs[5][config.form.name]}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text)[0];
should.equal(resFav[config.form._id], favs[5][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[5][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[5][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[5][config.form.name]);
done();
});
});
it(`should get empty array if there is no match: GET /favorites/search?${config.form.name}`, function(done){
request(config.url)
.get(`/favorites/search?&${config.form.name}=NoName`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavArray = JSON.parse(res.text);
should.equal(resFavArray.length, 0)
done();
});
});
});
});
function seed(done){
//seed the db
seedDb.seed(function(seedData){
done();
}, favs);
}

View File

@ -0,0 +1,44 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData');
describe('Task 4: Testing Update on /favorites routes', function(){
describe('PUT /favorites/:favoriteid', function(){
it('should change the name of an existing favorite', function(done){
let reqBody = {}
reqBody[config.form.name] = 'newName'
reqBody[config.form.dataURL] = favs[3][config.form.dataURL] // maggicl: added to comply with assignment
request(config.url)
.put('/favorites/' + favs[3]._id)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(reqBody)
.expect(201)
.end(function(err, res){
let resPutFav = JSON.parse(res.text)
should(resPutFav[config.form.name], 'newName')
done()
});
});
})
describe('GET /favorites/:favoriteid', function(){
it('the name should be changed', function(done){
request(config.url)
.get('/favorites/' + favs[3]._id)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
.expect(200)
.end(function(err, res){
var favGetFav = JSON.parse(res.text);
should.equal(favGetFav[config.form.name], 'newName');
done();
});
});
});
});

View File

@ -0,0 +1,60 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData')
describe('Task 5: Testing Delete for /favorites routes', function(){
describe('DELETE /favorites/:favoriteid', function(){
it('should delete an existing favorite', function(done){
request(config.url)
.del('/favorites/' + favs[1]._id)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(204)
.end(function(err, res){
res.text.should.be.empty;
res.body.should.be.empty;
done();
});
});
it('should not list the previously deleted resource', function(done){
request(config.url)
.del('/favorites/' + favs[2]._id)
.expect(200)
.end(function(err, res){
request(config.url)
.get('/favorites')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
resFavs.forEach(function(fav){
should.notEqual(fav[config.form._id], favs[2]._id);
});
done();
});
});
});
it('should respond with a 404 for a previously deleted resource', function(done){
request(config.url)
.delete('/favorites/' + favs[1]._id)
.set('Accept', 'application/json')
.expect(404, done);
});
it('should respond with a 404 deleting a resource which does not exist', function(done){
request(config.url)
.delete('/favorites/invalidId')
.set('Accept', 'application/json')
.expect(404, done);
});
});
});

View File

@ -0,0 +1,91 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData')
describe('Task 6: Testing /bookmarked routes and /favorites/:favoriteid/bookmarked', function(){
describe('GET /bookmarked', function(){
it('should list only the bookmarked favs', function(done){
request(config.url)
.get('/bookmarked')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
resFavs.forEach(function(fav){
should.equal(fav[config.form.bookmarked], "true");
});
done();
});
});
});
describe('PUT /favorites/:favoriteid/bookmarked', function(){
after(drop)
it('initial bookmarked value should be false', function(done){
request(config.url)
.get(`/favorites/${favs[0]._id}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "false")
done();
});
});
it('should change the bookmarked value', function(done){
const reqBody = {}
reqBody[config.form.bookmarked] = "true"
request(config.url)
.put(`/favorites/${favs[0]._id}/bookmarked`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(reqBody)
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "true")
done();
});
});
it('bookmarked value should be changed', function(done){
request(config.url)
.get(`/favorites/${favs[0]._id}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "true")
done();
});
});
});
});
function drop(done){
let deleteIds = ['t0','t3','t4','t5', 'tt1']
let count = 0
for (let i = 0; i < deleteIds.length; i++) {
request(config.url)
.delete(`/favorites/${deleteIds[i]}`)
.end(function() {
count++
if(count == deleteIds.length) {
done()
}
})
}
}

View File

@ -0,0 +1,46 @@
'use strict';
var config = require('../config');
var request = require('request');
//seedData
var seedData = require('./seedData')
//total callbacks (one for each model)
var totalCbs = 0;
var cbCnt = 0;
/**
* Recursive function that goes through
* seedData populating each item of it
*/
var seedModel = function(done, s){
if(s != undefined) {
seedData = s
}
totalCbs = seedData.length
for (let i = 0; i < seedData.length; i++) {
const form = {}
form[config.form._id] = seedData[i]._id
form[config.form.name] = seedData[i].name
form[config.form.dataURL] = seedData[i].dataURL
form[config.form.bookmarked] = seedData[i].bookmarked
request.post(`${config.url}/favorites`, {
form: form
}, function(error, response, body){
cbCnt++
if(cbCnt == totalCbs) {
done(seedData)
}
})
}
}
/**
* This is where everything starts
*/
module.exports.seed = function (done, s){
seedModel(done, s)
}

View File

@ -0,0 +1,47 @@
'use strict';
var favorites = [
{
"_id" : "t0",
"name" : "My Nice Image",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t1",
"name" : "My Nice Image 1",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
},
{
"_id" : "t2",
"name" : "My Nice Image 2",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t3",
"name" : "My Nice Image 3",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
},
{
"_id" : "t4",
"name" : "My Nice Image 4",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t5",
"name" : "MyNiceImage5",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
}
]
module.exports = favorites;

View File

@ -0,0 +1,14 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
<h1>Error 500</h1>
<pre>
{err}
</pre>
</body>
</html>

View File

@ -0,0 +1,13 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset="utf-8">
<title>{name}</title>
</head>
<body>
{>"favourite_partial" /}
</body>
</html>

View File

@ -0,0 +1,28 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<h3>{name}</h3>
<img src="{dataURL}" alt="{name}">
{?b}
<p>
<strong>Bookmarked</strong>
</p>
{/b}
{?details}
<a href="/favorites/{_id}">Details</a>
{:else}
<form method="POST" action="/favorites/{_id}?_method=PUT">
<input type="hidden" name="dataURL" value="{dataURL}">
<label for="name">Name:</label>
<input type="text" name="name" placeholder="Name" value="{name}"><br>
<button>Update</button><br>
<button formaction="/favorites/{_id}?_method=DELETE">Delete</button><br>
{?bookmarked}
<button name="bookmarked" value="false"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Remove bookmark</button>
{:else}
<button name="bookmarked" value="true"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Add bookmark</button>
{/bookmarked}
</form>
<a href="/favorites">Favourites list</a>
{/details}

View File

@ -0,0 +1,28 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset="utf-8">
{?bookmarked}
<title>Bookmarked</title>
{:else}
<title>Favourites</title>
{/bookmarked}
</head>
<body>
{?bookmarked}
<h1>Bookmarked</h1>
{:else}
<h1>Favourites</h1>
{/bookmarked}
{#favs}
<div>
{>"favourite_partial" name=name dataURL=dataURL _id=_id bookmarked=bookmarked details="true" /}
</div>
{:else}
<strong>No favourites.</strong>
{/favs}
</body>
</html>

View File

@ -0,0 +1,41 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OO-JS Exercise - Web Atelier 2017</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body onload="init()">
<h1>
OO-JS Exercise: Canvas
</h1>
<div id="app">
<div id="left-toolbar" class="toolbar">
<button id="clear-btn">Clear</button>
<button id="undo-btn">Undo</button>
<button id="camera-btn"><i class="fa fa-camera" aria-hidden="true"></i></button>
</div>
<canvas id="canvas" width="600" height="400"></canvas>
<div id="brush-toolbar" class="toolbar">
<!-- Brushes buttons go here (programmatically). Each button should be a <button> element -->
</div>
</div>
<h2>Favourites</h2>
<div id="favourites">
{#favs}
{>"favourite_partial" name=name dataURL=dataURL _id=_id bookmarked=bookmarked details="true" /}
{/favs}
</div>
<script src="scripts/brushes.js"></script>
<script src="scripts/undo.js"></script>
<!-- <script src="scripts/clock.js"></script> -->
<script src="scripts/app.js"></script>
<script src="main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff