web-connect 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1451 @@
1
+ window.onload = function() {
2
+ nf.Testing.init();
3
+ if(navigator.userAgent.match(/msie/i)) {
4
+ alert('TestCentre uses advanced software features not currently supported ' +
5
+ 'by Internet Explorer. We recommend Firefox or Safari as alternatives.')
6
+ }
7
+ };
8
+
9
+ // test controller class
10
+ nf.Testing = {
11
+
12
+ TEST_PROMPT: 'Enter a descriptive name for this test',
13
+
14
+ DEFAULT_TEST: "# Netfira WebConnect Test Script\n" +
15
+ "\n" +
16
+ "# Please refer to WebConnect documentation for script syntax.",
17
+
18
+ DEFAULT_TEST_NAME: 'Untitled Test',
19
+
20
+ DEFAULT_ORDER_TIMEOUT: 60,
21
+
22
+ isBusy: null,
23
+
24
+ isTesting: null,
25
+
26
+ busy: function(isBusy) {
27
+ this.isBusy = !!isBusy;
28
+ nf.className(document.body, 'busy', isBusy ? 1 : -1);
29
+ },
30
+
31
+ tests: [],
32
+
33
+ activeTestIndex: -1,
34
+
35
+ activeTest: function() {
36
+ return this.tests.length
37
+ ? this.tests[this.activeTestIndex]
38
+ : null;
39
+ },
40
+
41
+ init: function() {
42
+
43
+ this.busy(true);
44
+
45
+ this.testList = new nf.ListView(nf('#tests'));
46
+ this.results.list = new nf.ListView(nf('#actions'));
47
+
48
+ nf.api.get('info', function(x) {
49
+ this.owner.receiveSchema(x.info.schema);
50
+ this.owner.locale = x.info.locale;
51
+ this.owner.allowCustomFields = x.info.customFields;
52
+ }).owner = this;
53
+
54
+ this.results.rootElement = nf('#results');
55
+
56
+ document.getElementsByTagName('head')[0].appendChild(nf(
57
+ '<link>',
58
+ null,
59
+ {
60
+ rel: 'shortcut icon',
61
+ type: 'image/gif',
62
+ href: 'data:image/gif;base64,' + this.icon
63
+ }
64
+ ));
65
+
66
+ },
67
+
68
+ receiveSchema: function(schema) {
69
+
70
+ this.schema = schema;
71
+
72
+ // reverse lookup
73
+
74
+ var i;
75
+ this.singular = {};
76
+ for(i in schema) {
77
+ this.singular[schema[i].singular] = i;
78
+ }
79
+
80
+ nf.api.get('settings/testScripts', function(x) {
81
+ this.owner.receiveTests(x)
82
+ }).owner = this;
83
+
84
+ },
85
+
86
+ receiveTests: function(r) {
87
+
88
+ var i;
89
+
90
+ if(r instanceof Array) {
91
+ for(i = 0; i < r.length; i++) {
92
+ this.addTest(new this.Test(r[i].name, r[i].source));
93
+ }
94
+ this.showTest(0);
95
+ } else {
96
+ this.create();
97
+ }
98
+
99
+ this.busy(false);
100
+ },
101
+
102
+ rename: function() {
103
+
104
+ if(this.isBusy || this.isTesting) {
105
+ return;
106
+ }
107
+
108
+ var test = this.activeTest(),
109
+ a = test.listItem.firstChild,
110
+ newName = prompt(this.TEST_PROMPT, test.name);
111
+
112
+ if(!newName) {
113
+ return;
114
+ }
115
+
116
+ test.name = newName;
117
+ a.innerHTML = '';
118
+ nf('<', a, newName);
119
+
120
+ nf('#source').focus();
121
+
122
+ },
123
+
124
+ del: function() {
125
+
126
+ if(this.isBusy || this.isTesting) {
127
+ return;
128
+ }
129
+
130
+ if(this.tests.length == 1) {
131
+ alert("You can't delete the only remaining test. Please add another first.");
132
+ }
133
+
134
+ else if(confirm("Delete test '" + this.activeTest().name + "'?")) {
135
+ this.testList.removeItem(this.activeTest().listItem);
136
+ this.tests.splice(this.activeTestIndex, 1);
137
+ var i = this.activeTestIndex;
138
+ this.activeTestIndex = -1;
139
+ if(i == this.tests.length) {
140
+ i--;
141
+ }
142
+ this.showTest(i);
143
+ }
144
+ },
145
+
146
+ add: function() {
147
+
148
+ if(this.isBusy || this.isTesting) {
149
+ return;
150
+ }
151
+
152
+ var x = prompt(this.TEST_PROMPT, this.DEFAULT_TEST_NAME);
153
+ if(x) {
154
+ this.create(x);
155
+ }
156
+ },
157
+
158
+ create: function(name) {
159
+ this.addTest(new this.Test(name || this.DEFAULT_TEST_NAME, this.DEFAULT_TEST))
160
+ },
161
+
162
+ save: function() {
163
+
164
+ if(this.isBusy || this.isTesting) {
165
+ return;
166
+ }
167
+
168
+ this.readSource();
169
+
170
+ var data = [],
171
+ i;
172
+
173
+ for(i = 0; i < this.tests.length; i++) {
174
+ data.push({
175
+ name: this.tests[i].name,
176
+ source: this.tests[i].source
177
+ });
178
+ }
179
+
180
+ this.busy(true);
181
+
182
+ nf.api.put('settings/testScripts', data, function() {
183
+ this.owner.busy(false)
184
+ }).owner = this;
185
+
186
+ },
187
+
188
+ addTest: function(test) {
189
+ this.tests.push(test);
190
+ this.showTest(this.tests.length - 1);
191
+ },
192
+
193
+ showTest: function(test) {
194
+ if(this.activeTestIndex >= 0) {
195
+ this.readSource();
196
+ }
197
+ if(typeof test == 'number') {
198
+ test = this.tests[test];
199
+ }
200
+ this.activeTestIndex = test.index();
201
+ this.testList.activateItem(test.listItem);
202
+ with(nf('#source')) {
203
+ value = test.source;
204
+ focus();
205
+ }
206
+
207
+ },
208
+
209
+ run: function() {
210
+
211
+ if(this.isBusy || this.isTesting) {
212
+ return;
213
+ }
214
+
215
+ this.isTesting = true;
216
+
217
+ this.readSource();
218
+
219
+ var test = this.activeTest();
220
+ if(!test) {
221
+ return;
222
+ }
223
+
224
+ this.results.list.clear();
225
+
226
+ test.run();
227
+
228
+ },
229
+
230
+ readSource: function() {
231
+ var test = this.activeTest();
232
+ if(test) {
233
+ test.source = nf('#source').value;
234
+ }
235
+ },
236
+
237
+ // results view
238
+ results: {
239
+
240
+ lastResult: null,
241
+
242
+ list: null,
243
+
244
+ scroll: function() {
245
+ var div = nf('#results');
246
+ div.scrollTop = div.scrollHeight;
247
+ },
248
+
249
+ newResultView: function() {
250
+ var ret = this.lastResult = new nf.Testing.ResultView(this.list.addItem());
251
+ this.scroll();
252
+ return ret;
253
+ },
254
+
255
+ addStartTime: function() {
256
+ var result = this.newResultView();
257
+ result.setClass('time');
258
+ result.setHeading('Begin at ' + nf.timeString());
259
+ },
260
+
261
+ addEndTime: function(success) {
262
+ var result = this.newResultView();
263
+ result.setClass('time');
264
+ result.setHeading((success ? 'Complete at ' : 'Fail at ') + nf.timeString());
265
+ },
266
+
267
+ sendFile: function(type, name, size) {
268
+ var result = this.newResultView();
269
+ result.setClass('sendFile');
270
+ result.setHeading('Send ' + type + ' ', name, ' (', size, ' bytes)');
271
+ },
272
+
273
+ addAction: function(action) {
274
+
275
+ var result = this.newResultView(),
276
+ i, c = nf.Testing.Test.COMMANDS, commandName, ext;
277
+
278
+ for(i in c) {
279
+ if(c[i] == action.command) {
280
+ result.setClass(commandName = i.toLowerCase());
281
+ }
282
+ }
283
+
284
+ if(action.command == c.UPDATE || action.command == c.DELETE || action.command == c.REPLICATE) {
285
+ result.setHeading(
286
+ nf.ucfirst(commandName) + ' ' + action.type + ' ',
287
+ action.id()
288
+ );
289
+ }
290
+
291
+ else if(action.command == c.ADD || action.command == c.REMOVE) {
292
+ result.setHeading(
293
+ nf.ucfirst(commandName) + ' ' + action.types[0] + ' ',
294
+ action.ids[0],
295
+ (action.command == c.ADD ? ' to ' : ' from ') + action.types[1] + ' ',
296
+ action.ids[1]
297
+ );
298
+ }
299
+
300
+ else if(action.command == c.WAIT) {
301
+ result.setHeading(
302
+ 'Wait ',
303
+ Math.max(0, Math.round(parseFloat(action.duration) * 10) / 10),
304
+ action.duration == 1 ? ' second' : ' seconds'
305
+ );
306
+ }
307
+
308
+ else if(action.command == c.COMMIT) {
309
+ if('timeout' in action) {
310
+ result.setHeading('Commit ', i = action.test.commitSize(), (i == 1 ? ' change' : ' changes') + ' (', action.timeout, ' sec max)');
311
+ }
312
+ else {
313
+ result.setHeading('Commit ', i = action.test.commitSize(), i == 1 ? ' change' : ' changes');
314
+ }
315
+ if(!i) {
316
+ result.setStatus('Nothing to commit');
317
+ }
318
+ }
319
+
320
+ else if(action.command == c.PURGE) {
321
+ if('timeout' in action) {
322
+ result.setHeading('Purge ', action.table, ' (', action.timeout, ' sec max)');
323
+ }
324
+ else {
325
+ result.setHeading('Purge ', action.table);
326
+ }
327
+ }
328
+
329
+ else if(action.command == c.ORDERS) {
330
+ result.setHeading('Fetch Orders (', action.timeout, ' sec max)');
331
+ }
332
+
333
+ else if(action.command == c.LOCALE) {
334
+ result.setHeading('Use ', action.locale, ' as default locale');
335
+ }
336
+
337
+ if(action.command == c.UPDATE) {
338
+
339
+ if(fileTables.indexOf(action.table) != -1) {
340
+
341
+ // file records
342
+
343
+ result.setClass('file');
344
+ if(ext = action.id().match(/\.(jpe?g|gif|png|bmp)$/i)) {
345
+ result.setImage('data:image/' + ext[1] + ';base64,' + action.base64);
346
+ }
347
+
348
+ result.setChecksum(action.fields.checksum);
349
+
350
+ result.setFileSize(action.binary.length);
351
+
352
+ } else {
353
+
354
+ // regular records
355
+
356
+ var fields = {};
357
+ for(i in action.fields) {
358
+ if(i != action.def.primaryKey[0]) {
359
+ fields[i] = action.fields[i];
360
+ }
361
+ }
362
+
363
+ result.setDetails(fields);
364
+
365
+ }
366
+ }
367
+
368
+ return result;
369
+ },
370
+
371
+ addData: function(title, data, excludeTime) {
372
+ this.lastResult.addData(title, data, excludeTime);
373
+ this.scroll();
374
+ },
375
+
376
+ showWaitTime: function(seconds) {
377
+
378
+ seconds = Math.max(0, Math.round(parseFloat(seconds) * 10) / 10);
379
+
380
+ if(!seconds) {
381
+ this.lastResult.setClass('done');
382
+ this.lastResult.setStatus('Done')
383
+ } else {
384
+ this.lastResult.setStatus('', seconds.toFixed(1), ' seconds left');
385
+ }
386
+
387
+ this.scroll();
388
+
389
+ },
390
+
391
+ showElapsed: function(seconds, complete) {
392
+
393
+ seconds = Math.max(0, Math.round(parseFloat(seconds) * 10) / 10);
394
+
395
+ if(!complete) {
396
+ this.lastResult.setStatus('', seconds.toFixed(1), ' seconds elapsed');
397
+ }
398
+ else {
399
+ this.lastResult.setStatus('Complete in ', seconds.toFixed(1), ' seconds');
400
+ this.lastResult.setClass('done');
401
+ }
402
+
403
+ },
404
+
405
+ showChangeCount: function(portion, total) {
406
+ this.lastResult.setInfo(
407
+ 'Processed ',
408
+ portion,
409
+ ' of ',
410
+ total,
411
+ ' change' + (total == 1 ? '' : 's')
412
+ );
413
+ this.scroll();
414
+ },
415
+
416
+ showCompleteStatus: function(complete) {
417
+ this.lastResult.setInfo(complete ? 'All records deleted' : 'Some records remain');
418
+ this.scroll();
419
+ },
420
+
421
+ showParseException: function(e) {
422
+ var result = this.newResultView();
423
+ result.setClass('parse error');
424
+ result.setHeading('Parse error on line ', e.lineNumber + 1);
425
+ result.setStatus(e.description);
426
+ result.setErrorData(e.line);
427
+ this.scroll();
428
+ },
429
+
430
+ showUserError: function(data) {
431
+ var result = this.newResultView();
432
+ result.setClass('user error');
433
+ result.setHeading('Server returned user error code ', data.errorCode);
434
+ result.setErrorData(data);
435
+ this.scroll();
436
+ },
437
+
438
+ showServerError: function(code, data) {
439
+ var result = this.newResultView();
440
+ result.setClass('server error');
441
+ result.setHeading('Server responded with code ', code);
442
+ result.setErrorData(data);
443
+ this.scroll();
444
+ },
445
+
446
+ showError: function(heading, info) {
447
+ var result = this.newResultView();
448
+ result.setClass('error');
449
+ result.setHeading(heading);
450
+ result.setStatus(info);
451
+ this.scroll();
452
+ },
453
+
454
+ confirmOrder: function(order) {
455
+ var result = this.newResultView();
456
+ result.setClass('confirmOrder');
457
+ result.setHeading('Confirm Order ', order.fields.orderId);
458
+ this.addData('Order Data', order, true);
459
+ }
460
+
461
+ },
462
+
463
+ // result view controller
464
+ ResultView: function(rootElement) {
465
+ this.rootElement = rootElement;
466
+ },
467
+
468
+ // test model class
469
+ Test: function(name, source) {
470
+ var that = this;
471
+ this.source = source;
472
+ this.name = name;
473
+ this.actions = [];
474
+ this.actionIndex = -1;
475
+ this.listItem = nf.Testing.testList.addItem();
476
+ nf(
477
+ '<',
478
+ nf(
479
+ '<a>',
480
+ this.listItem,
481
+ {
482
+ href: 'javascript:',
483
+ onclick: function() {
484
+ if(!(nf.Testing.isBusy || nf.Testing.isTesting)) {
485
+ nf.Testing.showTest(that)
486
+ }
487
+ }
488
+ }
489
+ ),
490
+ name
491
+ );
492
+ this.listItem.firstChild.ondblclick = function() {
493
+ nf.Testing.rename()
494
+ };
495
+ },
496
+
497
+ ImportExport: {
498
+
499
+ importing: null,
500
+
501
+ im: function() {
502
+ nf('#importExport').className = 'import';
503
+ nf('#series').value = '';
504
+ this.show(true);
505
+ this.importing = true;
506
+ },
507
+
508
+ ex: function() {
509
+ nf('#importExport').className = 'export';
510
+
511
+ var i, x = '', t = nf.Testing.tests;
512
+
513
+ for(i = 0; i < t.length; i++) {
514
+ x += "- " + t[i].name + "\n\n" + t[i].source + "\n\n";
515
+ }
516
+
517
+ nf('#series').value = x;
518
+
519
+ this.show(true);
520
+ this.importing = false;
521
+ },
522
+
523
+ show: function(show) {
524
+ nf.Testing.busy(show);
525
+ nf('#importExport').style.display = show ? 'block' : 'none';
526
+ if(show) {
527
+ with(nf('#series')) {
528
+ focus();
529
+ select();
530
+ }
531
+ }
532
+ },
533
+
534
+ ok: function() {
535
+ if(this.importing) {
536
+ var t = nf('#series').value.trim().split(/\s*(?:[\r\n]+|^)-\s*/),
537
+ i, m;
538
+ if(!t || t.length <= 1) {
539
+ return alert('The input you entered appears to be invalid.');
540
+ }
541
+ nf.Testing.tests = [];
542
+ nf.Testing.testList.clear();
543
+ nf.Testing.activeTestIndex = -1;
544
+ for(i = 1; i < t.length; i++) {
545
+ m = t[i].match(/^(.*?)[\r\n]+([\s\S]*)$/);
546
+ if(m) {
547
+ nf.Testing.addTest(new nf.Testing.Test(m[1].trim(), m[2].trim()));
548
+ }
549
+ }
550
+ nf.Testing.showTest(0);
551
+ }
552
+ this.show(false);
553
+ },
554
+
555
+ cancel: function() {
556
+ this.show(false);
557
+ }
558
+ },
559
+
560
+ icon: 'R0lGODlhEAAQALMAAON4ce2Siu2wrtg4ONdJRfXOzeeEe+BtZ9hbVNdSTNomLNxkXdkuMtc/Puqf' +
561
+ 'nv///yH5BAAAAAAALAAAAAAQABAAAASQMMhgDADnLISSl9WVbZ2XEJV1LYKQLB5xqkfwGK1xEzyB' +
562
+ 'HYaDo4QYLngNzeHhMJ18AkSjwUEYirVggLCYUhOthMFRKAgODYBhMPC0ZOby9MJ+Ak6OS2CKYDNk' +
563
+ 'CA9bXmwHAgQMfwRpCQ4EbA1bCYmKU0tCAgsPB5QKXg0Djw4ODA0MCqieoGx+lKeprK2JqbQRADs='
564
+
565
+ };
566
+
567
+ nf.Testing.ResultView.prototype = {
568
+ setClass: function(c) {
569
+ nf.className(this.rootElement, c, 1);
570
+ },
571
+ inClass: function(c) {
572
+ return nf.className(this.rootElement, c);
573
+ },
574
+ addData: function(title, data, excludeTime, expanded) {
575
+ var div = nf('<div>', null, {className: 'data ' + title.toLowerCase().replace(/\s+([a-z])/, function(a, b) {
576
+ return b.toUpperCase()
577
+ })}),
578
+ formatted = nf('<div>', div, {className: 'formatted'});
579
+ this.addExpander(title, div, false, expanded);
580
+ if(!excludeTime) {
581
+ this.addTime();
582
+ }
583
+ this.rootElement.appendChild(div);
584
+ this.showDataIn(data, formatted);
585
+ if(typeof data == 'object') {
586
+ var raw = nf('<div>', div, {className: 'raw'});
587
+ this.addExpander('Raw view', raw, div);
588
+ div.appendChild(raw);
589
+ nf('<', raw, nf.json_encode(data));
590
+ }
591
+ },
592
+ showDataIn: function(data, target) {
593
+
594
+ var ol, i, tbody, tr;
595
+
596
+ if(data === null) {
597
+ nf.className(target, 'null', 1);
598
+ target.innerHTML = 'NULL';
599
+ }
600
+
601
+ else if(data === true || data === false) {
602
+ nf.className(target, 'boolean', 1);
603
+ target.innerHTML = data ? 'True' : 'False';
604
+ }
605
+
606
+ else if(typeof data === 'number' || typeof data === 'string') {
607
+ nf.className(target, typeof data, 1);
608
+ target.innerHTML = '';
609
+ nf('<', target, data.toString());
610
+ }
611
+
612
+ else if(data instanceof Array) {
613
+ nf.className(target, 'array', 1);
614
+ ol = nf('<ol>', target);
615
+ ol.start = 0;
616
+ ol.style.counterReset = 'item 0';
617
+ for(i = 0; i < data.length; i++) {
618
+ arguments.callee(data[i], nf('<li>', ol));
619
+ }
620
+ }
621
+
622
+ else {
623
+ nf.className(target, 'object', 1);
624
+ tbody = nf('<tbody>', nf('<table>', target, {cellSpacing: 0}));
625
+ for(i in data) {
626
+ tr = nf('<tr>', tbody);
627
+ nf('<', nf('<th>', tr), nf.Testing.ResultView.prototype.hyphenate(i));
628
+ arguments.callee(data[i], nf('<td>', tr));
629
+ }
630
+ }
631
+
632
+ },
633
+
634
+ hyphenate: function(str) {
635
+ return str
636
+ .replace(/([^A-Z])([A-Z])/g, '$1 $2')
637
+ .replace(/&/g, ' & ')
638
+ .replace(/^([a-z])/, function(a) {
639
+ return a.toUpperCase()
640
+ });
641
+ },
642
+
643
+ addTime: function() {
644
+ nf('<', nf('<span>', this.rootElement.lastChild, {className: 'time'}), ' at ' + nf.timeString());
645
+ },
646
+ setElement: function(className, strings, nodeName) {
647
+ if(!(className in this)) {
648
+ this[className] = nf('<' + (nodeName || 'div') + '>', this.rootElement, {className: className});
649
+ }
650
+ this[className].innerHTML = '';
651
+ for(var i = 0; i < strings.length; i++) {
652
+ if(i % 2) {
653
+ nf('<', nf('<strong>', this[className]), strings[i]);
654
+ }
655
+ else {
656
+ nf('<', this[className], strings[i]);
657
+ }
658
+ }
659
+ },
660
+ setHeading: function() {
661
+ this.setElement('heading', arguments, 'h4');
662
+ },
663
+ setStatus: function() {
664
+ this.setElement('status', arguments);
665
+ },
666
+ setInfo: function() {
667
+ this.setElement('info', arguments);
668
+ this.setClass('hasInfo');
669
+ },
670
+ setErrorData: function(data) {
671
+ if(typeof data == 'string') {
672
+ this.setElement('errorData', arguments, 'pre');
673
+ }
674
+ else {
675
+ this.addData('Details', data, true, true);
676
+ }
677
+ },
678
+ setImage: function(src) {
679
+ var pa = this.getPreviewArea(),
680
+ img;
681
+ pa.innerHTML = '';
682
+ img = nf('<img>', pa, {src: src});
683
+ if(img.height > 130) {
684
+ img.height = 130;
685
+ }
686
+ if(img.width > 160) {
687
+ delete img['height'];
688
+ img.width = 160;
689
+ }
690
+ },
691
+ setFileSize: function(bytes) {
692
+ if(!('fileSize' in this)) {
693
+ this.fileSize = nf('<div>', this.getPreviewArea(), {className: 'fileSize'});
694
+ }
695
+ this.fileSize.innerHTML = bytes + ' bytes';
696
+ },
697
+ setChecksum: function(md5) {
698
+ if(!('checksum' in this)) {
699
+ this.checksum = nf('<pre>', this.getPreviewArea(), {className: 'checksum'});
700
+ }
701
+ this.checksum.innerHTML = '';
702
+ nf('<', this.checksum, this.splitChecksum(md5.substr(0, 11)) + "\n" + this.splitChecksum(md5.substr(11)));
703
+ },
704
+ splitChecksum: function(half) {
705
+ return half.substr(0, 3) + ' ' +
706
+ half.substr(3, 3) + ' ' +
707
+ half.substr(6, 3) + ' ' +
708
+ half.substr(9);
709
+ },
710
+ getPreviewArea: function() {
711
+ if(!('previewArea' in this)) {
712
+ this.previewArea = nf('<div>', null, {className: 'preview'});
713
+ this.addExpander('Preview', this.previewArea);
714
+ this.rootElement.appendChild(this.previewArea);
715
+ }
716
+ return this.previewArea;
717
+ },
718
+ setDetails: function(data) {
719
+ if(!('detailsTable' in this)) {
720
+ this.detailsTable = nf('<table>');
721
+ nf.className(this.addExpander('Details', this.detailsTable), 'details', 1);
722
+ this.detailsTable.cellSpacing = 0;
723
+ this.detailsTable.className = 'details';
724
+ nf('<tbody>', this.detailsTable);
725
+ this.rootElement.appendChild(this.detailsTable);
726
+ }
727
+ var tbody = this.detailsTable.tBodies[0],
728
+ i, j, tr = null, c = false;
729
+
730
+ while(tbody.firstChild) {
731
+ tbody.removeChild(tbody.firstChild);
732
+ }
733
+
734
+ for(i in data) {
735
+ if(typeof data[i] == 'object') {
736
+ for(j in data[i]) {
737
+ tr = this.addDetailToTBody(i + '.' + j, data[i][j], tbody, c ? '' : 'first');
738
+ c = true;
739
+ }
740
+ }
741
+ else {
742
+ tr = this.addDetailToTBody(i, data[i], tbody, c ? '' : 'first');
743
+ }
744
+ c = true;
745
+ }
746
+ if(tr) {
747
+ nf.className('tr', 'last', 1);
748
+ }
749
+
750
+ },
751
+ addDetailToTBody: function(field, value, tbody, className) {
752
+ var tr = nf('<tr>', tbody, {className: className}),
753
+ c = {className: value.match(/^-?(\d+(\.\d*)|\.\d+)$/) ? 'number' : 'string'};
754
+ nf('<', nf('<th>', tbody, c), field);
755
+ nf('<', nf('<td>', tbody, c), c.className == 'number' ? parseFloat(value).toString() : value);
756
+ return tr;
757
+ },
758
+ addExpander: function(text, target, parent, expanded) {
759
+ var div = nf('<div>', parent || this.rootElement, {className: 'expander'}),
760
+ ret = nf('<a>', div);
761
+ nf('<', ret, text);
762
+ ret.href = 'javascript:';
763
+ ret.className = expanded ? 'open' : 'closed';
764
+ if(!expanded) {
765
+ target.style.display = 'none';
766
+ }
767
+ ret.eTarget = target;
768
+ ret.onclick = function() {
769
+ var open = nf.className(this, 'open');
770
+ nf.className(this, 'open', open ? -1 : 1);
771
+ nf.className(this, 'closed', open ? 1 : -1);
772
+ this.eTarget.style.display = open ? 'none' : '';
773
+ };
774
+ }
775
+ };
776
+
777
+ // test model class
778
+ nf.Testing.Test.prototype = {
779
+
780
+ index: function() {
781
+ var i = nf.Testing.tests.length;
782
+ while(i--) {
783
+ if(nf.Testing.tests[i] === this) {
784
+ return i;
785
+ }
786
+ }
787
+ },
788
+
789
+ run: function() {
790
+
791
+ var e;
792
+
793
+ if(this.actionIndex != -1) // already running
794
+ {
795
+ return;
796
+ }
797
+
798
+ nf.Testing.results.addStartTime();
799
+
800
+ try {
801
+ this.parse();
802
+ } catch(e) {
803
+ if(e instanceof this.ParseException) {
804
+ nf.Testing.results.showParseException(e);
805
+ return this.terminate(false);
806
+ } else {
807
+ throw(e);
808
+ }
809
+ }
810
+
811
+ this.commit = {
812
+ records: {},
813
+ relations: {}
814
+ };
815
+
816
+ this.runNextAction();
817
+
818
+ },
819
+
820
+ commitSize: function() {
821
+ if(!('commit' in this)) {
822
+ return 0;
823
+ }
824
+ var ret = 0, i, j;
825
+ for(i in this.commit) {
826
+ for(j in this.commit[i]) {
827
+ ret += this.commit[i][j].length;
828
+ }
829
+ }
830
+ return ret;
831
+ },
832
+
833
+ terminate: function(success) {
834
+ if('commit' in this) {
835
+ delete this['commit'];
836
+ }
837
+ this.actionIndex = -1;
838
+ nf.Testing.results.addEndTime(success);
839
+ nf.Testing.isTesting = false;
840
+ },
841
+
842
+ runNextAction: function(doNotIncrement) {
843
+
844
+ if(!doNotIncrement) {
845
+ this.actionIndex++;
846
+ }
847
+
848
+ if(this.actionIndex >= this.actions.length) {
849
+ return this.terminate(true);
850
+ }
851
+
852
+ var action = this.actions[this.actionIndex],
853
+ commands = nf.Testing.Test.COMMANDS,
854
+ i, group;
855
+
856
+ nf.Testing.results.addAction(action);
857
+
858
+ switch(action.command) {
859
+
860
+ case commands.UPDATE:
861
+ case commands.DELETE:
862
+
863
+ if(!(action.type in this.commit.records)) {
864
+ this.commit.records[action.type] = [];
865
+ }
866
+
867
+ group = this.commit.records[action.type];
868
+
869
+ i = group.length;
870
+ while(i--) {
871
+ if(group[i][action.def.primaryKey[0]] == action.id()) {
872
+ group.splice(i, 1);
873
+ break;
874
+ }
875
+ }
876
+
877
+ group.push(action.fields);
878
+
879
+ break;
880
+
881
+ case commands.ADD:
882
+ case commands.REMOVE:
883
+
884
+ if(action.types[0] > action.types[1]) {
885
+ action.types = [action.types[1], action.types[0]];
886
+ action.ids = [action.ids[1], action.ids[0]];
887
+ }
888
+
889
+ group = encodeURIComponent(action.types[0])
890
+ + '&'
891
+ + encodeURIComponent(action.types[1]);
892
+
893
+ if(!(group in this.commit.relations)) {
894
+ this.commit.relations[group] = [];
895
+ }
896
+
897
+ group = this.commit.relations[group];
898
+
899
+ i = group.length;
900
+ while(i--) {
901
+ if(group[i].a == action.ids[0] && group[i].b == action.ids[1]) {
902
+ group.splice(i, 1);
903
+ return;
904
+ }
905
+ }
906
+
907
+ group.push({
908
+ a: action.ids[0],
909
+ b: action.ids[1],
910
+ x: action.command == commands.ADD
911
+ });
912
+
913
+ break;
914
+
915
+ case commands.WAIT:
916
+
917
+ this.startTimer(action.duration);
918
+ this.ontimeout = this.runNextAction;
919
+
920
+ return;
921
+
922
+ case commands.COMMIT:
923
+
924
+ if(this.commitSize()) {
925
+ this.setApiTimeout(action);
926
+ this.startTimer();
927
+ nf.Testing.results.addData('Request', this.commit);
928
+ this.lastCall = nf.api.put(
929
+ 'commit', this.commit,
930
+ this.commitResponse
931
+ ).set('owner', this);
932
+ return;
933
+ }
934
+
935
+ break;
936
+
937
+ case commands.REPLICATE:
938
+
939
+ this.lastCall = nf.api.post(
940
+ 'records/' + action.type + '/external',
941
+ action.id(),
942
+ this.replicateResponse
943
+ ).set('owner', this);
944
+ nf.Testing.results.addData('Request', this.lastCall.body);
945
+
946
+ return;
947
+
948
+ case commands.PURGE:
949
+
950
+ this.setApiTimeout(action);
951
+ this.startTimer();
952
+ this.lastCall = nf.api.del(
953
+ "records/" + action.type,
954
+ this.purgeResponse
955
+ ).set('owner', this);
956
+ nf.Testing.results.addData('Request', this.lastCall.body);
957
+
958
+ return;
959
+
960
+ case commands.ORDERS:
961
+
962
+ this.setApiTimeout(action);
963
+ this.startTimer();
964
+ this.lastCall = nf.api.get(
965
+ 'newOrders',
966
+ this.ordersResponse
967
+ ).set('owner', this);
968
+ nf.Testing.results.addData('Request', this.lastCall.body);
969
+
970
+ return;
971
+
972
+ }
973
+
974
+ this.runNextAction();
975
+
976
+ },
977
+
978
+ replicateResponse: function(r) {
979
+ if(!this.success) {
980
+ return this.owner.showErrorResponse(r);
981
+ }
982
+ nf.Testing.results.addData('Response', r);
983
+ this.owner.runNextAction();
984
+ },
985
+
986
+ setApiTimeout: function(action) {
987
+ var secs = (action && ('timeout' in action)) ? action.timeout : null;
988
+ if(typeof secs == 'number') {
989
+ nf.api.headers['X-Timeout'] = secs;
990
+ }
991
+ else if('X-Timeout' in nf.api.headers) {
992
+ delete nf.api.headers['X-Timeout'];
993
+ }
994
+ },
995
+
996
+ showErrorResponse: function(r) {
997
+ var code = this.lastCall.xh.status;
998
+ if(code >= 300) {
999
+ nf.Testing.results.addData('Error', 'Status code ' + code + ' (see report below)');
1000
+ nf.Testing.results.showServerError(code, r);
1001
+ } else {
1002
+ nf.Testing.results.addData('Error', 'User error code ' + r.errorCode + ' (see report below)');
1003
+ nf.Testing.results.showUserError(r);
1004
+ }
1005
+ return this.terminate(false);
1006
+ },
1007
+
1008
+ purgeResponse: function(r) {
1009
+
1010
+ this.owner.stopTimer();
1011
+ if(!this.success) {
1012
+ return this.owner.showErrorResponse(r);
1013
+ }
1014
+ nf.Testing.results.showCompleteStatus(!r.incomplete);
1015
+ nf.Testing.results.addData('Response', r);
1016
+ this.owner.runNextAction(r.incomplete);
1017
+
1018
+ },
1019
+
1020
+ ordersResponse: function(r) {
1021
+ this.owner.stopTimer();
1022
+ if(!this.success) {
1023
+ return this.owner.showErrorResponse(r);
1024
+ }
1025
+ nf.Testing.results.addData('Response', r);
1026
+ this.owner.confirmOrders(r.orders);
1027
+ },
1028
+
1029
+ confirmOrders: function(orders) {
1030
+ if(!orders.length) {
1031
+ return this.runNextAction();
1032
+ }
1033
+ var order = orders.shift();
1034
+ nf.Testing.results.confirmOrder(order);
1035
+ this.startTimer();
1036
+ this.setApiTimeout();
1037
+ this.lastCall = nf.api.put(
1038
+ 'confirmOrder?id=' + encodeURIComponent(order.fields.orderId),
1039
+ null,
1040
+ this.confirmOrderResponse
1041
+ ).set('owner', this).set('orders', orders);
1042
+ nf.Testing.results.addData('Request', this.lastCall.body);
1043
+ },
1044
+
1045
+ confirmOrderResponse: function(r) {
1046
+ this.owner.stopTimer();
1047
+ if(!this.success) {
1048
+ return this.owner.showErrorResponse(r);
1049
+ }
1050
+ nf.Testing.results.addData('Response', r);
1051
+ this.owner.confirmOrders(this.orders);
1052
+ },
1053
+
1054
+ commitResponse: function(r) {
1055
+
1056
+ this.owner.stopTimer();
1057
+
1058
+ if(!this.success) {
1059
+ return this.owner.showErrorResponse(r);
1060
+ }
1061
+
1062
+ nf.Testing.results.addData('Response', r);
1063
+
1064
+ var type, i, j, commitSize = this.owner.commitSize();
1065
+
1066
+ if('records' in r.complete) {
1067
+ for(type in r.complete.records) {
1068
+ for(i in r.complete.records[type]) {
1069
+ for(j = 0; j < this.owner.commit.records[type].length; j++) {
1070
+ if(this.owner.commit.records[type][j][nf.Testing.schema[nf.Testing.singular[type]].primaryKey[0]] == i) {
1071
+ this.owner.commit.records[type].splice(j, 1);
1072
+ }
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ if('relations' in r.complete) {
1079
+ for(type in r.complete.relations) {
1080
+ for(i = 0; i < r.complete.relations[type].length; i++) {
1081
+ for(j = 0; j < this.owner.commit.relations[type].length; j++) {
1082
+ if(r.complete.relations[type][i].a === this.owner.commit.relations[type][j].a &&
1083
+ r.complete.relations[type][i].b === this.owner.commit.relations[type][j].b) {
1084
+ this.owner.commit.relations[type].splice(j, 1);
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ if(commitSize == this.owner.commitSize()) {
1092
+ nf.Testing.results.showError('Fatal error', 'No changes were processed. Try increasing timeout.');
1093
+ return this.owner.terminate(false);
1094
+ }
1095
+
1096
+ nf.Testing.results.showChangeCount(commitSize - this.owner.commitSize(), commitSize);
1097
+
1098
+ this.owner.filesToSend = r.filesToSend;
1099
+
1100
+ this.owner.sendFiles();
1101
+
1102
+ },
1103
+
1104
+ sendFiles: function() {
1105
+
1106
+ var type, id, i;
1107
+ for(type in this.filesToSend) {
1108
+ if(this.filesToSend[type].length) {
1109
+ id = this.filesToSend[type].shift();
1110
+ i = this.actions.length;
1111
+ while(i--) {
1112
+ if(this.actions[i].command == nf.Testing.Test.COMMANDS.UPDATE &&
1113
+ this.actions[i].type == type &&
1114
+ this.actions[i].id() == id) {
1115
+ break;
1116
+ }
1117
+ }
1118
+ nf.api.post('files/' + type + '/' + id, this.actions[i].binary, this.fileSent).owner = this;
1119
+ nf.Testing.results.sendFile(type, id, this.actions[i].binary.length);
1120
+ this.startTimer();
1121
+ return;
1122
+ }
1123
+ }
1124
+ this.runNextAction(this.commitSize());
1125
+ },
1126
+
1127
+ fileSent: function(r) {
1128
+ this.owner.stopTimer();
1129
+ this.owner.sendFiles();
1130
+ },
1131
+
1132
+ startTimer: function(from) {
1133
+ var that = this;
1134
+ this.countDownFrom = from || null;
1135
+ this.startTime = new Date();
1136
+ this.updateTimer();
1137
+ this.timerInterval = setInterval(function() {
1138
+ that.updateTimer()
1139
+ }, 100);
1140
+ },
1141
+
1142
+ stopTimer: function() {
1143
+ clearInterval(this.timerInterval);
1144
+ if(this.countDownFrom) {
1145
+ nf.Testing.results.showWaitTime(0);
1146
+ }
1147
+ else {
1148
+ nf.Testing.results.showElapsed(this.elapsed(), true);
1149
+ }
1150
+ },
1151
+
1152
+ elapsed: function() {
1153
+ return ((new Date()).getTime() - this.startTime.getTime()) / 1000;
1154
+ },
1155
+
1156
+ updateTimer: function() {
1157
+ if(this.countDownFrom) {
1158
+ if(this.elapsed() >= this.countDownFrom) {
1159
+ this.stopTimer();
1160
+ this.runNextAction();
1161
+ } else {
1162
+ nf.Testing.results.showWaitTime(this.countDownFrom - this.elapsed());
1163
+ }
1164
+ }
1165
+ else {
1166
+ nf.Testing.results.showElapsed(this.elapsed());
1167
+ }
1168
+ },
1169
+
1170
+ parse: function() {
1171
+
1172
+ var lines = this.source.trim().split(/\s*[\r\n]+\s*/),
1173
+ i, line, command, commandId, action, attr, parts, j, k,
1174
+ lastAction = null, binary = false, localize,
1175
+ commands = nf.Testing.Test.COMMANDS;
1176
+
1177
+ this.actions = [];
1178
+ this.locale = nf.Testing.locale;
1179
+
1180
+ for(i = 0; i < lines.length; i++) {
1181
+
1182
+ line = lines[i];
1183
+
1184
+ // blanks
1185
+ if(line == '') {
1186
+ throw new this.ParseException(i, line, "The test source is empty.");
1187
+ }
1188
+
1189
+ // comments
1190
+ if(line.substr(0, 1) == '#') {
1191
+ continue;
1192
+ }
1193
+
1194
+ // attributes
1195
+ if(!binary && (attr = line.match(/^(\w+)(?:\.(\w+))?\s*=\s*(.*)$/))) {
1196
+
1197
+ if(lastAction === null) {
1198
+ throw new this.ParseException(i, line, "Tried to assign property without and UPDATE command.");
1199
+ }
1200
+
1201
+ localize = false;
1202
+
1203
+ if(lastAction.def.localize.indexOf(attr[1]) != -1) {
1204
+ localize = true;
1205
+ }
1206
+ else if(attr[1] in lastAction.def.columns) {
1207
+ if(attr[2]) {
1208
+ throw new this.ParseException(i, line,
1209
+ "Cannot localize field " + lastTable.type + "." + attr[1]);
1210
+ }
1211
+ } else {
1212
+ if(!nf.Testing.allowCustomFields) {
1213
+ throw new this.ParseException(i, line,
1214
+ "Field '" + attr[1] + "' is not in " + lastAction.table +
1215
+ " schema and the server does not allow custom fields.");
1216
+ }
1217
+ localize = true;
1218
+ }
1219
+
1220
+ if(localize) {
1221
+ if(!(attr[1] in lastAction.fields)) {
1222
+ lastAction.fields[attr[1]] = {};
1223
+ }
1224
+ lastAction.fields[attr[1]][attr[2] || this.locale] = attr[3];
1225
+ } else {
1226
+ lastAction.fields[attr[1]] = attr[3];
1227
+ }
1228
+
1229
+ continue;
1230
+ } else if(binary === null) {
1231
+ binary = true;
1232
+ }
1233
+
1234
+ // binary (base64)
1235
+ if(binary) {
1236
+ if(line.match(/;$/)) {
1237
+ line = line.replace(';', '');
1238
+ binary = false;
1239
+ }
1240
+ lastAction.base64 += line;
1241
+ if(!binary) {
1242
+ if(!nf.base64.validate(lastAction.base64)) {
1243
+ throw new this.ParseException(i, lastAction.base64, "Invalid base64 blob.");
1244
+ }
1245
+ lastAction.binary = nf.base64.decode(lastAction.base64);
1246
+ lastAction.fields.checksum = nf.base64.encode(nf.md5(lastAction.binary, true)).substr(0, 22);
1247
+ }
1248
+ continue;
1249
+ }
1250
+
1251
+ // commands
1252
+ command = line.match(/^\S*/)[0].toUpperCase();
1253
+ if(command in commands) {
1254
+
1255
+ commandId = commands[command];
1256
+ action = new nf.Testing.Test.Action(commandId);
1257
+ action.test = this;
1258
+
1259
+ parts = line.match(arguments.callee.patterns[commandId]);
1260
+ if(!parts) {
1261
+ throw new this.ParseException(i, line, "Invalid " + command + " command.");
1262
+ }
1263
+
1264
+ switch(commandId) {
1265
+
1266
+ case commands.UPDATE:
1267
+ case commands.DELETE:
1268
+ case commands.REPLICATE:
1269
+
1270
+ action.type = null;
1271
+ for(j in nf.Testing.singular) {
1272
+ if(j.toLowerCase() == parts[1].toLowerCase()) {
1273
+ action.type = j;
1274
+ }
1275
+ }
1276
+ if(action.type === null) {
1277
+ throw new this.ParseException(i, line, "Unknown type '" + parts[1] + "'.");
1278
+ }
1279
+
1280
+ action.table = nf.Testing.singular[action.type];
1281
+ action.def = nf.Testing.schema[action.table];
1282
+
1283
+ // Work-around for server's dependence on the @active flag
1284
+ // action.fields = {'@active': commandId === commands.DELETE ? '0' : '1'};
1285
+ action.fields = {};
1286
+
1287
+ action.fields[action.def.primaryKey[0]] = parts[2];
1288
+
1289
+ if(commandId == commands.DELETE) {
1290
+ action.fields[DELETE_KEY] = true;
1291
+ }
1292
+ else if(commandId == commands.UPDATE) {
1293
+ lastAction = action;
1294
+ if(fileTables.indexOf(action.table) != -1) {
1295
+ action.base64 = '';
1296
+ binary = null;
1297
+ }
1298
+ }
1299
+
1300
+ break;
1301
+
1302
+ case commands.PURGE:
1303
+
1304
+ action.table = null;
1305
+ for(j in nf.Testing.schema) {
1306
+ if(j.toLowerCase() == parts[1].toLowerCase()) {
1307
+ action.table = j;
1308
+ }
1309
+ }
1310
+ if(action.table === null) {
1311
+ throw new this.ParseException(i, line, "Unknown table '" + parts[1] + "'.");
1312
+ }
1313
+
1314
+ action.type = nf.Testing.schema[action.table].singular;
1315
+
1316
+ if(typeof parts[2] == 'string') {
1317
+ action.timeout = parseInt(parts[2]);
1318
+ }
1319
+
1320
+ break;
1321
+
1322
+ case commands.ADD:
1323
+ case commands.REMOVE:
1324
+
1325
+ action.types = [null, null];
1326
+
1327
+ var types = [parts[1], parts[3]];
1328
+
1329
+ for(k = 0; k < 2; k++) {
1330
+ for(j in nf.Testing.singular) {
1331
+ if(j.toLowerCase() == types[k].toLowerCase()) {
1332
+ action.types[k] = j;
1333
+ }
1334
+ }
1335
+ if(action.types[k] === null) {
1336
+ throw new this.ParseException(i, line, "Unknown type '" + types[k] + "'.");
1337
+ }
1338
+ }
1339
+
1340
+ action.ids = [parts[2], parts[4]];
1341
+
1342
+ break;
1343
+
1344
+ case commands.ORDERS:
1345
+
1346
+ action.timeout = nf.Testing.DEFAULT_ORDER_TIMEOUT;
1347
+
1348
+ case commands.COMMIT:
1349
+
1350
+ if(typeof parts[1] == 'string') {
1351
+ action.timeout = parseInt(parts[1]);
1352
+ }
1353
+
1354
+ break;
1355
+
1356
+ case commands.WAIT:
1357
+
1358
+ action.duration = parseFloat(parts[1]);
1359
+
1360
+ break;
1361
+
1362
+ case commands.LOCALE:
1363
+
1364
+ action.locale = this.locale = parts[1];
1365
+
1366
+ break;
1367
+
1368
+ }
1369
+
1370
+ this.actions.push(action);
1371
+
1372
+ } else {
1373
+ throw new this.ParseException(i, line, "Unknown command.");
1374
+ }
1375
+ }
1376
+
1377
+ },
1378
+
1379
+ // parse exception
1380
+ ParseException: function(i, line, description) {
1381
+
1382
+ this.lineNumber = i;
1383
+ this.line = line;
1384
+ this.description = description;
1385
+
1386
+ }
1387
+
1388
+ };
1389
+
1390
+ nf.Testing.Test.prototype.ParseException.prototype = {
1391
+
1392
+ toString: function() {
1393
+ return "Line " + this.lineNumber + " : " + this.line + "\n" + this.description;
1394
+ }
1395
+
1396
+ };
1397
+
1398
+ // test action model class
1399
+ nf.Testing.Test.Action = function(command) {
1400
+
1401
+ if(typeof command == 'number') {
1402
+ return this.command = command;
1403
+ }
1404
+
1405
+ var c = nf.Testing.Test.COMMANDS,
1406
+ i;
1407
+
1408
+ for(i in c) {
1409
+ if(i == command.toUpperCase()) {
1410
+ this.command = c[i];
1411
+ }
1412
+ }
1413
+
1414
+ };
1415
+
1416
+ nf.Testing.Test.Action.prototype = {
1417
+
1418
+ id: function() {
1419
+ if('fields' in this) {
1420
+ return this.fields[this.def.primaryKey[0]];
1421
+ }
1422
+ }
1423
+
1424
+ };
1425
+
1426
+ nf.Testing.Test.COMMANDS = {
1427
+ UPDATE: 1,
1428
+ DELETE: 2,
1429
+ PURGE: 3,
1430
+ ADD: 4,
1431
+ REMOVE: 5,
1432
+ COMMIT: 6,
1433
+ WAIT: 7,
1434
+ ORDERS: 8,
1435
+ REPLICATE: 9,
1436
+ LOCALE: 10
1437
+ };
1438
+
1439
+ nf.Testing.Test.prototype.parse.patterns = {};
1440
+ with(nf.Testing.Test.prototype.parse) {
1441
+ patterns[nf.Testing.Test.COMMANDS.UPDATE] = /^UPDATE\s+(\w+)\s+(\S.*)$/i;
1442
+ patterns[nf.Testing.Test.COMMANDS.DELETE] = /^DELETE\s+(\w+)\s+(\S.*)$/i;
1443
+ patterns[nf.Testing.Test.COMMANDS.REPLICATE] = /^REPLICATE\s+(\w+)\s+(\S.*)$/i;
1444
+ patterns[nf.Testing.Test.COMMANDS.PURGE] = /^PURGE\s+(\w+)(?:\s+(\d+))?$/i;
1445
+ patterns[nf.Testing.Test.COMMANDS.ADD] = /^ADD\s+(\w+)\s+(.+?)\s+TO\s+(\w+)\s+(.+)$/i;
1446
+ patterns[nf.Testing.Test.COMMANDS.REMOVE] = /^REMOVE\s+(\w+)\s+(.+?)\s+FROM\s+(\w+)\s+(.+)$/i;
1447
+ patterns[nf.Testing.Test.COMMANDS.COMMIT] = /^COMMIT(?:\s+(\d+))?$/i;
1448
+ patterns[nf.Testing.Test.COMMANDS.WAIT] = /^WAIT\s+(\d+(?:\.\d*)?|\.\d+)$/i;
1449
+ patterns[nf.Testing.Test.COMMANDS.ORDERS] = /^ORDERS(?:\s+(\d+))?$/i;
1450
+ patterns[nf.Testing.Test.COMMANDS.LOCALE] = /^LOCALE\s+(\w+)\s*$/i;
1451
+ }