├── dist └── .gitignore ├── pkg └── .gitignore ├── .gitignore ├── spec ├── script │ ├── autotest │ └── rstakeout ├── base.js ├── json.html ├── results.html ├── lib │ ├── JSSpec.css │ ├── diff_match_patch.js │ └── JSSpec.js ├── gsa.html └── fixtures │ └── results.js ├── src ├── gsa-prototype.js.erb ├── json.js ├── results.js └── gsa.js ├── lib ├── protodoc.rb └── builder.js ├── LICENSE ├── README ├── Rakefile └── xsl └── json.xsl /dist/.gitignore: -------------------------------------------------------------------------------- 1 | [^.]* 2 | -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | [^.]* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | demo.html -------------------------------------------------------------------------------- /spec/script/autotest: -------------------------------------------------------------------------------- 1 | spec/script/rstakeout "rake spec" spec/*.html src/*.js -------------------------------------------------------------------------------- /spec/base.js: -------------------------------------------------------------------------------- 1 | //Prototype and Scriptaculous' Builder are required 2 | if(typeof Prototype == 'undefined') 3 | throw("prototype.js version > 1.6 is required"); 4 | if(typeof Builder == 'undefined') 5 | throw("script.aculo.us' builder.js library is required"); 6 | 7 | var Gsa = {}; -------------------------------------------------------------------------------- /src/gsa-prototype.js.erb: -------------------------------------------------------------------------------- 1 | <%= include '../lib/prototype.js' if ENV['WITH_PROTOTYPE'] || ENV['WITH_BUILDER'] %><%= include '../lib/builder.js' if ENV['WITH_BUILDER'] %>/* gsa-prototype, version <%= GSA_VERSION %> 2 | * (c) 2008 Jesse Newland 3 | * jnewland@gmail.com 4 | * 5 | * gsa-prototype is freely distributable under the terms of an MIT-style license. 6 | *--------------------------------------------------------------------------*/ 7 | 8 | //Prototype and Scriptaculous' Builder are required 9 | if(typeof Prototype == 'undefined') 10 | throw("prototype.js version > 1.6 is required"); 11 | if(typeof Builder == 'undefined') 12 | throw("script.aculo.us' builder.js library is required"); 13 | 14 | var Gsa = {}; 15 | 16 | <%= include 'gsa.js', 'results.js', 'json.js' %> -------------------------------------------------------------------------------- /lib/protodoc.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | class String 4 | def lines 5 | split $/ 6 | end 7 | 8 | def strip_whitespace_at_line_ends 9 | lines.map {|line| line.gsub(/\s+$/, '')} * $/ 10 | end 11 | end 12 | 13 | module Protodoc 14 | module Environment 15 | def include(*filenames) 16 | filenames.map {|filename| Preprocessor.new(filename).to_s}.join("\n") 17 | end 18 | end 19 | 20 | class Preprocessor 21 | include Environment 22 | 23 | def initialize(filename) 24 | @filename = File.expand_path(filename) 25 | @template = ERB.new(IO.read(@filename), nil, '%') 26 | end 27 | 28 | def to_s 29 | @template.result(binding).strip_whitespace_at_line_ends 30 | end 31 | end 32 | end 33 | 34 | if __FILE__ == $0 35 | print Protodoc::Preprocessor.new(ARGV.first) 36 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Jesse Newland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | gsa-prototype 2 | ============= 3 | 4 | Prototype/Javascript wrapper for the Google Search Appliance Search Protocol. Fancy cross-domain JSON support included. 5 | 6 | Install 7 | ======= 8 | 9 | gsa-prototype requires a custom XSL be installed on your Google Search Appliance 10 | 11 | * Login to the GSA Admin Console 12 | * Click 'Serving' on the sidebar 13 | * Create a new frontend named 'json' 14 | * Click 'Edit' beside the newly created frontend 15 | * Click 'Edit underlying XSLT code' 16 | * Select 'Import Stylesheet' 17 | * Import the template at xsl/json.xsl 18 | * Done! 19 | 20 | Usage 21 | ===== 22 | 23 | >>> var gsa = new Gsa('foo.com') 24 | >>> gsa.search('jesse newland') 25 | true 26 | >>> gsa.results.first().get('title') 27 | "LexBlog IT Director talks about today's platform upgrade : Real ..." 28 | >>> gsa.results.first().get('url') 29 | "http://kevin.lexblog.com/2007/07/articles/cool-stuff/lexblog-it-director-talks-about-todays-platform-upgrade/" 30 | 31 | See inline documentation in gsa.js for more details. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/packagetask' 3 | 4 | desc 'Specs!' 5 | task :test => :spec 6 | 7 | GSA_ROOT = File.expand_path(File.dirname(__FILE__)) 8 | GSA_SRC_DIR = File.join(GSA_ROOT, 'src') 9 | GSA_DIST_DIR = File.join(GSA_ROOT, 'dist') 10 | GSA_PKG_DIR = File.join(GSA_ROOT, 'pkg') 11 | GSA_VERSION = '0.2.0' 12 | 13 | desc 'Build a combined JS file for distibution' 14 | task :dist do 15 | $:.unshift File.join(GSA_ROOT, 'lib') 16 | require 'protodoc' 17 | 18 | Dir.chdir(GSA_SRC_DIR) do 19 | File.open(File.join(GSA_DIST_DIR, 'gsa-prototype.js'), 'w+') do |dist| 20 | dist << Protodoc::Preprocessor.new('gsa-prototype.js.erb') 21 | end 22 | end 23 | end 24 | 25 | Rake::PackageTask.new('gsa-prototype', GSA_VERSION) do |package| 26 | package.need_tar_gz = true 27 | package.package_dir = GSA_PKG_DIR 28 | package.package_files.include( 29 | '[A-Z]*', 30 | 'dist/gsa-prototype.js', 31 | 'lib/**', 32 | 'src/**', 33 | 'spec/**', 34 | 'xsl/**' 35 | ) 36 | end 37 | 38 | task :clean_package_source do 39 | rm_rf File.join(GSA_PKG_DIR, "gsa-prototype-#{VERSION}") 40 | end 41 | 42 | task :spec do 43 | files = ENV['STAKEOUT'] rescue 'spec/*.html' 44 | files = FileList[files] 45 | 46 | files.each do |file| 47 | if file =~ /\/([^\/]+)\.js$/ 48 | file = "spec/#{$1}.html" 49 | end 50 | unless File.exists?(file) 51 | puts "Notice: Test file does not exist: #{file}" 52 | next 53 | end 54 | `open #{file} -a Safari -g` 55 | end 56 | end 57 | 58 | #TODO create a task to build with builder.js prepended 59 | #TODO create a task to build with prototype.js and builder.js prepended 60 | #TODO create a task to compress the JS file we build -------------------------------------------------------------------------------- /spec/json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |#{snippet}...
#{snippet}...
" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "
actual value:
'); 650 | sb.push('' + JSSpec.util.inspect(this.actual) + '
'); 651 | sb.push('should ' + (this.condition ? '' : 'not') + ' include:
'); 652 | sb.push('' + JSSpec.util.inspect(this.expected) + '
'); 653 | sb.push('but since it\s not an array, include or not doesn\'t make any sense.
'); 654 | return sb.join(""); 655 | } 656 | ; 657 | JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() { 658 | var matches; 659 | if(this.condition) { 660 | for(var i = 0; i < this.actual.length; i++) { 661 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 662 | if(matches) { 663 | this.match = true; 664 | break; 665 | } 666 | } 667 | } else { 668 | for(var i = 0; i < this.actual.length; i++) { 669 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 670 | if(matches) { 671 | this.match = false; 672 | break; 673 | } 674 | } 675 | } 676 | 677 | if(this.match) return ""; 678 | 679 | var sb = []; 680 | sb.push('actual value:
'); 681 | sb.push('' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '
'); 682 | sb.push('should ' + (this.condition ? '' : 'not') + ' include:
'); 683 | sb.push('' + JSSpec.util.inspect(this.expected) + '
'); 684 | return sb.join(""); 685 | }; 686 | 687 | /** 688 | * PropertyLengthMatcher 689 | */ 690 | JSSpec.PropertyLengthMatcher = function(num, property, o, condition) { 691 | this.num = num; 692 | this.o = o; 693 | this.property = property; 694 | if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') { 695 | this.property = 'length'; 696 | } 697 | 698 | this.condition = condition; 699 | this.conditionMet = function(x) { 700 | if(condition == 'exactly') return x.length == num; 701 | if(condition == 'at least') return x.length >= num; 702 | if(condition == 'at most') return x.length <= num; 703 | 704 | throw "Unknown condition '" + condition + "'"; 705 | }; 706 | this.match = false; 707 | this.explaination = this.makeExplain(); 708 | }; 709 | 710 | JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() { 711 | if(this.o._type == 'String' && this.property == 'length') { 712 | this.match = this.conditionMet(this.o); 713 | return this.match ? '' : this.makeExplainForString(); 714 | } else if(typeof this.o.length != 'undefined' && this.property == "length") { 715 | this.match = this.conditionMet(this.o); 716 | return this.match ? '' : this.makeExplainForArray(); 717 | } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) { 718 | this.match = this.conditionMet(this.o[this.property]); 719 | return this.match ? '' : this.makeExplainForObject(); 720 | } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) { 721 | this.match = false; 722 | return this.makeExplainForNoProperty(); 723 | } 724 | 725 | this.match = true; 726 | }; 727 | 728 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() { 729 | var sb = []; 730 | 731 | var exp = this.num == 0 ? 732 | 'be an empty string' : 733 | 'have ' + this.condition + ' ' + this.num + ' characters'; 734 | 735 | sb.push('actual value has ' + this.o.length + ' characters:
'); 736 | sb.push('' + JSSpec.util.inspect(this.o) + '
'); 737 | sb.push('but it should ' + exp + '.
'); 738 | 739 | return sb.join(""); 740 | }; 741 | 742 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() { 743 | var sb = []; 744 | 745 | var exp = this.num == 0 ? 746 | 'be an empty array' : 747 | 'have ' + this.condition + ' ' + this.num + ' items'; 748 | 749 | sb.push('actual value has ' + this.o.length + ' items:
'); 750 | sb.push('' + JSSpec.util.inspect(this.o) + '
'); 751 | sb.push('but it should ' + exp + '.
'); 752 | 753 | return sb.join(""); 754 | }; 755 | 756 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() { 757 | var sb = []; 758 | 759 | var exp = this.num == 0 ? 760 | 'be empty' : 761 | 'have ' + this.condition + ' ' + this.num + ' ' + this.property + '.'; 762 | 763 | sb.push('actual value has ' + this.o[this.property].length + ' ' + this.property + ':
'); 764 | sb.push('' + JSSpec.util.inspect(this.o, false, this.property) + '
'); 765 | sb.push('but it should ' + exp + '.
'); 766 | 767 | return sb.join(""); 768 | }; 769 | 770 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() { 771 | var sb = []; 772 | 773 | sb.push('actual value:
'); 774 | sb.push('' + JSSpec.util.inspect(this.o) + '
'); 775 | sb.push('should have ' + this.condition + ' ' + this.num + ' ' + this.property + ' but there\'s no such property.
'); 776 | 777 | return sb.join(""); 778 | }; 779 | 780 | JSSpec.PropertyLengthMatcher.prototype.matches = function() { 781 | return this.match; 782 | }; 783 | 784 | JSSpec.PropertyLengthMatcher.prototype.explain = function() { 785 | return this.explaination; 786 | }; 787 | 788 | JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) { 789 | return new JSSpec.PropertyLengthMatcher(num, property, o, condition); 790 | }; 791 | 792 | /** 793 | * EqualityMatcher 794 | */ 795 | JSSpec.EqualityMatcher = {}; 796 | 797 | JSSpec.EqualityMatcher.createInstance = function(expected, actual) { 798 | if(expected == null || actual == null) { 799 | return new JSSpec.NullEqualityMatcher(expected, actual); 800 | } else if(expected._type && expected._type == actual._type) { 801 | if(expected._type == "String") { 802 | return new JSSpec.StringEqualityMatcher(expected, actual); 803 | } else if(expected._type == "Date") { 804 | return new JSSpec.DateEqualityMatcher(expected, actual); 805 | } else if(expected._type == "Number") { 806 | return new JSSpec.NumberEqualityMatcher(expected, actual); 807 | } else if(expected._type == "Array") { 808 | return new JSSpec.ArrayEqualityMatcher(expected, actual); 809 | } else if(expected._type == "Boolean") { 810 | return new JSSpec.BooleanEqualityMatcher(expected, actual); 811 | } 812 | } 813 | 814 | return new JSSpec.ObjectEqualityMatcher(expected, actual); 815 | }; 816 | 817 | JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) { 818 | var sb = []; 819 | 820 | sb.push(actualDesc || 'actual value:
'); 821 | sb.push('' + JSSpec.util.inspect(actual) + '
'); 822 | sb.push(expectedDesc || 'should be:
'); 823 | sb.push('' + JSSpec.util.inspect(expected) + '
'); 824 | 825 | return sb.join(""); 826 | }; 827 | 828 | JSSpec.EqualityMatcher.diffExplain = function(expected, actual) { 829 | var sb = []; 830 | 831 | sb.push('diff:
'); 832 | sb.push(''); 833 | 834 | var dmp = new diff_match_patch(); 835 | var diff = dmp.diff_main(expected, actual); 836 | dmp.diff_cleanupEfficiency(diff); 837 | 838 | sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true)); 839 | 840 | sb.push('
'); 841 | 842 | return sb.join(""); 843 | }; 844 | 845 | /** 846 | * BooleanEqualityMatcher 847 | */ 848 | JSSpec.BooleanEqualityMatcher = function(expected, actual) { 849 | this.expected = expected; 850 | this.actual = actual; 851 | }; 852 | 853 | JSSpec.BooleanEqualityMatcher.prototype.explain = function() { 854 | var sb = []; 855 | 856 | sb.push('actual value:
'); 857 | sb.push('' + JSSpec.util.inspect(this.actual) + '
'); 858 | sb.push('should be:
'); 859 | sb.push('' + JSSpec.util.inspect(this.expected) + '
'); 860 | 861 | return sb.join(""); 862 | }; 863 | 864 | JSSpec.BooleanEqualityMatcher.prototype.matches = function() { 865 | return this.expected == this.actual; 866 | }; 867 | 868 | /** 869 | * NullEqualityMatcher 870 | */ 871 | JSSpec.NullEqualityMatcher = function(expected, actual) { 872 | this.expected = expected; 873 | this.actual = actual; 874 | }; 875 | 876 | JSSpec.NullEqualityMatcher.prototype.matches = function() { 877 | return this.expected == this.actual && typeof this.expected == typeof this.actual; 878 | }; 879 | 880 | JSSpec.NullEqualityMatcher.prototype.explain = function() { 881 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 882 | }; 883 | 884 | JSSpec.DateEqualityMatcher = function(expected, actual) { 885 | this.expected = expected; 886 | this.actual = actual; 887 | }; 888 | 889 | JSSpec.DateEqualityMatcher.prototype.matches = function() { 890 | return this.expected.getTime() == this.actual.getTime(); 891 | }; 892 | 893 | JSSpec.DateEqualityMatcher.prototype.explain = function() { 894 | var sb = []; 895 | 896 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 897 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString())); 898 | 899 | return sb.join(""); 900 | }; 901 | 902 | /** 903 | * ObjectEqualityMatcher 904 | */ 905 | JSSpec.ObjectEqualityMatcher = function(expected, actual) { 906 | this.expected = expected; 907 | this.actual = actual; 908 | this.match = this.expected == this.actual; 909 | this.explaination = this.makeExplain(); 910 | }; 911 | 912 | JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match}; 913 | 914 | JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination}; 915 | 916 | JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() { 917 | if(this.expected == this.actual) { 918 | this.match = true; 919 | return ""; 920 | } 921 | 922 | if(JSSpec.util.isDomNode(this.expected)) { 923 | return this.makeExplainForDomNode(); 924 | } 925 | 926 | var key, expectedHasItem, actualHasItem; 927 | 928 | for(key in this.expected) { 929 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 930 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 931 | if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key); 932 | } 933 | for(key in this.actual) { 934 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 935 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 936 | if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key); 937 | } 938 | 939 | for(key in this.expected) { 940 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]); 941 | if(!matcher.matches()) return this.makeExplainForItemMismatch(key); 942 | } 943 | 944 | this.match = true; 945 | }; 946 | 947 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) { 948 | var sb = []; 949 | 950 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 951 | 952 | return sb.join(""); 953 | }; 954 | 955 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) { 956 | var sb = []; 957 | 958 | sb.push('actual value has no item named ' + JSSpec.util.inspect(key) + '
'); 959 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); 960 | sb.push('but it should have the item whose value is ' + JSSpec.util.inspect(this.expected[key]) + '
'); 961 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); 962 | 963 | return sb.join(""); 964 | }; 965 | 966 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) { 967 | var sb = []; 968 | 969 | sb.push('actual value has item named ' + JSSpec.util.inspect(key) + '
'); 970 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); 971 | sb.push('but there should be no such item
'); 972 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); 973 | 974 | return sb.join(""); 975 | }; 976 | 977 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) { 978 | var sb = []; 979 | 980 | sb.push('actual value has an item named ' + JSSpec.util.inspect(key) + ' whose value is ' + JSSpec.util.inspect(this.actual[key]) + '
'); 981 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); 982 | sb.push('but it\'s value should be ' + JSSpec.util.inspect(this.expected[key]) + '
'); 983 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); 984 | 985 | return sb.join(""); 986 | }; 987 | 988 | 989 | 990 | 991 | /** 992 | * ArrayEqualityMatcher 993 | */ 994 | JSSpec.ArrayEqualityMatcher = function(expected, actual) { 995 | this.expected = expected; 996 | this.actual = actual; 997 | this.match = this.expected == this.actual; 998 | this.explaination = this.makeExplain(); 999 | }; 1000 | 1001 | JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match}; 1002 | 1003 | JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination}; 1004 | 1005 | JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() { 1006 | if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch(); 1007 | 1008 | for(var i = 0; i < this.expected.length; i++) { 1009 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]); 1010 | if(!matcher.matches()) return this.makeExplainForItemMismatch(i); 1011 | } 1012 | 1013 | this.match = true; 1014 | }; 1015 | 1016 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() { 1017 | return JSSpec.EqualityMatcher.basicExplain( 1018 | this.expected, 1019 | this.actual, 1020 | 'but it should be ' + this.expected.length + '
', 1021 | 'actual value has ' + this.actual.length + ' items
' 1022 | ); 1023 | }; 1024 | 1025 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) { 1026 | var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)]; 1027 | 1028 | var sb = []; 1029 | 1030 | sb.push('' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is ' + JSSpec.util.inspect(this.actual[index]) + ':
'); 1031 | sb.push('' + JSSpec.util.inspect(this.actual, false, index) + '
'); 1032 | sb.push('but it should be ' + JSSpec.util.inspect(this.expected[index]) + ':
'); 1033 | sb.push('' + JSSpec.util.inspect(this.expected, false, index) + '
'); 1034 | 1035 | return sb.join(""); 1036 | }; 1037 | 1038 | /** 1039 | * NumberEqualityMatcher 1040 | */ 1041 | JSSpec.NumberEqualityMatcher = function(expected, actual) { 1042 | this.expected = expected; 1043 | this.actual = actual; 1044 | }; 1045 | 1046 | JSSpec.NumberEqualityMatcher.prototype.matches = function() { 1047 | if(this.expected == this.actual) return true; 1048 | }; 1049 | 1050 | JSSpec.NumberEqualityMatcher.prototype.explain = function() { 1051 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 1052 | }; 1053 | 1054 | /** 1055 | * StringEqualityMatcher 1056 | */ 1057 | JSSpec.StringEqualityMatcher = function(expected, actual) { 1058 | this.expected = expected; 1059 | this.actual = actual; 1060 | }; 1061 | 1062 | JSSpec.StringEqualityMatcher.prototype.matches = function() { 1063 | if(this.expected == this.actual) return true; 1064 | }; 1065 | 1066 | JSSpec.StringEqualityMatcher.prototype.explain = function() { 1067 | var sb = []; 1068 | 1069 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 1070 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual)); 1071 | return sb.join(""); 1072 | }; 1073 | 1074 | /** 1075 | * PatternMatcher 1076 | */ 1077 | JSSpec.PatternMatcher = function(actual, pattern, condition) { 1078 | this.actual = actual; 1079 | this.pattern = pattern; 1080 | this.condition = condition; 1081 | this.match = false; 1082 | this.explaination = this.makeExplain(); 1083 | }; 1084 | 1085 | JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) { 1086 | return new JSSpec.PatternMatcher(actual, pattern, condition); 1087 | }; 1088 | 1089 | JSSpec.PatternMatcher.prototype.makeExplain = function() { 1090 | var sb; 1091 | if(this.actual == null || this.actual._type != 'String') { 1092 | sb = []; 1093 | sb.push('actual value:
'); 1094 | sb.push('' + JSSpec.util.inspect(this.actual) + '
'); 1095 | sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
'); 1096 | sb.push('' + JSSpec.util.inspect(this.pattern) + '
'); 1097 | sb.push('but pattern matching cannot be performed.
'); 1098 | return sb.join(""); 1099 | } else { 1100 | this.match = this.condition == !!this.actual.match(this.pattern); 1101 | if(this.match) return ""; 1102 | 1103 | sb = []; 1104 | sb.push('actual value:
'); 1105 | sb.push('' + JSSpec.util.inspect(this.actual) + '
'); 1106 | sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
'); 1107 | sb.push('' + JSSpec.util.inspect(this.pattern) + '
'); 1108 | return sb.join(""); 1109 | } 1110 | }; 1111 | 1112 | JSSpec.PatternMatcher.prototype.matches = function() { 1113 | return this.match; 1114 | }; 1115 | 1116 | JSSpec.PatternMatcher.prototype.explain = function() { 1117 | return this.explaination; 1118 | }; 1119 | 1120 | /** 1121 | * Domain Specific Languages 1122 | */ 1123 | JSSpec.DSL = {}; 1124 | 1125 | JSSpec.DSL.forString = { 1126 | normalizeHtml: function() { 1127 | var html = this; 1128 | 1129 | // Uniformize quotation, turn tag names and attribute names into lower case 1130 | html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) { 1131 | var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase()) 1132 | return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">" 1133 | }); 1134 | 1135 | // validation self-closing tags 1136 | html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) { 1137 | return "<" + tag + attrs + " />"; 1138 | }); 1139 | 1140 | // append semi-colon at the end of style value 1141 | html = html.replace(/style="(.*?)"/mg, function(str, styleStr) { 1142 | styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari 1143 | if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";" 1144 | 1145 | return 'style="' + styleStr + '"' 1146 | }); 1147 | 1148 | // sort style entries 1149 | 1150 | // remove empty style attributes 1151 | html = html.replace(/ style=";"/mg, ""); 1152 | 1153 | // remove new-lines 1154 | html = html.replace(/\r/mg, ''); 1155 | html = html.replace(/\n/mg, ''); 1156 | 1157 | return html; 1158 | } 1159 | }; 1160 | 1161 | 1162 | 1163 | JSSpec.DSL.describe = function(context, entries) { 1164 | JSSpec.specs.push(new JSSpec.Spec(context, entries)); 1165 | }; 1166 | 1167 | JSSpec.DSL.value_of = function(target) { 1168 | if(JSSpec._secondPass) return {}; 1169 | 1170 | var subject = new JSSpec.DSL.Subject(target); 1171 | return subject; 1172 | }; 1173 | 1174 | JSSpec.DSL.Subject = function(target) { 1175 | this.target = target; 1176 | }; 1177 | 1178 | JSSpec.DSL.Subject.prototype._type = 'Subject'; 1179 | 1180 | JSSpec.DSL.Subject.prototype.should_fail = function(message) { 1181 | JSSpec._assertionFailure = {message:message}; 1182 | throw JSSpec._assertionFailure; 1183 | }; 1184 | 1185 | JSSpec.DSL.Subject.prototype.should_be = function(expected) { 1186 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1187 | if(!matcher.matches()) { 1188 | JSSpec._assertionFailure = {message:matcher.explain()}; 1189 | throw JSSpec._assertionFailure; 1190 | } 1191 | }; 1192 | 1193 | JSSpec.DSL.Subject.prototype.should_not_be = function(expected) { 1194 | // TODO JSSpec.EqualityMatcher should support 'condition' 1195 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1196 | if(matcher.matches()) { 1197 | JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"}; 1198 | throw JSSpec._assertionFailure; 1199 | } 1200 | }; 1201 | 1202 | JSSpec.DSL.Subject.prototype.should_be_empty = function() { 1203 | this.should_have(0, this.getType() == 'String' ? 'characters' : 'items'); 1204 | }; 1205 | 1206 | JSSpec.DSL.Subject.prototype.should_not_be_empty = function() { 1207 | this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items'); 1208 | }; 1209 | 1210 | JSSpec.DSL.Subject.prototype.should_be_true = function() { 1211 | this.should_be(true); 1212 | }; 1213 | 1214 | JSSpec.DSL.Subject.prototype.should_be_false = function() { 1215 | this.should_be(false); 1216 | }; 1217 | 1218 | JSSpec.DSL.Subject.prototype.should_be_null = function() { 1219 | this.should_be(null); 1220 | }; 1221 | 1222 | JSSpec.DSL.Subject.prototype.should_be_undefined = function() { 1223 | this.should_be(undefined); 1224 | }; 1225 | 1226 | JSSpec.DSL.Subject.prototype.should_not_be_null = function() { 1227 | this.should_not_be(null); 1228 | }; 1229 | 1230 | JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() { 1231 | this.should_not_be(undefined); 1232 | }; 1233 | 1234 | JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) { 1235 | var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition); 1236 | if(!matcher.matches()) { 1237 | JSSpec._assertionFailure = {message:matcher.explain()}; 1238 | throw JSSpec._assertionFailure; 1239 | } 1240 | }; 1241 | 1242 | JSSpec.DSL.Subject.prototype.should_have = function(num, property) { 1243 | this._should_have(num, property, "exactly"); 1244 | }; 1245 | 1246 | JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) { 1247 | this._should_have(num, property, "exactly"); 1248 | }; 1249 | 1250 | JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) { 1251 | this._should_have(num, property, "at least"); 1252 | }; 1253 | 1254 | JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) { 1255 | this._should_have(num, property, "at most"); 1256 | }; 1257 | 1258 | JSSpec.DSL.Subject.prototype.should_include = function(expected) { 1259 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true); 1260 | if(!matcher.matches()) { 1261 | JSSpec._assertionFailure = {message:matcher.explain()}; 1262 | throw JSSpec._assertionFailure; 1263 | } 1264 | }; 1265 | 1266 | JSSpec.DSL.Subject.prototype.should_not_include = function(expected) { 1267 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false); 1268 | if(!matcher.matches()) { 1269 | JSSpec._assertionFailure = {message:matcher.explain()}; 1270 | throw JSSpec._assertionFailure; 1271 | } 1272 | }; 1273 | 1274 | JSSpec.DSL.Subject.prototype.should_match = function(pattern) { 1275 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true); 1276 | if(!matcher.matches()) { 1277 | JSSpec._assertionFailure = {message:matcher.explain()}; 1278 | throw JSSpec._assertionFailure; 1279 | } 1280 | } 1281 | JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) { 1282 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false); 1283 | if(!matcher.matches()) { 1284 | JSSpec._assertionFailure = {message:matcher.explain()}; 1285 | throw JSSpec._assertionFailure; 1286 | } 1287 | }; 1288 | 1289 | JSSpec.DSL.Subject.prototype.getType = function() { 1290 | if(typeof this.target == 'undefined') { 1291 | return 'undefined'; 1292 | } else if(this.target == null) { 1293 | return 'null'; 1294 | } else if(this.target._type) { 1295 | return this.target._type; 1296 | } else if(JSSpec.util.isDomNode(this.target)) { 1297 | return 'DomNode'; 1298 | } else { 1299 | return 'object'; 1300 | } 1301 | }; 1302 | 1303 | /** 1304 | * Utilities 1305 | */ 1306 | JSSpec.util = { 1307 | escapeTags: function(string) { 1308 | return string.replace(//img, '>'); 1309 | }, 1310 | parseOptions: function(defaults) { 1311 | var options = defaults; 1312 | 1313 | var url = location.href; 1314 | var queryIndex = url.indexOf('?'); 1315 | if(queryIndex == -1) return options; 1316 | 1317 | var query = url.substring(queryIndex + 1); 1318 | var pairs = query.split('&'); 1319 | for(var i = 0; i < pairs.length; i++) { 1320 | var tokens = pairs[i].split('='); 1321 | options[tokens[0]] = tokens[1]; 1322 | } 1323 | 1324 | return options; 1325 | }, 1326 | correctHtmlAttrQuotation: function(html) { 1327 | html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1328 | html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1329 | html = html.replace(/'/mg, '"'); 1330 | 1331 | return html; 1332 | }, 1333 | sortHtmlAttrs: function(html) { 1334 | var attrs = []; 1335 | html.replace(/((\w+)="[^"]+")/mg, function(str, matched) { 1336 | attrs.push(matched); 1337 | }); 1338 | return attrs.length == 0 ? "" : " " + attrs.sort().join(" "); 1339 | }, 1340 | sortStyleEntries: function(styleText) { 1341 | var entries = styleText.split(/; /); 1342 | return entries.sort().join("; "); 1343 | }, 1344 | escapeHtml: function(str) { 1345 | if(!this._div) { 1346 | this._div = document.createElement("DIV"); 1347 | this._text = document.createTextNode(''); 1348 | this._div.appendChild(this._text); 1349 | } 1350 | this._text.data = str; 1351 | return this._div.innerHTML; 1352 | }, 1353 | isDomNode: function(o) { 1354 | // TODO: make it more stricter 1355 | return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number'); 1356 | }, 1357 | inspectDomPath: function(o) { 1358 | var sb = []; 1359 | while(o && o.nodeName != '#document' && o.parent) { 1360 | var siblings = o.parentNode.childNodes; 1361 | for(var i = 0; i < siblings.length; i++) { 1362 | if(siblings[i] == o) { 1363 | sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']')); 1364 | break; 1365 | } 1366 | } 1367 | o = o.parentNode; 1368 | } 1369 | return sb.join(" > "); 1370 | }, 1371 | inspectDomNode: function(o) { 1372 | if(o.nodeType == 1) { 1373 | var nodeName = o.nodeName.toLowerCase(); 1374 | var sb = []; 1375 | sb.push(''); 1376 | sb.push("<"); 1377 | sb.push(nodeName); 1378 | 1379 | var attrs = o.attributes; 1380 | for(var i = 0; i < attrs.length; i++) { 1381 | if( 1382 | attrs[i].nodeValue && 1383 | attrs[i].nodeName != 'contentEditable' && 1384 | attrs[i].nodeName != 'style' && 1385 | typeof attrs[i].nodeValue != 'function' 1386 | ) sb.push(' ' + attrs[i].nodeName.toLowerCase() + '="' + attrs[i].nodeValue + '"'); 1387 | } 1388 | if(o.style && o.style.cssText) { 1389 | sb.push(' style="' + o.style.cssText + '"'); 1390 | } 1391 | sb.push('>'); 1392 | sb.push(JSSpec.util.escapeHtml(o.innerHTML)); 1393 | sb.push('</' + nodeName + '>'); 1394 | sb.push(' (' + JSSpec.util.inspectDomPath(o) + ')' ); 1395 | sb.push(''); 1396 | return sb.join(""); 1397 | } else if(o.nodeType == 3) { 1398 | return '#text ' + o.nodeValue + ''; 1399 | } else { 1400 | return 'UnknownDomNode'; 1401 | } 1402 | }, 1403 | inspect: function(o, dontEscape, emphasisKey) { 1404 | var sb, inspected; 1405 | 1406 | if(typeof o == 'undefined') return 'undefined'; 1407 | if(o == null) return 'null'; 1408 | if(o._type == 'String') return '"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '"'; 1409 | 1410 | if(o._type == 'Date') { 1411 | return '"' + o.toString() + '"'; 1412 | } 1413 | 1414 | if(o._type == 'Number') return '' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + ''; 1415 | 1416 | if(o._type == 'Boolean') return '' + o + ''; 1417 | 1418 | if(o._type == 'RegExp') return '' + JSSpec.util.escapeHtml(o.toString()) + ''; 1419 | 1420 | if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o); 1421 | 1422 | if(o._type == 'Array' || typeof o.length != 'undefined') { 1423 | sb = []; 1424 | for(var i = 0; i < o.length; i++) { 1425 | inspected = JSSpec.util.inspect(o[i]); 1426 | sb.push(i == emphasisKey ? ('' + inspected + '') : inspected); 1427 | } 1428 | return '[' + sb.join(', ') + ']'; 1429 | } 1430 | 1431 | // object 1432 | sb = []; 1433 | for(var key in o) { 1434 | if(key == 'should') continue; 1435 | 1436 | inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]); 1437 | sb.push(key == emphasisKey ? ('' + inspected + '') : inspected); 1438 | } 1439 | return '{' + sb.join(', ') + '}'; 1440 | } 1441 | }; 1442 | 1443 | describe = JSSpec.DSL.describe; 1444 | behavior_of = JSSpec.DSL.describe; 1445 | value_of = JSSpec.DSL.value_of; 1446 | expect = JSSpec.DSL.value_of; // @deprecated 1447 | 1448 | String.prototype._type = "String"; 1449 | Number.prototype._type = "Number"; 1450 | Date.prototype._type = "Date"; 1451 | Array.prototype._type = "Array"; 1452 | Boolean.prototype._type = "Boolean"; 1453 | RegExp.prototype._type = "RegExp"; 1454 | 1455 | var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype]; 1456 | 1457 | String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml; 1458 | String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated 1459 | 1460 | 1461 | 1462 | /** 1463 | * Main 1464 | */ 1465 | JSSpec.defaultOptions = { 1466 | autorun: 1, 1467 | specIdBeginsWith: 0, 1468 | exampleIdBeginsWith: 0, 1469 | autocollapse: 1 1470 | }; 1471 | JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions); 1472 | 1473 | JSSpec.Spec.id = JSSpec.options.specIdBeginsWith; 1474 | JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith; 1475 | 1476 | 1477 | 1478 | window.onload = function() { 1479 | if(JSSpec.specs.length > 0) { 1480 | if(!JSSpec.options.inSuite) { 1481 | JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger()); 1482 | if(JSSpec.options.rerun) { 1483 | JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun)); 1484 | } else { 1485 | JSSpec.runner.run(); 1486 | } 1487 | } else { 1488 | // in suite, send all specs to parent 1489 | var parentWindow = window.frames.parent.window; 1490 | for(var i = 0; i < JSSpec.specs.length; i++) { 1491 | parentWindow.JSSpec.specs.push(JSSpec.specs[i]); 1492 | } 1493 | } 1494 | } else { 1495 | var links = document.getElementById('list').getElementsByTagName('A'); 1496 | var frameContainer = document.createElement('DIV'); 1497 | frameContainer.style.display = 'none'; 1498 | document.body.appendChild(frameContainer); 1499 | 1500 | for(var i = 0; i < links.length; i++) { 1501 | var frame = document.createElement('IFRAME'); 1502 | frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000); 1503 | frameContainer.appendChild(frame); 1504 | } 1505 | } 1506 | } --------------------------------------------------------------------------------