window.onload = function() {
nf.Testing.init(); if(navigator.userAgent.match(/msie/i)) { alert('TestCentre uses advanced software features not currently supported ' + 'by Internet Explorer. We recommend Firefox or Safari as alternatives.') }
};
// test controller class nf.Testing = {
TEST_PROMPT: 'Enter a descriptive name for this test', DEFAULT_TEST: "# Netfira WebConnect Test Script\n" + "\n" + "# Please refer to WebConnect documentation for script syntax.", DEFAULT_TEST_NAME: 'Untitled Test', DEFAULT_ORDER_TIMEOUT: 60, isBusy: null, isTesting: null, busy: function(isBusy) { this.isBusy = !!isBusy; nf.className(document.body, 'busy', isBusy ? 1 : -1); }, tests: [], activeTestIndex: -1, activeTest: function() { return this.tests.length ? this.tests[this.activeTestIndex] : null; }, init: function() { this.busy(true); this.testList = new nf.ListView(nf('#tests')); this.results.list = new nf.ListView(nf('#actions')); nf.api.get('info', function(x) { this.owner.receiveSchema(x.info.schema); this.owner.locale = x.info.locale; this.owner.allowCustomFields = x.info.customFields; }).owner = this; this.results.rootElement = nf('#results'); document.getElementsByTagName('head')[0].appendChild(nf( '<link>', null, { rel: 'shortcut icon', type: 'image/gif', href: 'data:image/gif;base64,' + this.icon } )); }, receiveSchema: function(schema) { this.schema = schema; // reverse lookup var i; this.singular = {}; for(i in schema) { this.singular[schema[i].singular] = i; } nf.api.get('settings/testScripts', function(x) { this.owner.receiveTests(x) }).owner = this; }, receiveTests: function(r) { var i; if(r instanceof Array) { for(i = 0; i < r.length; i++) { this.addTest(new this.Test(r[i].name, r[i].source)); } this.showTest(0); } else { this.create(); } this.busy(false); }, rename: function() { if(this.isBusy || this.isTesting) { return; } var test = this.activeTest(), a = test.listItem.firstChild, newName = prompt(this.TEST_PROMPT, test.name); if(!newName) { return; } test.name = newName; a.innerHTML = ''; nf('<', a, newName); nf('#source').focus(); }, del: function() { if(this.isBusy || this.isTesting) { return; } if(this.tests.length == 1) { alert("You can't delete the only remaining test. Please add another first."); } else if(confirm("Delete test '" + this.activeTest().name + "'?")) { this.testList.removeItem(this.activeTest().listItem); this.tests.splice(this.activeTestIndex, 1); var i = this.activeTestIndex; this.activeTestIndex = -1; if(i == this.tests.length) { i--; } this.showTest(i); } }, add: function() { if(this.isBusy || this.isTesting) { return; } var x = prompt(this.TEST_PROMPT, this.DEFAULT_TEST_NAME); if(x) { this.create(x); } }, create: function(name) { this.addTest(new this.Test(name || this.DEFAULT_TEST_NAME, this.DEFAULT_TEST)) }, save: function() { if(this.isBusy || this.isTesting) { return; } this.readSource(); var data = [], i; for(i = 0; i < this.tests.length; i++) { data.push({ name: this.tests[i].name, source: this.tests[i].source }); } this.busy(true); nf.api.put('settings/testScripts', data, function() { this.owner.busy(false) }).owner = this; }, addTest: function(test) { this.tests.push(test); this.showTest(this.tests.length - 1); }, showTest: function(test) { if(this.activeTestIndex >= 0) { this.readSource(); } if(typeof test == 'number') { test = this.tests[test]; } this.activeTestIndex = test.index(); this.testList.activateItem(test.listItem); with(nf('#source')) { value = test.source; focus(); } }, run: function() { if(this.isBusy || this.isTesting) { return; } this.isTesting = true; this.readSource(); var test = this.activeTest(); if(!test) { return; } this.results.list.clear(); test.run(); }, readSource: function() { var test = this.activeTest(); if(test) { test.source = nf('#source').value; } }, // results view results: { lastResult: null, list: null, scroll: function() { var div = nf('#results'); div.scrollTop = div.scrollHeight; }, newResultView: function() { var ret = this.lastResult = new nf.Testing.ResultView(this.list.addItem()); this.scroll(); return ret; }, addStartTime: function() { var result = this.newResultView(); result.setClass('time'); result.setHeading('Begin at ' + nf.timeString()); }, addEndTime: function(success) { var result = this.newResultView(); result.setClass('time'); result.setHeading((success ? 'Complete at ' : 'Fail at ') + nf.timeString()); }, sendFile: function(type, name, size) { var result = this.newResultView(); result.setClass('sendFile'); result.setHeading('Send ' + type + ' ', name, ' (', size, ' bytes)'); }, addAction: function(action) { var result = this.newResultView(), i, c = nf.Testing.Test.COMMANDS, commandName, ext; for(i in c) { if(c[i] == action.command) { result.setClass(commandName = i.toLowerCase()); } } if(action.command == c.UPDATE || action.command == c.DELETE || action.command == c.REPLICATE) { result.setHeading( nf.ucfirst(commandName) + ' ' + action.type + ' ', action.id() ); } else if(action.command == c.ADD || action.command == c.REMOVE) { result.setHeading( nf.ucfirst(commandName) + ' ' + action.types[0] + ' ', action.ids[0], (action.command == c.ADD ? ' to ' : ' from ') + action.types[1] + ' ', action.ids[1] ); } else if(action.command == c.WAIT) { result.setHeading( 'Wait ', Math.max(0, Math.round(parseFloat(action.duration) * 10) / 10), action.duration == 1 ? ' second' : ' seconds' ); } else if(action.command == c.COMMIT) { if('timeout' in action) { result.setHeading('Commit ', i = action.test.commitSize(), (i == 1 ? ' change' : ' changes') + ' (', action.timeout, ' sec max)'); } else { result.setHeading('Commit ', i = action.test.commitSize(), i == 1 ? ' change' : ' changes'); } if(!i) { result.setStatus('Nothing to commit'); } } else if(action.command == c.PURGE) { if('timeout' in action) { result.setHeading('Purge ', action.table, ' (', action.timeout, ' sec max)'); } else { result.setHeading('Purge ', action.table); } } else if(action.command == c.ORDERS) { result.setHeading('Fetch Orders (', action.timeout, ' sec max)'); } else if(action.command == c.LOCALE) { result.setHeading('Use ', action.locale, ' as default locale'); } if(action.command == c.UPDATE) { if(fileTables.indexOf(action.table) != -1) { // file records result.setClass('file'); if(ext = action.id().match(/\.(jpe?g|gif|png|bmp)$/i)) { result.setImage('data:image/' + ext[1] + ';base64,' + action.base64); } result.setChecksum(action.fields.checksum); result.setFileSize(action.binary.length); } else { // regular records var fields = {}; for(i in action.fields) { if(i != action.def.primaryKey[0]) { fields[i] = action.fields[i]; } } result.setDetails(fields); } } return result; }, addData: function(title, data, excludeTime) { this.lastResult.addData(title, data, excludeTime); this.scroll(); }, showWaitTime: function(seconds) { seconds = Math.max(0, Math.round(parseFloat(seconds) * 10) / 10); if(!seconds) { this.lastResult.setClass('done'); this.lastResult.setStatus('Done') } else { this.lastResult.setStatus('', seconds.toFixed(1), ' seconds left'); } this.scroll(); }, showElapsed: function(seconds, complete) { seconds = Math.max(0, Math.round(parseFloat(seconds) * 10) / 10); if(!complete) { this.lastResult.setStatus('', seconds.toFixed(1), ' seconds elapsed'); } else { this.lastResult.setStatus('Complete in ', seconds.toFixed(1), ' seconds'); this.lastResult.setClass('done'); } }, showChangeCount: function(portion, total) { this.lastResult.setInfo( 'Processed ', portion, ' of ', total, ' change' + (total == 1 ? '' : 's') ); this.scroll(); }, showCompleteStatus: function(complete) { this.lastResult.setInfo(complete ? 'All records deleted' : 'Some records remain'); this.scroll(); }, showParseException: function(e) { var result = this.newResultView(); result.setClass('parse error'); result.setHeading('Parse error on line ', e.lineNumber + 1); result.setStatus(e.description); result.setErrorData(e.line); this.scroll(); }, showUserError: function(data) { var result = this.newResultView(); result.setClass('user error'); result.setHeading('Server returned user error code ', data.errorCode); result.setErrorData(data); this.scroll(); }, showServerError: function(code, data) { var result = this.newResultView(); result.setClass('server error'); result.setHeading('Server responded with code ', code); result.setErrorData(data); this.scroll(); }, showError: function(heading, info) { var result = this.newResultView(); result.setClass('error'); result.setHeading(heading); result.setStatus(info); this.scroll(); }, confirmOrder: function(order) { var result = this.newResultView(); result.setClass('confirmOrder'); result.setHeading('Confirm Order ', order.fields.orderId); this.addData('Order Data', order, true); } }, // result view controller ResultView: function(rootElement) { this.rootElement = rootElement; }, // test model class Test: function(name, source) { var that = this; this.source = source; this.name = name; this.actions = []; this.actionIndex = -1; this.listItem = nf.Testing.testList.addItem(); nf( '<', nf( '<a>', this.listItem, { href: 'javascript:', onclick: function() { if(!(nf.Testing.isBusy || nf.Testing.isTesting)) { nf.Testing.showTest(that) } } } ), name ); this.listItem.firstChild.ondblclick = function() { nf.Testing.rename() }; }, ImportExport: { importing: null, im: function() { nf('#importExport').className = 'import'; nf('#series').value = ''; this.show(true); this.importing = true; }, ex: function() { nf('#importExport').className = 'export'; var i, x = '', t = nf.Testing.tests; for(i = 0; i < t.length; i++) { x += "- " + t[i].name + "\n\n" + t[i].source + "\n\n"; } nf('#series').value = x; this.show(true); this.importing = false; }, show: function(show) { nf.Testing.busy(show); nf('#importExport').style.display = show ? 'block' : 'none'; if(show) { with(nf('#series')) { focus(); select(); } } }, ok: function() { if(this.importing) { var t = nf('#series').value.trim().split(/\s*(?:[\r\n]+|^)-\s*/), i, m; if(!t || t.length <= 1) { return alert('The input you entered appears to be invalid.'); } nf.Testing.tests = []; nf.Testing.testList.clear(); nf.Testing.activeTestIndex = -1; for(i = 1; i < t.length; i++) { m = t[i].match(/^(.*?)[\r\n]+([\s\S]*)$/); if(m) { nf.Testing.addTest(new nf.Testing.Test(m[1].trim(), m[2].trim())); } } nf.Testing.showTest(0); } this.show(false); }, cancel: function() { this.show(false); } }, icon: 'R0lGODlhEAAQALMAAON4ce2Siu2wrtg4ONdJRfXOzeeEe+BtZ9hbVNdSTNomLNxkXdkuMtc/Puqf' + 'nv///yH5BAAAAAAALAAAAAAQABAAAASQMMhgDADnLISSl9WVbZ2XEJV1LYKQLB5xqkfwGK1xEzyB' + 'HYaDo4QYLngNzeHhMJ18AkSjwUEYirVggLCYUhOthMFRKAgODYBhMPC0ZOby9MJ+Ak6OS2CKYDNk' + 'CA9bXmwHAgQMfwRpCQ4EbA1bCYmKU0tCAgsPB5QKXg0Djw4ODA0MCqieoGx+lKeprK2JqbQRADs='
};
nf.Testing.ResultView.prototype = {
setClass: function(c) { nf.className(this.rootElement, c, 1); }, inClass: function(c) { return nf.className(this.rootElement, c); }, addData: function(title, data, excludeTime, expanded) { var div = nf('<div>', null, {className: 'data ' + title.toLowerCase().replace(/\s+([a-z])/, function(a, b) { return b.toUpperCase() })}), formatted = nf('<div>', div, {className: 'formatted'}); this.addExpander(title, div, false, expanded); if(!excludeTime) { this.addTime(); } this.rootElement.appendChild(div); this.showDataIn(data, formatted); if(typeof data == 'object') { var raw = nf('<div>', div, {className: 'raw'}); this.addExpander('Raw view', raw, div); div.appendChild(raw); nf('<', raw, nf.json_encode(data)); } }, showDataIn: function(data, target) { var ol, i, tbody, tr; if(data === null) { nf.className(target, 'null', 1); target.innerHTML = 'NULL'; } else if(data === true || data === false) { nf.className(target, 'boolean', 1); target.innerHTML = data ? 'True' : 'False'; } else if(typeof data === 'number' || typeof data === 'string') { nf.className(target, typeof data, 1); target.innerHTML = ''; nf('<', target, data.toString()); } else if(data instanceof Array) { nf.className(target, 'array', 1); ol = nf('<ol>', target); ol.start = 0; ol.style.counterReset = 'item 0'; for(i = 0; i < data.length; i++) { arguments.callee(data[i], nf('<li>', ol)); } } else { nf.className(target, 'object', 1); tbody = nf('<tbody>', nf('<table>', target, {cellSpacing: 0})); for(i in data) { tr = nf('<tr>', tbody); nf('<', nf('<th>', tr), nf.Testing.ResultView.prototype.hyphenate(i)); arguments.callee(data[i], nf('<td>', tr)); } } }, hyphenate: function(str) { return str .replace(/([^A-Z])([A-Z])/g, '$1 $2') .replace(/&/g, ' & ') .replace(/^([a-z])/, function(a) { return a.toUpperCase() }); }, addTime: function() { nf('<', nf('<span>', this.rootElement.lastChild, {className: 'time'}), ' at ' + nf.timeString()); }, setElement: function(className, strings, nodeName) { if(!(className in this)) { this[className] = nf('<' + (nodeName || 'div') + '>', this.rootElement, {className: className}); } this[className].innerHTML = ''; for(var i = 0; i < strings.length; i++) { if(i % 2) { nf('<', nf('<strong>', this[className]), strings[i]); } else { nf('<', this[className], strings[i]); } } }, setHeading: function() { this.setElement('heading', arguments, 'h4'); }, setStatus: function() { this.setElement('status', arguments); }, setInfo: function() { this.setElement('info', arguments); this.setClass('hasInfo'); }, setErrorData: function(data) { if(typeof data == 'string') { this.setElement('errorData', arguments, 'pre'); } else { this.addData('Details', data, true, true); } }, setImage: function(src) { var pa = this.getPreviewArea(), img; pa.innerHTML = ''; img = nf('<img>', pa, {src: src}); if(img.height > 130) { img.height = 130; } if(img.width > 160) { delete img['height']; img.width = 160; } }, setFileSize: function(bytes) { if(!('fileSize' in this)) { this.fileSize = nf('<div>', this.getPreviewArea(), {className: 'fileSize'}); } this.fileSize.innerHTML = bytes + ' bytes'; }, setChecksum: function(md5) { if(!('checksum' in this)) { this.checksum = nf('<pre>', this.getPreviewArea(), {className: 'checksum'}); } this.checksum.innerHTML = ''; nf('<', this.checksum, this.splitChecksum(md5.substr(0, 11)) + "\n" + this.splitChecksum(md5.substr(11))); }, splitChecksum: function(half) { return half.substr(0, 3) + ' ' + half.substr(3, 3) + ' ' + half.substr(6, 3) + ' ' + half.substr(9); }, getPreviewArea: function() { if(!('previewArea' in this)) { this.previewArea = nf('<div>', null, {className: 'preview'}); this.addExpander('Preview', this.previewArea); this.rootElement.appendChild(this.previewArea); } return this.previewArea; }, setDetails: function(data) { if(!('detailsTable' in this)) { this.detailsTable = nf('<table>'); nf.className(this.addExpander('Details', this.detailsTable), 'details', 1); this.detailsTable.cellSpacing = 0; this.detailsTable.className = 'details'; nf('<tbody>', this.detailsTable); this.rootElement.appendChild(this.detailsTable); } var tbody = this.detailsTable.tBodies[0], i, j, tr = null, c = false; while(tbody.firstChild) { tbody.removeChild(tbody.firstChild); } for(i in data) { if(typeof data[i] == 'object') { for(j in data[i]) { tr = this.addDetailToTBody(i + '.' + j, data[i][j], tbody, c ? '' : 'first'); c = true; } } else { tr = this.addDetailToTBody(i, data[i], tbody, c ? '' : 'first'); } c = true; } if(tr) { nf.className('tr', 'last', 1); } }, addDetailToTBody: function(field, value, tbody, className) { var tr = nf('<tr>', tbody, {className: className}), c = {className: value.match(/^-?(\d+(\.\d*)|\.\d+)$/) ? 'number' : 'string'}; nf('<', nf('<th>', tbody, c), field); nf('<', nf('<td>', tbody, c), c.className == 'number' ? parseFloat(value).toString() : value); return tr; }, addExpander: function(text, target, parent, expanded) { var div = nf('<div>', parent || this.rootElement, {className: 'expander'}), ret = nf('<a>', div); nf('<', ret, text); ret.href = 'javascript:'; ret.className = expanded ? 'open' : 'closed'; if(!expanded) { target.style.display = 'none'; } ret.eTarget = target; ret.onclick = function() { var open = nf.className(this, 'open'); nf.className(this, 'open', open ? -1 : 1); nf.className(this, 'closed', open ? 1 : -1); this.eTarget.style.display = open ? 'none' : ''; }; }
};
// test model class nf.Testing.Test.prototype = {
index: function() { var i = nf.Testing.tests.length; while(i--) { if(nf.Testing.tests[i] === this) { return i; } } }, run: function() { var e; if(this.actionIndex != -1) // already running { return; } nf.Testing.results.addStartTime(); try { this.parse(); } catch(e) { if(e instanceof this.ParseException) { nf.Testing.results.showParseException(e); return this.terminate(false); } else { throw(e); } } this.commit = { records: {}, relations: {} }; this.runNextAction(); }, commitSize: function() { if(!('commit' in this)) { return 0; } var ret = 0, i, j; for(i in this.commit) { for(j in this.commit[i]) { ret += this.commit[i][j].length; } } return ret; }, terminate: function(success) { if('commit' in this) { delete this['commit']; } this.actionIndex = -1; nf.Testing.results.addEndTime(success); nf.Testing.isTesting = false; }, runNextAction: function(doNotIncrement) { if(!doNotIncrement) { this.actionIndex++; } if(this.actionIndex >= this.actions.length) { return this.terminate(true); } var action = this.actions[this.actionIndex], commands = nf.Testing.Test.COMMANDS, i, group; nf.Testing.results.addAction(action); switch(action.command) { case commands.UPDATE: case commands.DELETE: if(!(action.type in this.commit.records)) { this.commit.records[action.type] = []; } group = this.commit.records[action.type]; i = group.length; while(i--) { if(group[i][action.def.primaryKey[0]] == action.id()) { group.splice(i, 1); break; } } group.push(action.fields); break; case commands.ADD: case commands.REMOVE: if(action.types[0] > action.types[1]) { action.types = [action.types[1], action.types[0]]; action.ids = [action.ids[1], action.ids[0]]; } group = encodeURIComponent(action.types[0]) + '&' + encodeURIComponent(action.types[1]); if(!(group in this.commit.relations)) { this.commit.relations[group] = []; } group = this.commit.relations[group]; i = group.length; while(i--) { if(group[i].a == action.ids[0] && group[i].b == action.ids[1]) { group.splice(i, 1); return; } } group.push({ a: action.ids[0], b: action.ids[1], x: action.command == commands.ADD }); break; case commands.WAIT: this.startTimer(action.duration); this.ontimeout = this.runNextAction; return; case commands.COMMIT: if(this.commitSize()) { this.setApiTimeout(action); this.startTimer(); nf.Testing.results.addData('Request', this.commit); this.lastCall = nf.api.put( 'commit', this.commit, this.commitResponse ).set('owner', this); return; } break; case commands.REPLICATE: this.lastCall = nf.api.post( 'records/' + action.type + '/external', action.id(), this.replicateResponse ).set('owner', this); nf.Testing.results.addData('Request', this.lastCall.body); return; case commands.PURGE: this.setApiTimeout(action); this.startTimer(); this.lastCall = nf.api.del( "records/" + action.type, this.purgeResponse ).set('owner', this); nf.Testing.results.addData('Request', this.lastCall.body); return; case commands.ORDERS: this.setApiTimeout(action); this.startTimer(); this.lastCall = nf.api.get( 'newOrders', this.ordersResponse ).set('owner', this); nf.Testing.results.addData('Request', this.lastCall.body); return; } this.runNextAction(); }, replicateResponse: function(r) { if(!this.success) { return this.owner.showErrorResponse(r); } nf.Testing.results.addData('Response', r); this.owner.runNextAction(); }, setApiTimeout: function(action) { var secs = (action && ('timeout' in action)) ? action.timeout : null; if(typeof secs == 'number') { nf.api.headers['X-Timeout'] = secs; } else if('X-Timeout' in nf.api.headers) { delete nf.api.headers['X-Timeout']; } }, showErrorResponse: function(r) { var code = this.lastCall.xh.status; if(code >= 300) { nf.Testing.results.addData('Error', 'Status code ' + code + ' (see report below)'); nf.Testing.results.showServerError(code, r); } else { nf.Testing.results.addData('Error', 'User error code ' + r.errorCode + ' (see report below)'); nf.Testing.results.showUserError(r); } return this.terminate(false); }, purgeResponse: function(r) { this.owner.stopTimer(); if(!this.success) { return this.owner.showErrorResponse(r); } nf.Testing.results.showCompleteStatus(!r.incomplete); nf.Testing.results.addData('Response', r); this.owner.runNextAction(r.incomplete); }, ordersResponse: function(r) { this.owner.stopTimer(); if(!this.success) { return this.owner.showErrorResponse(r); } nf.Testing.results.addData('Response', r); this.owner.confirmOrders(r.orders); }, confirmOrders: function(orders) { if(!orders.length) { return this.runNextAction(); } var order = orders.shift(); nf.Testing.results.confirmOrder(order); this.startTimer(); this.setApiTimeout(); this.lastCall = nf.api.put( 'confirmOrder?id=' + encodeURIComponent(order.fields.orderId), null, this.confirmOrderResponse ).set('owner', this).set('orders', orders); nf.Testing.results.addData('Request', this.lastCall.body); }, confirmOrderResponse: function(r) { this.owner.stopTimer(); if(!this.success) { return this.owner.showErrorResponse(r); } nf.Testing.results.addData('Response', r); this.owner.confirmOrders(this.orders); }, commitResponse: function(r) { this.owner.stopTimer(); if(!this.success) { return this.owner.showErrorResponse(r); } nf.Testing.results.addData('Response', r); var type, i, j, commitSize = this.owner.commitSize(); if('records' in r.complete) { for(type in r.complete.records) { for(i in r.complete.records[type]) { for(j = 0; j < this.owner.commit.records[type].length; j++) { if(this.owner.commit.records[type][j][nf.Testing.schema[nf.Testing.singular[type]].primaryKey[0]] == i) { this.owner.commit.records[type].splice(j, 1); } } } } } if('relations' in r.complete) { for(type in r.complete.relations) { for(i = 0; i < r.complete.relations[type].length; i++) { for(j = 0; j < this.owner.commit.relations[type].length; j++) { if(r.complete.relations[type][i].a === this.owner.commit.relations[type][j].a && r.complete.relations[type][i].b === this.owner.commit.relations[type][j].b) { this.owner.commit.relations[type].splice(j, 1); } } } } } if(commitSize == this.owner.commitSize()) { nf.Testing.results.showError('Fatal error', 'No changes were processed. Try increasing timeout.'); return this.owner.terminate(false); } nf.Testing.results.showChangeCount(commitSize - this.owner.commitSize(), commitSize); this.owner.filesToSend = r.filesToSend; this.owner.sendFiles(); }, sendFiles: function() { var type, id, i; for(type in this.filesToSend) { if(this.filesToSend[type].length) { id = this.filesToSend[type].shift(); i = this.actions.length; while(i--) { if(this.actions[i].command == nf.Testing.Test.COMMANDS.UPDATE && this.actions[i].type == type && this.actions[i].id() == id) { break; } } nf.api.headers['Content-Type'] = 'application/octet-stream'; nf.api.post('files/' + type + '/' + id, this.actions[i].binary, this.fileSent).owner = this; delete nf.api.headers['Content-Type']; nf.Testing.results.sendFile(type, id, this.actions[i].binary.length); this.startTimer(); return; } } this.runNextAction(this.commitSize()); }, fileSent: function(r) { this.owner.stopTimer(); this.owner.sendFiles(); }, startTimer: function(from) { var that = this; this.countDownFrom = from || null; this.startTime = new Date(); this.updateTimer(); this.timerInterval = setInterval(function() { that.updateTimer() }, 100); }, stopTimer: function() { clearInterval(this.timerInterval); if(this.countDownFrom) { nf.Testing.results.showWaitTime(0); } else { nf.Testing.results.showElapsed(this.elapsed(), true); } }, elapsed: function() { return ((new Date()).getTime() - this.startTime.getTime()) / 1000; }, updateTimer: function() { if(this.countDownFrom) { if(this.elapsed() >= this.countDownFrom) { this.stopTimer(); this.runNextAction(); } else { nf.Testing.results.showWaitTime(this.countDownFrom - this.elapsed()); } } else { nf.Testing.results.showElapsed(this.elapsed()); } }, parse: function() { var lines = this.source.trim().split(/\s*[\r\n]+\s*/), i, line, command, commandId, action, attr, parts, j, k, lastAction = null, binary = false, localize, commands = nf.Testing.Test.COMMANDS; this.actions = []; this.locale = nf.Testing.locale; for(i = 0; i < lines.length; i++) { line = lines[i]; // blanks if(line == '') { throw new this.ParseException(i, line, "The test source is empty."); } // comments if(line.substr(0, 1) == '#') { continue; } // attributes if(!binary && (attr = line.match(/^(\w+)(?:\.(\w+))?\s*=\s*(.*)$/))) { if(lastAction === null) { throw new this.ParseException(i, line, "Tried to assign property without and UPDATE command."); } localize = false; if(lastAction.def.localize.indexOf(attr[1]) != -1) { localize = true; } else if(attr[1] in lastAction.def.columns) { if(attr[2]) { throw new this.ParseException(i, line, "Cannot localize field " + lastTable.type + "." + attr[1]); } } else { if(!nf.Testing.allowCustomFields) { throw new this.ParseException(i, line, "Field '" + attr[1] + "' is not in " + lastAction.table + " schema and the server does not allow custom fields."); } localize = true; } if(localize) { if(!(attr[1] in lastAction.fields)) { lastAction.fields[attr[1]] = {}; } lastAction.fields[attr[1]][attr[2] || this.locale] = attr[3]; } else { lastAction.fields[attr[1]] = attr[3]; } continue; } else if(binary === null) { binary = true; } // binary (base64) if(binary) { if(line.match(/;$/)) { line = line.replace(';', ''); binary = false; } lastAction.base64 += line; if(!binary) { if(!nf.base64.validate(lastAction.base64)) { throw new this.ParseException(i, lastAction.base64, "Invalid base64 blob."); } lastAction.binary = nf.base64.decode(lastAction.base64); lastAction.fields.checksum = nf.base64.encode(nf.md5(lastAction.binary, true)).substr(0, 22); } continue; } // commands command = line.match(/^\S*/)[0].toUpperCase(); if(command in commands) { commandId = commands[command]; action = new nf.Testing.Test.Action(commandId); action.test = this; parts = line.match(arguments.callee.patterns[commandId]); if(!parts) { throw new this.ParseException(i, line, "Invalid " + command + " command."); } switch(commandId) { case commands.UPDATE: case commands.DELETE: case commands.REPLICATE: action.type = null; for(j in nf.Testing.singular) { if(j.toLowerCase() == parts[1].toLowerCase()) { action.type = j; } } if(action.type === null) { throw new this.ParseException(i, line, "Unknown type '" + parts[1] + "'."); } action.table = nf.Testing.singular[action.type]; action.def = nf.Testing.schema[action.table]; // Work-around for server's dependence on the @active flag
// action.fields = {'@active': commandId === commands.DELETE ? '0' : '1'};
action.fields = {}; action.fields[action.def.primaryKey[0]] = parts[2]; if(commandId == commands.DELETE) { action.fields[DELETE_KEY] = true; } else if(commandId == commands.UPDATE) { lastAction = action; if(fileTables.indexOf(action.table) != -1) { action.base64 = ''; binary = null; } } break; case commands.PURGE: action.table = null; for(j in nf.Testing.schema) { if(j.toLowerCase() == parts[1].toLowerCase()) { action.table = j; } } if(action.table === null) { throw new this.ParseException(i, line, "Unknown table '" + parts[1] + "'."); } action.type = nf.Testing.schema[action.table].singular; if(typeof parts[2] == 'string') { action.timeout = parseInt(parts[2]); } break; case commands.ADD: case commands.REMOVE: action.types = [null, null]; var types = [parts[1], parts[3]]; for(k = 0; k < 2; k++) { for(j in nf.Testing.singular) { if(j.toLowerCase() == types[k].toLowerCase()) { action.types[k] = j; } } if(action.types[k] === null) { throw new this.ParseException(i, line, "Unknown type '" + types[k] + "'."); } } action.ids = [parts[2], parts[4]]; break; case commands.ORDERS: action.timeout = nf.Testing.DEFAULT_ORDER_TIMEOUT; case commands.COMMIT: if(typeof parts[1] == 'string') { action.timeout = parseInt(parts[1]); } break; case commands.WAIT: action.duration = parseFloat(parts[1]); break; case commands.LOCALE: action.locale = this.locale = parts[1]; break; } this.actions.push(action); } else { throw new this.ParseException(i, line, "Unknown command."); } } }, // parse exception ParseException: function(i, line, description) { this.lineNumber = i; this.line = line; this.description = description; }
};
nf.Testing.Test.prototype.ParseException.prototype = {
toString: function() { return "Line " + this.lineNumber + " : " + this.line + "\n" + this.description; }
};
// test action model class nf.Testing.Test.Action = function(command) {
if(typeof command == 'number') { return this.command = command; } var c = nf.Testing.Test.COMMANDS, i; for(i in c) { if(i == command.toUpperCase()) { this.command = c[i]; } }
};
nf.Testing.Test.Action.prototype = {
id: function() { if('fields' in this) { return this.fields[this.def.primaryKey[0]]; } }
};
nf.Testing.Test.COMMANDS = {
UPDATE: 1, DELETE: 2, PURGE: 3, ADD: 4, REMOVE: 5, COMMIT: 6, WAIT: 7, ORDERS: 8, REPLICATE: 9, LOCALE: 10
};
nf.Testing.Test.prototype.parse.patterns = {}; with(nf.Testing.Test.prototype.parse) {
patterns[nf.Testing.Test.COMMANDS.UPDATE] = /^UPDATE\s+(\w+)\s+(\S.*)$/i; patterns[nf.Testing.Test.COMMANDS.DELETE] = /^DELETE\s+(\w+)\s+(\S.*)$/i; patterns[nf.Testing.Test.COMMANDS.REPLICATE] = /^REPLICATE\s+(\w+)\s+(\S.*)$/i; patterns[nf.Testing.Test.COMMANDS.PURGE] = /^PURGE\s+(\w+)(?:\s+(\d+))?$/i; patterns[nf.Testing.Test.COMMANDS.ADD] = /^ADD\s+(\w+)\s+(.+?)\s+TO\s+(\w+)\s+(.+)$/i; patterns[nf.Testing.Test.COMMANDS.REMOVE] = /^REMOVE\s+(\w+)\s+(.+?)\s+FROM\s+(\w+)\s+(.+)$/i; patterns[nf.Testing.Test.COMMANDS.COMMIT] = /^COMMIT(?:\s+(\d+))?$/i; patterns[nf.Testing.Test.COMMANDS.WAIT] = /^WAIT\s+(\d+(?:\.\d*)?|\.\d+)$/i; patterns[nf.Testing.Test.COMMANDS.ORDERS] = /^ORDERS(?:\s+(\d+))?$/i; patterns[nf.Testing.Test.COMMANDS.LOCALE] = /^LOCALE\s+(\w+)\s*$/i;
}