├── .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 |
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 |
--------------------------------------------------------------------------------