assert_xpath 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,594 @@
1
+ require 'assert_xpath'
2
+ require 'tempfile'
3
+ require 'cgi'
4
+
5
+ # These assertions soak in Javascript::PurePerl goodness
6
+
7
+ #:stopdoc:
8
+ # ERGO this module should use search
9
+ # ERGO assert_javascript should only use assert_rexml not assert_xml
10
+ # ERGO tell crew to set eblogs' background color correctly...
11
+ # ERGO return value on each method
12
+ # ERGO assert_js_remote_function should take url_for args
13
+ # ERGO move got_pure_perl? rdoc to AssertJavaScript page
14
+ # ERGO clicking in Konsole navigates in Kate
15
+ # ERGO any matcher in an assert_js should search
16
+ #:startdoc:
17
+
18
+ def temporarily(obj, member, new_value)
19
+ old_value = obj.send(member)
20
+
21
+ begin
22
+ obj.send(member.to_s+'=', new_value) # CONSIDER look up the assign thinger?
23
+ yield
24
+ ensure
25
+ obj.send(member.to_s+'=', old_value)
26
+ end
27
+ end # ERGO document!
28
+
29
+ =begin rdoc
30
+ See: http://assertxpath.rubyforge.org/
31
+ %html <pre>
32
+ #\ #.:# +------------------------------------------+
33
+ #\ #.:# ^^^^^ ___/ these assertions use Javascript::PurePerl \
34
+ #\ #.:# }OvO{ <___ to convert JS into XML. assert_xpath can |
35
+ ##.:# {| |} | query this to find important details, |
36
+ #..# \| |/ \ and safely skip over unimportant details! /
37
+ #..# \ _ / +------------------------------------------+
38
+ #..# | |
39
+ #..#>=======d=b=======
40
+ #..#
41
+ %html </pre>
42
+ =end
43
+
44
+ module AssertJavaScript
45
+ include AssertXPath
46
+
47
+ # %html <a name='assert_javascript'></a>
48
+ # Wraps <tt>Javascript::PurePerl</tt> to convert JavaScript into XML describing
49
+ # each lexeme. This allows subsequent +assert_xpath+ calls to read the JavaScript.
50
+ #
51
+ # See {Javascript::PurePerl for Ruby Enthusiasts}[http://phlip.eblogs.com/2007/07/28/javascriptpureperl-for-ruby-enthusiasts/]
52
+ # to learn to install Javascript::PurePerl
53
+ # * +source+ - optional string containing JavaScript. The default is
54
+ # <tt>@response.body</tt>
55
+ # * +diagnostic+ - optional string to add to failure message
56
+ # Other +assert_js_+* methods call +assert_javascript+ if the secret
57
+ # <tt>@xdoc</tt> instance variable is +nil+
58
+ #
59
+ # To extract JS from an XHTML page, place the +assert_js+ inside the +assert_xpath+
60
+ # which located your JavaScript. For example:
61
+ # assert_xpath './/input[ @type = "image" ]' do |input|
62
+ # assert_javascript input.onclick
63
+ # assert_xpath './/Statement[2]' do
64
+ # assert_js_remote_function '/user/inventory/' do
65
+ # json = assert_js_argument(2)
66
+ # params = assert_params('uri?' + json[:parameters]).last
67
+ # assert_equal @user.id.to_s, params[:user_id]
68
+ # assert_equal @user.inventory.first.id.to_s, params[:inventory_id]
69
+ # end
70
+ # end
71
+ # end
72
+ # assert_tag_id :div, :inventory_bar # <-- applies to original XML
73
+ #
74
+ # See: AssertJavaScriptTest#test_assert_javascript
75
+ #
76
+ def assert_javascript(source = nil, diagnostic = nil)
77
+ javascript_to_xml(source, diagnostic) do |tmp_error|
78
+ @xdoc = assert_xpath(
79
+ '/AST/Program/SourceElements',
80
+ build_message(
81
+ diagnostic,
82
+ "JavaScript <#{source}> contained errors:\n#{File.read(tmp_error) rescue nil}"
83
+ )
84
+ )
85
+ end # ERGO if any diagnostic is a lambda, lazily evaluate it
86
+ end
87
+ alias assert_js assert_javascript
88
+
89
+ def assert_javascript_too(&block) #:nodoc: # ERGO merge with assert_javascript!!!
90
+ @xdoc or assert_javascript
91
+
92
+ sit_and_spin = './/VariableDeclaration/' +
93
+ 'Identifier[ @name = "Identifier" ]/../' +
94
+ 'Initializer[ @name = "Initializer" ]/' +
95
+ 'Number[ @name = "AssignmentExpression" ]/../..'
96
+ stuff = {}
97
+
98
+ @xdoc.each_element(sit_and_spin) do |node|
99
+ name = REXML::XPath.first(node, './/Identifier').text
100
+ number = REXML::XPath.first(node, './/Number').text
101
+ stuff[name.to_sym] = number.to_f
102
+ # ERGO is to_f best?
103
+ # ERGO simpler way to add an ostruct member?
104
+ # ERGO cleaner XPath...
105
+ end
106
+
107
+ js = OpenStruct.new(stuff)
108
+ block.call(js)
109
+ end
110
+
111
+ # ERGO what is "id('content')/div[1]/" ? Can we do that?
112
+ # ERGO "put the subtle to the metal..."
113
+
114
+ # Negates +assert_javascript+. Inexplicably passes if a string does not contain
115
+ # anything which satisfies <tt>Javascript::PurePerl</tt>'s narrow definition of
116
+ # JavaScript. Depends on +assert_javascript+
117
+ #
118
+ # * +source+ - optional string that should not contain JavaScript. The default is @response.body
119
+ # * +diagnostic+ - optional string to add to failure message
120
+ #
121
+ # Note that "var = 2" will pass, (and fail in +assert_javascript+)
122
+ # because <tt>Javascript::PurePerl</tt> requires a trailing <tt>;</tt>
123
+ #
124
+ # Example:
125
+ # The author would be interested to hear if anyone finds a use for this
126
+ #
127
+ def deny_javascript(source = nil, diagnostic = nil)
128
+ javascript_to_xml(source, diagnostic) do
129
+ stash_xdoc do
130
+ deny_xpath( '/AST/Program/SourceElements',
131
+ build_message(
132
+ diagnostic,
133
+ "string <#{_esc source}> should not be well-formed JavaScript"
134
+ )
135
+ )
136
+ end
137
+ end
138
+ end
139
+ alias deny_js deny_javascript
140
+
141
+ unless respond_to? :returning
142
+ def returning(value) #:nodoc:
143
+ yield(value)
144
+ return value
145
+ end
146
+ end
147
+
148
+ # ERGO "if that Congress still can't budge
149
+ # daddy's in tight with a Supreme Court judge"
150
+
151
+ # %html <a name='assert_json'></a>
152
+ # Converts a REXML::Element into a Hash containing named elements. Currently
153
+ # we only support Booleans or Strings
154
+ #
155
+ # This depend on methods like assert_js_argument to locate a JSON argument.
156
+ # Otherwise, the method searches for the first JSON it finds in
157
+ # the current context
158
+ # * +jsonic+ - the input REXML::Element node - defaults to the secret <tt>@xdoc</tt>
159
+ # variable set by +assert_javascript+.
160
+ #
161
+ # Example:
162
+ #
163
+ # %transclude AssertJavaScriptTest#test_assert_json
164
+ #
165
+ def assert_json(jsonic = @xdoc || assert_javascript, diagnostic = nil)
166
+ @xdoc or assert_javascript
167
+
168
+ returning Hash.new do |json|
169
+ stash_xdoc do
170
+ @xdoc = jsonic
171
+ assert_any_xpath 'descendant-or-self::PropertyNameAndValueList/PropertyPair', diagnostic
172
+ end # ERGO use assert_any_xpath
173
+
174
+ jsonic.each_element('descendant-or-self::PropertyNameAndValueList/PropertyPair') do |node|
175
+ name = REXML::XPath.first(node, 'PropertyName/*')
176
+ name = name.text.to_sym
177
+
178
+ json[name] =
179
+ case
180
+ when b = REXML::XPath.first(node, 'ObjectLiteral/PropertyNameAndValueList')
181
+ assert_json(b)
182
+
183
+ when b = REXML::XPath.first(node, 'Boolean')
184
+ b.text == '1'
185
+
186
+ when b = REXML::XPath.first(node, 'Number')
187
+ b.text
188
+
189
+ # ERGO how to do an or in an XPath??
190
+
191
+ when b = REXML::XPath.first(node, 'String')
192
+ b.text
193
+
194
+ when b = REXML::XPath.first(node, 'Identifier')
195
+ b.text # ERGO test me!
196
+
197
+ when b = REXML::XPath.first(node, 'ArrayLiteral/ElementList')
198
+ # ERGO recurse here
199
+ returning [] do |list|
200
+ b.each_element('*') do |item|
201
+ list << item.text # ERGO more accurate
202
+ end
203
+ end
204
+
205
+ else
206
+ # ERGO handle embedded JS expressons!
207
+ #puts indent_xml(node)
208
+ # ERGO flunk "Add type #{node.children.last.name} to assert_json!"
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ # Explores a function call's argument list
215
+ # * +index+ - 1-based index into the arguments
216
+ # * +diagnostic+ - optional string to add to failure message
217
+ #
218
+ # Example: AssertJavaScriptTest#test_assert_js_argument
219
+ #
220
+ def assert_js_argument(index = 1, diagnostic = nil)
221
+ @xdoc or assert_javascript
222
+ # ERGO rdoc: what do we depend on?
223
+ # ERGO propagate xpathic 'descendant-or-self'
224
+
225
+ assert_xpath xpath_argument(index), diagnostic do |node|
226
+ return node.text if %w(String Identifier).include?(node.name)
227
+ return assert_json if node.name == 'ObjectLiteral'
228
+ end # ERGO test we slip not into a nested argument list
229
+ end
230
+
231
+ def deny_js_argument(index = 1, diagnostic = nil)
232
+ @xdoc or assert_javascript
233
+ # ERGO rdoc: what do we depend on?
234
+ deny_xpath xpath_argument(index), diagnostic
235
+ end
236
+
237
+ # Not ready for public use yet!
238
+ #
239
+ def assert_params(path_query)
240
+ path, query = path_query.split('?')
241
+ params = {}
242
+
243
+ if query
244
+ query.split('&').each do |item|
245
+ key, value = item.split('=')
246
+ params[key.to_sym] = CGI::unescape(value)
247
+ end
248
+ end
249
+
250
+ return [path, params]
251
+ end # ERGO get this working on incomplete URLs
252
+
253
+ # ERGO sick local tests for all this stuff that the MMORPG tests
254
+ # ERGO links out to all this stuff
255
+ # ERGO tag_id -> element_id
256
+
257
+ # Detect the JavaScriptGenerator method +replace_html+
258
+ # * +element_id+ - first argument to +replace_html+ -
259
+ # the target HTML Element's +id+
260
+ # * +matcher+ - optional regular expression to match
261
+ # the raw contents of the second argument to +replace_html+
262
+ # * +diagnostic+ - optional string to add to failure message
263
+ # * +block+ - optional; permits +assert_xpath+ calls
264
+ # into the HTML contents of the replacement
265
+ #
266
+ # Example: AssertJavaScriptTest#test_assert_js_replace_html
267
+ #
268
+ def assert_js_replace_html(element_id, matcher = nil, diagnostic = nil, &block)
269
+ assert_xpath object_method_xpath('Element', 'update', element_id), diagnostic do
270
+ assert_equal element_id.to_s, assert_js_argument(1)
271
+ assert_match matcher, assert_js_argument(2), diagnostic if matcher
272
+ # ERGO fetch argument 2 as yielded xml and match its inner text
273
+ if block
274
+ assert_js_xml(assert_js_argument(2)) # ERGO do this even without the if
275
+ block.call
276
+ end
277
+ end
278
+ end
279
+
280
+ # Negates assert_js_replace_html
281
+ # * +element_id+ - first argument to +replace_html+ -
282
+ # the target HTML Element's +id+
283
+ # * +matcher+ - optional regular expression to not match
284
+ # the raw contents of the second argument to +replace_html+
285
+ # * +diagnostic+ - optional string to add to failure message
286
+ # If the +matcher+ is +nil+ or not provided, the +element_id+
287
+ # must /not/ match any +Element.update+ call in its context.
288
+ # If the +matcher+ /is/ provided, the +element_id+ /must/
289
+ # match, and the +matcher+ must /not/ agree with its contents
290
+ #
291
+ # Example: AssertJavaScriptTest#test_deny_js_replace_html
292
+ #
293
+ def deny_js_replace_html(element_id, matcher = nil, diagnostic = nil)
294
+ path = object_method_xpath('Element', 'update', element_id)
295
+
296
+ # ERGO don't let subsequent updates with the same element_id confuse the matcher!
297
+
298
+ if matcher and node = REXML::XPath.first(@xdoc, path)
299
+ stash_xdoc do
300
+ @xdoc = node
301
+ assert_no_match matcher, assert_js_argument(2), diagnostic
302
+ return # ERGO pass node for 1st arg to assert_js_argument
303
+ end
304
+ end
305
+
306
+ deny_xpath path, diagnostic
307
+ end
308
+
309
+ # ERGO this line should not crash on bad page:
310
+ # yar = YarWiki.new(params[:id].to_s)
311
+ # ERGO provide link for Flash if you have none
312
+
313
+ # Detects an Insertion attack.
314
+ # * +orientation+ - <tt>:top</tt>, <tt>:bottom</tt>, etc...
315
+ # Remaining arguments similar to assert_js_replace_html
316
+ #
317
+ # Example:
318
+ # %transclude AssertJavaScriptTest#test_assert_js_insert_html
319
+ #
320
+ def assert_js_insert_html(orientation, element_id, matcher = //, diagnostic = nil)
321
+ orientation = orientation.to_s.capitalize
322
+
323
+ # ERGO the matcher should work the same as assert_js_replace_html
324
+
325
+ assert_any_xpath object_method_xpath('Insertion', orientation, element_id), matcher, diagnostic do
326
+ assert_equal element_id.to_s, assert_js_argument(1), diagnostic # note: shouldn't happen!
327
+ assert_js_xml(assert_js_argument(2)) # ERGO good error diagnostic if _this_ fails!
328
+ yield if block_given? # ERGO yield something?
329
+ return @xdoc # ERGO everyone should return their guts like this
330
+ end
331
+ end # ERGO local test for me
332
+
333
+ # Detects an +Ajax.Update+ or +Ajax.Request+ call
334
+ # * +action+ - optional - the HTTP Post action - the first
335
+ # argument to +Ajax.Update+ or +Ajax.Request+
336
+ # * +tag_id+ - any of the following
337
+ # * - <tt>:request</tt> -
338
+ # * - <tt>:update</tt> -
339
+ # * +diagnostic+ - optional string to add to failure message
340
+ #
341
+ # Example:
342
+ #
343
+ def assert_js_remote_function( action = nil,
344
+ tag_id = nil,
345
+ diagnostic = nil )
346
+ matcher = // # ERGO
347
+
348
+ method = if tag_id.class === String or
349
+ tag_id.class == Regexp # ERGO test regexp
350
+ 'Update'
351
+ elsif tag_id == :request # ERGO test this branch
352
+ 'Request'
353
+ elsif tag_id == :update # ERGO test this branch
354
+ 'Update'
355
+ else
356
+ "Request' or . = 'Update"
357
+ end
358
+
359
+ xpath = object_method_xpath('Ajax', method, action)
360
+
361
+ # ERGO search via tag_id!!!
362
+
363
+ assert_any_xpath xpath, //, diagnostic do
364
+ if (tag_id.class != String and tag_id.class != Regexp) or
365
+ /#{tag_id}/ =~ assert_js_argument(2)
366
+ if action
367
+ assert_equal action.to_s.gsub('&amp;', '&'),
368
+ assert_js_argument(1),
369
+ diagnostic
370
+ end
371
+ # ERGO a better system to extract GET parameters
372
+ yield if block_given? # ERGO test this block
373
+ return @xdoc # ERGO test that return
374
+ end
375
+ end # ERGO less confusing error diagnostic if this fails
376
+
377
+ flunk build_message(diagnostic, "expected id <#{tag_id}>")
378
+ end
379
+
380
+ # ERGO test via a javascript-like dsl:
381
+ # assert_javascript do
382
+ # js_if, js_replace_html, etc...
383
+ # var(:x) = 42
384
+ # object(:Render).update :div_name etc.
385
+ # end
386
+
387
+ # ERGO diagnostic
388
+
389
+ # Not ready for public use!
390
+ #
391
+ def assert_js_xml(q) # ERGO explain this beast; hide inside assert_js_*
392
+ xml = eval('"' + q + '"')
393
+ assert_xml xml # ERGO what are the errors if these fail?
394
+ end # ERGO test the error recoverer in assert_xml
395
+
396
+ # Not ready for public use!
397
+ #
398
+ # Example:
399
+ #
400
+ def assert_js_call(*args, &block)
401
+ path, arg1, arg2, diagnostic = *calling_js_path(args)
402
+ assert_xpath path, diagnostic, &block
403
+ end # ERGO local tests for all these
404
+
405
+ #
406
+ # * ++ -
407
+ # * +diagnostic+ - optional string to add to failure message
408
+ #
409
+ # Example:
410
+ #
411
+ def deny_js_call(*args)
412
+ path, arg1, arg2, diagnostic = calling_js_path(args)
413
+ # ERGO do something with arg1 and arg2
414
+ deny_xpath path, diagnostic
415
+ end
416
+
417
+ # %html <a name='assert_js_show'></a>
418
+ # Detects the Element.show or Element.hide commands
419
+ # * +element_id+ - HTML Element +id+ to target
420
+ # * +visibility+ - defaults to :show; optionally :hide
421
+ #
422
+ def assert_js_show(element_id, visibility = :show) # ERGO also deny, also diagnostic
423
+ assert_xpath object_method_xpath(:Element, visibility, element_id) do
424
+ assert_equal element_id.to_s, assert_js_argument(1)
425
+ end
426
+ # ERGO what if the visibility is wrong?
427
+ end # ERGO deny for every assert
428
+
429
+ # Alias for assert_js_show(element_id, :hide)
430
+ #
431
+ def assert_js_hide(element_id) assert_js_show(element_id, :hide) end
432
+
433
+ # Find a JavaScript if statement, or one of its elements
434
+ # * +*args+ - ERGO!
435
+ # * +diagnostic+ - optional string to add to failure message
436
+ #
437
+ # Example:
438
+ #
439
+ def assert_js_if(*args, &block)
440
+ return assert_any_xpath(*_re_arg_if(*args), &block)
441
+ end
442
+
443
+ # Fails if a JavaScript +if+ expression or block has the given characteristics
444
+ # * +*args+ - ERGO!
445
+ # * +diagnostic+ - optional string to add to failure message
446
+ #
447
+ # Example:
448
+ #
449
+ def deny_js_if(condition = :all, matcher = nil, diagnostic = nil)
450
+ xpath, matcher, diagnostic = _re_arg_if(condition, matcher, diagnostic)
451
+ return deny_xpath(xpath, diagnostic) unless matcher
452
+ return deny_any_xpath(xpath, matcher, diagnostic)
453
+ # ERGO flunk here - with param explanation (if humanly possible!)
454
+ end # ERGO assert_any_xpath should take an explicit block
455
+
456
+ # ERGO extract-method the x[@name=y] stuff!
457
+
458
+ # ERGO test nesting if statements to require this temporarily
459
+ # ERGO rdoc should provide external-able links for important methods
460
+
461
+ private # ERGO assert_js.js_like_ruby_statements_as_dsl
462
+
463
+ def javascript_to_xml(source, diagnostic)
464
+ source ||= default_js_source(diagnostic)
465
+ here = File.dirname(__FILE__)
466
+ jsToXml_pl = File.join(here, 'jsToXml.pl')
467
+
468
+ Tempfile.open('assert_javascript_sample') do |sample|
469
+ Tempfile.open('assert_javascript_error') do |error|
470
+ sample.write(source)
471
+ sample.flush
472
+ got = `perl "#{jsToXml_pl}" "#{sample.path}" 2>#{error.path}`
473
+ assert_xml got
474
+ yield(error.path)
475
+ end
476
+ end
477
+ end # ERGO use @xdoc = assert_xpath in certain other places
478
+
479
+ def default_js_source(diagnostic = nil)
480
+ @response.respond_to?(:body) || flunk(build_message(diagnostic, "no @response.body found - use assert_js(my_js)"))
481
+ return @response.body.relevance || flunk(build_message(diagnostic, "no JavaScript found in @response.body"))
482
+ end # ERGO publish relevance
483
+
484
+ # CONSIDER permit symbols for both element_id and matcher
485
+
486
+ def _re_arg_if(condition = :all, matcher = nil, diagnostic = nil)
487
+ @xdoc or assert_javascript
488
+
489
+ matcher = condition.respond_to?(:match) ? condition : matcher
490
+ aspect = :all
491
+ aspect = condition if [:condition, :all, :true, :false].include?(condition)
492
+
493
+ xpath = './/IfStatement[ @name = "IfStatement" ]' +
494
+ if aspect == :condition
495
+ '/Expression[ @name = "Expression" ]'
496
+ elsif aspect == :all
497
+ '/..' # CONSIDER why is this here??
498
+ else
499
+ "/Statement[ @name='#{aspect}' ]"
500
+ end
501
+
502
+ # ERGO flunk here - with param explanation (if humanly possible!)
503
+
504
+ return [xpath, matcher, diagnostic]
505
+ end
506
+
507
+ def yield_xdoc_block(xdoc, aspect, matcher, diagnostic, &block)
508
+ @if_condition = @xdoc.parent.parent if aspect == :condition
509
+ yield(@xdoc = xdoc) if block_given?
510
+ assert_match matcher, @xdoc.inner_text, diagnostic if matcher
511
+ return @xdoc
512
+ end # ERGO better name and/or tighter abstraction
513
+
514
+ def xpath_argument(index)
515
+ return "descendant-or-self::ArgumentList/*[position() = #{index}]"
516
+ end
517
+
518
+ # ERGO patch test:recent to run tests with same names as
519
+ # edited code - hook SVN diff to do it!
520
+
521
+ def object_method_xpath(object, method, element_id)
522
+ @xdoc or assert_javascript
523
+ # ERGO: element_id may be nil. Propagate this
524
+ # ERGO: element_id -> first_argument
525
+
526
+ return ".//ArgumentList[ @name = 'Arguments' ]/" +
527
+ (element_id ? "String[ . = '#{element_id}' ]/../" : '') +
528
+ '../' + member_expression(object, method)
529
+ end # ERGO quanti-friably
530
+
531
+ def member_expression(object, method)
532
+ return "MemberMemberExpression[ @name = 'Callee' or
533
+ @name = 'ClassExpression' ]" + # ERGO productively reconcile them two
534
+ "/Identifier[ position() = 1 and
535
+ @name = 'lhs' and
536
+ . = '#{ object }' ]" +
537
+ "/../Identifier[ position() = 2 and
538
+ @name = 'member' and
539
+ . = '#{ method }' ]" +
540
+ "/../../ArgumentList[ @name = 'Arguments' ]"
541
+ end
542
+
543
+ def calling_js_path(args)
544
+ @xdoc or assert_javascript
545
+ object, method = args.first.to_s.split('.')
546
+ element_id, diagnostic = [args[1], args[2]]
547
+
548
+ # ERGO this should also work without element_id
549
+ # ERGO local test that this calls its blocks correctly
550
+
551
+ if object and method and element_id
552
+ return object_method_xpath(object, method, element_id), *args
553
+ elsif object and method # ERGO document - pass nil for element_id if you need a diagnostic
554
+ # ERGO descendent or self?
555
+ return './/' + member_expression(object, method), *args
556
+ else
557
+ return ".//Identifier[ @name = 'Callee' and . = '#{args.shift}' ]" +
558
+ '/following-sibling::ArgumentList', *args
559
+ end
560
+ end
561
+
562
+ end
563
+
564
+ # ERGO: Don't put all your eggs in one basket before they're hatched.
565
+
566
+ # Detect if your kit is complete, and detect if you have <tt>Javascript::PurePerl</tt>.
567
+ # If you don't, we warn one time. Use this method to defend portable tests that
568
+ # should not break on computers without Javascript::PurePerl
569
+ #
570
+ # Example:
571
+ # if RAILS_ENV == 'test' and got_pure_perl?
572
+ # class AssertJavaScriptTest < Test::Unit::TestCase
573
+ # # ...
574
+ # end
575
+ # end
576
+ #
577
+ def got_pure_perl?
578
+ perl_version = `perl -v 2>&1` rescue 'perl, v0'
579
+ perl_version =~ /perl, v(\d+)/
580
+
581
+ if $1.to_i < 5
582
+ puts "\ninsufficient Perl for assert_js!" unless $already_warned_about_missing_pure_perl
583
+ $already_warned_about_missing_pure_perl = true
584
+ return false
585
+ end
586
+
587
+ unless system('perl -e "use Javascript::PurePerl" 2>/dev/null')
588
+ puts "\ninstall Javascript::PurePerl for best results!" unless $already_warned_about_missing_pure_perl
589
+ $already_warned_about_missing_pure_perl = true
590
+ return false
591
+ end
592
+
593
+ return true
594
+ end