├── .gitignore ├── .gitpod.yml ├── README.md ├── package.json ├── public ├── style.css └── client.js ├── views └── index.html ├── tests ├── 2_functional-tests.js └── 1_unit-tests.js ├── test-runner.js ├── server.js └── assertion-analyser.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-node-lts 2 | 3 | ports: 4 | - port: 3000 5 | onOpen: open-preview 6 | visibility: public 7 | 8 | tasks: 9 | - init: npm install 10 | command: npm run start 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quality Assurance with Chai 2 | 3 | This is the boilerplate for the Quality Assurance with Chai lessons. Instructions for completing these lessons start at https://www.freecodecamp.org/learn/quality-assurance/quality-assurance-and-testing-with-chai/ 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automated-testing-app", 3 | "version": "0.0.1", 4 | "description": "Boilerplate for the Quality Assurance and Testing with Chai challenges", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "1.19.0", 11 | "chai": "4.3.4", 12 | "chai-http": "4.3.0", 13 | "cors": "2.8.5", 14 | "express": "4.17.1", 15 | "mocha": "9.0.3", 16 | "zombie": "6.1.4" 17 | }, 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #222; 3 | background-color: #eee; 4 | font-family: sans-serif; 5 | text-align: center; 6 | } 7 | #tn { 8 | margin-top:30px; 9 | } 10 | h1 { 11 | margin-bottom: 40px; 12 | } 13 | #tn span { 14 | font-weight: bold; 15 | } 16 | .pane { 17 | border: solid 1px #999; 18 | border-radius: 10px; 19 | max-width: 650px; 20 | margin: 10px auto; 21 | padding: 10px; 22 | box-sizing: border-box; 23 | } 24 | .specs { 25 | font-family: monospace; 26 | font-size: 1.1em; 27 | margin: 10px auto; 28 | text-align: left; 29 | padding-left: 10px; 30 | color: #555; 31 | } -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quality Assurance with Chai | freeCodeCamp.org 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

15 | Automated Testing 16 |

17 |
18 |
19 |

Functional Tests Example

20 |
21 |

Famous Italian Explorers

22 |
23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |

31 | Input form HTML:
32 | ----------------
33 | <input type="text" name="surname">
34 | <button type="submit">submit</button>
35 |
36 | Results div HTML (after Ajax):
37 | ------------------------------
38 | <p>first name:<span id="name">[name]</span></p>
39 | <p>last name:<span id="surname">[surname]</span></p>
40 | <p>dates:<span id="dates">[birth - death]</span></p>
41 |

