The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Test::JavaScript;

=head1 NAME

Test::JavaScript - JavaScript Testing Module

=head1 SYNOPSIS

    use Test::JavaScript qw(no_plan);

    js_eval_ok("/path/to/MyFile.js");

    js_ok("var obj = new MyFile", "Create a MyFile object");

    js_ok("obj.someFunction = function () { return 'ok' }");

    js_is("obj.someFunction()", "ok");

=head1 DESCRIPTION

Test::JavaScript provides a method of unit testing javascript code from within
perl. This module uses the JavaScript::SpiderMonkey package to evaluate
JavaScript using the SpiderMonkey JavaScript engine.

=cut

use strict 'vars';
use warnings;

use Exporter;
use Carp qw(croak);

use Test::Builder;
our $Test = Test::Builder->new;

use JavaScript::SpiderMonkey;
our $js = JavaScript::SpiderMonkey->new();
$js->init();
END { $js->destroy };

our $VERSION = 0.06;
our @ISA     = qw(Exporter);
our @EXPORT  = qw(js_ok js_eval_ok js_is js_isnt js_diag);

sub no_ending { $Test->no_ending(@_) }
sub plan { $Test->plan(@_) }

sub import {
    my $self = shift;
    my $caller = caller;

    for my $f (@EXPORT) {
	*{$caller.'::'.$f} = \&$f;
    }

    $Test->exported_to($caller);
    $Test->plan(@_);
}

sub try_eval {
    my ($code, $name) = @_;
    my $rc = $js->eval($code);
    unless ($rc) {
	my $ok = $Test->ok( !$@, $name );
        $Test->diag(<<DIAGNOSTIC);
    $@
DIAGNOSTIC
	$@ = '';
    }
}

sub escape_args {
    my $name = pop @_;
    my @args = @_;
    $args[0] = $name and $name = '' unless @args;
    s/'/\\'/g foreach @args;
    (my $escaped = $name) =~ s/'/\\'/g;
    return (@args,$escaped,$name);
}

=item B<js_eval_ok>

  js_eval_ok($filename)

This reads a file and evals it in JavaScript

For example:

    js_eval_ok( "/path/to/some/file.js" );

=cut

sub js_eval_ok ($;@) {
    my $filename = shift || croak "filename required";
    croak "$filename doesn't exist" unless $filename;

    open my $fh, $filename or die "Couldn't read $filename: $!";
    my @lines = <$fh>;
    close $fh or die "Couldn't read $filename: $!";

    my $rc = $js->eval(join("\n", @lines));
    my $ok = $Test->ok( !$@, "use $filename;" );

    unless( $rc ) {
        $Test->diag(<<DIAGNOSTIC);
    Tried to use '$filename'.
    $@
DIAGNOSTIC
    }
}

=item B<js_is>

=item B<js_isnt>

  js_is  ( $this, $that, $test_name );
  js_isnt( $this, $that, $test_name );

This compares two values in JavaScript land. They can be literal strings
passed from perl or variables defined earlier.

For example:

    js_ok("var i = 3");					// ok
    js_is("i", 3, "i is 3");				// ok
    js_is("3", 3, "3 is 3");				// ok
    js_is("3", 2, "3 is 2");				// not ok

    js_ok("function three () { return 3 }");		// ok
    js_is("three()", 3);				// ok
    js_is("three()", 4);				// not ok

    js_isnt("3", 4, "3 is not 4");			// ok

=cut

$js->function_set("is", sub { $Test->is_eq(@_) });

sub js_is {
    my ($test,$actual,$ename,$name) = escape_args(@_);
    my $code = <<EOT;
is( $test, '$actual', '$ename'.replace(/\\'/,"'"));
EOT
    try_eval($code, $name);
}

$js->function_set("isnt", sub { $Test->isnt_eq(@_) });

sub js_isnt {
    my ($test,$actual,$ename,$name) = escape_args(@_);
    my $code = <<EOT;
isnt( $test, '$actual', '$ename'.replace(/\\'/,"'"));
EOT
    try_eval($code, $name);
}

=item B<js_ok>

  js_ok("var monkey = 3", $test_name);

The expression passed as the first parameter is evaluated as either true or
false. The test fails if the expression explicitly returns false, or if a
syntax error occurs in JavaScript land

For example:

    js_ok("var i = 3");					    // ok
    js_ok("true", "true is true");			    // ok
    js_ok("1 == 2", "1 is equal to 2");			    // not ok
    js_ok("false", "false is false");			    // not ok
    js_ok("var array = ['one','two',non_existing_var];")    // not ok

=cut

$js->function_set("ok", sub { $Test->ok(@_) });
sub js_ok {
    my ($test,$ename,$name) = escape_args(@_);
    my $lines = join"\n", map { "code.push('$_');" } split("\n", $test);
    my $code = <<EOT;
var code = new Array;
$lines
var result = eval(code.join("\\n")) ? true : false;
ok( result, '$ename'.replace(/\\'/,"'"));
EOT

    try_eval($code, $name);
}

=item B<diag>

  js_ok("var myval = 3; diag('the variable myval is ' + myval)");

This subroutine simply logs the parameters passed as a comment

=cut

$js->function_set("diag", sub { $Test->diag(@_) });

=head1 AUTHORS

Kevin Jones, C<< <kevinj at cpan.org> >>

=head1 COPYRIGHT

Copyright 2006 Kevin Jones, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

return 1;