azebiki 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +75 -0
- data/ROADMAP.md +0 -0
- data/lib/azebiki.rb +1 -0
- data/lib/azebiki/azebiki.rb +234 -0
- data/lib/azebiki/version.rb +3 -0
- metadata +85 -0
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Eric Allam
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
## Overview
|
2
|
+
|
3
|
+
Azebiki provides a simple dsl for declaring CSS and XPATH matchers against a block of HTML content. It is built on top of Webrat matchers and nokogiri.
|
4
|
+
|
5
|
+
## Example:
|
6
|
+
|
7
|
+
Given this block of HTML:
|
8
|
+
|
9
|
+
<html>
|
10
|
+
<head><title>Example</title></head>
|
11
|
+
<body>
|
12
|
+
<!--
|
13
|
+
<a href="http://incomment.com">In Comment</a>
|
14
|
+
-->
|
15
|
+
<div id='main' class="big">
|
16
|
+
<p id="body">
|
17
|
+
<table class='short table'>
|
18
|
+
<thead>
|
19
|
+
<tr>
|
20
|
+
<th>First Column</th>
|
21
|
+
</tr>
|
22
|
+
</thead>
|
23
|
+
</table>
|
24
|
+
</p>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
Given this Azebiki::Checker definition:
|
28
|
+
|
29
|
+
c = Azebiki::Checker.new(HTML) do
|
30
|
+
div('#main.big') do
|
31
|
+
p('#body') do
|
32
|
+
table do
|
33
|
+
thead do
|
34
|
+
tr do
|
35
|
+
th(:content => 'First Column')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Will result in:
|
44
|
+
|
45
|
+
c.success?
|
46
|
+
# => true
|
47
|
+
|
48
|
+
Or given this definition:
|
49
|
+
|
50
|
+
c = Azebiki::Checker.new(HTML) do
|
51
|
+
a(:href => 'http://incomment.com', :content => 'In Comment')
|
52
|
+
end
|
53
|
+
|
54
|
+
Will result in:
|
55
|
+
|
56
|
+
c.success?
|
57
|
+
# => false
|
58
|
+
c.errors.inspect
|
59
|
+
# => ['Content should have included <a href="http://incomment.com">In Comment</a>, but did not']
|
60
|
+
|
61
|
+
Or give it a custom error message:
|
62
|
+
|
63
|
+
c = Azebiki::Checker.new(HTML) do
|
64
|
+
a(:href => 'http://incomment.com', :content => 'In Comment').failure_message('No tag :tag, SORRY!')
|
65
|
+
end
|
66
|
+
|
67
|
+
c.errors.inspect
|
68
|
+
# => ['No tag <a href="http://incomment.com">In Comment</a>, SORRY!']
|
69
|
+
|
70
|
+
|
71
|
+
## Install
|
72
|
+
|
73
|
+
gem install azebiki
|
74
|
+
|
75
|
+
|
data/ROADMAP.md
ADDED
File without changes
|
data/lib/azebiki.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "azebiki/azebiki"
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'webrat/core/matchers/have_selector'
|
2
|
+
require 'webrat/core/matchers/have_content'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
# This class is useful for making sure an HTML text has certain tags/content
|
6
|
+
# It supports both XML and HTML. For example, if you want to make sure someone has included
|
7
|
+
# a link that points to google, has no follow, with the text 'Google Sucks!':
|
8
|
+
# c = Checker.new(html) do |v|
|
9
|
+
# v.matches('a', :href => "http://google.com", :rel => "nofollow", :content => "Google Sucks!")
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# c.success? == true # if html does include the link to google
|
13
|
+
# c.errors == [] # errors is a list of error messages for each match that did not succeed, which can be customized:
|
14
|
+
#
|
15
|
+
# c = Checker.new(html) do |v|
|
16
|
+
# v.matches('a', :href => "http://google.com", :rel => "nofollow", :content => "Google Sucks!").failure_message('Sorry, no google link, should have :tag')
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# c.errors
|
20
|
+
#=> ["Sorry, no google link, should have <a href='http://google.com' rel='nofollow'>Google Sucks!</a>"]
|
21
|
+
#
|
22
|
+
# You can also nest matches, to match against children content. For example, if you
|
23
|
+
# want check for an image link:
|
24
|
+
#
|
25
|
+
# c = Checker.new(html) do |v|
|
26
|
+
# v.matches('a', :href => "http://google.com", :rel => "nofollow", :content => "Google Sucks!") do |a|
|
27
|
+
# a.matches('img', :src => 'http://google.com/sucks.png')
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# which will succeed only if the img tag is nested below the a tag. You can nest matchers pretty deep
|
32
|
+
module Azebiki
|
33
|
+
class Checker
|
34
|
+
|
35
|
+
class MyHaveSelector < Webrat::Matchers::HaveSelector
|
36
|
+
def add_attributes_conditions_to(query)
|
37
|
+
attribute_conditions = []
|
38
|
+
|
39
|
+
@options.each do |key, value|
|
40
|
+
next if [:content, :count].include?(key)
|
41
|
+
if value.is_a?(Hash)
|
42
|
+
func, match = value.keys.first, value.values.first
|
43
|
+
attribute_conditions << "#{func}(@#{key}, #{xpath_escape(match)})"
|
44
|
+
else
|
45
|
+
attribute_conditions << "@#{key} = #{xpath_escape(value)}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if attribute_conditions.any?
|
50
|
+
query << "[#{attribute_conditions.join(' and ')}]"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def matches?(stringlike, &block)
|
55
|
+
@block ||= block
|
56
|
+
matched = matches(stringlike)
|
57
|
+
|
58
|
+
if @options[:count]
|
59
|
+
matched.size == @options[:count] && (!@block || @block.call(matched))
|
60
|
+
else
|
61
|
+
matched.any? && (!@block || @block.call(matched))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class MatcherProxy
|
69
|
+
|
70
|
+
def initialize(have_matcher)
|
71
|
+
@have_matcher = have_matcher
|
72
|
+
@failure_message = "Content should have included #{content_message}, but did not"
|
73
|
+
end
|
74
|
+
|
75
|
+
def failure_message(new_failure_message)
|
76
|
+
@failure_message = new_failure_message.gsub(/:tag/, content_message)
|
77
|
+
end
|
78
|
+
|
79
|
+
def content_message
|
80
|
+
if @have_matcher.respond_to?(:tag_inspect)
|
81
|
+
@have_matcher.tag_inspect
|
82
|
+
else
|
83
|
+
@have_matcher.content_message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def matches?(content)
|
88
|
+
@have_matcher.matches?(content)
|
89
|
+
end
|
90
|
+
|
91
|
+
def message
|
92
|
+
@failure_message
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class MatcherBuilder < BasicObject
|
98
|
+
|
99
|
+
attr_reader :tags, :contents, :failure_message
|
100
|
+
|
101
|
+
def initialize(&block)
|
102
|
+
@tags = []
|
103
|
+
@contents = []
|
104
|
+
instance_eval &block
|
105
|
+
end
|
106
|
+
|
107
|
+
def has_content(text)
|
108
|
+
@contents << text
|
109
|
+
end
|
110
|
+
|
111
|
+
def method_missing(method, *args, &block)
|
112
|
+
tag = {:tag_name => method.to_s}
|
113
|
+
|
114
|
+
if args.first.is_a?(::String)
|
115
|
+
id_or_class = args.first
|
116
|
+
|
117
|
+
if id_or_class.split('#').size == 2
|
118
|
+
id_and_classes = id_or_class.split('#').last
|
119
|
+
id_and_classes = id_and_classes.split('.')
|
120
|
+
tag[:id] = id_and_classes.shift
|
121
|
+
tag[:class] = id_and_classes
|
122
|
+
end
|
123
|
+
|
124
|
+
elsif args.first.is_a?(::Hash)
|
125
|
+
tag.merge!(args.first)
|
126
|
+
end
|
127
|
+
|
128
|
+
if args[1] && args[1].is_a?(::Hash)
|
129
|
+
if classes = args[1][:class]
|
130
|
+
tag[:class] ||= []
|
131
|
+
classes.split('.').reject {|s| s.empty? }.each do |s|
|
132
|
+
tag[:class] << s
|
133
|
+
end
|
134
|
+
|
135
|
+
tag[:class].uniq!
|
136
|
+
end
|
137
|
+
|
138
|
+
tag.merge!(args[1])
|
139
|
+
end
|
140
|
+
|
141
|
+
if tag[:class]
|
142
|
+
tag[:class] = tag[:class].join(' ')
|
143
|
+
tag.delete(:class) if tag[:class].strip.empty?
|
144
|
+
end
|
145
|
+
|
146
|
+
tag[:child] = block
|
147
|
+
|
148
|
+
@tags << tag
|
149
|
+
|
150
|
+
def tag.failure_message(text)
|
151
|
+
self[:failure_message] = text
|
152
|
+
end
|
153
|
+
|
154
|
+
return tag
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
attr_accessor :content, :have_matchers, :errors
|
159
|
+
|
160
|
+
def initialize(content, &block)
|
161
|
+
@content = content
|
162
|
+
@errors = []
|
163
|
+
@have_matchers = []
|
164
|
+
@self_before_instance_eval = eval "self", block.binding
|
165
|
+
@matcher_builder = MatcherBuilder.new(&block)
|
166
|
+
build_contents
|
167
|
+
build_matchers
|
168
|
+
run_matchers
|
169
|
+
end
|
170
|
+
|
171
|
+
def contains(matching_text)
|
172
|
+
selector = MatcherProxy.new(Webrat::Matchers::HasContent.new(matching_text))
|
173
|
+
@have_matchers << selector
|
174
|
+
selector
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
def matches(name, attributes = {}, &block)
|
179
|
+
if block_given?
|
180
|
+
|
181
|
+
have = MyHaveSelector.new(name, attributes) do |n|
|
182
|
+
Azebiki::Checker.new(n, &block).success?
|
183
|
+
end
|
184
|
+
|
185
|
+
selector = MatcherProxy.new(have)
|
186
|
+
else
|
187
|
+
selector = MatcherProxy.new(MyHaveSelector.new(name, attributes))
|
188
|
+
end
|
189
|
+
|
190
|
+
@have_matchers << selector
|
191
|
+
selector
|
192
|
+
end
|
193
|
+
|
194
|
+
def success?
|
195
|
+
@success
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def build_contents
|
201
|
+
@matcher_builder.contents.each do |s|
|
202
|
+
contains s
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def method_missing(method, *args, &block)
|
207
|
+
@self_before_instance_eval.send method, *args, &block
|
208
|
+
end
|
209
|
+
|
210
|
+
def build_matchers
|
211
|
+
@matcher_builder.tags.each do |tag|
|
212
|
+
name = tag.delete(:tag_name)
|
213
|
+
b = tag.delete(:child)
|
214
|
+
attributes = tag
|
215
|
+
selector = matches(name, attributes, &b)
|
216
|
+
if tag[:failure_message]
|
217
|
+
selector.failure_message(tag[:failure_message])
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def run_matchers
|
223
|
+
return true if @have_matchers.empty?
|
224
|
+
@have_matchers.each do |selector|
|
225
|
+
if !selector.matches?(@content)
|
226
|
+
@errors << selector.message
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
@success = @errors.empty?
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: azebiki
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Eric Allam
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-06-20 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: webrat
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: ""
|
34
|
+
email:
|
35
|
+
- eric@envylabs.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/azebiki/azebiki.rb
|
44
|
+
- lib/azebiki/version.rb
|
45
|
+
- lib/azebiki.rb
|
46
|
+
- LICENSE
|
47
|
+
- CHANGELOG.md
|
48
|
+
- README.md
|
49
|
+
- ROADMAP.md
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/rubymaverick/azebiki
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 3
|
75
|
+
- 6
|
76
|
+
version: 1.3.6
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project: azebiki
|
80
|
+
rubygems_version: 1.3.7
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: A DSL for validating HTML
|
84
|
+
test_files: []
|
85
|
+
|