42 |
43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/2_functional-tests.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const assert = chai.assert; 3 | 4 | const server = require('../server'); 5 | 6 | const chaiHttp = require('chai-http'); 7 | chai.use(chaiHttp); 8 | 9 | suite('Functional Tests', function () { 10 | this.timeout(5000); 11 | suite('Integration tests with chai-http', function () { 12 | // #1 13 | test('Test GET /hello with no name', function (done) { 14 | chai 15 | .request(server) 16 | .keepOpen() 17 | .get('/hello') 18 | .end(function (err, res) { 19 | assert.fail(res.status, 200); 20 | assert.fail(res.text, 'hello Guest'); 21 | done(); 22 | }); 23 | }); 24 | // #2 25 | test('Test GET /hello with your name', function (done) { 26 | chai 27 | .request(server) 28 | .keepOpen() 29 | .get('/hello?name=xy_z') 30 | .end(function (err, res) { 31 | assert.fail(res.status, 200); 32 | assert.fail(res.text, 'hello xy_z'); 33 | done(); 34 | }); 35 | }); 36 | // #3 37 | test('Send {surname: "Colombo"}', function (done) { 38 | chai 39 | .request(server) 40 | .keepOpen() 41 | .put('/travellers') 42 | 43 | .end(function (err, res) { 44 | assert.fail(); 45 | 46 | done(); 47 | }); 48 | }); 49 | // #4 50 | test('Send {surname: "da Verrazzano"}', function (done) { 51 | assert.fail(); 52 | 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | const Browser = require('zombie'); 59 | 60 | suite('Functional Tests with Zombie.js', function () { 61 | this.timeout(5000); 62 | 63 | 64 | 65 | suite('Headless browser', function () { 66 | test('should have a working "site" property', function() { 67 | assert.isNotNull(browser.site); 68 | }); 69 | }); 70 | 71 | suite('"Famous Italian Explorers" form', function () { 72 | // #5 73 | test('Submit the surname "Colombo" in the HTML form', function (done) { 74 | assert.fail(); 75 | 76 | done(); 77 | }); 78 | // #6 79 | test('Submit the surname "Vespucci" in the HTML form', function (done) { 80 | assert.fail(); 81 | 82 | done(); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test-runner.js: -------------------------------------------------------------------------------- 1 | const analyser = require('./assertion-analyser'); 2 | const EventEmitter = require('events').EventEmitter; 3 | 4 | const Mocha = require('mocha'), 5 | fs = require('fs'), 6 | path = require('path'); 7 | 8 | const mocha = new Mocha(); 9 | const testDir = './tests' 10 | 11 | 12 | // Add each .js file to the mocha instance 13 | fs.readdirSync(testDir).filter(function (file) { 14 | // Only keep the .js files 15 | return file.substr(-3) === '.js'; 16 | 17 | }).forEach(function (file) { 18 | mocha.addFile( 19 | path.join(testDir, file) 20 | ); 21 | }); 22 | 23 | const emitter = new EventEmitter(); 24 | emitter.run = function () { 25 | 26 | const tests = []; 27 | let context = ""; 28 | const separator = ' -> '; 29 | // Run the tests. 30 | try { 31 | const runner = mocha.ui('tdd').run() 32 | .on('test end', function (test) { 33 | // remove comments 34 | let body = test.body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 35 | // collapse spaces 36 | body = body.replace(/\s+/g, ' '); 37 | const obj = { 38 | title: test.title, 39 | context: context.slice(0, -separator.length), 40 | state: test.state, 41 | // body: body, 42 | assertions: analyser(body) 43 | }; 44 | tests.push(obj); 45 | }) 46 | .on('end', function () { 47 | emitter.report = tests; 48 | emitter.emit('done', tests) 49 | }) 50 | .on('suite', function (s) { 51 | context += (s.title + separator); 52 | 53 | }) 54 | .on('suite end', function (s) { 55 | context = context.slice(0, -(s.title.length + separator.length)) 56 | }) 57 | } catch (e) { 58 | throw (e); 59 | } 60 | }; 61 | 62 | module.exports = emitter; 63 | 64 | /* 65 | * Mocha.runner Events: 66 | * can be used to build a better custom report 67 | * 68 | * - `start` execution started 69 | * - `end` execution complete 70 | * - `suite` (suite) test suite execution started 71 | * - `suite end` (suite) all tests (and sub-suites) have finished 72 | * - `test` (test) test execution started 73 | * - `test end` (test) test completed 74 | * - `hook` (hook) hook execution started 75 | * - `hook end` (hook) hook complete 76 | * - `pass` (test) test passed 77 | * - `fail` (test, err) test failed 78 | * - `pending` (test) test pending 79 | */ -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const express = require('express'); 3 | const app = express(); 4 | 5 | const cors = require('cors'); 6 | const runner = require('./test-runner'); 7 | 8 | const bodyParser = require('body-parser'); 9 | app.use(bodyParser.json()); 10 | 11 | app.get('/', function (req, res) { 12 | res.sendFile(__dirname + '/views/index.html'); 13 | }) 14 | 15 | app.use(express.static(__dirname + '/public')); 16 | 17 | app.get('/hello', function (req, res) { 18 | const name = req.query.name || 'Guest'; 19 | res.type('txt').send('hello ' + name); 20 | }) 21 | 22 | const travellers = function (req, res) { 23 | let data = {}; 24 | if (req.body && req.body.surname) { 25 | switch (req.body.surname.toLowerCase()) { 26 | case 'polo': 27 | data = { 28 | name: 'Marco', 29 | surname: 'Polo', 30 | dates: '1254 - 1324' 31 | }; 32 | break; 33 | case 'colombo': 34 | data = { 35 | name: 'Cristoforo', 36 | surname: 'Colombo', 37 | dates: '1451 - 1506' 38 | }; 39 | break; 40 | case 'vespucci': 41 | data = { 42 | name: 'Amerigo', 43 | surname: 'Vespucci', 44 | dates: '1454 - 1512' 45 | }; 46 | break; 47 | case 'da verrazzano': 48 | case 'verrazzano': 49 | data = { 50 | name: 'Giovanni', 51 | surname: 'da Verrazzano', 52 | dates: '1485 - 1528' 53 | }; 54 | break; 55 | default: 56 | data = { 57 | name: 'unknown' 58 | } 59 | } 60 | } 61 | res.json(data); 62 | }; 63 | 64 | 65 | app.route('/travellers') 66 | .put(travellers); 67 | 68 | let error; 69 | app.get('/_api/get-tests', cors(), function (req, res, next) { 70 | if (error) 71 | return res.json({ status: 'unavailable' }); 72 | next(); 73 | }, 74 | function (req, res, next) { 75 | if (!runner.report) return next(); 76 | res.json(testFilter(runner.report, req.query.type, req.query.n)); 77 | }, 78 | function (req, res) { 79 | runner.on('done', function (report) { 80 | process.nextTick(() => res.json(testFilter(runner.report, req.query.type, req.query.n))); 81 | }); 82 | }); 83 | 84 | 85 | const port = process.env.PORT || 3000; 86 | app.listen(port, function () { 87 | console.log("Listening on port " + port); 88 | console.log('Running Tests...'); 89 | setTimeout(function () { 90 | try { 91 | runner.run(); 92 | } catch (e) { 93 | error = e; 94 | console.log('Tests are not valid:'); 95 | console.log(error); 96 | } 97 | }, 1500); 98 | }); 99 | 100 | 101 | module.exports = app; // for testing 102 | 103 | function testFilter(tests, type, n) { 104 | let out; 105 | switch (type) { 106 | case 'unit': 107 | out = tests.filter(t => t.context.match('Unit Tests')); 108 | break; 109 | case 'functional': 110 | out = tests.filter(t => t.context.match('Functional Tests')); 111 | break; 112 | default: 113 | out = tests; 114 | } 115 | if (n !== undefined) { 116 | return out[n] || out; 117 | } 118 | return out; 119 | } 120 | -------------------------------------------------------------------------------- /public/client.js: -------------------------------------------------------------------------------- 1 | function Utils() { 2 | this.ready = function (fn) { 3 | if (typeof fn !== 'function') { 4 | return; 5 | } 6 | 7 | if (document.readyState === 'complete') { 8 | return fn(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', fn, false); 12 | }; 13 | 14 | this.ajax = function (options, cb) { 15 | const xmlhttp = new XMLHttpRequest(); 16 | 17 | xmlhttp.onreadystatechange = function () { 18 | if (xmlhttp.readyState === 4){ 19 | if(Math.floor(xmlhttp.status/100) === 2) { 20 | var results = xmlhttp.responseText; 21 | var type = xmlhttp.getResponseHeader('Content-Type'); 22 | if(type.match('application/json')) { 23 | results = JSON.parse(results); 24 | } 25 | cb(null, results); 26 | } else { 27 | cb(xmlhttp); 28 | } 29 | } 30 | }; 31 | 32 | const method = options.method || 'get'; 33 | let url = options.url || '/'; 34 | 35 | if(url.charAt(url.length - 1) === '/') 36 | url = url.slice(0,url.length-1); 37 | 38 | if (options.data) { 39 | let query; 40 | let contentType = "application/x-www-form-urlencoded"; 41 | if(options.type && options.type === 'json') { 42 | query = JSON.stringify(options.data); 43 | contentType = "application/json"; 44 | } else { 45 | query = []; 46 | for (let key in params) { 47 | query.push(key + '=' + encodeURIComponent(params[key])); 48 | query.push('&'); 49 | } 50 | query.pop(); 51 | query = query.join(''); 52 | } 53 | 54 | switch(method.toLowerCase()){ 55 | case 'get': 56 | url += ('?' + query); 57 | xmlhttp.open(method, url, true); 58 | xmlhttp.send(); 59 | break; 60 | case 'put': 61 | case 'patch': 62 | case 'delete': 63 | case 'post': 64 | xmlhttp.open(method, url, true); 65 | xmlhttp.setRequestHeader("Content-type", contentType); 66 | xmlhttp.send(query); 67 | break; 68 | default: 69 | return; 70 | } 71 | } else { 72 | xmlhttp.open(method, url, true); 73 | xmlhttp.send(); 74 | } 75 | 76 | }; 77 | } 78 | 79 | const utils = new Utils(); 80 | 81 | utils.ready(function() { 82 | 83 | const form = document.getElementById('f1'); 84 | const input = document.getElementById('i1'); 85 | const div = document.getElementById('tn'); 86 | form.addEventListener('submit', function(e){ 87 | e.preventDefault(); 88 | if(input.value) { 89 | const options = { 90 | method: 'put', 91 | url: '/travellers', 92 | type: 'json', 93 | data: {surname: input.value} 94 | }; 95 | div.innerHTML = '

Loading...

'; 96 | utils.ajax(options, function(err, res) { 97 | if(err) return console.log(err); 98 | div.innerHTML = '

first name: ' + res.name + '

' + 99 | '

last name: ' + res.surname + '

' + 100 | '

dates: ' + res.dates + '

'; 101 | }); 102 | } 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /assertion-analyser.js: -------------------------------------------------------------------------------- 1 | function objParser(str, init) { 2 | // finds objects, arrays, strings, and function arguments 3 | // between parens, because they may contain ',' 4 | const openSym = ["[", "{", '"', "'", "("]; 5 | const closeSym = ["]", "}", '"', "'", ")"]; 6 | let type; 7 | for (var i = init || 0; i < str.length; i++) { 8 | type = openSym.indexOf(str[i]); 9 | if (type !== -1) break; 10 | } 11 | if (type === -1) return null; 12 | const open = openSym[type]; 13 | const close = closeSym[type]; 14 | let count = 1; 15 | for (var k = i + 1; k < str.length; k++) { 16 | if (open === '"' || open === "'") { 17 | if (str[k] === close) count--; 18 | if (str[k] === "\\") k++; 19 | } else { 20 | if (str[k] === open) count++; 21 | if (str[k] === close) count--; 22 | } 23 | if (count === 0) break; 24 | } 25 | if (count !== 0) return null; 26 | const obj = str.slice(i, k + 1); 27 | return { 28 | start: i, 29 | end: k, 30 | obj: obj, 31 | }; 32 | } 33 | 34 | function replacer(str) { 35 | // replace objects with a symbol ( __#n) 36 | let obj; 37 | let cnt = 0; 38 | let data = []; 39 | // obj = objParser(str) is not a syntax error 40 | while ((obj = objParser(str))) { 41 | data[cnt] = obj.obj; 42 | str = 43 | str.substring(0, obj.start) + "__#" + cnt++ + str.substring(obj.end + 1); 44 | } 45 | return { 46 | str: str, 47 | dictionary: data, 48 | }; 49 | } 50 | 51 | function splitter(str) { 52 | // split on commas, then restore the objects 53 | const strObj = replacer(str); 54 | let args = strObj.str.split(","); 55 | args = args.map(function (a) { 56 | let m = a.match(/__#(\d+)/); 57 | while (m) { 58 | a = a.replace(/__#(\d+)/, strObj.dictionary[m[1]]); 59 | m = a.match(/__#(\d+)/); 60 | } 61 | return a.trim(); 62 | }); 63 | return args; 64 | } 65 | 66 | function assertionAnalyser(body) { 67 | // already filtered in the test runner 68 | // // remove comments 69 | // body = body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 70 | // // get test function body 71 | // body = body.match(/\{\s*([\s\S]*)\}\s*$/)[1]; 72 | 73 | if (!body) return "invalid assertion"; 74 | // replace assertions bodies, so that they cannot 75 | // contain the word 'assertion' 76 | 77 | const cleanedBody = body.match( 78 | /(?:browser\s*\.\s*)?assert\s*\.\s*\w*\([\s\S]*\)/ 79 | ); 80 | if (cleanedBody && Array.isArray(cleanedBody)) { 81 | body = cleanedBody[0]; 82 | } else { 83 | // No assertions present 84 | return []; 85 | } 86 | const s = replacer(body); 87 | // split on 'assertion' 88 | const splittedAssertions = s.str.split("assert"); 89 | let assertions = splittedAssertions.slice(1); 90 | // match the METHODS 91 | 92 | let assertionBodies = []; 93 | const methods = assertions.map(function (a, i) { 94 | const m = a.match(/^\s*\.\s*(\w+)__#(\d+)/); 95 | assertionBodies.push(parseInt(m[2])); 96 | const pre = splittedAssertions[i].match(/browser\s*\.\s*/) 97 | ? "browser." 98 | : ""; 99 | return pre + m[1]; 100 | }); 101 | if ( 102 | methods.some(function (m) { 103 | return !m; 104 | }) 105 | ) 106 | return "invalid assertion"; 107 | // remove parens from the assertions bodies 108 | const bodies = assertionBodies.map(function (b) { 109 | return s.dictionary[b].slice(1, -1).trim(); 110 | }); 111 | assertions = methods.map(function (m, i) { 112 | return { 113 | method: m, 114 | args: splitter(bodies[i]), //replace objects, split on ',' ,then restore objects 115 | }; 116 | }); 117 | return assertions; 118 | } 119 | 120 | module.exports = assertionAnalyser; 121 | -------------------------------------------------------------------------------- /tests/1_unit-tests.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const assert = chai.assert; 3 | 4 | suite('Unit Tests', function () { 5 | suite('Basic Assertions', function () { 6 | // #1 7 | test('#isNull, #isNotNull', function () { 8 | assert.fail(null, 'This is an optional error description - e.g. null is null'); 9 | assert.fail(1, '1 is not null'); 10 | }); 11 | // #2 12 | test('#isDefined, #isUndefined', function () { 13 | assert.fail(null, 'null is not undefined'); 14 | assert.fail(undefined, 'undefined IS undefined'); 15 | assert.fail('hello', 'A string is not undefined'); 16 | }); 17 | // #3 18 | test('#isOk, #isNotOk', function () { 19 | assert.fail(null, 'null is falsey'); 20 | assert.fail("I'm truthy", 'A string is truthy'); 21 | assert.fail(true, 'true is truthy'); 22 | }); 23 | // #4 24 | test('#isTrue, #isNotTrue', function () { 25 | assert.fail(true, 'true is true'); 26 | assert.fail(!!'double negation', 'Double negation of a truthy value is true'); 27 | assert.fail({ value: 'truthy' }, 'Objects are truthy, but are not boolean values'); 28 | }); 29 | }); 30 | 31 | // ----------------------------------------------------------------------------- 32 | 33 | suite('Equality', function () { 34 | // #5 35 | test('#equal, #notEqual', function () { 36 | assert.fail(12, '12', 'Numbers are coerced into strings with =='); 37 | assert.fail({ value: 1 }, { value: 1 }, '== compares object references'); 38 | assert.fail(6 * '2', '12'); 39 | assert.fail(6 + '2', '12'); 40 | }); 41 | // #6 42 | test('#strictEqual, #notStrictEqual', function () { 43 | assert.fail(6, '6'); 44 | assert.fail(6, 3 * 2); 45 | assert.fail(6 * '2', 12); 46 | assert.fail([1, 'a', {}], [1, 'a', {}]); 47 | }); 48 | // #7 49 | test('#deepEqual, #notDeepEqual', function () { 50 | assert.fail({ a: '1', b: 5 }, { b: 5, a: '1' }, "The order of keys doesn't matter"); 51 | assert.fail({ a: [5, 6] }, { a: [6, 5] }, 'The order of array elements does matter'); 52 | }); 53 | }); 54 | 55 | // ----------------------------------------------------------------------------- 56 | 57 | function weirdNumbers(delta) { 58 | return 1 + delta - Math.random(); 59 | } 60 | 61 | suite('Comparisons', function () { 62 | // #8 63 | test('#isAbove, #isAtMost', function () { 64 | assert.fail('hello'.length, 5); 65 | assert.fail(1, 0); 66 | assert.fail(Math.PI, 3); 67 | assert.fail(1 - Math.random(), 1); 68 | }); 69 | // #9 70 | test('#isBelow, #isAtLeast', function () { 71 | assert.fail('world'.length, 5); 72 | assert.fail(2 * Math.random(), 0); 73 | assert.fail(5 % 2, 2); 74 | assert.fail(2 / 3, 1); 75 | }); 76 | // #10 77 | test('#approximately', function () { 78 | assert.fail(weirdNumbers(0.5), 1, 0); 79 | assert.fail(weirdNumbers(0.2), 1, 0); 80 | }); 81 | }); 82 | 83 | // ----------------------------------------------------------------------------- 84 | 85 | const winterMonths = ['dec,', 'jan', 'feb', 'mar']; 86 | const backendLanguages = ['php', 'python', 'javascript', 'ruby', 'asp']; 87 | suite('Arrays', function () { 88 | // #11 89 | test('#isArray, #isNotArray', function () { 90 | assert.fail('isThisAnArray?'.split(''), 'String.prototype.split() returns an array'); 91 | assert.fail([1, 2, 3].indexOf(2), 'indexOf returns a number'); 92 | }); 93 | // #12 94 | test('Array #include, #notInclude', function () { 95 | assert.fail(winterMonths, 'jul', "It's summer in july..."); 96 | assert.fail(backendLanguages, 'javascript', 'JS is a backend language'); 97 | }); 98 | }); 99 | 100 | // ----------------------------------------------------------------------------- 101 | 102 | const formatPeople = function (name, age) { 103 | return '# name: ' + name + ', age: ' + age + '\n'; 104 | }; 105 | suite('Strings', function () { 106 | // #13 107 | test('#isString, #isNotString', function () { 108 | assert.fail(Math.sin(Math.PI / 4), 'A float is not a string'); 109 | assert.fail(process.env.PATH, 'An env variable is a string (or undefined)'); 110 | assert.fail(JSON.stringify({ type: 'object' }), 'JSON is a string'); 111 | }); 112 | // #14 113 | test('String #include, #notInclude', function () { 114 | assert.fail('Arrow', 'row', "'Arrow' contains 'row'"); 115 | assert.fail('dart', 'queue', "But 'dart' doesn't contain 'queue'"); 116 | }); 117 | // #15 118 | test('#match, #notMatch', function () { 119 | const regex = /^#\sname\:\s[\w\s]+,\sage\:\s\d+\s?$/; 120 | assert.fail(formatPeople('John Doe', 35), regex); 121 | assert.fail(formatPeople('Paul Smith III', 'twenty-four'), regex); 122 | }); 123 | }); 124 | 125 | // ----------------------------------------------------------------------------- 126 | 127 | const Car = function () { 128 | this.model = 'sedan'; 129 | this.engines = 1; 130 | this.wheels = 4; 131 | }; 132 | 133 | const Plane = function () { 134 | this.model = '737'; 135 | this.engines = ['left', 'right']; 136 | this.wheels = 6; 137 | this.wings = 2; 138 | }; 139 | 140 | const myCar = new Car(); 141 | const airlinePlane = new Plane(); 142 | 143 | suite('Objects', function () { 144 | // #16 145 | test('#property, #notProperty', function () { 146 | assert.fail(myCar, 'wings', "Cars don't have wings"); 147 | assert.fail(airlinePlane, 'engines', 'Planes have engines'); 148 | assert.fail(myCar, 'wheels', 'Cars have wheels'); 149 | }); 150 | // #17 151 | test('#typeOf, #notTypeOf', function () { 152 | assert.fail(myCar, 'object'); 153 | assert.fail(myCar.model, 'string'); 154 | assert.fail(airlinePlane.wings, 'string'); 155 | assert.fail(airlinePlane.engines, 'array'); 156 | assert.fail(myCar.wheels, 'number'); 157 | }); 158 | // #18 159 | test('#instanceOf, #notInstanceOf', function () { 160 | assert.fail(myCar, Plane); 161 | assert.fail(airlinePlane, Plane); 162 | assert.fail(airlinePlane, Object); 163 | assert.fail(myCar.wheels, String); 164 | }); 165 | }); 166 | 167 | // ----------------------------------------------------------------------------- 168 | }); 169 | --------------------------------------------------------------------------------